flatc-wasm
https://digitalarsenal.github.io/flatbuffers/
FlatBuffers compiler as WebAssembly — run flatc in Node.js or the browser with zero native dependencies
Features
| Schema | Add, remove, list, and export FlatBuffer schemas |
| Conversion | JSON ↔ FlatBuffer binary with auto-detection |
| Code Gen | 13 languages: C++, TypeScript, Go, Rust, Python, Java, C#, Swift, Kotlin, Dart, PHP, Lua, Nim |
| JSON Schema | Import JSON Schema as input, export FlatBuffers to JSON Schema |
| Encryption | Per-field AES-256-CTR encryption with (encrypted) attribute |
| Streaming | Process large data with streaming APIs |
| Cross-Lang | Same WASM runs in Node.js, Go, Python, Rust, Java, C#, Swift |
| Runtimes | Embedded language runtimes for 11 languages, retrievable as JSON or ZIP |
| Zero Deps | Self-contained with inlined WASM binaries |
Installation
npm install flatc-wasm
Requirements
| Node.js | 18.0.0 or higher |
| Chrome | 57+ |
| Firefox | 52+ |
| Safari | 11+ |
| Edge | 79+ |
Dependencies:
- No native dependencies required (self-contained WASM)
- Optional:
hd-wallet-wasm (included) for HD key derivation
For building from source:
- Emscripten SDK (emsdk)
- CMake 3.16+
- Python 3.8+
Table of Contents
Quick Start
The recommended way to use flatc-wasm is through the FlatcRunner class, which provides a clean CLI-style interface:
import { FlatcRunner } from 'flatc-wasm';
const flatc = await FlatcRunner.init();
console.log(flatc.version());
const schemaInput = {
entry: '/schemas/monster.fbs',
files: {
'/schemas/monster.fbs': `
namespace Game;
table Monster {
name: string;
hp: short = 100;
}
root_type Monster;
`
}
};
const binary = flatc.generateBinary(schemaInput, '{"name": "Orc", "hp": 150}');
console.log('Binary size:', binary.length, 'bytes');
const json = flatc.generateJSON(schemaInput, {
path: '/data/monster.bin',
data: binary
});
console.log('JSON:', json);
const code = flatc.generateCode(schemaInput, 'ts');
console.log('Generated files:', Object.keys(code));
Alternative: Low-Level Module API
For advanced use cases, you can also use the raw WASM module directly:
import createFlatcWasm from 'flatc-wasm';
const flatc = await createFlatcWasm();
console.log('FlatBuffers version:', flatc.getVersion());
const handle = flatc.createSchema('monster.fbs', schema);
console.log('Schema ID:', handle.id());
FlatcRunner API
The FlatcRunner class provides a high-level, type-safe API for all flatc operations. It wraps the flatc CLI with a virtual filesystem, making it easy to use in Node.js and browser environments.
Initialization
import { FlatcRunner } from 'flatc-wasm';
const flatc = await FlatcRunner.init();
const flatc = await FlatcRunner.init({
print: (text) => console.log('[flatc]', text),
printErr: (text) => console.error('[flatc]', text),
});
console.log(flatc.version());
console.log(flatc.help());
Schema Input Format
All operations use a schema input tree that represents virtual files:
const simpleSchema = {
entry: '/schema.fbs',
files: {
'/schema.fbs': `
table Message { text: string; }
root_type Message;
`
}
};
const multiFileSchema = {
entry: '/schemas/game.fbs',
files: {
'/schemas/game.fbs': `
include "common.fbs";
namespace Game;
table Player {
id: uint64;
position: Common.Vec3;
name: string;
}
root_type Player;
`,
'/schemas/common.fbs': `
namespace Common;
struct Vec3 {
x: float;
y: float;
z: float;
}
`
}
};
Binary Generation (JSON → FlatBuffer)
Convert JSON data to FlatBuffer binary format:
const binary = flatc.generateBinary(schemaInput, jsonData, {
unknownJson: true,
strictJson: false,
});
const schema = {
entry: '/player.fbs',
files: {
'/player.fbs': `
table Player { name: string; score: int; }
root_type Player;
`
}
};
const json = JSON.stringify({ name: 'Alice', score: 100 });
const binary = flatc.generateBinary(schema, json);
console.log('Binary size:', binary.length, 'bytes');
JSON Generation (FlatBuffer → JSON)
Convert FlatBuffer binary back to JSON:
const json = flatc.generateJSON(schemaInput, {
path: '/data/input.bin',
data: binaryData
}, {
strictJson: true,
rawBinary: true,
defaultsJson: false,
encoding: 'utf8',
});
const originalJson = '{"name": "Bob", "score": 250}';
const binary = flatc.generateBinary(schema, originalJson);
const recoveredJson = flatc.generateJSON(schema, {
path: '/player.bin',
data: binary
});
console.log(JSON.parse(recoveredJson));
Code Generation
Generate source code for any supported language:
const files = flatc.generateCode(schemaInput, language, options);
Supported Languages
| C++ | cpp | .h |
| C# | csharp | .cs |
| Dart | dart | .dart |
| Go | go | .go |
| Java | java | .java |
| Kotlin | kotlin | .kt |
| Kotlin KMP | kotlin-kmp | .kt |
| Lobster | lobster | .lobster |
| Lua | lua | .lua |
| Nim | nim | .nim |
| PHP | php | .php |
| Python | python | .py |
| Rust | rust | .rs |
| Swift | swift | .swift |
| TypeScript | ts | .ts |
| JSON | json | .json |
| JSON Schema | jsonschema | .schema.json |
Code Generation Options
const files = flatc.generateCode(schemaInput, 'cpp', {
genObjectApi: true,
genOnefile: true,
genMutable: true,
genCompare: true,
genNameStrings: true,
reflectNames: true,
reflectTypes: true,
genJsonEmit: true,
noIncludes: true,
keepPrefix: true,
noWarnings: true,
genAll: true,
pythonTyping: true,
tsFlexBuffers: true,
tsNoImportExt: true,
goModule: 'mymodule',
goPackagePrefix: 'pkg'
});
for (const [filename, content] of Object.entries(files)) {
console.log(`Generated: ${filename} (${content.length} bytes)`);
}
Code Generation Examples
const tsFiles = flatc.generateCode(schema, 'ts', { genObjectApi: true });
const pyFiles = flatc.generateCode(schema, 'python', { pythonTyping: true });
const rsFiles = flatc.generateCode(schema, 'rust');
const cppFiles = flatc.generateCode(schema, 'cpp', {
genObjectApi: true,
genMutable: true,
genCompare: true,
});
JSON Schema Support
Export FlatBuffer Schema to JSON Schema
const jsonSchema = flatc.generateJsonSchema(schemaInput);
const parsed = JSON.parse(jsonSchema);
console.log(parsed.$schema);
Import JSON Schema
You can use JSON Schema files as input to FlatcRunner:
const jsonSchemaInput = {
entry: '/person.schema.json',
files: {
'/person.schema.json': JSON.stringify({
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" }
},
"required": ["name"]
})
}
};
const code = flatc.generateCode(jsonSchemaInput, 'typescript');
Virtual Filesystem Operations
The FlatcRunner provides direct access to the Emscripten virtual filesystem:
flatc.mountFile('/schemas/types.fbs', schemaContent);
flatc.mountFiles([
{ path: '/schemas/a.fbs', data: 'table A { x: int; }' },
{ path: '/schemas/b.fbs', data: 'table B { y: int; }' },
{ path: '/data/input.json', data: new Uint8Array([...]) },
]);
const content = flatc.readFile('/schemas/a.fbs', { encoding: 'utf8' });
const binary = flatc.readFile('/data/output.bin');
const files = flatc.readdir('/schemas');
const allFiles = flatc.listAllFiles('/schemas');
flatc.unlink('/data/input.json');
flatc.rmdir('/data');
Low-Level CLI Access
For advanced use cases, you can run any flatc command directly:
const result = flatc.runCommand(['--help']);
console.log(result.code);
console.log(result.stdout);
console.log(result.stderr);
flatc.mountFile('/schema.fbs', schemaContent);
const result = flatc.runCommand([
'--binary',
'--schema',
'-o', '/output',
'/schema.fbs'
]);
if (result.code === 0) {
const bfbs = flatc.readFile('/output/schema.bfbs');
}
flatc.runCommand([
'--cpp',
'--gen-object-api',
'--gen-mutable',
'--scoped-enums',
'-o', '/output',
'/schema.fbs'
]);
Error Handling
All FlatcRunner methods throw errors with descriptive messages:
try {
const binary = flatc.generateBinary(schema, '{ invalid json }');
} catch (error) {
console.error('Conversion failed:', error.message);
}
try {
const code = flatc.generateCode(schema, 'invalid-language');
} catch (error) {
console.error('Code generation failed:', error.message);
}
const result = flatc.runCommand(['--invalid-flag']);
if (result.code !== 0 || result.stderr.includes('error:')) {
console.error('Command failed:', result.stderr);
}
Complete Example: Build Pipeline
import { FlatcRunner } from 'flatc-wasm';
import { writeFileSync } from 'fs';
async function buildSchemas() {
const flatc = await FlatcRunner.init();
const schema = {
entry: '/schemas/game.fbs',
files: {
'/schemas/game.fbs': `
namespace Game;
enum ItemType : byte { Weapon, Armor, Potion }
table Item {
id: uint32;
name: string (required);
type: ItemType;
value: int = 0;
}
table Inventory {
items: [Item];
gold: int;
}
root_type Inventory;
`
}
};
const languages = ['typescript', 'python', 'rust', 'go'];
for (const lang of languages) {
const files = flatc.generateCode(schema, lang, {
genObjectApi: true,
});
for (const [filename, content] of Object.entries(files)) {
const outPath = `./generated/${lang}/${filename}`;
writeFileSync(outPath, content);
console.log(`Generated: ${outPath}`);
}
}
const jsonSchema = flatc.generateJsonSchema(schema);
writeFileSync('./docs/inventory.schema.json', jsonSchema);
const testData = {
items: [
{ id: 1, name: 'Sword', type: 'Weapon', value: 100 },
{ id: 2, name: 'Shield', type: 'Armor', value: 50 },
],
gold: 500
};
const binary = flatc.generateBinary(schema, JSON.stringify(testData));
console.log(`Binary size: ${binary.length} bytes`);
const recovered = flatc.generateJSON(schema, {
path: '/inventory.bin',
data: binary
});
console.log('Round-trip successful:', JSON.parse(recovered));
}
buildSchemas().catch(console.error);
Low-Level API Reference
Module Initialization
ESM (recommended)
import createFlatcWasm from 'flatc-wasm';
const flatc = await createFlatcWasm();
CommonJS
const createFlatcWasm = require('flatc-wasm');
const flatc = await createFlatcWasm();
Embind High-Level API
The module provides a high-level API via Emscripten's Embind:
const flatc = await createFlatcWasm();
flatc.getVersion();
flatc.getLastError();
const handle = flatc.createSchema(name, source);
handle.id();
handle.name();
handle.valid();
handle.release();
const handles = flatc.getAllSchemas();
Schema Management
Adding Schemas
const handle = flatc.createSchema('monster.fbs', `
namespace Game;
table Monster {
name: string;
hp: int = 100;
}
root_type Monster;
`);
if (handle.valid()) {
console.log('Schema added with ID:', handle.id());
}
Adding JSON Schema
JSON Schema files are automatically detected and converted:
const handle = flatc.createSchema('person.schema.json', `{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" }
},
"required": ["name"]
}`);
Listing and Removing Schemas
const schemas = flatc.getAllSchemas();
for (const schema of schemas) {
console.log(`ID: ${schema.id()}, Name: ${schema.name()}`);
}
handle.release();
console.log('Valid after release:', handle.valid());
JSON/Binary Conversion
For conversions, use the low-level C API which provides direct memory access:
Helper Functions
const encoder = new TextEncoder();
const decoder = new TextDecoder();
function writeString(str) {
const bytes = encoder.encode(str);
const ptr = flatc._malloc(bytes.length);
flatc.HEAPU8.set(bytes, ptr);
return [ptr, bytes.length];
}
function writeBytes(data) {
const ptr = flatc._malloc(data.length);
flatc.HEAPU8.set(data, ptr);
return ptr;
}
function getLastError() {
const ptr = flatc._wasm_get_last_error();
return ptr ? flatc.UTF8ToString(ptr) : 'Unknown error';
}
JSON to Binary
const schemaId = handle.id();
const json = '{"name": "Goblin", "hp": 50}';
const [jsonPtr, jsonLen] = writeString(json);
const outLenPtr = flatc._malloc(4);
try {
const resultPtr = flatc._wasm_json_to_binary(schemaId, jsonPtr, jsonLen, outLenPtr);
if (resultPtr === 0) {
throw new Error(getLastError());
}
const len = flatc.getValue(outLenPtr, 'i32');
const binary = flatc.HEAPU8.slice(resultPtr, resultPtr + len);
console.log('Binary size:', binary.length, 'bytes');
} finally {
flatc._free(jsonPtr);
flatc._free(outLenPtr);
}
Binary to JSON
const binPtr = writeBytes(binary);
const outLenPtr = flatc._malloc(4);
try {
const resultPtr = flatc._wasm_binary_to_json(schemaId, binPtr, binary.length, outLenPtr);
if (resultPtr === 0) {
throw new Error(getLastError());
}
const len = flatc.getValue(outLenPtr, 'i32');
const jsonBytes = flatc.HEAPU8.slice(resultPtr, resultPtr + len);
const json = decoder.decode(jsonBytes);
console.log('JSON:', json);
} finally {
flatc._free(binPtr);
flatc._free(outLenPtr);
}
Auto-Detect Format
const dataPtr = writeBytes(data);
const format = flatc._wasm_detect_format(dataPtr, data.length);
flatc._free(dataPtr);
console.log('Format:', format === 0 ? 'JSON' : format === 1 ? 'Binary' : 'Unknown');
Auto-Convert
const dataPtr = writeBytes(data);
const outPtrPtr = flatc._malloc(4);
const outLenPtr = flatc._malloc(4);
try {
const format = flatc._wasm_convert_auto(schemaId, dataPtr, data.length, outPtrPtr, outLenPtr);
if (format < 0) {
throw new Error(getLastError());
}
const outPtr = flatc.getValue(outPtrPtr, 'i32');
const outLen = flatc.getValue(outLenPtr, 'i32');
const result = flatc.HEAPU8.slice(outPtr, outPtr + outLen);
if (format === 0) {
console.log('Converted JSON to binary:', result.length, 'bytes');
} else {
console.log('Converted binary to JSON:', decoder.decode(result));
}
} finally {
flatc._free(dataPtr);
flatc._free(outPtrPtr);
flatc._free(outLenPtr);
}
Code Generation
Generate code for any supported language:
const Language = {
CPP: 0,
CSharp: 1,
Dart: 2,
Go: 3,
Java: 4,
Kotlin: 5,
Python: 6,
Rust: 7,
Swift: 8,
TypeScript: 9,
PHP: 10,
JSONSchema: 11,
FBS: 12,
};
const outLenPtr = flatc._malloc(4);
try {
const resultPtr = flatc._wasm_generate_code(schemaId, Language.TypeScript, outLenPtr);
if (resultPtr === 0) {
throw new Error(getLastError());
}
const len = flatc.getValue(outLenPtr, 'i32');
const codeBytes = flatc.HEAPU8.slice(resultPtr, resultPtr + len);
const code = decoder.decode(codeBytes);
console.log(code);
} finally {
flatc._free(outLenPtr);
}
Get Language ID by Name
const [namePtr, nameLen] = writeString('typescript');
const langId = flatc._wasm_get_language_id(namePtr);
flatc._free(namePtr);
console.log('TypeScript ID:', langId);
List Supported Languages
const languages = flatc._wasm_get_supported_languages();
console.log(flatc.UTF8ToString(languages));
Streaming API
For processing large data without multiple JavaScript/WASM boundary crossings:
Stream Buffer Operations
flatc._wasm_stream_reset();
const chunk1 = encoder.encode('{"name":');
const chunk2 = encoder.encode('"Dragon", "hp": 500}');
let ptr = flatc._wasm_stream_prepare(chunk1.length);
flatc.HEAPU8.set(chunk1, ptr);
flatc._wasm_stream_commit(chunk1.length);
ptr = flatc._wasm_stream_prepare(chunk2.length);
flatc.HEAPU8.set(chunk2, ptr);
flatc._wasm_stream_commit(chunk2.length);
console.log('Stream size:', flatc._wasm_stream_size());
const outPtrPtr = flatc._malloc(4);
const outLenPtr = flatc._malloc(4);
const format = flatc._wasm_stream_convert(schemaId, outPtrPtr, outLenPtr);
if (format >= 0) {
const outPtr = flatc.getValue(outPtrPtr, 'i32');
const outLen = flatc.getValue(outLenPtr, 'i32');
const result = flatc.HEAPU8.slice(outPtr, outPtr + outLen);
console.log('Converted:', result.length, 'bytes');
}
flatc._free(outPtrPtr);
flatc._free(outLenPtr);
Add Schema via Streaming
flatc._wasm_stream_reset();
for (const chunk of schemaChunks) {
const ptr = flatc._wasm_stream_prepare(chunk.length);
flatc.HEAPU8.set(chunk, ptr);
flatc._wasm_stream_commit(chunk.length);
}
const [namePtr, nameLen] = writeString('large_schema.fbs');
const schemaId = flatc._wasm_stream_add_schema(namePtr, nameLen);
flatc._free(namePtr);
if (schemaId < 0) {
console.error('Failed:', getLastError());
}
Low-Level C API
Complete list of exported C functions:
Utility Functions
_wasm_get_version() | Get FlatBuffers version string |
_wasm_get_last_error() | Get last error message |
_wasm_clear_error() | Clear error state |
Memory Management
_wasm_malloc(size) | Allocate memory |
_wasm_free(ptr) | Free memory |
_wasm_realloc(ptr, size) | Reallocate memory |
_malloc(size) | Standard malloc |
_free(ptr) | Standard free |
Schema Management
_wasm_schema_add(name, nameLen, src, srcLen) | Add schema, returns ID or -1 |
_wasm_schema_remove(id) | Remove schema by ID |
_wasm_schema_count() | Get number of loaded schemas |
_wasm_schema_list(outIds, maxCount) | List schema IDs |
_wasm_schema_get_name(id) | Get schema name by ID |
_wasm_schema_export(id, format, outLen) | Export schema (0=FBS, 1=JSON Schema) |
Conversion Functions
_wasm_json_to_binary(schemaId, json, jsonLen, outLen) | JSON to FlatBuffer |
_wasm_binary_to_json(schemaId, bin, binLen, outLen) | FlatBuffer to JSON |
_wasm_convert_auto(schemaId, data, dataLen, outPtr, outLen) | Auto-detect and convert |
_wasm_detect_format(data, dataLen) | Detect format (0=JSON, 1=Binary, -1=Unknown) |
Output Buffer Management
_wasm_get_output_ptr() | Get output buffer pointer |
_wasm_get_output_size() | Get output buffer size |
_wasm_reserve_output(capacity) | Pre-allocate output buffer |
_wasm_clear_output() | Clear output buffer |
Stream Buffer Management
_wasm_stream_reset() | Clear stream buffer |
_wasm_stream_prepare(bytes) | Prepare buffer for writing, returns pointer |
_wasm_stream_commit(bytes) | Confirm bytes written |
_wasm_stream_size() | Get current stream size |
_wasm_stream_data() | Get stream buffer pointer |
_wasm_stream_convert(schemaId, outPtr, outLen) | Convert stream buffer |
_wasm_stream_add_schema(name, nameLen) | Add schema from stream buffer |
Code Generation
_wasm_generate_code(schemaId, langId, outLen) | Generate code |
_wasm_get_supported_languages() | Get comma-separated language list |
_wasm_get_language_id(name) | Get language ID from name |
Browser Usage
ES Module
<script type="module">
import createFlatcWasm from 'https://unpkg.com/flatc-wasm/dist/flatc-wasm.js';
async function main() {
const flatc = await createFlatcWasm();
console.log('Version:', flatc.getVersion());
const handle = flatc.createSchema('person.fbs', `
table Person {
name: string;
age: int;
}
root_type Person;
`);
const encoder = new TextEncoder();
const json = '{"name": "Alice", "age": 30}';
const jsonBytes = encoder.encode(json);
const jsonPtr = flatc._malloc(jsonBytes.length);
flatc.HEAPU8.set(jsonBytes, jsonPtr);
const outLenPtr = flatc._malloc(4);
const resultPtr = flatc._wasm_json_to_binary(
handle.id(), jsonPtr, jsonBytes.length, outLenPtr
);
if (resultPtr) {
const len = flatc.getValue(outLenPtr, 'i32');
const binary = flatc.HEAPU8.slice(resultPtr, resultPtr + len);
console.log('Binary size:', binary.length);
const blob = new Blob([binary], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'person.bin';
a.click();
}
flatc._free(jsonPtr);
flatc._free(outLenPtr);
}
main();
</script>
With Bundlers (Webpack, Vite, etc.)
import createFlatcWasm from 'flatc-wasm';
const flatc = await createFlatcWasm();
Streaming Server
For high-throughput scenarios, use the streaming CLI server:
Start Server
npx flatc-wasm --tcp 9876
npx flatc-wasm --socket /tmp/flatc.sock
npx flatc-wasm --daemon
JSON-RPC Protocol
Send JSON-RPC 2.0 requests (one per line):
echo '{"jsonrpc":"2.0","id":1,"method":"version"}' | nc localhost 9876
echo '{"jsonrpc":"2.0","id":2,"method":"addSchema","params":{"name":"monster.fbs","source":"table Monster { name:string; } root_type Monster;"}}' | nc localhost 9876
echo '{"jsonrpc":"2.0","id":3,"method":"jsonToBinary","params":{"schema":"monster.fbs","json":"{\"name\":\"Orc\"}"}}' | nc localhost 9876
echo '{"jsonrpc":"2.0","id":4,"method":"generateCode","params":{"schema":"monster.fbs","language":"typescript"}}' | nc localhost 9876
Available RPC Methods
version | - | Get FlatBuffers version |
addSchema | name, source | Add schema from string |
addSchemaFile | path | Add schema from file path |
removeSchema | name | Remove schema by name |
listSchemas | - | List loaded schema names |
jsonToBinary | schema, json | Convert JSON to binary (base64) |
binaryToJson | schema, binary | Convert binary (base64) to JSON |
convert | schema, data | Auto-detect and convert |
generateCode | schema, language | Generate code for language |
ping | - | Health check |
stats | - | Server statistics |
Folder Watch Mode
Auto-convert files as they appear:
npx flatc-wasm --watch ./json_input --output ./bin_output --schema monster.fbs
npx flatc-wasm --watch ./bin_input --output ./json_output --schema monster.fbs --to-json
Pipe Mode
Single-shot conversion via pipes:
echo '{"name":"Orc","hp":100}' | npx flatc-wasm --schema monster.fbs --to-binary > monster.bin
cat monster.bin | npx flatc-wasm --schema monster.fbs --to-json
npx flatc-wasm --schema monster.fbs --generate typescript > monster.ts
Examples
Complete Conversion Example
import createFlatcWasm from 'flatc-wasm';
async function example() {
const flatc = await createFlatcWasm();
const encoder = new TextEncoder();
const decoder = new TextDecoder();
function writeString(str) {
const bytes = encoder.encode(str);
const ptr = flatc._malloc(bytes.length);
flatc.HEAPU8.set(bytes, ptr);
return [ptr, bytes.length];
}
const schema = `
namespace RPG;
enum Class : byte { Warrior, Mage, Rogue }
struct Vec3 {
x: float;
y: float;
z: float;
}
table Weapon {
name: string;
damage: int;
}
table Character {
name: string (required);
class: Class = Warrior;
level: int = 1;
position: Vec3;
weapons: [Weapon];
}
root_type Character;
`;
const [namePtr, nameLen] = writeString('rpg.fbs');
const [srcPtr, srcLen] = writeString(schema);
const schemaId = flatc._wasm_schema_add(namePtr, nameLen, srcPtr, srcLen);
flatc._free(namePtr);
flatc._free(srcPtr);
if (schemaId < 0) {
const errPtr = flatc._wasm_get_last_error();
throw new Error(flatc.UTF8ToString(errPtr));
}
console.log('Schema ID:', schemaId);
const characterJson = JSON.stringify({
name: "Aragorn",
class: "Warrior",
level: 87,
position: { x: 100.5, y: 50.0, z: 25.3 },
weapons: [
{ name: "Anduril", damage: 150 },
{ name: "Dagger", damage: 30 }
]
});
const [jsonPtr, jsonLen] = writeString(characterJson);
const outLenPtr = flatc._malloc(4);
const binPtr = flatc._wasm_json_to_binary(schemaId, jsonPtr, jsonLen, outLenPtr);
flatc._free(jsonPtr);
if (binPtr === 0) {
flatc._free(outLenPtr);
const errPtr = flatc._wasm_get_last_error();
throw new Error(flatc.UTF8ToString(errPtr));
}
const binLen = flatc.getValue(outLenPtr, 'i32');
const binary = flatc.HEAPU8.slice(binPtr, binPtr + binLen);
flatc._free(outLenPtr);
console.log('Binary size:', binary.length, 'bytes');
console.log('Compression ratio:', (characterJson.length / binary.length).toFixed(2) + 'x');
const bin2Ptr = flatc._malloc(binary.length);
flatc.HEAPU8.set(binary, bin2Ptr);
const outLen2Ptr = flatc._malloc(4);
const jsonOutPtr = flatc._wasm_binary_to_json(schemaId, bin2Ptr, binary.length, outLen2Ptr);
flatc._free(bin2Ptr);
if (jsonOutPtr === 0) {
flatc._free(outLen2Ptr);
const errPtr = flatc._wasm_get_last_error();
throw new Error(flatc.UTF8ToString(errPtr));
}
const jsonOutLen = flatc.getValue(outLen2Ptr, 'i32');
const jsonBytes = flatc.HEAPU8.slice(jsonOutPtr, jsonOutPtr + jsonOutLen);
const jsonOut = decoder.decode(jsonBytes);
flatc._free(outLen2Ptr);
console.log('Round-trip JSON:', jsonOut);
const codeLenPtr = flatc._malloc(4);
const codePtr = flatc._wasm_generate_code(schemaId, 9, codeLenPtr);
if (codePtr) {
const codeLen = flatc.getValue(codeLenPtr, 'i32');
const codeBytes = flatc.HEAPU8.slice(codePtr, codePtr + codeLen);
const code = decoder.decode(codeBytes);
console.log('Generated TypeScript:\n', code.substring(0, 500) + '...');
}
flatc._free(codeLenPtr);
flatc._wasm_schema_remove(schemaId);
}
example().catch(console.error);
Wrapper Class Example
import createFlatcWasm from 'flatc-wasm';
class FlatBuffersCompiler {
constructor(module) {
this.module = module;
this.encoder = new TextEncoder();
this.decoder = new TextDecoder();
this.schemas = new Map();
}
static async create() {
const module = await createFlatcWasm();
return new FlatBuffersCompiler(module);
}
getVersion() {
return this.module.getVersion();
}
addSchema(name, source) {
const [namePtr, nameLen] = this._writeString(name);
const [srcPtr, srcLen] = this._writeString(source);
try {
const id = this.module._wasm_schema_add(namePtr, nameLen, srcPtr, srcLen);
if (id < 0) throw new Error(this._getLastError());
this.schemas.set(name, id);
return id;
} finally {
this.module._free(namePtr);
this.module._free(srcPtr);
}
}
removeSchema(name) {
const id = this.schemas.get(name);
if (id === undefined) throw new Error(`Schema '${name}' not found`);
this.module._wasm_schema_remove(id);
this.schemas.delete(name);
}
jsonToBinary(schemaName, json) {
const id = this._getSchemaId(schemaName);
const [jsonPtr, jsonLen] = this._writeString(json);
const outLenPtr = this.module._malloc(4);
try {
const ptr = this.module._wasm_json_to_binary(id, jsonPtr, jsonLen, outLenPtr);
if (!ptr) throw new Error(this._getLastError());
const len = this.module.getValue(outLenPtr, 'i32');
return this.module.HEAPU8.slice(ptr, ptr + len);
} finally {
this.module._free(jsonPtr);
this.module._free(outLenPtr);
}
}
binaryToJson(schemaName, binary) {
const id = this._getSchemaId(schemaName);
const binPtr = this._writeBytes(binary);
const outLenPtr = this.module._malloc(4);
try {
const ptr = this.module._wasm_binary_to_json(id, binPtr, binary.length, outLenPtr);
if (!ptr) throw new Error(this._getLastError());
const len = this.module.getValue(outLenPtr, 'i32');
return this.decoder.decode(this.module.HEAPU8.slice(ptr, ptr + len));
} finally {
this.module._free(binPtr);
this.module._free(outLenPtr);
}
}
generateCode(schemaName, language) {
const id = this._getSchemaId(schemaName);
const langId = typeof language === 'number' ? language : this._getLanguageId(language);
const outLenPtr = this.module._malloc(4);
try {
const ptr = this.module._wasm_generate_code(id, langId, outLenPtr);
if (!ptr) throw new Error(this._getLastError());
const len = this.module.getValue(outLenPtr, 'i32');
return this.decoder.decode(this.module.HEAPU8.slice(ptr, ptr + len));
} finally {
this.module._free(outLenPtr);
}
}
_writeString(str) {
const bytes = this.encoder.encode(str);
const ptr = this.module._malloc(bytes.length);
this.module.HEAPU8.set(bytes, ptr);
return [ptr, bytes.length];
}
_writeBytes(data) {
const ptr = this.module._malloc(data.length);
this.module.HEAPU8.set(data, ptr);
return ptr;
}
_getSchemaId(name) {
const id = this.schemas.get(name);
if (id === undefined) throw new Error(`Schema '${name}' not found`);
return id;
}
_getLastError() {
const ptr = this.module._wasm_get_last_error();
return ptr ? this.module.UTF8ToString(ptr) : 'Unknown error';
}
_getLanguageId(name) {
const map = {
cpp: 0, 'c++': 0, csharp: 1, 'c#': 1, dart: 2, go: 3,
java: 4, kotlin: 5, python: 6, rust: 7, swift: 8,
typescript: 9, ts: 9, php: 10, jsonschema: 11, fbs: 12
};
const id = map[name.toLowerCase()];
if (id === undefined) throw new Error(`Unknown language: ${name}`);
return id;
}
}
const compiler = await FlatBuffersCompiler.create();
compiler.addSchema('game.fbs', 'table Player { name: string; } root_type Player;');
const binary = compiler.jsonToBinary('game.fbs', '{"name": "Hero"}');
const json = compiler.binaryToJson('game.fbs', binary);
const tsCode = compiler.generateCode('game.fbs', 'typescript');
Building from Source
git clone https://github.com/google/flatbuffers.git
cd flatbuffers
cmake -B build/wasm -S . -DFLATBUFFERS_BUILD_WASM=ON
cmake --build build/wasm --target flatc_wasm_npm
ls wasm/dist/
cd wasm && npm test
CMake Targets
Demo/Webserver Targets (no Emscripten required)
These targets run the interactive demo webserver using pre-built WASM modules:
cmake -B build -S .
cmake --build build --target wasm_demo
cmake --build build --target wasm_demo_build
wasm_demo | Start development webserver at http://localhost:3000 |
wasm_demo_build | Build demo for production (outputs to wasm/docs/dist/) |
WASM Build Targets (requires Emscripten)
These targets build the WASM modules from source:
cmake -B build -S . -DFLATBUFFERS_BUILD_WASM=ON
cmake --build build --target wasm_build
cmake --build build --target wasm_build_and_serve
wasm_build | Build all WASM modules (flatc_wasm + flatc_wasm_wasi) |
wasm_build_and_serve | Build WASM modules then start development webserver |
flatc_wasm | Build main WASM module (separate .js and .wasm files) |
flatc_wasm_inline | Build single .js file with inlined WASM |
flatc_wasm_npm | Build NPM package (uses inline version) |
flatc_wasm_wasi | Build WASI standalone encryption module |
Test Targets
flatc_wasm_test | Run basic WASM tests |
flatc_wasm_test_all | Run comprehensive test suite |
flatc_wasm_test_parity | Run WASM vs native parity tests |
flatc_wasm_benchmark | Run performance benchmarks |
Browser Example Targets
browser_wallet_serve | Start crypto wallet demo (port 3000) |
browser_wallet_build | Build wallet demo for production |
browser_examples | Start all browser demos |
TypeScript Support
Full TypeScript definitions are included:
import createFlatcWasm from 'flatc-wasm';
import type { FlatcWasm, SchemaFormat, Language, DataFormat } from 'flatc-wasm';
const flatc: FlatcWasm = await createFlatcWasm();
const version: string = flatc.getVersion();
const handle = flatc.createSchema('test.fbs', schema);
const isValid: boolean = handle.valid();
Aligned Binary Format
The aligned binary format provides zero-overhead, fixed-size structs from FlatBuffers schemas, optimized for WASM/native interop and shared memory scenarios.
Why Use Aligned Format?
| Variable-size with vtables | Fixed-size structs |
| Requires deserialization | Zero-copy TypedArray views |
| Schema evolution support | No schema evolution |
| Strings and vectors | Fixed-size arrays and strings |
Use aligned format when you need:
- Direct TypedArray views into WASM linear memory
- Zero deserialization overhead
- Predictable memory layout for arrays of structs
- C++/WASM and JavaScript/TypeScript interop
Basic Usage
import { generateAlignedCode, parseSchema } from 'flatc-wasm/aligned-codegen';
const schema = `
namespace MyGame;
struct Vec3 {
x:float;
y:float;
z:float;
}
table Entity {
position:Vec3;
health:int;
mana:int;
}
`;
const result = generateAlignedCode(schema);
console.log(result.cpp);
console.log(result.ts);
console.log(result.js);
Fixed-Length Strings
By default, strings are variable-length and not supported. Enable fixed-length strings by setting defaultStringLength:
const schema = `
table Player {
name:string;
guild:string;
health:int;
}
`;
const result = generateAlignedCode(schema, { defaultStringLength: 255 });
Supported Types
bool | 1 byte | |
byte, ubyte, int8, uint8 | 1 byte | |
short, ushort, int16, uint16 | 2 bytes | |
int, uint, int32, uint32, float | 4 bytes | |
long, ulong, int64, uint64, double | 8 bytes | |
[type:N] | N × size | Fixed-size arrays |
[ubyte:0x100] | 256 bytes | Hex array sizes |
string | configurable | Requires defaultStringLength |
Generated Code Example
C++ Header:
#pragma once
#include <cstdint>
#include <cstring>
namespace MyGame {
struct Vec3 {
float x;
float y;
float z;
};
static_assert(sizeof(Vec3) == 12, "Vec3 size mismatch");
struct Entity {
Vec3 position;
int32_t health;
int32_t mana;
};
static_assert(sizeof(Entity) == 20, "Entity size mismatch");
}
TypeScript:
export const ENTITY_SIZE = 20;
export const ENTITY_ALIGN = 4;
export class EntityView {
private _view: DataView;
private _offset: number;
constructor(view: DataView, offset: number = 0) {
this._view = view;
this._offset = offset;
}
get position(): Vec3View {
return new Vec3View(this._view, this._offset + 0);
}
get health(): number {
return this._view.getInt32(this._offset + 12, true);
}
set health(value: number) {
this._view.setInt32(this._offset + 12, value, true);
}
get mana(): number {
return this._view.getInt32(this._offset + 16, true);
}
set mana(value: number) {
this._view.setInt32(this._offset + 16, value, true);
}
}
WASM Interop Example
import { EntityView, ENTITY_SIZE } from './aligned_types.mjs';
const memory = wasmInstance.exports.memory;
const entityPtr = wasmInstance.exports.get_entity_array();
const count = wasmInstance.exports.get_entity_count();
const view = new DataView(memory.buffer, entityPtr);
for (let i = 0; i < count; i++) {
const entity = new EntityView(view, i * ENTITY_SIZE);
console.log(`Entity ${i}: health=${entity.health}, mana=${entity.mana}`);
}
#include "aligned_types.h"
static Entity entities[1000];
extern "C" {
Entity* get_entity_array() { return entities; }
int get_entity_count() { return 1000; }
void update_entities(float dt) {
for (auto& e : entities) {
e.position.x += e.velocity.x * dt;
e.health = std::max(0, e.health - 1);
}
}
}
Sharing Arrays Between WASM Modules
Since aligned binary structs have no embedded length metadata (unlike FlatBuffers vectors), you need to communicate array bounds out-of-band. This section covers patterns for sharing arrays of aligned structs between WASM modules or across the JS/WASM boundary.
Pattern 1: Pointer + Count (Recommended)
The simplest pattern - pass the pointer and count as separate values:
static Cartesian3 positions[10000];
static uint32_t position_count = 0;
extern "C" {
Cartesian3* get_positions() { return positions; }
uint32_t get_position_count() { return position_count; }
}
const ptr = wasm.exports.get_positions();
const count = wasm.exports.get_position_count();
const positions = Cartesian3ArrayView.fromMemory(wasm.exports.memory, ptr, count);
for (const pos of positions) {
console.log(`(${pos.x}, ${pos.y}, ${pos.z})`);
}
Pattern 2: Index-Based Lookup (Fixed Offset Known)
When struct size is known at compile time, store indices separately and compute offsets on access. This is ideal for sparse access, cross-references between arrays, or when indices are embedded in other structures.
// Schema with cross-references via indices
namespace Space;
struct Cartesian3 {
x: double;
y: double;
z: double;
}
// Satellite references positions by index, not pointer
table Satellite {
norad_id: uint32;
name: string;
position_index: uint32; // Index into positions array
velocity_index: uint32; // Index into velocities array
}
// Observation references multiple satellites by index
table Observation {
timestamp: double;
satellite_indices: [uint32:64]; // Up to 64 satellite indices
satellite_count: uint32;
}
#include "space_aligned.h"
static Cartesian3 positions[10000];
static Cartesian3 velocities[10000];
static Satellite satellites[1000];
extern "C" {
Cartesian3* get_positions_base() { return positions; }
Cartesian3* get_velocities_base() { return velocities; }
Satellite* get_satellites_base() { return satellites; }
Cartesian3* get_satellite_position(uint32_t sat_idx) {
uint32_t pos_idx = satellites[sat_idx].position_index;
return &positions[pos_idx];
}
}
import { Cartesian3View, SatelliteView, CARTESIAN3_SIZE, SATELLITE_SIZE } from './space_aligned.mjs';
class SpaceDataManager {
private memory: WebAssembly.Memory;
private positionsBase: number;
private velocitiesBase: number;
private satellitesBase: number;
constructor(wasm: WasmExports) {
this.memory = wasm.memory;
this.positionsBase = wasm.get_positions_base();
this.velocitiesBase = wasm.get_velocities_base();
this.satellitesBase = wasm.get_satellites_base();
}
getPositionByIndex(index: number): Cartesian3View {
const offset = this.positionsBase + index * CARTESIAN3_SIZE;
return Cartesian3View.fromMemory(this.memory, offset);
}
getVelocityByIndex(index: number): Cartesian3View {
const offset = this.velocitiesBase + index * CARTESIAN3_SIZE;
return Cartesian3View.fromMemory(this.memory, offset);
}
getSatelliteByIndex(index: number): SatelliteView {
const offset = this.satellitesBase + index * SATELLITE_SIZE;
return SatelliteView.fromMemory(this.memory, offset);
}
getSatellitePosition(satIndex: number): Cartesian3View {
const sat = this.getSatelliteByIndex(satIndex);
const posIndex = sat.position_index;
return this.getPositionByIndex(posIndex);
}
getPositionsForSatellites(satIndices: number[]): Cartesian3View[] {
return satIndices.map(satIdx => {
const sat = this.getSatelliteByIndex(satIdx);
return this.getPositionByIndex(sat.position_index);
});
}
}
const manager = new SpaceDataManager(wasmExports);
const pos = manager.getPositionByIndex(42);
console.log(`Position 42: (${pos.x}, ${pos.y}, ${pos.z})`);
const satPos = manager.getSatellitePosition(0);
console.log(`Satellite 0 position: (${satPos.x}, ${satPos.y}, ${satPos.z})`);
Store indices in a metadata structure that references into data arrays:
// Manifest with indices into data arrays
table EphemerisManifest {
// Metadata
epoch_start: double;
epoch_end: double;
step_seconds: double;
// Indices into the points array (one range per satellite)
satellite_start_indices: [uint32:100]; // Start index for each satellite
satellite_point_counts: [uint32:100]; // Point count for each satellite
satellite_count: uint32;
}
struct EphemerisPoint {
jd: double;
x: double;
y: double;
z: double;
vx: double;
vy: double;
vz: double;
}
import {
EphemerisManifestView,
EphemerisPointView,
EphemerisPointArrayView,
EPHEMERISPOINT_SIZE
} from './ephemeris_aligned.mjs';
class EphemerisReader {
private manifest: EphemerisManifestView;
private pointsBase: number;
private memory: WebAssembly.Memory;
constructor(memory: WebAssembly.Memory, manifestPtr: number, pointsPtr: number) {
this.memory = memory;
this.manifest = EphemerisManifestView.fromMemory(memory, manifestPtr);
this.pointsBase = pointsPtr;
}
getSatellitePoints(satIndex: number): EphemerisPointArrayView {
const startIdx = this.manifest.satellite_start_indices[satIndex];
const count = this.manifest.satellite_point_counts[satIndex];
const offset = this.pointsBase + startIdx * EPHEMERISPOINT_SIZE;
return new EphemerisPointArrayView(this.memory.buffer, offset, count);
}
getPoint(satIndex: number, timeIndex: number): EphemerisPointView {
const startIdx = this.manifest.satellite_start_indices[satIndex];
const globalIdx = startIdx + timeIndex;
const offset = this.pointsBase + globalIdx * EPHEMERISPOINT_SIZE;
return EphemerisPointView.fromMemory(this.memory, offset);
}
*iterateSatellites(): Generator<{index: number, points: EphemerisPointArrayView}> {
const count = this.manifest.satellite_count;
for (let i = 0; i < count; i++) {
yield { index: i, points: this.getSatellitePoints(i) };
}
}
}
const reader = new EphemerisReader(memory, manifestPtr, pointsPtr);
const issPoints = reader.getSatellitePoints(0);
console.log(`ISS has ${issPoints.length} ephemeris points`);
const point = reader.getPoint(0, 100);
console.log(`Position at t=100: (${point.x}, ${point.y}, ${point.z})`);
Pattern 4: Pre-computed Offset Table
For variable-sized records or complex layouts, pre-compute byte offsets:
// Offset table for complex data
table DataDirectory {
record_count: uint32;
byte_offsets: [uint32:10000]; // Byte offset of each record
byte_sizes: [uint32:10000]; // Size of each record (if variable)
}
class OffsetTableReader<T> {
constructor(
private memory: WebAssembly.Memory,
private directory: DataDirectoryView,
private dataBase: number,
private viewFactory: (buffer: ArrayBuffer, offset: number) => T
) {}
get(index: number): T {
const byteOffset = this.directory.byte_offsets[index];
return this.viewFactory(this.memory.buffer, this.dataBase + byteOffset);
}
getSize(index: number): number {
return this.directory.byte_sizes[index];
}
get length(): number {
return this.directory.record_count;
}
}
Real-World Example: Satellite Ephemeris
Complete example for sharing orbital data between WASM propagation and JS visualization:
// satellite_ephemeris.fbs
namespace Astrodynamics;
struct StateVector {
x: double; // km (ECI)
y: double;
z: double;
vx: double; // km/s
vy: double;
vz: double;
}
struct EphemerisPoint {
julian_date: double;
state: StateVector;
}
// Manifest stores indices, data is in separate dense array
table EphemerisManifest {
satellite_ids: [uint32:100];
start_indices: [uint32:100]; // Index into points array
point_counts: [uint32:100]; // How many points per satellite
total_satellites: uint32;
total_points: uint32;
}
#include "ephemeris_aligned.h"
static EphemerisManifest manifest;
static EphemerisPoint points[1000000];
extern "C" {
EphemerisManifest* get_manifest() { return &manifest; }
EphemerisPoint* get_points_base() { return points; }
void add_satellite_ephemeris(uint32_t norad_id, EphemerisPoint* pts, uint32_t count) {
uint32_t sat_idx = manifest.total_satellites++;
uint32_t start_idx = manifest.total_points;
manifest.satellite_ids[sat_idx] = norad_id;
manifest.start_indices[sat_idx] = start_idx;
manifest.point_counts[sat_idx] = count;
memcpy(&points[start_idx], pts, count * sizeof(EphemerisPoint));
manifest.total_points += count;
}
}
import {
EphemerisManifestView,
EphemerisPointView,
StateVectorView,
EPHEMERISPOINT_SIZE
} from './ephemeris_aligned.mjs';
class EphemerisVisualizer {
private manifest: EphemerisManifestView;
private pointsBase: number;
private memory: WebAssembly.Memory;
constructor(wasm: WasmExports) {
this.memory = wasm.memory;
this.manifest = EphemerisManifestView.fromMemory(
this.memory,
wasm.get_manifest()
);
this.pointsBase = wasm.get_points_base();
}
getPositionAtIndex(satIndex: number, timeIndex: number): StateVectorView {
const startIdx = this.manifest.start_indices[satIndex];
const pointOffset = this.pointsBase + (startIdx + timeIndex) * EPHEMERISPOINT_SIZE;
const pt = EphemerisPointView.fromMemory(this.memory, pointOffset);
return pt.state;
}
render(ctx: CanvasRenderingContext2D, timeIndex: number) {
const satCount = this.manifest.total_satellites;
for (let i = 0; i < satCount; i++) {
const pointCount = this.manifest.point_counts[i];
if (timeIndex >= pointCount) continue;
const state = this.getPositionAtIndex(i, timeIndex);
const screenX = ctx.canvas.width/2 + state.x / 100;
const screenY = ctx.canvas.height/2 - state.y / 100;
ctx.fillStyle = '#0f0';
ctx.fillRect(screenX - 2, screenY - 2, 4, 4);
}
}
}
Memory Layout Summary
┌─────────────────────────────────────────────────────────────┐
│ EphemerisManifest (at manifest_ptr) │
│ ├─ satellite_ids[100] - NORAD catalog numbers │
│ ├─ start_indices[100] - Index into points array │
│ ├─ point_counts[100] - Points per satellite │
│ ├─ total_satellites - Active satellite count │
│ └─ total_points - Total points in array │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ EphemerisPoint[] (at points_base) │
│ │
│ Satellite 0: indices [0, point_counts[0]) │
│ ├─ points[0]: {jd, x, y, z, vx, vy, vz} │
│ ├─ points[1]: ... │
│ └─ points[point_counts[0]-1] │
│ │
│ Satellite 1: indices [start_indices[1], ...) │
│ ├─ points[start_indices[1]]: ... │
│ └─ ... │
│ │
│ Access formula: │
│ offset = points_base + (start_indices[sat] + time) * 56 │
│ where 56 = EPHEMERISPOINT_SIZE │
└─────────────────────────────────────────────────────────────┘
Encryption
flatc-wasm supports per-field AES-256-CTR encryption for FlatBuffer data. Fields marked with the (encrypted) attribute are transparently encrypted and decrypted, with key derivation via HKDF so each field gets a unique key/IV pair.
Generated Code Encryption Support
All 12 code generators now emit encryption support automatically when your schema contains (encrypted) fields:
| C++ | flatbuffers/encryption.h | Inline helpers in encryption namespace |
| TypeScript | Pure TypeScript AES-256-CTR | No external dependencies |
| Python | cryptography library | Uses Fernet-compatible primitives |
| Go | crypto/aes + crypto/cipher | Standard library only |
| Rust | Pure Rust AES-256-CTR | No external crates required |
| Java | javax.crypto.Cipher | Standard JCE APIs |
| C# | System.Security.Cryptography | .NET built-in crypto |
| Swift | Pure Swift AES-256-CTR | No Foundation dependencies |
| Kotlin | javax.crypto.Cipher | Android/JVM compatible |
| PHP | openssl_encrypt/decrypt | OpenSSL extension |
| Dart | pointycastle library | Pure Dart implementation |
| Lobster | Placeholder | Language lacks crypto library |
The generated code automatically:
- Adds an
encryptionCtx field to tables with encrypted fields
- Generates
withEncryption() factory constructors
- Transparently decrypts fields when accessed with a valid context
- Returns raw (encrypted) bytes when accessed without context
Per-Field Encryption
Mark fields in your schema with the (encrypted) attribute:
table UserRecord {
id: uint64;
name: string;
ssn: string (encrypted);
credit_card: string (encrypted);
email: string;
}
root_type UserRecord;
When encryption is active, only the ssn and credit_card fields are encrypted. Other fields remain in plaintext, allowing indexing and queries on non-sensitive data.
How it works:
- A shared secret is derived via ECDH (X25519, secp256k1, P-256, or P-384)
- HKDF derives a unique AES-256 key per session using the context string
- Each field gets a unique nonce via 96-bit addition:
nonceStart + (recordIndex * 65536 + fieldId)
- Each field is encrypted independently with AES-256-CTR
- An
EncryptionHeader FlatBuffer stores the ephemeral public key, algorithm metadata, and starting nonce
Encryption Sessions & Nonce Management
flatc-wasm uses a nonce incrementor system to ensure cryptographic security when encrypting multiple fields or records. This prevents nonce reuse, which would compromise AES-CTR mode security.
Starting an Encrypted Session
To decrypt data, the recipient must first receive the EncryptionHeader. This header contains:
- Ephemeral public key - For ECDH key derivation
- Starting nonce (
nonceStart) - 12-byte random value generated via CSPRNG
- Algorithm metadata - Key exchange, symmetric cipher, and KDF identifiers
- Optional context - Domain separation string for HKDF
import { EncryptionContext, generateNonceStart } from 'flatc-wasm';
const ctx = EncryptionContext.forEncryption(recipientPublicKey, {
algorithm: 'x25519',
context: 'my-app-v1',
});
const header = ctx.getHeader();
const headerJSON = ctx.getHeaderJSON();
await sendToRecipient(header);
for (let i = 0; i < records.length; i++) {
ctx.setRecordIndex(i);
const encrypted = encryptRecord(records[i], ctx);
await sendToRecipient(encrypted);
}
Nonce Derivation Algorithm
Each field in each record gets a unique 96-bit nonce derived via big-endian addition:
derived_nonce = nonceStart + (recordIndex × 65536 + fieldId)
This ensures:
- No nonce reuse - Every (recordIndex, fieldId) pair produces a unique nonce
- Deterministic derivation - Same inputs always produce the same nonce
- Efficient computation - Simple 96-bit addition with carry
import { deriveNonce, generateNonceStart, NONCE_SIZE } from 'flatc-wasm';
const nonceStart = generateNonceStart();
const nonce0 = deriveNonce(nonceStart, 0);
const nonce5_3 = deriveNonce(nonceStart, 5 * 65536 + 3);
const ctx = new EncryptionContext(key, nonceStart);
const fieldNonce = ctx.deriveFieldNonce(fieldId, recordIndex);
Offline & Out-of-Order Decryption
Because nonce derivation is deterministic, encrypted records can be decrypted:
- Offline - No connection to the sender required after receiving the header
- Out of order - Records can arrive or be processed in any sequence
- Partially - Only specific records/fields need to be decrypted
- In parallel - Multiple workers can decrypt different records simultaneously
const ctx = EncryptionContext.forDecryption(
myPrivateKey,
receivedHeader,
'my-app-v1'
);
ctx.setRecordIndex(42);
const record42 = decryptRecord(encryptedData42, ctx);
ctx.setRecordIndex(7);
const record7 = decryptRecord(encryptedData7, ctx);
const workers = records.map((data, index) => {
return decryptInWorker(data, index, receivedHeader, myPrivateKey);
});
await Promise.all(workers);
Unknown Record Index Recovery
If the record index is lost (e.g., packet loss without sequence numbers), the recipient can still decrypt by trying sequential indices:
async function recoverAndDecrypt(encryptedData, ctx, maxAttempts = 1000) {
for (let i = 0; i < maxAttempts; i++) {
try {
ctx.setRecordIndex(i);
const decrypted = await tryDecrypt(encryptedData, ctx);
if (isValidFlatBuffer(decrypted)) {
console.log(`Recovered at recordIndex ${i}`);
return { data: decrypted, recordIndex: i };
}
} catch {
}
}
throw new Error('Could not recover record index');
}
Note: For production use, include the record index in your message framing or use authenticated encryption (AEAD) to detect incorrect indices immediately.
Security: Why Nonce Incrementing Matters
AES-CTR mode generates a keystream by encrypting a counter with the key. The ciphertext is plaintext XOR keystream. If the same (key, nonce) pair is used twice:
ciphertext1 XOR ciphertext2 = plaintext1 XOR plaintext2
This leaks information about both plaintexts. An attacker with two ciphertexts encrypted with the same nonce can:
- Recover plaintext if one message is known
- Perform statistical analysis on the XOR of plaintexts
- Potentially recover both plaintexts with enough ciphertext pairs
The nonce incrementor guarantees a unique nonce for every field in every record, preventing this attack class entirely.
FlatcRunner Encryption API
The FlatcRunner class provides high-level encryption methods:
import { FlatcRunner } from 'flatc-wasm';
const flatc = await FlatcRunner.init();
const schema = {
entry: '/user.fbs',
files: {
'/user.fbs': `
table UserRecord {
id: uint64;
name: string;
ssn: string (encrypted);
}
root_type UserRecord;
`
}
};
const json = JSON.stringify({ id: 1, name: 'Alice', ssn: '123-45-6789' });
const { header, data } = flatc.generateBinaryEncrypted(schema, json, {
publicKey: recipientPublicKey,
algorithm: 'x25519',
context: 'user-records',
});
const decryptedJson = flatc.generateJSONDecrypted(schema, { path: '/user.bin', data }, {
privateKey: recipientPrivateKey,
header: header,
});
console.log(JSON.parse(decryptedJson));
Encryption Options
{
publicKey: Uint8Array,
algorithm: 'x25519' | 'secp256k1' | 'p256' | 'p384',
fields: ['ssn', 'credit_card'],
context: 'my-app',
fips: false,
}
{
privateKey: Uint8Array,
header: Uint8Array,
}
Streaming Encryption
The StreamingDispatcher supports persistent encryption sessions for processing multiple messages:
import { StreamingDispatcher } from 'flatc-wasm';
const dispatcher = new StreamingDispatcher(wasmModule);
dispatcher.setEncryption(recipientPublicKey, {
algorithm: 'x25519',
context: 'stream-session',
});
dispatcher.dispatch(messageBuffer);
console.log(dispatcher.isEncryptionActive());
dispatcher.clearEncryption();
Encryption Configuration
The encryption configuration is defined as a FlatBuffer schema (encryption_config.fbs):
enum DataFormat : byte { FlatBuffer, JSON }
enum EncryptionDirection : byte { Encrypt, Decrypt }
table EncryptionConfig {
recipient_public_key: [ubyte];
algorithm: string; // "x25519", "secp256k1", "p256", "p384"
field_names: [string]; // Fields to encrypt (empty = use schema attributes)
context: string; // HKDF domain separation
fips_mode: bool = false;
direction: EncryptionDirection = Encrypt;
private_key: [ubyte]; // For decryption
}
The EncryptionHeader stored with encrypted data:
enum KeyExchangeAlgorithm : byte { X25519, Secp256k1, P256, P384 }
table EncryptionHeader {
version: ubyte = 2; // Version 2 requires nonce_start
key_exchange: KeyExchangeAlgorithm;
ephemeral_public_key: [ubyte] (required);
nonce_start: [ubyte] (required); // 12-byte starting nonce (CSPRNG)
context: string;
timestamp: ulong; // Unix epoch milliseconds
}
FIPS Mode
For environments requiring FIPS 140-2 compliance, build with OpenSSL instead of Crypto++:
cmake -B build/wasm -S . \
-DFLATBUFFERS_BUILD_WASM=ON \
-DFLATBUFFERS_WASM_USE_OPENSSL=ON
cmake --build build/wasm --target flatc_wasm_npm
When FIPS mode is enabled:
- All cryptographic operations use OpenSSL EVP APIs
- AES-256-CTR via
EVP_aes_256_ctr()
- HKDF via
EVP_PKEY_derive() with EVP_PKEY_HKDF
- X25519, P-256, P-384 ECDH via
EVP_PKEY_derive()
- Ed25519 and ECDSA signatures via
EVP_DigestSign/EVP_DigestVerify
To use FIPS mode at runtime, set fips: true in encryption options:
const { header, data } = flatc.generateBinaryEncrypted(schema, json, {
publicKey: recipientPublicKey,
algorithm: 'p256',
fips: true,
});
Supported Key Exchange Algorithms
x25519 | 32 bytes | Curve25519 | Default, fast, modern |
secp256k1 | 33 bytes (compressed) | secp256k1 | Bitcoin/blockchain compatibility |
p256 | 33 bytes (compressed) | NIST P-256 | FIPS compliance, broad support |
p384 | 49 bytes (compressed) | NIST P-384 | Higher security margin |
Encryption Module API (encryption.mjs)
The encryption.mjs module provides a complete JavaScript API for all cryptographic operations, wrapping the WASM binary with type-safe, memory-safe helpers. All functions are re-exported from the main flatc-wasm package entry point.
Initialization
import {
loadEncryptionWasm,
isInitialized,
hasCryptopp,
getVersion,
} from 'flatc-wasm';
await loadEncryptionWasm();
console.log(isInitialized());
console.log(getVersion());
console.log(hasCryptopp());
Constants
import {
KEY_SIZE,
IV_SIZE,
NONCE_SIZE,
SHA256_SIZE,
HMAC_SIZE,
X25519_PRIVATE_KEY_SIZE,
X25519_PUBLIC_KEY_SIZE,
SECP256K1_PRIVATE_KEY_SIZE,
SECP256K1_PUBLIC_KEY_SIZE,
P384_PRIVATE_KEY_SIZE,
P384_PUBLIC_KEY_SIZE,
ED25519_PRIVATE_KEY_SIZE,
ED25519_PUBLIC_KEY_SIZE,
ED25519_SIGNATURE_SIZE,
} from 'flatc-wasm';
Error Handling
import { CryptoError, CryptoErrorCode } from 'flatc-wasm';
try {
encryptBytes(data, shortKey, iv);
} catch (e) {
if (e instanceof CryptoError) {
console.log(e.code);
console.log(e.message);
}
}
Hash Functions
import { sha256, hkdf, hmacSha256, hmacSha256Verify } from 'flatc-wasm';
const hash = sha256(new TextEncoder().encode('Hello'));
const derived = hkdf(inputKeyMaterial, salt, info, 32);
const mac = hmacSha256(key, data);
const valid = hmacSha256Verify(key, data, mac);
AES-256-CTR Encryption
import {
encryptBytes, decryptBytes,
encryptBytesCopy, decryptBytesCopy,
clearIVTracking, clearAllIVTracking,
} from 'flatc-wasm';
const data = new TextEncoder().encode('Secret');
encryptBytes(data, key, iv);
decryptBytes(data, key, iv);
const { ciphertext, iv: generatedIV } = encryptBytesCopy(plaintext, key);
const decrypted = decryptBytesCopy(ciphertext, key, generatedIV);
clearIVTracking(key);
clearAllIVTracking();
Authenticated Encryption (AES-CTR + HMAC-SHA256)
import { encryptAuthenticated, decryptAuthenticated } from 'flatc-wasm';
const sealed = encryptAuthenticated(plaintext, key);
const opened = decryptAuthenticated(sealed, key);
const sealed = encryptAuthenticated(plaintext, key, aad);
const opened = decryptAuthenticated(sealed, key, aad);
X25519 Key Exchange
import {
x25519GenerateKeyPair,
x25519SharedSecret,
x25519DeriveKey,
} from 'flatc-wasm';
const alice = x25519GenerateKeyPair();
const bob = x25519GenerateKeyPair();
const secret = x25519SharedSecret(alice.privateKey, bob.publicKey);
const aesKey = x25519DeriveKey(secret, 'my-app-encryption');
secp256k1 (Bitcoin/Ethereum compatible)
import {
secp256k1GenerateKeyPair,
secp256k1SharedSecret,
secp256k1DeriveKey,
secp256k1Sign,
secp256k1Verify,
} from 'flatc-wasm';
const kp = secp256k1GenerateKeyPair();
const secret = secp256k1SharedSecret(kp.privateKey, peerPublicKey);
const aesKey = secp256k1DeriveKey(secret, 'context');
const sig = secp256k1Sign(kp.privateKey, message);
const valid = secp256k1Verify(kp.publicKey, message, sig);
P-256 / P-384 (NIST, Web Crypto)
P-256 and P-384 operations use the Web Crypto API (crypto.subtle) for FIPS-grade implementations. All functions are async.
import {
p256GenerateKeyPairAsync, p256SharedSecretAsync,
p256DeriveKey, p256SignAsync, p256VerifyAsync,
p384GenerateKeyPairAsync, p384SharedSecretAsync,
p384DeriveKey, p384SignAsync, p384VerifyAsync,
} from 'flatc-wasm';
const alice = await p256GenerateKeyPairAsync();
const secret = await p256SharedSecretAsync(alice.privateKey, bob.publicKey);
const aesKey = p256DeriveKey(secret, 'context');
const sig = await p256SignAsync(alice.privateKey, message);
const valid = await p256VerifyAsync(alice.publicKey, message, sig);
const kp384 = await p384GenerateKeyPairAsync();
Ed25519 Signatures
import { ed25519GenerateKeyPair, ed25519Sign, ed25519Verify } from 'flatc-wasm';
const kp = ed25519GenerateKeyPair();
const sig = ed25519Sign(kp.privateKey, message);
const valid = ed25519Verify(kp.publicKey, message, sig);
Nonce Generation & Derivation
import { generateNonceStart, deriveNonce, NONCE_SIZE } from 'flatc-wasm';
const nonce = generateNonceStart();
const derived = deriveNonce(nonce, 42);
const derived2 = deriveNonce(nonce, 42n);
EncryptionContext (ECIES)
import { EncryptionContext, encryptionHeaderFromJSON } from 'flatc-wasm';
const ctx = new EncryptionContext(key);
const ctx2 = new EncryptionContext('ab'.repeat(32));
const ctx3 = EncryptionContext.fromHex(hexKey);
const encCtx = EncryptionContext.forEncryption(recipientPubKey, {
algorithm: 'x25519',
context: 'app-v1',
nonceStart: customNonce,
});
encCtx.encryptScalar(buffer, offset, length, fieldId, recordIndex);
const header = encCtx.getHeader();
const headerJSON = encCtx.getHeaderJSON();
const decCtx = EncryptionContext.forDecryption(
myPrivateKey,
encryptionHeaderFromJSON(headerJSON),
'app-v1'
);
decCtx.decryptScalar(buffer, offset, length, fieldId, recordIndex);
ctx.isValid();
ctx.getKey();
ctx.getNonceStart();
ctx.getRecordIndex();
ctx.setRecordIndex(n);
ctx.nextRecordIndex();
ctx.deriveFieldKey(fieldId, recordIndex);
ctx.deriveFieldNonce(fieldId, recordIndex);
ctx.getEphemeralPublicKey();
ctx.getAlgorithm();
ctx.getContext();
import {
createEncryptionHeader,
computeKeyId,
encryptionHeaderToJSON,
encryptionHeaderFromJSON,
} from 'flatc-wasm';
const header = createEncryptionHeader({
algorithm: 'x25519',
senderPublicKey: ephemeralPubKey,
recipientKeyId: computeKeyId(recipientPubKey),
nonceStart: nonce,
context: 'app-v1',
});
const json = encryptionHeaderToJSON(header);
const restored = encryptionHeaderFromJSON(json);
Embedded Language Runtimes
flatc-wasm ships with the complete FlatBuffers runtime library source code for 11 languages, Brotli-compressed and embedded directly in the WASM binary. This allows you to retrieve the runtime files needed for any target language without network access or separate package installation.
Overview
When you generate code with flatc, the output files depend on a language-specific runtime library (e.g., flatbuffers Python package, flatbuffers npm package, etc.). The embedded runtimes bundle all of these into the WASM module itself:
- Brotli-compressed at build time (quality 11) for minimal binary size overhead (~264 KB compressed for all 11 languages)
- Decompressed on demand inside the WASM module — no JavaScript decompression libraries needed
- Two retrieval formats: JSON file map or ZIP archive
Retrieving Runtimes
Using FlatcRunner (Recommended)
import { FlatcRunner } from 'flatc-wasm';
const flatc = await FlatcRunner.init();
const languages = flatc.listEmbeddedRuntimes();
console.log(languages);
const pythonFiles = flatc.getEmbeddedRuntime('python');
for (const [path, content] of Object.entries(pythonFiles)) {
console.log(` ${path} (${content.length} bytes)`);
}
const zipData = flatc.getEmbeddedRuntimeZip('go');
import { writeFileSync } from 'fs';
writeFileSync('go-runtime.zip', zipData);
const blob = new Blob([zipData], { type: 'application/zip' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'flatbuffers-go-runtime.zip';
a.click();
Language Aliases
The FlatcRunner accepts common aliases in addition to canonical language names:
typescript | ts |
javascript, js | ts |
c++ | cpp |
c#, cs | csharp |
flatc.getEmbeddedRuntime('typescript');
flatc.getEmbeddedRuntime('ts');
flatc.getEmbeddedRuntime('js');
Low-Level Runtime API
For direct WASM module access:
_wasm_list_embedded_runtimes(outSize) | (i32) -> i32 | Returns pointer to JSON array of language names |
_wasm_get_embedded_runtime_json(lang, outSize) | (i32, i32) -> i32 | Decompress and return as JSON file map |
_wasm_get_embedded_runtime_zip(lang, outSize) | (i32, i32) -> i32 | Decompress, build ZIP archive, return pointer |
_wasm_get_embedded_runtime_info(lang, fileCount, rawSize, compressedSize) | (i32, i32, i32, i32) -> i32 | Get metadata (returns 1 if found, 0 if not) |
const flatc = await createFlatcWasm();
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const outSizePtr = flatc._malloc(4);
const listPtr = flatc._wasm_list_embedded_runtimes(outSizePtr);
const listLen = flatc.getValue(outSizePtr, 'i32');
const languages = JSON.parse(decoder.decode(
flatc.HEAPU8.slice(listPtr, listPtr + listLen)
));
console.log('Available:', languages);
const langBytes = encoder.encode('python\0');
const langPtr = flatc._malloc(langBytes.length);
flatc.HEAPU8.set(langBytes, langPtr);
const jsonPtr = flatc._wasm_get_embedded_runtime_json(langPtr, outSizePtr);
const jsonLen = flatc.getValue(outSizePtr, 'i32');
const files = JSON.parse(decoder.decode(
flatc.HEAPU8.slice(jsonPtr, jsonPtr + jsonLen)
));
console.log('Python runtime files:', Object.keys(files));
const goBytes = encoder.encode('go\0');
const goPtr = flatc._malloc(goBytes.length);
flatc.HEAPU8.set(goBytes, goPtr);
const zipPtr = flatc._wasm_get_embedded_runtime_zip(goPtr, outSizePtr);
const zipLen = flatc.getValue(outSizePtr, 'i32');
const zipData = flatc.HEAPU8.slice(zipPtr, zipPtr + zipLen);
flatc._free(langPtr);
flatc._free(goPtr);
flatc._free(outSizePtr);
Embedded Runtime Languages
python | python/flatbuffers/ | FlatBuffers Python package |
ts | ts/ | TypeScript runtime |
go | go/ | Go runtime package |
java | java/.../com/google/flatbuffers/ | Java runtime classes |
kotlin | kotlin/.../src/ | Kotlin Multiplatform runtime |
swift | swift/Sources/ | Swift runtime |
dart | dart/lib/ | Dart runtime package |
php | php/ | PHP runtime |
csharp | net/FlatBuffers/ | C# (.NET) runtime |
cpp | include/flatbuffers/ | C++ headers (runtime only, no compiler headers) |
rust | rust/flatbuffers/src/ | Rust crate source |
Build-Time Generation
The embedded runtime data is generated at build time by a Node.js script:
node scripts/generate_embedded_runtimes.mjs <flatbuffers-repo-path> <output-header>
This script:
- Reads runtime source files for each language from the FlatBuffers repository
- Creates a JSON map of
{ "relative/path": "file-content" } per language
- Brotli-compresses each JSON blob at quality 11 (maximum compression)
- Emits a C header (
src/embedded_runtimes_data.h) with static byte arrays
The header is then compiled into the WASM binary. At runtime, Brotli decompression happens inside the WASM module using the Brotli C library (fetched via CMake FetchContent).
Plugin Architecture
The flatc-wasm package supports an extensible plugin architecture for custom code generators and transformations.
Code Generator Plugins
Create custom code generators that extend the standard flatc output:
import { FlatcRunner } from 'flatc-wasm';
import { parseSchema, generateCppHeader, generateTypeScript } from 'flatc-wasm/aligned-codegen';
class EncryptionPlugin {
constructor(options = {}) {
this.encryptedFields = options.encryptedFields || [];
}
transform(schema, generatedCode) {
const parsed = parseSchema(schema);
return generatedCode;
}
}
const flatc = await FlatcRunner.init();
const plugin = new EncryptionPlugin({
encryptedFields: ['ssn', 'credit_card']
});
const code = flatc.generateCode(schema, 'ts');
const transformedCode = plugin.transform(schema, code);
Schema Transformation Plugins
Transform schemas before code generation:
function tableToStructPlugin(schema, options = {}) {
const parsed = parseSchema(schema, options);
const alignableTypes = parsed.tables.filter(table => {
return table.fields.every(field => {
return field.size !== undefined && field.size > 0;
});
});
return {
...parsed,
alignableTypes,
canAlign: alignableTypes.length > 0,
};
}
Available Extension Points
parseSchema() | Parse FlatBuffers schema to AST |
computeLayout() | Calculate memory layout for types |
generateCppHeader() | Generate C++ header from parsed schema |
generateTypeScript() | Generate TypeScript module from parsed schema |
generateJavaScript() | Generate JavaScript module from parsed schema |
generateAlignedCode() | Generate all languages at once |
Custom Language Generator Example
import { parseSchema, computeLayout } from 'flatc-wasm/aligned-codegen';
function generateRustAligned(schemaContent, options = {}) {
const schema = parseSchema(schemaContent, options);
let code = '// Auto-generated Rust aligned types\n\n';
for (const structDef of schema.structs) {
const layout = computeLayout(structDef);
code += `#[repr(C)]\n`;
code += `pub struct ${structDef.name} {\n`;
for (const field of layout.fields) {
const rustType = toRustType(field);
code += ` pub ${field.name}: ${rustType},\n`;
}
code += `}\n\n`;
}
return code;
}
function toRustType(field) {
const typeMap = {
'int32': 'i32',
'uint32': 'u32',
'float': 'f32',
'double': 'f64',
};
return typeMap[field.type] || field.type;
}
Performance Tips
- Reuse schemas: Add schemas once and reuse them for multiple conversions
- Use streaming for large data: The stream API avoids multiple memory copies
- Pre-allocate output buffer: Call
_wasm_reserve_output(size) for known output sizes
- Batch operations: The WASM module has startup overhead; batch multiple conversions
- Use binary protocol: For high-throughput scenarios, use the length-prefixed binary TCP protocol
License
Apache-2.0
This package a fork of the FlatBuffers project by Google.