diff --git a/USAGE.md b/USAGE.md index 8a83c14..99339c4 100644 --- a/USAGE.md +++ b/USAGE.md @@ -10,8 +10,10 @@ * [`bitser.includeMetatables`](#includeMetatables) * [`bitser.register`](#register) * [`bitser.registerClass`](#registerclass) + * [`bitser.registerUserdata`](#registerUserdata) * [`bitser.unregister`](#unregister) * [`bitser.unregisterClass`](#unregisterclass) + * [`bitser.unregisterUserdata`](#unregisterUserdata) * [`bitser.reserveBuffer`](#reservebuffer) * [`bitser.clearBuffer`](#clearbuffer) @@ -202,6 +204,55 @@ Returns the registered class as a convenience. See also: [`bitser.unregisterClass`](#unregisterclass). +## registerUserdata + +```lua +bitser.registerUserdata(id, matcher, serializer, deserializer) +``` + +This is an advanved-users-only feature, allowing you to register a custom userdata handler. +This makes it possible for bitser to serialize and deserialize userdata with your own callbacks. + +- The `id` parameter has to be a number from 0 to 255 and unique, and identifies your userdata handler. +It must be unique per userdata "type". + +- The `matcher` function takes the userdata value as parameter, and must return a boolean: whether the +current userdata value is (matches) the one you registered the handler for. This could be done via metatable +lookup, for instance, or any other way that your userdata provides. + +- The `serializer` function takes the value (your userdata) and an `env` table as parameters. + The `env` table provides access to common internal functions useful for (de)serializing, and the `seen` data. + +- The `deserializer` function takes an `env` table as parameter. + The `env` table provides access to common internal functions useful for (de)serializing, and the `seen` data. + +Full usage example with a `ByteArray` class as userdata, which under the hood, stores bytes sequentially: +(The internal format would be `[len][data...]`) +```lua +bitser.registerUserdata(1, + function(value) + return getmetatable(value) == ByteArray + end, + function(value, env) + local len = #value + env.serialize_value(len, env.seen) + env.Buffer_write_raw(ffi.string(ffi.cast("uint8_t*", value:getDataPtr()), len), len) + end, + function(env) + local len = env.deserialize_value(env.seen) + local ba = ByteArray(len) + env.Buffer_read_raw(ffi.cast("uint8_t*", ba:getDataPtr()), len) + return ba +end) + +local test = ByteArray({ 0x01, 0x02, 0x00, 0x04 }) +local test2 = bitser.loads(bitser.dumps(test)) + +print(test:tostring("base64") == test2:tostring("base64")) -- true +``` + +See also: [`bitser.unregisterUserdata`](#unregisterUserdata). + ## unregister ```lua @@ -223,6 +274,16 @@ which is useful in a context where you don't have a reference to the class you w See also: [`bitser.registerClass`](#registerclass). +## unregisterUserdata + +```lua +bitser.unregisterUserdata(id) +``` + +Deregisters the previously registered userdata with the id `id`. + +See also: [`bitser.registerUserdata`](#registerUserdata). + ## reserveBuffer ```lua diff --git a/bitser.lua b/bitser.lua index b76a9d7..9387762 100644 --- a/bitser.lua +++ b/bitser.lua @@ -32,6 +32,7 @@ local writable_buf = nil local writable_buf_size = nil local includeMetatables = true -- togglable with bitser.includeMetatables(false) local SEEN_LEN = {} +local shared_env = {} -- for userdata callbacks local function Buffer_prereserve(min_size) if buf_size < min_size then @@ -139,6 +140,7 @@ end local resource_registry = {} local resource_name_registry = {} +local userdata_registry = {} local class_registry = {} local class_name_registry = {} local classkey_registry = {} @@ -243,7 +245,20 @@ local function write_cdata(value, seen) Buffer_write_raw(ffi.typeof('$[1]', ty)(value), len) end -local types = {number = write_number, string = write_string, table = write_table, boolean = write_boolean, ["nil"] = write_nil, cdata = write_cdata} +local function write_userdata(value, seen) + for ud_id, ud_handler in pairs(userdata_registry) do + if ud_handler.match(value) then + Buffer_write_byte(254) + Buffer_write_byte(ud_id) + shared_env.seen = seen + ud_handler.serialize(value, shared_env) + return + end + end + error("cannot serialize this userdata") +end + +local types = {number = write_number, string = write_string, table = write_table, boolean = write_boolean, ["nil"] = write_nil, cdata = write_cdata, userdata = write_userdata} serialize_value = function(value, seen) if seen[value] then @@ -259,7 +274,7 @@ serialize_value = function(value, seen) return end local t = type(value) - if t ~= 'number' and t ~= 'boolean' and t ~= 'nil' and t ~= 'cdata' then + if t ~= 'number' and t ~= 'boolean' and t ~= 'nil' and t ~= 'cdata' and t ~= 'userdata' then seen[value] = seen[SEEN_LEN] seen[SEEN_LEN] = seen[SEEN_LEN] + 1 end @@ -388,11 +403,25 @@ local function deserialize_value(seen) local read_into = ffi.typeof('$[1]', ctype)() Buffer_read_raw(read_into, len) return ctype(read_into[0]) + elseif t == 254 then + --userdata + local ud_id = Buffer_read_byte() + local ud_handler = userdata_registry[ud_id] + if ud_handler then + shared_env.seen = seen + return ud_handler.deserialize(shared_env) + end + error("unsupported serialized userdata id " .. ud_id) else error("unsupported serialized type " .. t) end end +shared_env.serialize_value = serialize_value +shared_env.deserialize_value = deserialize_value +shared_env.Buffer_write_raw = Buffer_write_raw +shared_env.Buffer_read_raw = Buffer_read_raw + local function deserialize_MiddleClass(instance, class) return setmetatable(instance, class.__instanceDict) end @@ -493,4 +522,10 @@ end, unregisterClass = function(name) classkey_registry[name] = nil class_deserialize_registry[name] = nil class_registry[name] = nil +end, registerUserdata = function(id, matcher, serializer, deserializer) + assert(type(id) == "number" and id >= 0 and id <= 255, "registerUserdata: id must be a number between 0 and 255") + assert(not userdata_registry[id], "registerUserdata: id " .. id .. " already registered") + userdata_registry[id] = { match = matcher, serialize = serializer, deserialize = deserializer} +end, unregisterUserdata = function(id) + userdata_registry[id] = nil end, reserveBuffer = Buffer_prereserve, clearBuffer = Buffer_clear, version = VERSION} diff --git a/spec/bitser_spec.lua b/spec/bitser_spec.lua index c292b99..b0c0c95 100644 --- a/spec/bitser_spec.lua +++ b/spec/bitser_spec.lua @@ -274,7 +274,6 @@ describe("bitser", function() assert.has_error(function() bitser.registerClass('Horse', {mane = 'majestic'}) end, "no deserializer given for unsupported class library") end) it("cannot deserialize values from unassigned type bytes", function() - assert.has_error(function() bitser.loads("\254") end, "unsupported serialized type 254") assert.has_error(function() bitser.loads("\255") end, "unsupported serialized type 255") end) it("can load from raw data", function()