modlib/doc/bluon.md
2023-06-01 12:28:21 +02:00

3.9 KiB

Bluon

Binary Lua object notation.

new(def)

def = {
	aux_is_valid = function(object)
		return is_valid
	end,
	aux_len = function(object)
		return length_in_bytes
	end,
	-- read type byte, stream providing :read(count), map of references -> id
	aux_read = function(type, stream, references)
		... = stream:read(...)
		return object
	end,
	-- object to be written, stream providing :write(text), list of references
	aux_write = function(object, stream, references)
		stream:write(...)
	end
}

:is_valid(object)

Returns whether the given object can be represented by the instance as boolean.

:len(object)

Returns the expected length of the object if serialized by the current instance in bytes.

:write(object, stream)

Writes the object to a stream supporting :write(text). Throws an error if invalid.

:read(stream)

Reads a single bluon object from a stream supporting :read(count). Throws an error if invalid bluon.

Checking whether the stream has been fully consumed by doing assert(not stream:read(1)) is left up to the user.

Format

Bluon uses a "tagged union" binary format: Values are stored as a one-byte tag followed by the contents of the union. For frequently used "constants", only a tag is used.

nil is an exception; since it can't appear in tables, it gets no tag. If the value to be written by Bluon is nil, Bluon simply writes nothing.

The following is an enumeration of tag numbers, which are assigned in this order.

  • false: 0
  • true: 1
  • Numbers:
    • Constants: 0, nan, +inf, -inf
    • Integers: Little endian:
      • Unsigned: U8, U16, U32, U64
      • Negative: -U8, -U16, -U32, -U64
    • Floats: Little endian F32, F64
  • Strings:
    • Constant: ""
    • Length is written as unsigned integer according to the tag: S8, S16, S32, S64
      • followed by the raw bytes
  • Tables:
    • Tags: M0, M8, M16, M32, M64 times L0, L8, L16, L32, L64
    • M is more significant than L: The order of the cartesian product is M0L0, M0L1, ...
    • List and map part count encoded as unsigned integers according to the tag, list part count comes first
    • followed by all values in the list part written as Bluon
    • followed by all key-value pairs in the map part written as Bluon (first the key is written as Bluon, then the value)
  • Reference:
    • Reference ID as unsigned integer: R8, R16, R32, R64
    • References a previously encountered table or string by an index: All tables and strings are numbered in the order they occur in the Bluon
  • Reserved tags:
    • All tags <= 55 are reserved. This gives 200 free tags.

Features

  • Embeddable: Written in pure Lua
  • Storage efficient: No duplication of strings or reference-equal tables
  • Flexible: Can serialize circular references and strings containing null

Simple example

local object = ...
-- Write to file
local file = io.open(..., "wb")
modlib.bluon:write(object, file)
file:close()
-- Write to text
local rope = modlib.table.rope{}
modlib.bluon:write(object, rope)
text = rope:to_text()
-- Read from text
local inputstream = modlib.text.inputstream"\1"
assert(modlib.bluon:read(object, rope) == true)

Advanced example

-- Serializes all userdata to a constant string:
local custom_bluon = bluon.new{
	aux_is_valid = function(object)
		return type(object) == "userdata"
	end,
	aux_len = function(object)
		return 1 + ("userdata"):len())
	end,
	aux_read = function(type, stream, references)
		assert(type == 100, "unsupported type")
		assert(stream:read(("userdata"):len()) == "userdata")
		return userdata()
	end,
	-- object to be written, stream providing :write(text), list of references
	aux_write = function(object, stream, references)
		assert(type(object) == "userdata")
		stream:write"\100userdata"
	end
}
-- Write to text
local rope = modlib.table.rope{}
custom_bluon:write(userdata(), rope)
assert(rope:to_text() == "\100userdata")