blob: 78d56dec0f0515e1bcc18c7d49c4d71b91b45932 [file]
#!/usr/bin/env python3
"""Projects bindings.py into its generated files; --check diffs instead of
writing so test lanes can gate on drift."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from bindings import NAMESPACES, PODS
PACKAGES = Path(__file__).resolve().parents[4]
HEADER_PATH = PACKAGES / 'runtime/include/rive/wasm/rive_bindings_v1.h'
HOST_PATH = PACKAGES / 'runtime/src/wasm/wasm_natives_gen.hpp'
WEB_HOST_PATH = PACKAGES / 'runtime/src/wasm/wasm_natives_web_gen.hpp'
STUBS_PATH = PACKAGES / 'luau_wasm/tests/vm_host_stubs.mjs'
AS_PATH = PACKAGES / 'rasc/assemblyscript/std/rive/bindings.as'
WEB_PATH = PACKAGES / 'runtime/src/wasm/web/rive_module_imports.mjs'
GENERATED_NOTE = 'Generated by src/wasm/idl/generate.py from bindings.py.' \
' Do not edit.'
def camel(snake):
parts = snake.split('_')
return parts[0] + ''.join(p.capitalize() for p in parts[1:])
def pod_ctype(podname):
return 'rive_%s_v1' % podname
def pod_struct_lines():
lines = []
for p in PODS:
lines.append('typedef struct %s' % pod_ctype(p['name']))
lines.append('{')
for field in p['fields']:
ctype = 'float' if field['kind'] == 'f32' else 'uint32_t'
lines.append(' %s %s;' % (ctype, field['name']))
lines.append('} %s;' % pod_ctype(p['name']))
lines.append('')
return lines
def impl_name(namespace, op):
return camel(namespace['short'] + '_' + op['name'])
def c_symbol(namespace, op):
return 'rive_' + namespace['short'] + '_' + op['name']
def c_params(params):
out = []
for p in params:
kind = p['kind']
if kind in ('u32', 'handle'):
out.append('uint32_t ' + p['name'])
elif kind == 'i32':
out.append('int32_t ' + p['name'])
elif kind == 'f32':
out.append('float ' + p['name'])
elif kind == 'buf':
out.append('const %s* %s' % (p['elem'], p['name']))
out.append('uint32_t ' + p['count'])
elif kind == 'mutbuf':
out.append('%s* %s' % (p['elem'], p['name']))
out.append('uint32_t ' + p['count'])
elif kind == 'addr':
out.append('const %s* %s' % (p['elem'], p['name']))
elif kind == 'str':
out.append('const char* ' + p['name'])
out.append('uint32_t ' + p['count'])
elif kind == 'pod':
out.append('const %s* %s' % (pod_ctype(p['pod']), p['name']))
out.append('uint32_t %sByteCount' % p['name'])
else:
raise ValueError(kind)
return out
def host_params(params):
out = []
for p in params:
kind = p['kind']
if kind in ('u32', 'handle'):
out.append('uint32_t ' + p['name'])
elif kind == 'i32':
out.append('int32_t ' + p['name'])
elif kind == 'f32':
out.append('float ' + p['name'])
elif kind == 'buf':
out.append('const %s* %s' % (p['elem'], p['name']))
out.append('uint32_t ' + p['count'])
elif kind == 'mutbuf':
out.append('%s* %s' % (p['elem'], p['name']))
out.append('uint32_t ' + p['count'])
elif kind == 'addr':
# Raw guest address; the host validates and translates itself.
out.append('uint32_t ' + p['name'])
elif kind == 'str':
out.append('const char* ' + p['name'])
out.append('uint32_t ' + p['count'])
elif kind == 'pod':
out.append('const %s* %s' % (pod_ctype(p['pod']), p['name']))
out.append('uint32_t %sByteCount' % p['name'])
else:
raise ValueError(kind)
return out
def host_args(params):
out = []
for p in params:
kind = p['kind']
if kind in ('u32', 'handle', 'i32', 'f32', 'addr'):
out.append(p['name'])
elif kind in ('buf', 'mutbuf', 'str'):
out.append(p['name'])
out.append(p['count'])
elif kind == 'pod':
out.append(p['name'])
out.append('%sByteCount' % p['name'])
else:
raise ValueError(kind)
return out
def check_arg_budget(namespace, op):
# Beyond seven integer wire args (plus the implicit exec_env) arguments
# spill to the native stack, which WAMR's generic invocation drops.
# Wide ops must pack into descriptor structs instead.
ints = 0
for p in op['params']:
if p['kind'] in ('u32', 'handle', 'i32', 'addr'):
ints += 1
elif p['kind'] in ('buf', 'mutbuf', 'str', 'pod'):
ints += 2
if ints > 7:
raise SystemExit('%s.%s has %d integer wire args; max is 7' %
(namespace['module'], op['name'], ints))
def wamr_signature(op):
sig = '('
for p in op['params']:
kind = p['kind']
if kind in ('u32', 'handle', 'i32', 'addr'):
sig += 'i'
elif kind == 'f32':
sig += 'f'
elif kind in ('buf', 'mutbuf', 'str', 'pod'):
sig += '*~'
else:
raise ValueError(kind)
sig += ')'
if op['ret'] == 'f32':
sig += 'f'
elif op['ret'] is not None:
sig += 'i'
return sig
def ret_ctype(op):
if op['ret'] is None:
return 'void'
return 'float' if op['ret'] == 'f32' else 'uint32_t'
def check_all_ops():
for namespace in NAMESPACES:
for op in namespace['ops']:
check_arg_budget(namespace, op)
def emit_header():
lines = [
'#ifndef _RIVE_BINDINGS_V1_H_',
'#define _RIVE_BINDINGS_V1_H_',
'// %s' % GENERATED_NOTE,
'//',
'// The wasm import contract a script module compiles against: every',
'// native contact is one of these calls, objects cross as u32',
'// handles. Any language that can declare wasm imports can target',
'// this surface directly.',
'// clang-format off',
'',
'#include <stdint.h>',
'',
'#define RIVE_BINDINGS_ABI_VERSION 1',
'',
'#define RIVE_BINDING_IMPORT(module, name) \\',
' __attribute__((import_module(module), import_name(name)))',
'',
'#ifdef __cplusplus',
'extern "C" {',
'#endif',
'',
]
lines += pod_struct_lines()
for namespace in NAMESPACES:
lines.append('// %s' % namespace['module'])
for op in namespace['ops']:
lines.append('RIVE_BINDING_IMPORT("%s", "%s")' %
(namespace['module'], op['name']))
params = ', '.join(c_params(op['params'])) or 'void'
lines.append('%s %s(%s);' %
(ret_ctype(op), c_symbol(namespace, op), params))
lines.append('')
lines += [
'#ifdef __cplusplus',
'}',
'#endif',
'',
'#endif',
'',
]
return '\n'.join(lines)
def emit_host():
lines = [
'// %s' % GENERATED_NOTE,
'//',
'// Included inside wasm_scripting_vm.cpp\'s anonymous namespace before',
'// the native implementations; the declarations pin each hand-written',
'// impl core to the contract signature (a mismatch makes the table',
'// cast ambiguous). The WAMR wrappers adapt exec_env to the VM the',
'// cores share with the browser lane\'s generated C shims.',
'// clang-format off',
'',
]
lines += pod_struct_lines()
for namespace in NAMESPACES:
for op in namespace['ops']:
params = ['WasmScriptingVM* vm'] + host_params(op['params'])
lines.append('%s %sImpl(%s);' %
(ret_ctype(op), impl_name(namespace, op),
', '.join(params)))
lines.append('')
for namespace in NAMESPACES:
for op in namespace['ops']:
name = impl_name(namespace, op)
params = ['wasm_exec_env_t env'] + host_params(op['params'])
args = ['vmFromEnv(env)'] + host_args(op['params'])
call = '%sImpl(%s)' % (name, ', '.join(args))
lines.append('%s %s(%s)' %
(ret_ctype(op), name, ', '.join(params)))
lines.append('{')
lines.append(' %s%s;' %
('' if op['ret'] is None else 'return ', call))
lines.append('}')
lines.append('')
for namespace in NAMESPACES:
table = 'k%sNatives' % camel(namespace['short']).capitalize()
lines.append('NativeSymbol %s[] = {' % table)
for op in namespace['ops']:
lines.append(' {"%s", (void*)%s, "%s", nullptr},' %
(op['name'], impl_name(namespace, op),
wamr_signature(op)))
lines.append('};')
lines.append('')
lines.append('inline bool registerRiveBindingNatives()')
lines.append('{')
checks = []
for namespace in NAMESPACES:
table = 'k%sNatives' % camel(namespace['short']).capitalize()
checks.append(
'wasm_runtime_register_natives(\n'
' "%s",\n'
' %s,\n'
' sizeof(%s) / sizeof(NativeSymbol))' %
(namespace['module'], table, table))
lines.append(' return ' + ' &&\n '.join(checks) + ';')
lines.append('}')
lines.append('')
return '\n'.join(lines)
def emit_web_host():
lines = [
'// %s' % GENERATED_NOTE,
'//',
'// Browser-lane host entry points: one exported C symbol per op,',
'// taking the owning VM as a u32 handle plus the native-convention',
'// args (pointers are librive-heap pointers, exactly what',
'// web/rive_module_imports.mjs stages into). Included at the end of',
'// wasm_scripting_vm.cpp so the impl cores are visible; expands to',
'// nothing off emscripten.',
'// clang-format off',
'',
'#ifdef __EMSCRIPTEN__',
'',
'#include <emscripten/emscripten.h>',
'',
'extern "C" {',
'',
]
for namespace in NAMESPACES:
for op in namespace['ops']:
sym = 'rive_web_' + namespace['short'] + '_' + op['name']
params = ['uint32_t vmHandle'] + host_params(op['params'])
args = ['(WasmScriptingVM*)(uintptr_t)vmHandle'] + \
host_args(op['params'])
call = '%sImpl(%s)' % (impl_name(namespace, op), ', '.join(args))
lines.append('EMSCRIPTEN_KEEPALIVE')
lines.append('%s %s(%s)' % (ret_ctype(op), sym, ', '.join(params)))
lines.append('{')
lines.append(' %s%s;' %
('' if op['ret'] is None else 'return ', call))
lines.append('}')
lines += [
'',
'} // extern "C"',
'',
'#endif // __EMSCRIPTEN__',
'',
]
return '\n'.join(lines)
def emit_stubs():
lines = [
'// %s' % GENERATED_NOTE,
'//',
'// Test lane import namespaces: handle returning ops mint counters,',
'// void ops are no-ops, hook ops delegate to the harness.',
'export function createRiveStubs(hooks = {}) {',
' let nextHandle = 1;',
' const mint = () => nextHandle++;',
' const noop = () => {};',
' const zero = () => 0;',
' return {',
]
for namespace in NAMESPACES:
lines.append(' %s: {' % namespace['module'])
for op in namespace['ops']:
if op['stub'] == 'hook':
value = 'hooks.%s ?? noop' % camel(op['name'])
elif op['ret'] == 'f32':
value = 'zero'
elif op['ret'] is not None:
value = 'mint'
else:
value = 'noop'
lines.append(' %s: %s,' % (op['name'], value))
lines.append(' },')
lines += [
' };',
'}',
'',
]
return '\n'.join(lines)
def as_params(params):
out = []
for p in params:
kind = p['kind']
if kind in ('u32', 'handle'):
out.append('%s: u32' % p['name'])
elif kind == 'i32':
out.append('%s: i32' % p['name'])
elif kind == 'f32':
out.append('%s: f32' % p['name'])
elif kind in ('buf', 'mutbuf', 'str'):
out.append('%s: usize' % p['name'])
out.append('%s: u32' % p['count'])
elif kind == 'addr':
out.append('%s: usize' % p['name'])
elif kind == 'pod':
out.append('%s: usize' % p['name'])
out.append('%sByteCount: u32' % p['name'])
else:
raise ValueError(kind)
return out
def as_ret(op):
if op['ret'] is None:
return 'void'
return 'f32' if op['ret'] == 'f32' else 'u32'
def emit_as():
lines = [
'// %s' % GENERATED_NOTE,
'//',
'// The wasm import contract for AssemblyScript modules, under the',
'// same symbols as the C header: one ambient namespace whose',
'// members carry the import namespace and wire name (which may be',
'// a reserved word) via @external. The unmanaged POD classes',
'// mirror the descriptor structs byte for byte. Ergonomic wrappers',
'// live beside this file, never in it.',
'',
]
for p in PODS:
lines.append('@unmanaged')
lines.append('export class %s {' % pod_ctype(p['name']))
for field in p['fields']:
astype = 'f32' if field['kind'] == 'f32' else 'u32'
lines.append(' %s: %s;' % (field['name'], astype))
lines.append('}')
lines.append('export const %s_size: u32 = %d;' %
(pod_ctype(p['name']), len(p['fields']) * 4))
lines.append('')
lines.append('export declare namespace abi {')
for namespace in NAMESPACES:
lines.append(' // %s' % namespace['module'])
for op in namespace['ops']:
params = ', '.join(as_params(op['params']))
lines.append(' @external("%s", "%s")' %
(namespace['module'], op['name']))
lines.append(' function %s(%s): %s;' %
(c_symbol(namespace, op), params, as_ret(op)))
lines.append('}')
lines.append('')
return '\n'.join(lines)
def web_op(namespace, op):
"""One import-object entry: scalars forward, memory-crossing params
stage through the host heap."""
sym = c_symbol(namespace, op)
staged = [p for p in op['params']
if p['kind'] in ('str', 'buf', 'mutbuf', 'pod')]
addr = [p for p in op['params'] if p['kind'] == 'addr']
if addr:
return (' %s: unsupported(%r),' % (op['name'], sym), False)
if not staged:
return (' %s: calls.%s,' % (op['name'], sym), False)
args = []
pre = []
post = []
js_params = []
for p in op['params']:
kind = p['kind']
name = camel(p['name'])
if kind in ('u32', 'i32', 'f32', 'handle'):
js_params.append(name)
args.append(name)
elif kind in ('str', 'buf', 'mutbuf'):
count = camel(p['count'])
js_params.append(name)
js_params.append(count)
pre.append('const %s_p = stageIn(%s, %s);' % (name, name, count))
args.append('%s_p' % name)
args.append(count)
if kind == 'mutbuf':
post.append('copyOut(%s_p, %s, %s);' % (name, name, count))
elif kind == 'pod':
js_params.append(name)
js_params.append('%sByteCount' % name)
pre.append('const %s_p = stageIn(%s, %sByteCount);' %
(name, name, name))
args.append('%s_p' % name)
args.append('%sByteCount' % name)
else:
raise ValueError(kind)
body = [' %s: (%s) => {' % (op['name'], ', '.join(js_params)),
' stackReset();']
body += [' %s' % line for line in pre]
call = 'calls.%s(%s)' % (sym, ', '.join(args))
if op['ret'] is not None:
body.append(' const ret = %s;' % call)
else:
body.append(' %s;' % call)
body += [' %s' % line for line in post]
if op['ret'] is not None:
body.append(' return ret;')
body.append(' },')
return ('\n'.join(body), True)
def emit_web_imports():
lines = [
'// %s' % GENERATED_NOTE,
'//',
'// The runtime import object for script modules executing on the',
'// browser engine. Scalars forward straight into the host call',
'// table; str/buf/pod params stage through the host heap because',
'// the host cannot dereference module memory. `addr` ops throw',
'// until their per-op audit lands.',
'//',
'// host contract:',
'// calls per-op host entry points (native-pointer convention)',
'// heapU8() Uint8Array over the host heap (refresh per call)',
'// stackAlloc(size) -> host heap ptr valid until stackReset()',
'// stackReset()',
'// moduleMemory: late-bound accessor for the script module memory,',
'// set after instantiation.',
'export function createRiveModuleImports(host, moduleMemory) {',
' const calls = host.calls;',
' const stackReset = host.stackReset;',
' const stageIn = (ptr, len) => {',
' const dst = host.stackAlloc(len);',
' host.heapU8().set(',
' new Uint8Array(moduleMemory().buffer, ptr, len),',
' dst',
' );',
' return dst;',
' };',
' const copyOut = (src, ptr, len) => {',
' new Uint8Array(moduleMemory().buffer, ptr, len).set(',
' host.heapU8().subarray(src, src + len)',
' );',
' };',
' const unsupported = (name) => () => {',
" throw new Error('web lane does not carry ' + name + ' yet');",
' };',
' return {',
]
for namespace in NAMESPACES:
lines.append(' %s: {' % namespace['module'])
for op in namespace['ops']:
text, _ = web_op(namespace, op)
lines.append(text)
lines.append(' },')
lines += [
' };',
'}',
'',
]
return '\n'.join(lines)
def main():
check_all_ops()
outputs = {
HEADER_PATH: emit_header(),
HOST_PATH: emit_host(),
WEB_HOST_PATH: emit_web_host(),
STUBS_PATH: emit_stubs(),
WEB_PATH: emit_web_imports(),
AS_PATH: emit_as(),
}
check = '--check' in sys.argv
stale = []
for path, content in outputs.items():
current = path.read_text() if path.exists() else None
if current == content:
continue
if check:
stale.append(path)
else:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)
print('wrote %s' % path.relative_to(PACKAGES))
if stale:
for path in stale:
print('stale: %s' % path.relative_to(PACKAGES))
print('run src/wasm/idl/generate.py to regenerate')
return 1
if check:
print('binding IDL projections up to date')
return 0
if __name__ == '__main__':
sys.exit(main())