| #!/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' | |
| ELEM_SIZES = { | |
| 'char': 1, | |
| 'uint8_t': 1, | |
| 'uint16_t': 2, | |
| 'int32_t': 4, | |
| 'uint32_t': 4, | |
| 'float': 4, | |
| } | |
| 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/animascript/std/rive/bindings.as' | |
| AS_NATIVES_PATH = PACKAGES / 'rasc/animascript/std/rive/natives.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 pascal(snake): | |
| out = camel(snake) | |
| return out[:1].upper() + out[1:] | |
| # Bytes per element of a buffer: a scalar, or a pod of u32 and f32 fields. | |
| def elem_size(elem): | |
| if elem in ELEM_SIZES: | |
| return ELEM_SIZES[elem] | |
| for p in PODS: | |
| if pod_ctype(p['name']) == elem or p['name'] == elem: | |
| return 4 * len(p['fields']) | |
| raise ValueError(elem) | |
| 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 == 'f64': | |
| out.append('double ' + 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 == 'f64': | |
| out.append('double ' + 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', 'f64', 'addr'): | |
| out.append(p['name']) | |
| elif kind == 'str': | |
| out.append('%sUtf8.data()' % p['name']) | |
| out.append('%sUtf8.size()' % p['name']) | |
| elif kind in ('buf', 'mutbuf'): | |
| 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 host_string_prologue(params): | |
| # A module that passes its strings as UTF-16 has them transcoded here, | |
| # so the impl cores only ever see UTF-8. | |
| return [' WasmStringArg %sUtf8(vm, %s, %s);' % | |
| (p['name'], p['name'], p['count']) | |
| for p in params if p['kind'] == 'str'] | |
| # A count named in bytes is WAMR's own `~` length; any other buffer count is | |
| # in elements, so WAMR has checked only count bytes of it. | |
| def count_is_bytes(p): | |
| return p['count'].endswith('ByteCount') or p['count'].endswith('Bytes') | |
| def host_bounds_prologue(op): | |
| # Widens WAMR's check to the whole buffer; a failed check traps. | |
| lines = [] | |
| for p in op['params']: | |
| if p['kind'] not in ('buf', 'mutbuf') or count_is_bytes(p): | |
| continue | |
| size = elem_size(p['elem']) | |
| if size == 1: | |
| continue | |
| lines += [' if (%s != 0 && !wasm_runtime_validate_native_addr(' | |
| 'wasm_runtime_get_module_inst(env), (void*)%s, ' | |
| '(uint64_t)%s * %d))' % (p['count'], p['name'], p['count'], | |
| size), | |
| ' {', | |
| ' return%s;' % ('' if op['ret'] is None else ' {}'), | |
| ' }'] | |
| return lines | |
| def guarded(op, lines): | |
| if not op.get('guard'): | |
| return lines | |
| return ['#ifdef ' + op['guard']] + lines + ['#endif'] | |
| 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 == 'f64': | |
| sig += 'F' | |
| elif kind in ('buf', 'mutbuf', 'str', 'pod'): | |
| sig += '*~' | |
| else: | |
| raise ValueError(kind) | |
| sig += ')' | |
| if op['ret'] == 'f32': | |
| sig += 'f' | |
| elif op['ret'] == 'f64': | |
| sig += 'F' | |
| elif op['ret'] is not None: | |
| sig += 'i' | |
| return sig | |
| RET_CTYPES = {'f32': 'float', 'f64': 'double'} | |
| def ret_ctype(op): | |
| if op['ret'] is None: | |
| return 'void' | |
| return RET_CTYPES.get(op['ret'], '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 += guarded(op, ['%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 = ['vm'] + host_args(op['params']) | |
| call = '%sImpl(%s)' % (name, ', '.join(args)) | |
| body = ['%s %s(%s)' % (ret_ctype(op), name, ', '.join(params)), | |
| '{', | |
| ' WasmScriptingVM* vm = %s(env);' % | |
| ('bootVmFromEnv' if op['boot'] else 'vmFromEnv')] | |
| if op['boot']: | |
| assert op['ret'] is None, op['name'] | |
| body += [' if (vm == nullptr)', | |
| ' {', | |
| ' wasm_runtime_set_exception(' | |
| 'wasm_runtime_get_module_inst(env), "%s");' % | |
| op['boot'], | |
| ' return;', | |
| ' }'] | |
| body += host_bounds_prologue(op) | |
| body += host_string_prologue(op['params']) | |
| body += [' %s%s;' % | |
| ('' if op['ret'] is None else 'return ', call), | |
| '}'] | |
| lines += guarded(op, body) | |
| lines.append('') | |
| for namespace in NAMESPACES: | |
| table = 'k%sNatives' % pascal(namespace['short']) | |
| lines.append('NativeSymbol %s[] = {' % table) | |
| for op in namespace['ops']: | |
| attachment = ('(void*)&wasm_runtime_rive_leaf_native' | |
| if op.get('leaf') else 'nullptr') | |
| lines += guarded(op, [' {"%s", (void*)%s, "%s", %s},' % | |
| (op['name'], impl_name(namespace, op), | |
| wamr_signature(op), attachment)]) | |
| lines.append('};') | |
| lines.append('') | |
| lines.append('inline bool registerRiveBindingNatives()') | |
| lines.append('{') | |
| checks = [] | |
| for namespace in NAMESPACES: | |
| table = 'k%sNatives' % pascal(namespace['short']) | |
| 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 = ['vm'] + host_args(op['params']) | |
| call = '%sImpl(%s)' % (impl_name(namespace, op), ', '.join(args)) | |
| body = ['EMSCRIPTEN_KEEPALIVE', | |
| '%s %s(%s)' % (ret_ctype(op), sym, ', '.join(params)), | |
| '{', | |
| ' auto vm = (WasmScriptingVM*)(uintptr_t)vmHandle;'] | |
| body += host_string_prologue(op['params']) | |
| body += [' %s%s;' % | |
| ('' if op['ret'] is None else 'return ', call), | |
| '}'] | |
| lines += guarded(op, body) | |
| 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['stub'] == 'zero' or op['ret'] in ('f32', 'f64'): | |
| 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 == 'f64': | |
| out.append('%s: f64' % 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 op['ret'] if op['ret'] in ('f32', 'f64') else 'u32' | |
| def emit_as(): | |
| lines = [ | |
| '// %s' % GENERATED_NOTE, | |
| '//', | |
| '// The wasm import contract for AnimaScript 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) | |
| # Projection 5: one typed class per namespace over the handle helpers. A | |
| # namespace whose ops lead with a handle of its own kind (path, paint, ...) | |
| # wraps that handle; the rest are static. Strings encode to UTF-8 on the | |
| # way in; buffers stay raw address plus count for the ergonomic layer to | |
| # type. | |
| # Handle tag -> HandleKind in handles.as, for the owning factories below. | |
| AS_HANDLE_KINDS = { | |
| 'path': 'path', | |
| 'paint': 'paint', | |
| 'shader': 'renderShader', | |
| 'buffer': 'renderBuffer', | |
| 'mesh_instances': 'meshInstances', | |
| 'image': 'image', | |
| 'artboard': 'artboard', | |
| 'measure': 'measure', | |
| 'font': 'font', | |
| 'text': 'text', | |
| } | |
| # Static ops that mint a handle of the namespace's own kind; their wrappers | |
| # own the handle through the frame collector's mirror. | |
| AS_OWNING_FACTORIES = { | |
| ('path', 'new'), | |
| ('paint', 'new'), | |
| ('buffer', 'new'), | |
| ('mesh_instances', 'new'), | |
| ('image', 'from_asset'), | |
| ('shader', 'linear'), | |
| ('shader', 'radial'), | |
| ('measure', 'path_new'), | |
| ('measure', 'contours_new'), | |
| ('font', 'from_asset'), | |
| ('font', 'decode'), | |
| ('font', 'with_options'), | |
| ('text', 'new'), | |
| } | |
| def as_class_name(namespace): | |
| return pascal(namespace['short']) + 'Native' | |
| def as_self_tag(namespace): | |
| for op in namespace['ops']: | |
| params = op['params'] | |
| if params and params[0]['kind'] == 'handle' and \ | |
| params[0]['tag'] == namespace['short']: | |
| return namespace['short'] | |
| return None | |
| def as_wrapper_params(params): | |
| """(declaration list, call argument list, needs unsafe).""" | |
| decl = [] | |
| args = [] | |
| unsafe = False | |
| for p in params: | |
| kind = p['kind'] | |
| name = camel(p['name']) | |
| if kind in ('u32', 'handle'): | |
| decl.append('%s: u32' % name) | |
| args.append(name) | |
| elif kind == 'i32': | |
| decl.append('%s: i32' % name) | |
| args.append(name) | |
| elif kind == 'f32': | |
| decl.append('%s: f32' % name) | |
| args.append(name) | |
| elif kind == 'f64': | |
| decl.append('%s: f64' % name) | |
| args.append(name) | |
| elif kind == 'str': | |
| decl.append('%s: string' % name) | |
| args.append('changetype<usize>(%s)' % name) | |
| args.append('<u32>(%s.length << 1)' % name) | |
| unsafe = True | |
| elif kind in ('buf', 'mutbuf'): | |
| count = camel(p['count']) | |
| decl.append('%s: usize' % name) | |
| decl.append('%s: u32' % count) | |
| args.append(name) | |
| args.append(count) | |
| elif kind == 'addr': | |
| decl.append('%s: usize' % name) | |
| args.append(name) | |
| elif kind == 'pod': | |
| decl.append('%s: usize' % name) | |
| decl.append('%sByteCount: u32' % name) | |
| args.append(name) | |
| args.append('%sByteCount' % name) | |
| else: | |
| raise ValueError(kind) | |
| return decl, args, unsafe | |
| def as_string_prologue(params): | |
| # Strings cross as they are, UTF-16, announced by __riveUtf16Strings. | |
| return [] | |
| def emit_as_natives(): | |
| lines = [ | |
| '// %s' % GENERATED_NOTE, | |
| '//', | |
| '// Typed classes over the import contract, one per namespace:', | |
| '// instance methods where the namespace owns the leading handle,', | |
| '// statics elsewhere. Strings cross as they sit, UTF-16; buffers stay raw', | |
| '// address plus count so the ergonomic layer picks the element', | |
| '// type. Owning factories mirror their handle for the frame', | |
| '// collector; adopt() wraps a host owned handle without ownership.', | |
| '', | |
| 'import { abi } from "./bindings";', | |
| 'import { HandleKind, mirrorAddOwned, mirrorRemove } from "./handles";', | |
| '', | |
| ] | |
| for namespace in NAMESPACES: | |
| short = namespace['short'] | |
| cls = as_class_name(namespace) | |
| self_tag = as_self_tag(namespace) | |
| lines.append('// %s' % namespace['module']) | |
| lines.append('export class %s {' % cls) | |
| if self_tag is not None: | |
| lines.append(' handle: u32;') | |
| lines.append('') | |
| lines.append(' constructor(handle: u32) {') | |
| lines.append(' this.handle = handle;') | |
| lines.append(' }') | |
| lines.append('') | |
| lines.append(' static adopt(handle: u32): %s {' % cls) | |
| lines.append(' return new %s(handle);' % cls) | |
| lines.append(' }') | |
| lines.append('') | |
| for op in namespace['ops']: | |
| params = op['params'] | |
| is_method = self_tag is not None and params and \ | |
| params[0]['kind'] == 'handle' and params[0]['tag'] == self_tag | |
| rest = params[1:] if is_method else params | |
| decl, args, unsafe = as_wrapper_params(rest) | |
| if is_method: | |
| args = ['this.handle'] + args | |
| call = 'abi.%s(%s)' % (c_symbol(namespace, op), ', '.join(args)) | |
| name = camel(op['name']) | |
| owning = (short, op['name']) in AS_OWNING_FACTORIES | |
| if op['name'] == 'release' and is_method: | |
| lines.append(' release(): void {') | |
| lines.append(' if (this.handle == 0) {') | |
| lines.append(' return;') | |
| lines.append(' }') | |
| lines.append(' mirrorRemove(this.handle);') | |
| lines.append(' %s;' % call) | |
| lines.append(' this.handle = 0;') | |
| lines.append(' }') | |
| lines.append('') | |
| continue | |
| ret = as_ret(op) | |
| if owning: | |
| ret = cls | |
| head = ' %s%s%s(%s): %s {' % ( | |
| 'unsafe ' if unsafe or owning else '', | |
| '' if is_method else 'static ', | |
| name, | |
| ', '.join(decl), | |
| ret) | |
| lines.append(head) | |
| lines += as_string_prologue(rest) | |
| if owning: | |
| lines.append(' let handle = %s;' % call) | |
| lines.append(' let wrapper = new %s(handle);' % cls) | |
| lines.append(' mirrorAddOwned(handle, HandleKind.%s, ' | |
| 'wrapper);' % AS_HANDLE_KINDS[short]) | |
| lines.append(' return wrapper;') | |
| elif op['ret'] is None: | |
| lines.append(' %s;' % call) | |
| else: | |
| lines.append(' return %s;' % call) | |
| lines.append(' }') | |
| lines.append('') | |
| if lines[-1] == '': | |
| lines.pop() | |
| 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', 'f64', '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) | |
| # Staging copies bytes. | |
| size = (1 if kind == 'str' or count_is_bytes(p) else | |
| elem_size(p['elem'])) | |
| byte_count = count if size == 1 else '%s * %d' % (count, size) | |
| pre.append('const %s_p = stageIn(%s, %s);' % | |
| (name, name, byte_count)) | |
| args.append('%s_p' % name) | |
| args.append(count) | |
| if kind == 'mutbuf': | |
| post.append('copyOut(%s_p, %s, %s);' % | |
| (name, name, byte_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(), | |
| AS_NATIVES_PATH: emit_as_natives(), | |
| } | |
| 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()) |