add feature to register custom userdata handlers.

This commit is contained in:
Adrien Bertrand 2021-02-12 13:41:35 -05:00
parent 1d3ebe04cf
commit dff687147c
3 changed files with 98 additions and 3 deletions

View File

@ -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

View File

@ -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}

View File

@ -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()