Squashed 'src/lib/bitser/' content from commit e7480da

git-subtree-dir: src/lib/bitser
git-subtree-split: e7480da28e507bed62e93ca7b5a1239cfb14d016
This commit is contained in:
Paul Liverman III 2017-10-10 00:15:20 -07:00
commit fc95d6251d
12 changed files with 1259 additions and 0 deletions

26
.travis.yml Normal file
View File

@ -0,0 +1,26 @@
language: python
sudo: false
env:
- LUA="luajit=2.0"
before_install:
- pip install hererocks
- hererocks lua_install -r^ --$LUA
- export PATH=$PATH:$PWD/lua_install/bin
install:
- luarocks install luacheck
- luarocks install busted
- luarocks install luacov
- luarocks install luacov-coveralls
- luarocks install middleclass
- wget https://bitbucket.org/bartbes/slither/raw/bbd85db19b19d9d14470b4fa7b73029b6c070fb0/slither.lua
- wget https://raw.githubusercontent.com/vrld/hump/038bc9025f1cb850355f4b073357b087b8122da9/class.lua
script:
- luacheck --std max+busted bitser.lua spec --globals love
- busted --verbose --coverage
after_success:
- luacov-coveralls --include bitser -e $TRAVIS_BUILD_DIR/lua_install

53
README.md Normal file
View File

