Skip to content

Instantly share code, notes, and snippets.

@kylemanna
Created November 6, 2019 17:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kylemanna/03d280f9839cbd0cf0da8437ccd7767b to your computer and use it in GitHub Desktop.
Save kylemanna/03d280f9839cbd0cf0da8437ccd7767b to your computer and use it in GitHub Desktop.

Python Flatbuffer Object -> JSON

Crude proof of concept, criticism welcome.

Usage

$ flatc --python monster.fbs
$ python3 ./python-flatbuffer-to-json.py
FIXME: Equipped is a table
{"color": 0, "equipped": null, "hp": 300, "inventory": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "mana": 150, "name": "Orc", "path": [], "pos": {"x": 1.0, "y": 2.0, "z": 3.0}, "weapons": [{"damage": 3, "name": "Sword"}, {"damage": 5, "name": "Axe"}]}

Example JSON output for monster.fbs

./python-flatbuffer-to-json.py | jq -S .
FIXME: Equipped is a table
{
  "color": 0,
  "equipped": null,
  "hp": 300,
  "inventory": [
    0,
    1,
    2,
    3,
    4,
    5,
    6,
    7,
    8,
    9
  ],
  "mana": 150,
  "name": "Orc",
  "path": [],
  "pos": {
    "x": 1,
    "y": 2,
    "z": 3
  },
  "weapons": [
    {
      "damage": 3,
      "name": "Sword"
    },
    {
      "damage": 5,
      "name": "Axe"
    }
  ]
}
// Example IDL file for our monster's schema.
namespace MyGame.Sample;
enum Color:byte { Red = 0, Green, Blue = 2 }
union Equipment { Weapon } // Optionally add more tables.
struct Vec3 {
x:float;
y:float;
z:float;
}
table Monster {
pos:Vec3; // Struct.
mana:short = 150;
hp:short = 100;
name:string;
friendly:bool = false (deprecated);
inventory:[ubyte]; // Vector of scalars.
color:Color = Blue; // Enum.
weapons:[Weapon]; // Vector of tables.
equipped:Equipment; // Union.
path:[Vec3]; // Vector of structs.
}
table Weapon {
name:string;
damage:short;
}
root_type Monster;
#!/usr/bin/python3
# Flatbuffer code borrowed for flatbuffer example code.
import sys
import flatbuffers
import MyGame.Sample.Color
import MyGame.Sample.Equipment
import MyGame.Sample.Monster
import MyGame.Sample.Vec3
import MyGame.Sample.Weapon
# Example of how to use FlatBuffers to create and read binary buffers.
import inspect
import json
import re
#
# Useful for converting aribtrarily formatted keys to form_with_underscores
# Examples:
# * gm.OffsetScaledLogVariance -> gm_offset_scaled_log_variance
first_cap_re = re.compile('(.)(\.)?([A-Z][a-z]+)')
all_cap_re = re.compile('([a-z0-9])([A-Z])')
def camelcase_to_underscores(name):
s1 = first_cap_re.sub(r'\1_\3', name)
return all_cap_re.sub(r'\1_\2', s1).lower()
#
# Funny names get remembered, but probably bad idea.
def cerealize(obj):
if isinstance(obj, bytes):
return obj.decode()
elif isinstance(obj, (int, float, str, bool)):
return obj
d={}
# prune Flatbuffer methods that start with these prefixes
prune=('GetRootAs', 'Init')
members = [(name, value) for name, value in inspect.getmembers(obj) if inspect.ismethod(value) and not name.startswith(prune)]
for name, value in members:
args = inspect.getfullargspec(value).args
if len(args) == 2 and f"{name}Length" in dir(obj):
# Delete the length element from the list while iterating over it (dangerous?)
members[:] = [x for x in members if x[0] != f"{name}Length" and x[0] != f"{name}AsNumpy"]
length = getattr(obj, f"{name}Length")()
#print(f"vector {name} with length: {length}", file=sys.stderr)
out = [cerealize(value(i)) for i in range(length)]
elif len(args) > 1:
print(f"SKIP: {name} arg count too many method skipping...", file=sys.stderr)
# Consider adding whatever triggered this to the prune list
continue
elif f"{name}Type" in dir(obj):
# Delete the length element from the list while iterating over it (dangerous?)
members[:] = [x for x in members if x[0] != f"{name}Type"]
print(f"FIXME: {name} is a table", file=sys.stderr)
#d[name] = getattr(obj, f"{name}Type")(value())
out = None #'FIXME'
elif isinstance(value(), bytes):
out = value().decode()
elif isinstance(value(), (int, float, str, bool)):
out = value()
else:
out = cerealize(value())
name_out = camelcase_to_underscores(name)
d[name_out] = out
return d
def main():
builder = flatbuffers.Builder(0)
# Create some weapons for our Monster ('Sword' and 'Axe').
weapon_one = builder.CreateString('Sword')
weapon_two = builder.CreateString('Axe')
MyGame.Sample.Weapon.WeaponStart(builder)
MyGame.Sample.Weapon.WeaponAddName(builder, weapon_one)
MyGame.Sample.Weapon.WeaponAddDamage(builder, 3)
sword = MyGame.Sample.Weapon.WeaponEnd(builder)
MyGame.Sample.Weapon.WeaponStart(builder)
MyGame.Sample.Weapon.WeaponAddName(builder, weapon_two)
MyGame.Sample.Weapon.WeaponAddDamage(builder, 5)
axe = MyGame.Sample.Weapon.WeaponEnd(builder)
# Serialize the FlatBuffer data.
name = builder.CreateString('Orc')
MyGame.Sample.Monster.MonsterStartInventoryVector(builder, 10)
# Note: Since we prepend the bytes, this loop iterates in reverse order.
for i in reversed(list(range(0, 10))):
builder.PrependByte(i)
inv = builder.EndVector(10)
MyGame.Sample.Monster.MonsterStartWeaponsVector(builder, 2)
# Note: Since we prepend the data, prepend the weapons in reverse order.
builder.PrependUOffsetTRelative(axe)
builder.PrependUOffsetTRelative(sword)
weapons = builder.EndVector(2)
pos = MyGame.Sample.Vec3.CreateVec3(builder, 1.0, 2.0, 3.0)
MyGame.Sample.Monster.MonsterStart(builder)
MyGame.Sample.Monster.MonsterAddPos(builder, pos)
MyGame.Sample.Monster.MonsterAddHp(builder, 300)
MyGame.Sample.Monster.MonsterAddName(builder, name)
MyGame.Sample.Monster.MonsterAddInventory(builder, inv)
MyGame.Sample.Monster.MonsterAddColor(builder,
MyGame.Sample.Color.Color().Red)
MyGame.Sample.Monster.MonsterAddWeapons(builder, weapons)
MyGame.Sample.Monster.MonsterAddEquippedType(
builder, MyGame.Sample.Equipment.Equipment().Weapon)
MyGame.Sample.Monster.MonsterAddEquipped(builder, axe)
orc = MyGame.Sample.Monster.MonsterEnd(builder)
builder.Finish(orc)
# We now have a FlatBuffer that we could store on disk or send over a network.
# ...Saving to file or sending over a network code goes here...
# Instead, we are going to access this buffer right away (as if we just
# received it).
buf = builder.Output()
# Note: We use `0` for the offset here, since we got the data using the
# `builder.Output()` method. This simulates the data you would store/receive
# in your FlatBuffer. If you wanted to read from the `builder.Bytes` directly,
# you would need to pass in the offset of `builder.Head()`, as the builder
# actually constructs the buffer backwards.
monster = MyGame.Sample.Monster.Monster.GetRootAsMonster(buf, 0)
# Note: We did not set the `Mana` field explicitly, so we get a default value.
assert monster.Mana() == 150
assert monster.Hp() == 300
assert monster.Name() == b'Orc'
assert monster.Color() == MyGame.Sample.Color.Color().Red
assert monster.Pos().X() == 1.0
assert monster.Pos().Y() == 2.0
assert monster.Pos().Z() == 3.0
# Get and test the `inventory` FlatBuffer `vector`.
for i in range(monster.InventoryLength()):
assert monster.Inventory(i) == i
# Get and test the `weapons` FlatBuffer `vector` of `table`s.
expected_weapon_names = [b'Sword', b'Axe']
expected_weapon_damages = [3, 5]
for i in range(monster.WeaponsLength()):
assert monster.Weapons(i).Name() == expected_weapon_names[i]
assert monster.Weapons(i).Damage() == expected_weapon_damages[i]
# Get and test the `equipped` FlatBuffer `union`.
assert monster.EquippedType() == MyGame.Sample.Equipment.Equipment().Weapon
# An example of how you can appropriately convert the table depending on the
# FlatBuffer `union` type. You could add `elif` and `else` clauses to handle
# the other FlatBuffer `union` types for this field.
if monster.EquippedType() == MyGame.Sample.Equipment.Equipment().Weapon:
# `monster.Equipped()` returns a `flatbuffers.Table`, which can be used
# to initialize a `MyGame.Sample.Weapon.Weapon()`, in this case.
union_weapon = MyGame.Sample.Weapon.Weapon()
union_weapon.Init(monster.Equipped().Bytes, monster.Equipped().Pos)
assert union_weapon.Name() == b"Axe"
assert union_weapon.Damage() == 5
#print('The FlatBuffer was successfully created and verified!')
d=cerealize(monster)
#print(d)
print(json.dumps(d))
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment