From 0e3417c5cd87516393b7771dfa39cabd310d1acc Mon Sep 17 00:00:00 2001 From: Adrien Bertrand Date: Fri, 12 Feb 2021 13:41:35 -0500 Subject: [PATCH] add feature to register custom userdata handlers. --- USAGE.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ bitser.lua | 37 ++++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/USAGE.md b/USAGE.md index 2819c02..faa9bc1 100644 --- a/USAGE.md +++ b/USAGE.md @@ -9,8 +9,10 @@ * [`bitser.loadLoveFile`](#loadlovefile) * [`bitser.register`](#register) * [`bitser.registerClass`](#registerclass) + * [`bitser.registerUserdata`](#registerUserdata) * [`bitser.unregister`](#unregister) * [`bitser.unregisterClass`](#unregisterclass) + * [`bitser.unregisterUserdata`](#unregisterUserdata) * [`bitser.reserveBuffer`](#reservebuffer) * [`bitser.clearBuffer`](#clearbuffer) @@ -193,6 +195,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 the seen data (the buffer is positioned at 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. + +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(seen, env) + local len = env.deserialize_value(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 @@ -214,6 +265,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 e929baa..7a3dc98 100644 --- a/bitser.lua +++ b/bitser.lua @@ -31,6 +31,7 @@ local buf_is_writable = true local writable_buf = nil local writable_buf_size = nil local SEEN_LEN = {} +local shared_env = {} -- for userdata callbacks local function Buffer_prereserve(min_size) if buf_size < min_size then @@ -138,6 +139,7 @@ end local resource_registry = {} local resource_name_registry = {} +local userdata_registry = {} local class_registry = {} local class_name_registry = {} local classkey_registry = {} @@ -242,7 +244,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 @@ -385,11 +400,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(seen, 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 @@ -488,4 +517,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}