| #!/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()) |