@ -0,0 +1,53 @@
# bitser
[![Build Status](https://travis-ci.org/gvx/bitser.svg?branch=master)](https://travis-ci.org/gvx/bitser)
[![Coverage Status](https://coveralls.io/repos/github/gvx/bitser/badge.svg?branch=master)](https://coveralls.io/github/gvx/bitser?branch=master)
Serializes and deserializes Lua values with LuaJIT.
```lua
local bitser = require 'bitser'
bitser.register('someResource', someResource)
bitser.registerClass(SomeClass)
serializedString = bitser.dumps(someValue)
someValue = bitser.loads(serializedString)
```
Documentation can be found in [USAGE.md](USAGE.md).
Pull requests, bug reports and other feedback welcome! :heart:
Bitser is released under the ISC license (functionally equivalent to the BSD
2-Clause and MIT licenses).
Please note that bitser requires LuaJIT for its `ffi` library and JIT compilation. Without JIT, it may or may not run, but it will be much slower than usual. This primarily affects Android and iOS, because JIT is disabled on those platforms.
## Why would I use this?
Because it's fast. Because it produces tiny output. Because the name means "snappier"
or "unfriendlier" in Dutch. Because it's safe to use with untrusted data.
Because it's inspired by [binser](https://github.com/bakpakin/binser), which is great.
## How do I use the benchmark thingy?
Download zero or more of [binser.lua](https://raw.githubusercontent.com/bakpakin/binser/master/binser.lua),
[ser.lua](https://raw.githubusercontent.com/gvx/Ser/master/ser.lua),
[smallfolk.lua](https://raw.githubusercontent.com/gvx/Smallfolk/master/smallfolk.lua),
[serpent.lua](https://raw.githubusercontent.com/pkulchenko/serpent/master/src/serpent.lua) and
[MessagePack.lua](https://raw.githubusercontent.com/fperrad/lua-MessagePack/master/src/MessagePack.lua), and run:
love .
You do need [LÖVE](https://love2d.org/) for that.
You can add more cases in the folder `cases/` (check out `_new.lua`), and add other
serializers to the benchmark in `main.lua`. If you do either of those things, please
send me a pull request!
## You can register classes?
Yes. At the moment, bitser supports MiddleClass, SECL, hump.class, Slither and Moonscript classes (and
probably some other class libraries by accident).

234
USAGE.md Normal file
View File

@ -0,0 +1,234 @@
* [Basic usage](#basic-usage)
* [Serializing class instances](#serializing-class-instances)
* [Advanced usage](#advanced-usage)
* [Reference](#reference)
* [`bitser.dumps`](#dumps)
* [`bitser.dumpLoveFile`](#dumplovefile)
* [`bitser.loads`](#loads)
* [`bitser.loadData`](#loaddata)
* [`bitser.loadLoveFile`](#loadlovefile)
* [`bitser.register`](#register)
* [`bitser.registerClass`](#registerclass)
* [`bitser.unregister`](#unregister)
* [`bitser.unregisterClass`](#unregisterclass)
* [`bitser.reserveBuffer`](#reservebuffer)
* [`bitser.clearBuffer`](#clearbuffer)
# Basic usage
```lua
local bitser = require 'bitser'
-- some_thing can be almost any lua value
local binary_data = bitser.dumps(some_thing)
-- binary_data is a string containing some serialized value
local copy_of_some_thing = bitser.loads(binary_data)
```
Bitser can't dump values of type `function`, `userdata` or `thread`, or anything that
contains one of those. If you need to, look into [`bitser.register`](#register).
# Serializing class instances
All you need to make bitser correctly serialize your class instances is register that class:
```lua
-- this is usually enough
bitser.registerClass(MyClass)
-- if you use Slither, you can add it to __attributes__
class 'MyClass' {
__attributes__ = {bitser.registerClass},
-- insert rest of class here
}
local data = bitser.dumps(MyClass(42))
local instance = bitser.loads(data)
```
Note that classnames need to be unique to avoid confusion, so if you have two different classes named `Foo` you'll need to do
something like:
```lua
-- in module_a.lua
bitser.registerClass('module_a.Foo', Foo)
-- in module_b.lua
bitser.registerClass('module_b.Foo', Foo)
```
See the reference sections on [`bitser.registerClass`](#registerclass) and
[`bitser.unregisterClass`](#unregisterclass) for more information.
## Supported class libraries
* MiddleClass
* SECL
* hump.class
* Slither
* Moonscript classes
# Advanced usage
If you use [LÖVE](https://love2d.org/), you'll want to use [`bitser.dumpLoveFile`](#dumplovefile) and [`bitser.loadLoveFile`](#loadlovefile) if you want to serialize to the save directory. You also might have images and other resources that you'll need to register, like follows:
```lua
function love.load()
bad_guy_img = bitser.register('bad_guy_img', love.graphics.newImage('img/bad_guy.png'))
if love.filesystem.exists('save_point.dat') then
level_data = bitser.loadLoveFile('save_point.dat')
else
level_data = create_level_data()
end
end
function save_point_reached()
bitser.dumpLoveFile('save_point.dat', level_data)
end
```
# Reference
## dumps
```lua
string = bitser.dumps(value)
```
Basic serialization of `value` into a Lua string.
See also: [`bitser.loads`](#loads).
## dumpLoveFile
```lua
bitser.dumpLoveFile(file_name, value)
```
Serializes `value` and writes the result to `file_name` more efficiently than serializing to a string and writing
that string to a file. Only useful if you're running [LÖVE](https://love2d.org/).
See also: [`bitser.loadLoveFile`](#loadlovefile).
## loads
```lua
value = bitser.loads(string)
```
Deserializes `value` from `string`.
See also: [`bitser.dumps`](#dumps).
## loadData
```lua
value = bitser.loadData(light_userdata, size)
```
Deserializes `value` from raw data. You probably won't need to use this function ever.
When running [LÖVE](https://love2d.org/), you would use it like this:
```lua
value = bitser.loadData(data:getPointer(), data:getSize())
```
Where `data` is an instance of a subclass of [Data](https://love2d.org/wiki/Data).
## loadLoveFile
```lua
value = bitser.loadLoveFile(file_name)
```
Reads from `file_name` and deserializes `value` more efficiently than reading the file and then deserializing that string.
Only useful if you're running [LÖVE](https://love2d.org/).
See also: [`bitser.dumpLoveFile`](#dumplovefile).
## register
```lua
resource = bitser.register(name, resource)
```
Registers the value `resource` with the name `name`, which has to be a unique string. Registering static resources like images,
functions, classes and huge strings, makes sure bitser doesn't attempt to serialize them, but only stores a named
reference to them.
Returns the registered resource as a convenience.
See also: [`bitser.unregister`](#unregister).
## registerClass
```lua
class = bitser.registerClass(class)
class = bitser.registerClass(name, class)
class = bitser.registerClass(name, class, classkey, deserializer)
```
Registers the class `class`, so that bitser can correctly serialize and deserialize instances of `class`.
Note that if you want to serialize the class _itself_, you'll need to [register the class as a resource](#register).
Most of the time the first variant is enough, but some class libraries don't store the
class name on the class object itself, in which case you'll need to use the second variant.
Class names also have to be unique, so if you use multiple classes with the same name, you'll need to use the second
variant as well to give them different names.
The arguments `classkey` and `deserializer` exist so you can hook in unsupported class libraries without needing
to patch bitser. [See the list of supported class libraries](#supported-class-libraries).
If not nil, the argument `classkey` should be a string such that
`rawget(obj, classkey) == class` for any `obj` whose type is `class`. This is done so that key is skipped for serialization.
If not nil, the argument `deserializer` should be a function such that `deserializer(obj, class)` returns a valid
instance of `class` with the properties of `obj`. `deserializer` is allowed to mutate `obj`.
Returns the registered resource as a convenience.
See also: [`bitser.unregisterClass`](#unregisterclass).
## unregister
```lua
bitser.unregister(name)
```
Deregisters the previously registered value with the name `name`.
See also: [`bitser.register`](#register).
## unregisterClass
```lua
bitser.unregisterClass(name)
```
Deregisters the previously registered class with the name `name`. Note that this works by name and not value,
which is useful in a context where you don't have a reference to the class you want to unregister.
See also: [`bitser.registerClass`](#registerclass).
## reserveBuffer
```lua
bitser.reserveBuffer(num_bytes)
```
Makes sure the buffer used for reading and writing serialized data is at least `num_bytes` large.
You probably don't need to ever use this function.
## clearBuffer
```lua
bitser.clearBuffer()
```
Frees up the buffer used for reading and writing serialized data for garbage collection.
You'll rarely need to use this function, except if you needed a huge buffer before and now only need a small buffer
(or are done (de)serializing altogether). Most of the time, using this function will decrease performance needlessly.

432
bitser.lua Normal file
View File

@ -0,0 +1,432 @@
--[[
Copyright (c) 2016, Robin Wellner
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
]]
local floor = math.floor
local pairs = pairs
local type = type
local insert = table.insert
local getmetatable = getmetatable
local setmetatable = setmetatable
local ffi = require("ffi")
local buf_pos = 0
local buf_size = -1
local buf = nil
local writable_buf = nil
local writable_buf_size = nil
local function Buffer_prereserve(min_size)
if buf_size < min_size then
buf_size = min_size
buf = ffi.new("uint8_t[?]", buf_size)
end
end
local function Buffer_clear()
buf_size = -1
buf = nil
writable_buf = nil
writable_buf_size = nil
end
local function Buffer_makeBuffer(size)
if writable_buf then
buf = writable_buf
buf_size = writable_buf_size
writable_buf = nil
writable_buf_size = nil
end
buf_pos = 0
Buffer_prereserve(size)
end
local function Buffer_newReader(str)
Buffer_makeBuffer(#str)
ffi.copy(buf, str, #str)
end
local function Buffer_newDataReader(data, size)
writable_buf = buf
writable_buf_size = buf_size
buf_pos = 0
buf_size = size
buf = ffi.cast("uint8_t*", data)
end
local function Buffer_reserve(additional_size)
while buf_pos + additional_size > buf_size do
buf_size = buf_size * 2
local oldbuf = buf
buf = ffi.new("uint8_t[?]", buf_size)
ffi.copy(buf, oldbuf, buf_pos)
end
end
local function Buffer_write_byte(x)
Buffer_reserve(1)
buf[buf_pos] = x
buf_pos = buf_pos + 1
end
local function Buffer_write_string(s)
Buffer_reserve(#s)
ffi.copy(buf + buf_pos, s, #s)
buf_pos = buf_pos + #s
end
local function Buffer_write_data(ct, len, ...)
Buffer_reserve(len)
ffi.copy(buf + buf_pos, ffi.new(ct, ...), len)
buf_pos = buf_pos + len
end
local function Buffer_ensure(numbytes)
if buf_pos + numbytes > buf_size then
error("malformed serialized data")
end
end
local function Buffer_read_byte()
Buffer_ensure(1)
local x = buf[buf_pos]
buf_pos = buf_pos + 1
return x
end
local function Buffer_read_string(len)
Buffer_ensure(len)
local x = ffi.string(buf + buf_pos, len)
buf_pos = buf_pos + len
return x
end
local function Buffer_read_data(ct, len)
Buffer_ensure(len)
local x = ffi.new(ct)
ffi.copy(x, buf + buf_pos, len)
buf_pos = buf_pos + len
return x
end
local resource_registry = {}
local resource_name_registry = {}
local class_registry = {}
local class_name_registry = {}
local classkey_registry = {}
local class_deserialize_registry = {}
local serialize_value
local function write_number(value, _)
if floor(value) == value and value >= -2147483648 and value <= 2147483647 then
if value >= -27 and value <= 100 then
--small int
Buffer_write_byte(value + 27)
elseif value >= -32768 and value <= 32767 then
--short int
Buffer_write_byte(250)
Buffer_write_data("int16_t[1]", 2, value)
else
--long int
Buffer_write_byte(245)
Buffer_write_data("int32_t[1]", 4, value)
end
else
--double
Buffer_write_byte(246)
Buffer_write_data("double[1]", 8, value)
end
end
local function write_string(value, _)
if #value < 32 then
--short string
Buffer_write_byte(192 + #value)
else
--long string
Buffer_write_byte(244)
write_number(#value)
end
Buffer_write_string(value)
end
local function write_nil(_, _)
Buffer_write_byte(247)
end
local function write_boolean(value, _)
Buffer_write_byte(value and 249 or 248)
end
local function write_table(value, seen)
local classkey
local classname = (class_name_registry[value.class] -- MiddleClass
or class_name_registry[value.__baseclass] -- SECL
or class_name_registry[getmetatable(value)] -- hump.class
or class_name_registry[value.__class__] -- Slither
or class_name_registry[value.__class]) -- Moonscript class
if classname then
classkey = classkey_registry[classname]
Buffer_write_byte(242)
serialize_value(classname, seen)
else
Buffer_write_byte(240)
end
local len = #value
write_number(len, seen)
for i = 1, len do
serialize_value(value[i], seen)
end
local klen = 0
for k in pairs(value) do
if (type(k) ~= 'number' or floor(k) ~= k or k > len or k < 1) and k ~= classkey then
klen = klen + 1
end
end
write_number(klen, seen)
for k, v in pairs(value) do
if (type(k) ~= 'number' or floor(k) ~= k or k > len or k < 1) and k ~= classkey then
serialize_value(k, seen)
serialize_value(v, seen)
end
end
end
local types = {number = write_number, string = write_string, table = write_table, boolean = write_boolean, ["nil"] = write_nil}
serialize_value = function(value, seen)
if seen[value] then
local ref = seen[value]
if ref < 64 then
--small reference
Buffer_write_byte(128 + ref)
else
--long reference
Buffer_write_byte(243)
write_number(ref, seen)
end
return
end
local t = type(value)
if t ~= 'number' and t ~= 'boolean' and t ~= 'nil' then
seen[value] = seen.len
seen.len = seen.len + 1
end
if resource_name_registry[value] then
local name = resource_name_registry[value]
if #name < 16 then
--small resource
Buffer_write_byte(224 + #name)
Buffer_write_string(name)
else
--long resource
Buffer_write_byte(241)
write_string(name, seen)
end
return
end
(types[t] or
error("cannot serialize type " .. t)
)(value, seen)
end
local function serialize(value)
Buffer_makeBuffer(4096)
local seen = {len = 0}
serialize_value(value, seen)
end
local function add_to_seen(value, seen)
insert(seen, value)
return value
end
local function reserve_seen(seen)
insert(seen, 42)
return #seen
end
local function deserialize_value(seen)
local t = Buffer_read_byte()
if t < 128 then
--small int
return t - 27
elseif t < 192 then
--small reference
return seen[t - 127]
elseif t < 224 then
--small string
return add_to_seen(Buffer_read_string(t - 192), seen)
elseif t < 240 then
--small resource
return add_to_seen(resource_registry[Buffer_read_string(t - 224)], seen)
elseif t == 240 then
--table
local v = add_to_seen({}, seen)
local len = deserialize_value(seen)
for i = 1, len do
v[i] = deserialize_value(seen)
end
len = deserialize_value(seen)
for _ = 1, len do
local key = deserialize_value(seen)
v[key] = deserialize_value(seen)
end
return v
elseif t == 241 then
--long resource
local idx = reserve_seen(seen)
local value = resource_registry[deserialize_value(seen)]
seen[idx] = value
return value
elseif t == 242 then
--instance
local instance = add_to_seen({}, seen)
local classname = deserialize_value(seen)
local class = class_registry[classname]
local classkey = classkey_registry[classname]
local deserializer = class_deserialize_registry[classname]
local len = deserialize_value(seen)
for i = 1, len do
instance[i] = deserialize_value(seen)
end
len = deserialize_value(seen)
for _ = 1, len do
local key = deserialize_value(seen)
instance[key] = deserialize_value(seen)
end
if classkey then
instance[classkey] = class
end
return deserializer(instance, class)
elseif t == 243 then
--reference
return seen[deserialize_value(seen) + 1]
elseif t == 244 then
--long string
return add_to_seen(Buffer_read_string(deserialize_value(seen)), seen)
elseif t == 245 then
--long int
return Buffer_read_data("int32_t[1]", 4)[0]
elseif t == 246 then
--double
return Buffer_read_data("double[1]", 8)[0]
elseif t == 247 then
--nil
return nil
elseif t == 248 then
--false
return false
elseif t == 249 then
--true
return true
elseif t == 250 then
--short int
return Buffer_read_data("int16_t[1]", 2)[0]
else
error("unsupported serialized type " .. t)
end
end
local function deserialize_MiddleClass(instance, class)
return setmetatable(instance, class.__instanceDict)
end
local function deserialize_SECL(instance, class)
return setmetatable(instance, getmetatable(class))
end
local deserialize_humpclass = setmetatable
local function deserialize_Slither(instance, class)
return getmetatable(class).allocate(instance)
end
local function deserialize_Moonscript(instance, class)
return setmetatable(instance, class.__base)
end
return {dumps = function(value)
serialize(value)
return ffi.string(buf, buf_pos)
end, dumpLoveFile = function(fname, value)
serialize(value)
love.filesystem.write(fname, ffi.string(buf, buf_pos))
end, loadLoveFile = function(fname)
local serializedData = love.filesystem.newFileData(fname)
Buffer_newDataReader(serializedData:getPointer(), serializedData:getSize())
return deserialize_value({})
end, loadData = function(data, size)
Buffer_newDataReader(data, size)
return deserialize_value({})
end, loads = function(str)
Buffer_newReader(str)
return deserialize_value({})
end, register = function(name, resource)
assert(not resource_registry[name], name .. " already registered")
resource_registry[name] = resource
resource_name_registry[resource] = name
return resource
end, unregister = function(name)
resource_name_registry[resource_registry[name]] = nil
resource_registry[name] = nil
end, registerClass = function(name, class, classkey, deserializer)
if not class then
class = name
name = class.__name__ or class.name or class.__name
end
if not classkey then
if class.__instanceDict then
-- assume MiddleClass
classkey = 'class'
elseif class.__baseclass then
-- assume SECL
classkey = '__baseclass'
end
-- assume hump.class, Slither, Moonscript class or something else that doesn't store the
-- class directly on the instance
end
if not deserializer then
if class.__instanceDict then
-- assume MiddleClass
deserializer = deserialize_MiddleClass
elseif class.__baseclass then
-- assume SECL
deserializer = deserialize_SECL
elseif class.__index == class then
-- assume hump.class
deserializer = deserialize_humpclass
elseif class.__name__ then
-- assume Slither
deserializer = deserialize_Slither
elseif class.__base then
-- assume Moonscript class
deserializer = deserialize_Moonscript
else
error("no deserializer given for unsupported class library")
end
end
class_registry[name] = class
classkey_registry[name] = classkey
class_deserialize_registry[name] = deserializer
class_name_registry[class] = name
return class
end, unregisterClass = function(name)
class_name_registry[class_registry[name]] = nil
classkey_registry[name] = nil
class_deserialize_registry[name] = nil
class_registry[name] = nil
end, reserveBuffer = Buffer_prereserve, clearBuffer = Buffer_clear}

3
cases/_new.lua Normal file
View File

@ -0,0 +1,3 @@
-- write your own!
-- data to be tested, repetitions, number of tries
return {}, 10000, 3

7
cases/bigtable.lua Normal file
View File

@ -0,0 +1,7 @@
local t = {}
for i = 1, 2000 do
t[i] = 100
end
return t, 500, 5

7
cases/cthulhu.lua Normal file
View File

@ -0,0 +1,7 @@
local cthulhu = {{}, {}, {}}
cthulhu.fhtagn = cthulhu
cthulhu[1][cthulhu[2]] = cthulhu[3]
cthulhu[2][cthulhu[1]] = cthulhu[2]
cthulhu[3][cthulhu[3]] = cthulhu
return cthulhu, 10000, 3

7
cases/intkeys.lua Normal file
View File

@ -0,0 +1,7 @@
local t = {}
for i = 1, 200 do
t[math.random(1000)] = math.random(100)
end
return t, 30000, 5

6
cases/shared_table.lua Normal file
View File

@ -0,0 +1,6 @@
local t = {}
local x = {10, 50, 40, 30, 20}
for i = 1, 40 do
t[i] = x
end
return t, 10000, 3

4
conf.lua Normal file
View File

@ -0,0 +1,4 @@
function love.conf(t)
t.version = "0.10.0"
t.console = true
end

183
main.lua Normal file
View File

@ -0,0 +1,183 @@
local found_bitser, bitser = pcall(require, 'bitser')
local found_binser, binser = pcall(require, 'binser')
local found_ser, ser = pcall(require, 'ser')
local found_serpent, serpent = pcall(require, 'serpent')
local found_smallfolk, smallfolk = pcall(require, 'smallfolk')
local found_msgpack, msgpack = pcall(require, 'MessagePack')
local cases
local selected_case = 1
local sers = {}
local desers = {}
if found_bitser then
sers.bitser = bitser.dumps
desers.bitser = bitser.loads
bitser.reserveBuffer(1024 * 1024)
end
if found_binser then
sers.binser = binser.s
desers.binser = binser.d
end
if found_ser then
sers.ser = ser
desers.ser = loadstring
end
if found_serpent then
sers.serpent = serpent.dump
desers.serpent = loadstring
end
if found_smallfolk then
sers.smallfolk = smallfolk.dumps
desers.smallfolk = smallfolk.loads
end
if found_msgpack then
sers.msgpack = msgpack.pack
desers.msgpack = msgpack.unpack
end
local view_absolute = true
local resultname = "serialisation time in seconds"
function love.load()
cases = love.filesystem.getDirectoryItems("cases")
state = 'select_case'
love.graphics.setBackgroundColor(255, 230, 220)
love.graphics.setColor(40, 30, 0)
love.window.setTitle("Select a benchmark testcase")
end
function love.keypressed(key)
if state == 'select_case' then
if key == 'up' then
selected_case = (selected_case - 2) % #cases + 1
elseif key == 'down' then
selected_case = selected_case % #cases + 1
elseif key == 'return' then
state = 'calculate_results'
love.window.setTitle("Running benchmark...")
end
elseif state == 'results' then
if key == 'r' then
view_absolute = not view_absolute
elseif key == 'right' then
if results == results_ser then
results = results_deser
resultname = "deserialisation time in seconds"
elseif results == results_deser then
results = results_size
resultname = "size of output in bytes"
elseif results == results_size then
results = results_ser
resultname = "serialisation time in seconds"
end
elseif key == 'left' then
if results == results_ser then
results = results_size
resultname = "size of output in bytes"
elseif results == results_deser then
results = results_ser
resultname = "serialisation time in seconds"
elseif results == results_size then
results = results_deser
resultname = "deserialisation time in seconds"
end
elseif key == 'escape' then
state = 'select_case'
love.window.setTitle("Select a benchmark testcase")
end
end
end
function love.draw()
if state == 'select_case' then
for i, case in ipairs(cases) do
love.graphics.print(case, selected_case == i and 60 or 20, i * 20)
end
elseif state == 'calculate_results' then
love.graphics.print("Running benchmark...", 20, 20)
love.graphics.print("This may take a while", 20, 40)
state = 'calculate_results_2'
elseif state == 'calculate_results_2' then
local data, iters, tries = love.filesystem.load("cases/" .. cases[selected_case])()
results_ser = {}
results = results_ser
resultname = "serialisation time in seconds"
results_size = {}
results_deser = {}
for sername, serializer in pairs(sers) do
results_ser[sername] = math.huge
results_deser[sername] = math.huge
end
local outputs = {}
for try = 1, tries do
for sername, serializer in pairs(sers) do
local output
local success, diff = pcall(function()
local t = os.clock()
for i = 1, iters do
output = serializer(data)
end
return os.clock() - t
end)
if success and diff < results_ser[sername] then
results_ser[sername] = diff
end
if try == 1 then
outputs[sername] = output
results_size[sername] = output and #output or math.huge
end
end
end
for try = 1, tries do
for sername, deserializer in pairs(desers) do
local input = outputs[sername]
local success, diff = pcall(function()
local t = os.clock()
for i = 1, iters / 10 do
deserializer(input)
end
return os.clock() - t
end)
if success and diff < results_deser[sername] then
results_deser[sername] = diff
end
end
end
state = 'results'
love.window.setTitle("Results for " .. cases[selected_case])
elseif state == 'results' then
local results_min = math.huge
local results_max = -math.huge
for sername, result in pairs(results) do
if result < results_min then
results_min = result
end
if result > results_max and result < math.huge then
results_max = result
end
end
if view_absolute then results_min = 0 end
local i = 1
for sername, result in pairs(results) do
love.graphics.print(sername, 20, i * 20)
if result == math.huge then
love.graphics.setColor(220, 30, 0)
love.graphics.rectangle('fill', 100, i * 20, 780 - 100, 18)
love.graphics.setColor(40, 30, 0)
else
love.graphics.rectangle('fill', 100, i * 20, (780 - 100) * (result - results_min) / (results_max - results_min), 18)
end
i = i + 1
end
love.graphics.print(results_min, 100, i * 20)
love.graphics.print(results_max, 780 - love.graphics.getFont():getWidth(results_max), i * 20)
love.graphics.print(resultname .." (smaller is better; try left, right, R, escape)", 100, i * 20 + 20)
end
end

297
spec/bitser_spec.lua Normal file
View File

@ -0,0 +1,297 @@
local ffi = require 'ffi'
_G.love = {filesystem = {newFileData = function()
return {getPointer = function()
local buf = ffi.new("uint8_t[?]", #love.s)
ffi.copy(buf, love.s, #love.s)
return buf
end, getSize = function()
return #love.s
end}
end, write = function(_, s)
love.s = s
end}}
local bitser = require 'bitser'
local function serdeser(value)
return bitser.loads(bitser.dumps(value))
end
local function test_serdeser(value)
assert.are.same(serdeser(value), value)
end
describe("bitser", function()
it("serializes simple values", function()
test_serdeser(true)
test_serdeser(false)
test_serdeser(nil)
test_serdeser(1)
test_serdeser(-1)
test_serdeser(0)
test_serdeser(100000000)
test_serdeser(1.234)
test_serdeser(10 ^ 20)
test_serdeser(1/0)
test_serdeser(-1/0)
test_serdeser("")
test_serdeser("hullo")
test_serdeser([[this
is a longer string
such a long string
that it won't fit
in the "short string" representation
no it won't
listen to me
it won't]])
local nan = serdeser(0/0)
assert.is_not.equal(nan, nan)
end)
it("serializes simple tables", function()
test_serdeser({})
test_serdeser({10, 11, 12})
test_serdeser({foo = 10, bar = 99, [true] = false})
test_serdeser({[1000] = 9000})
test_serdeser({{}})
end)
it("serializes tables with tables as keys", function()
local thekey = {"Heyo"}
assert.are.same(thekey, (next(serdeser({[thekey] = 12}))))
end)
it("serializes cyclic tables", function()
local cthulhu = {{}, {}, {}}
cthulhu.fhtagn = cthulhu
--note: this does not test tables as keys because assert.are.same doesn't like that
cthulhu[1].cthulhu = cthulhu[3]
cthulhu[2].cthulhu = cthulhu[2]
cthulhu[3].cthulhu = cthulhu
test_serdeser(cthulhu)
end)
it("serializes resources", function()
local temp_resource = {}
bitser.register("temp_resource", temp_resource)
assert.are.equal(serdeser({this = temp_resource}).this, temp_resource)
bitser.unregister("temp_resource")
end)
it("serializes many resources", function()
local max = 1000
local t = {}
for i = 1, max do
bitser.register(tostring(i), i)
t[i] = i
end
test_serdeser(t)
for i = 1, max do
bitser.unregister(tostring(i))
end
end)
it("serializes deeply nested tables", function()
local max = 1000
local t = {}
for _ = 1, max do
t.t = {}
t = t.t
end
test_serdeser(t)
end)
it("serializes MiddleClass instances", function()
local class = require("middleclass")
local Horse = bitser.registerClass(class('Horse'))
function Horse:initialize(name)
self.name = name
self[1] = 'instance can be sequence'
end
local bojack = Horse('Bojack Horseman')
test_serdeser(bojack)
assert.is_true(serdeser(bojack):isInstanceOf(Horse))
bitser.unregisterClass('Horse')
end)
it("serializes SECL instances", function()
local class_mt = {}
function class_mt:__index(key)
return self.__baseclass[key]
end
local class = setmetatable({ __baseclass = {} }, class_mt)
function class:new(...)
local c = {}
c.__baseclass = self
setmetatable(c, getmetatable(self))
if c.init then
c:init(...)
end
return c
end
local Horse = bitser.registerClass('Horse', class:new())
function Horse:init(name)
self.name = name
self[1] = 'instance can be sequence'
end
local bojack = Horse:new('Bojack Horseman')
test_serdeser(bojack)
assert.are.equal(serdeser(bojack).__baseclass, Horse)
bitser.unregisterClass('Horse')
end)
it("serializes hump.class instances", function()
local class = require("class")
local Horse = bitser.registerClass('Horse', class{})
function Horse:init(name)
self.name = name
self[1] = 'instance can be sequence'
end
local bojack = Horse('Bojack Horseman')
test_serdeser(bojack)
assert.are.equal(getmetatable(serdeser(bojack)), Horse)
bitser.unregisterClass('Horse')
end)
it("serializes Slither instances", function()
local class = require("slither")
local Horse = class.private 'Horse' {
__attributes__ = {bitser.registerClass},
__init__ = function(self, name)
self.name = name
self[1] = 'instance can be sequence'
end
}
local bojack = Horse('Bojack Horseman')
test_serdeser(bojack)
assert.is_true(class.isinstance(serdeser(bojack), Horse))
bitser.unregisterClass('Horse')
end)
it("serializes Moonscript class instances", function()
local Horse
do
local _class_0
local _base_0 = {}
_base_0.__index = _base_0
_class_0 = setmetatable({
__init = function(self, name)
self.name = name
self[1] = 'instance can be sequence'
end,
__base = _base_0,
__name = "Horse"}, {
__index = _base_0,
__call = function(cls, ...)
local _self_0 = setmetatable({}, _base_0)
cls.__init(_self_0, ...)
return _self_0
end
})
_base_0.__class = _class_0
Horse = _class_0
end
assert.are.same(Horse.__name, "Horse") -- to shut coveralls up
bitser.registerClass(Horse)
local bojack = Horse('Bojack Horseman')
test_serdeser(bojack)
local new_bojack = serdeser(bojack)
assert.are.equal(new_bojack.__class, Horse)
bitser.unregisterClass('Horse')
end)
it("serializes custom class instances", function()
local Horse_mt = bitser.registerClass('Horse', {__index = {}}, nil, setmetatable)
local function Horse(name)
local self = {}
self.name = name
self[1] = 'instance can be sequence'
return setmetatable(self, Horse_mt)
end
local bojack = Horse('Bojack Horseman')
test_serdeser(bojack)
assert.are.equal(getmetatable(serdeser(bojack)), Horse_mt)
bitser.unregisterClass('Horse')
end)
it("serializes classes that repeat keys", function()
local my_mt = {"hi"}
local works = { foo = 'a', bar = {baz = 'b'}, }
local broken = { foo = 'a', bar = {foo = 'b'}, }
local more_broken = {
foo = 'a',
baz = {foo = 'b', bar = 'c'},
quz = {bar = 'd', bam = 'e'}
}
setmetatable(works, my_mt)
setmetatable(broken, my_mt)
setmetatable(more_broken, my_mt)
bitser.registerClass("Horse", my_mt, nil, setmetatable)
test_serdeser(works)
test_serdeser(broken)
test_serdeser(more_broken)
bitser.unregisterClass('Horse')
end)
it("serializes big data", function()
local text = "this is a lot of nonsense, please disregard, we need a lot of data to get past 4 KiB (114 characters should do it)"
local t = {}
for i = 1, 40 do
t[i] = text .. i -- no references allowed!
end
test_serdeser(t)
end)
it("serializes many references", function()
local max = 1000
local t = {}
local t2 = {}
for i = 1, max do
t.t = {}
t = t.t
t2[i] = t
end
test_serdeser({t, t2})
end)
it("serializes resources with long names", function()
local temp_resource = {}
bitser.register("temp_resource_or_whatever", temp_resource)
assert.are.equal(serdeser({this = temp_resource}).this, temp_resource)
bitser.unregister("temp_resource_or_whatever")
end)
it("serializes resources with the same name as serialized strings", function()
local temp_resource = {}
bitser.register('temp', temp_resource)
test_serdeser({temp='temp', {temp_resource}})
bitser.unregister('temp')
end)
it("serializes resources with the same long name as serialized strings", function()
local temp_resource = {}
bitser.register('temp_resource_or_whatever', temp_resource)
test_serdeser({temp_resource_or_whatever='temp_resource_or_whatever', {temp_resource}})
bitser.unregister('temp_resource_or_whatever')
end)
it("cannot serialize functions", function()
assert.has_error(function() bitser.dumps(function() end) end, "cannot serialize type function")
end)
it("cannot serialize unsupported class libraries without explicit deserializer", 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("\251") end, "unsupported serialized type 251")
assert.has_error(function() bitser.loads("\252") end, "unsupported serialized type 252")
assert.has_error(function() bitser.loads("\253") end, "unsupported serialized type 253")
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()
assert.are.same(bitser.loadData(ffi.new("uint8_t[4]", 195, 103, 118, 120), 4), "gvx")
end)
it("will not read past the end of the buffer", function()
assert.has_error(function() bitser.loadData(ffi.new("uint8_t[4]", 196, 103, 118, 120), 4) end)
end)
it("can clear the buffer", function()
bitser.clearBuffer()
end)
it("can write to new buffer after reading from read-only buffer", function()
test_serdeser("bitser")
bitser.loadData(ffi.new("uint8_t[4]", 195, 103, 118, 120), 4)
test_serdeser("bitser")
end)
it("it can dump and load LÖVE files", function()
local v = {value = "value"}
bitser.dumpLoveFile("some_file_name", v)
assert.are.same(v, bitser.loadLoveFile("some_file_name"))
end)
end)