| #!/usr/bin/env python3 | |
| """Projects bindings.py into its generated files; --check diffs instead of | |
| writing so test lanes can gate on drift.""" | |
| import hashlib | |
| import json | |
| import re | |
| import sys | |
| import textwrap | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).resolve().parent)) | |
| from bindings import (FEATURES, HOST_FEATURES, LANE_IMPORTS, LANES, | |
| NAMESPACES, PODS) | |
| from lane_layout import (BLOCK_BUDGET_MS, LANE_EXPORTS, LANE_KINDS, | |
| LANE_LAYOUT, LANE_NAMES_SUFFIX, LANE_STATES, | |
| LANE_TEXTS, WAIT_ANSWERS, WAIT_SETTLES) | |
| from shared_layout import SHARED_LAYOUT, SHARED_TEXTS | |
| 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' | |
| CONTRACT_PATH = PACKAGES / 'runtime/src/wasm/idl/contract_v1.txt' | |
| 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' | |
| WEB_RUNNER_PATH = WEB_PATH.parent / 'rive_script_runner.mjs' | |
| LANE_IMPORTS_HEADER_PATH = \ | |
| PACKAGES / 'runtime/src/wasm/wasm_lane_imports_gen.hpp' | |
| RASC_LANE_IMPORTS_PATH = \ | |
| PACKAGES / 'rasc/animascript/src/codegen/lane_imports_gen.h' | |
| WEB_LANE_IMPORTS_PATH = WEB_PATH.parent / 'rive_lane_imports.mjs' | |
| SHARED_LAYOUT_HEADER_PATH = \ | |
| PACKAGES / 'runtime/include/rive/wasm/wasm_shared_layout_gen.hpp' | |
| LANE_LAYOUT_HEADER_PATH = \ | |
| PACKAGES / 'runtime/include/rive/wasm/wasm_lane_layout_gen.hpp' | |
| AS_LANE_CODES_PATH = PACKAGES / 'rasc/animascript/std/rive/lane_codes.as' | |
| AS_RUNTIME_PATH = PACKAGES / 'rasc/animascript/std/rive/runtime.as' | |
| # The layout groups the std reads: natives' answers and a job's reuse cap. | |
| AS_LAYOUT_GROUPS = ('Join', 'JoinAny', 'Start', 'Create', 'RegionLock', | |
| 'Shared', 'RingWrite', 'Reuse', 'Parallel') | |
| AS_STD_PATH = AS_LANE_CODES_PATH.parent | |
| WEB_SHARED_LAYOUT_PATH = WEB_PATH.parent / 'rive_shared_layout.mjs' | |
| # The lane modules the runner and the NRDP worklet both inline, each after | |
| # what it imports. | |
| WEB_LANE_PATHS = [WEB_LANE_IMPORTS_PATH, | |
| WEB_SHARED_LAYOUT_PATH, | |
| WEB_PATH.parent / 'rive_shared_lane.mjs', | |
| WEB_PATH.parent / 'rive_lane.mjs'] | |
| WEB_WORKLET_PATH = WEB_PATH.parent / 'rive_audio_worklet.mjs' | |
| WEB_NRDP_WORKLET_PATH = WEB_PATH.parent / 'rive_audio_nrdp_worklet.mjs' | |
| WEB_NRDP_BUNDLE_PATH = WEB_PATH.parent / 'rive_audio_worklet_nrdp.js' | |
| WEB_PRE_JS_PATH = WEB_PATH.parent / 'rive_scripting_pre.js' | |
| WEB_WORKLET_PRE_JS_PATH = WEB_PATH.parent / 'rive_audio_worklet_pre.js' | |
| # The hand written web modules, whose uses decide which layout constants the | |
| # generated ones export. | |
| WEB_PARALLEL_PATH = WEB_PATH.parent / 'rive_parallel.mjs' | |
| WEB_SOURCE_PATHS = [WEB_RUNNER_PATH, | |
| WEB_PARALLEL_PATH, | |
| WEB_PATH.parent / 'rive_shared_lane.mjs', | |
| WEB_PATH.parent / 'rive_lane.mjs', | |
| WEB_WORKLET_PATH, | |
| WEB_NRDP_WORKLET_PATH] | |
| 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 web_symbol(namespace, op): | |
| return 'rive_web_' + 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('%s%s* %s' % ('' if p['writes'] else 'const ', | |
| 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) | |
| # Lanes pass handle 1, which only bootVmFromHandle reads as no VM. | |
| if not op['boot'] and (namespace['lanes'] or op['lanes']): | |
| raise SystemExit('%s.%s is lane reachable but not a boot op' % | |
| (namespace['module'], op['name'])) | |
| # The std's Feature enum carries the docs, so it is checked, not written. | |
| def check_features(): | |
| source = AS_RUNTIME_PATH.read_text() | |
| body = re.search(r'export enum Feature \{(.*?)\n\}', source, re.S) | |
| named = re.findall(r'^\s*(\w+) = (\d+),', body.group(1) if body else '', | |
| re.M) | |
| if named != [(name, str(bit)) for bit, name in enumerate(FEATURES)]: | |
| raise SystemExit('%s: enum Feature must list %s, each = its bit' % | |
| (AS_RUNTIME_PATH.relative_to(PACKAGES), FEATURES)) | |
| 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', | |
| '', | |
| '// The import surface below, digested. A wasm module built against', | |
| '// one contract and run against another links its imports to', | |
| '// nothing, so a build that carries this can be told apart from one', | |
| '// that predates a change to it.', | |
| '#define RIVE_BINDINGS_V1_CONTRACT "%s"' % contract_digest(), | |
| '', | |
| '// The bits rive_rt_features answers.', | |
| ] + ['#define RIVE_HOST_FEATURE_%s (1u << %d)' % | |
| (re.sub(r'(?<!^)(?=[A-Z])', '_', name).upper(), bit) | |
| for bit, name in enumerate(FEATURES)] + [ | |
| '', | |
| '#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('') | |
| # The browser lane shares the cores above and has no WAMR to adapt to. | |
| lines += [ | |
| '#ifndef __EMSCRIPTEN__', | |
| '// What rt.features answers.', | |
| 'constexpr uint32_t kHostFeatures = 0x%xu;' % HOST_FEATURES, | |
| '', | |
| ] | |
| 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 isinstance(op['boot'], str): | |
| assert op['ret'] is None, op['name'] | |
| # A string argument travels with the trap, so a parallel | |
| # call member's throw reaches the call that ran it. | |
| texts = ['%s, %s' % (p['name'], p['count']) | |
| for p in op['params'] if p['kind'] == 'str'][:1] | |
| body += [' if (vm == nullptr)', | |
| ' {', | |
| ' vmlessTrap(env, "%s"%s);' % | |
| (op['boot'], ''.join(', ' + t for t in texts)), | |
| ' 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('#endif') | |
| lines.append('') | |
| return '\n'.join(lines) | |
| # The ops the browser lane serves from librive, in the order its host call | |
| # table and the page's binder share. | |
| def web_host_ops(): | |
| return [(namespace, op) for namespace in NAMESPACES | |
| for op in namespace['ops'] if op['web_js'] is None] | |
| def emit_web_host(): | |
| lines = [ | |
| '// %s' % GENERATED_NOTE, | |
| '//', | |
| '// Browser-lane host entry points: one C function per op, found by', | |
| '// the page through the kRiveWebCalls table, 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, op in web_host_ops(): | |
| sym = web_symbol(namespace, op) | |
| params = ['uint32_t vmHandle'] + host_params(op['params']) | |
| args = ['vm'] + host_args(op['params']) | |
| call = '%sImpl(%s)' % (impl_name(namespace, op), ', '.join(args)) | |
| body = ['%s %s(%s)' % (ret_ctype(op), sym, ', '.join(params)), | |
| '{', | |
| ' WasmScriptingVM* vm = %s(vmHandle);' % | |
| ('bootVmFromHandle' if op['boot'] else 'vmFromHandle')] | |
| body += host_string_prologue(op['params']) | |
| body += [' %s%s;' % | |
| ('' if op['ret'] is None else 'return ', call), | |
| '}'] | |
| lines += guarded(op, body) | |
| # One export for the table keeps the op names out of the binary; a | |
| # missing op's null entry reads as unlinked on the page. | |
| lines += ['', 'static void* const kRiveWebCalls[] = {'] | |
| for namespace, op in web_host_ops(): | |
| entry = ' (void*)%s,' % web_symbol(namespace, op) | |
| lines += (['#ifdef ' + op['guard'], entry, '#else', ' nullptr,', | |
| '#endif'] if op['guard'] else [entry]) | |
| lines += [ | |
| ' (void*)rive_web_console,', | |
| ' (void*)rive_web_trace,', | |
| '};', | |
| '', | |
| 'EMSCRIPTEN_KEEPALIVE', | |
| 'void* const* rive_web_calls() { return kRiveWebCalls; }', | |
| '', | |
| '} // 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['web_js'] is not None: | |
| value = '() => %s' % op['web_js'] | |
| elif 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: | |
| if not namespace['as_class']: | |
| continue | |
| 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 written_bytes(written, count, size, byte_count): | |
| """The JS expression for the bytes a mutbuf's host wrote, from ret.""" | |
| scale = '' if size == 1 else ' * %d' % size | |
| if written is None: | |
| return byte_count | |
| if written == 'whole': | |
| return 'ret !== 0 ? %s : 0' % byte_count | |
| if written == 'size': | |
| return 'fitted(ret, %s)%s' % (count, scale) | |
| if written == 'size_plus_one': | |
| return 'fitted(ret - 1, %s)%s' % (count, scale) | |
| raise ValueError(written) | |
| def web_op(namespace, op): | |
| """One import-object entry: scalars forward, memory-crossing params | |
| stage through the host heap.""" | |
| sym = c_symbol(namespace, op) | |
| if op['web_js'] is not None: | |
| return " '%s': () => %s," % (op['name'], op['web_js']) | |
| staged = [p for p in op['params'] | |
| if p['kind'] in ('str', 'buf', 'mutbuf', 'pod')] | |
| # addr params stay module addresses: the impl core reads them through | |
| # the VM's resolveModulePtr seam, which copies on the web. | |
| if not staged: | |
| return " '%s': calls.%s," % (op['name'], sym) | |
| args = [] | |
| decls = [] | |
| pre = [] | |
| post = [] | |
| release = [] | |
| 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 == 'addr': | |
| 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) | |
| written = p.get('written') | |
| decls.append('let %s_p = 0;' % name) | |
| pre.append('%s_p = %s(%s, %s);' % | |
| (name, 'stageOut' if written else 'stageIn', name, | |
| byte_count)) | |
| release.append('unstage(%s_p, %s);' % (name, byte_count)) | |
| args.append('%s_p' % name) | |
| args.append(count) | |
| if kind == 'mutbuf': | |
| post.append('copyOut(%s_p, %s, %s);' % | |
| (name, name, written_bytes(written, count, size, | |
| byte_count))) | |
| elif kind == 'pod': | |
| js_params.append(name) | |
| js_params.append('%sByteCount' % name) | |
| decls.append('let %s_p = 0;' % name) | |
| pre.append('%s_p = stageIn(%s, %sByteCount);' % (name, name, name)) | |
| release.append('unstage(%s_p, %sByteCount);' % (name, name)) | |
| args.append('%s_p' % name) | |
| args.append('%sByteCount' % name) | |
| else: | |
| raise ValueError(kind) | |
| body = [" '%s': (%s) => {" % (op['name'], ', '.join(js_params)), | |
| ' const mark = stackSave();'] | |
| # Staged inside the try so a later param that throws frees the earlier. | |
| body += [' %s' % line for line in decls] | |
| body.append(' try {') | |
| body += [' %s' % line for line in pre] | |
| call = 'calls.%s(%s)' % (sym, ', '.join(args)) | |
| if op['ret'] is None: | |
| body.append(' %s;' % call) | |
| body += [' %s' % line for line in post] | |
| elif not post: | |
| body.append(' return %s;' % call) | |
| else: | |
| body.append(' const ret = %s;' % call) | |
| body += [' %s' % line for line in post] | |
| body.append(' return ret;') | |
| body.append(' } finally {') | |
| body += [' %s' % line for line in reversed(release)] | |
| body.append(' stackRestore(mark);') | |
| body.append(' }') | |
| body.append(' },') | |
| return '\n'.join(body) | |
| def emit_web_imports(): | |
| lines = [ | |
| '// %s' % GENERATED_NOTE, | |
| '', | |
| "import { managedLength } from './rive_shared_lane.mjs';", | |
| '', | |
| '// 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` params pass', | |
| '// through as module addresses, which the host reads through its', | |
| '// own memory seam.', | |
| '//', | |
| '// host contract:', | |
| '// calls per-op host entry points (native-pointer convention)', | |
| '// heapU8() Uint8Array over the host heap (refresh per call)', | |
| '// stackSave() -> mark', | |
| '// stackAlloc(size) -> host heap ptr valid until its mark is restored', | |
| '// stackRestore(mark)', | |
| '// malloc(size) -> host heap ptr, free(ptr)', | |
| '// counters optional bridge tallies, counted while counters.on', | |
| '// Staging nests: a host op may call back into the module, whose', | |
| '// imports stage again above the outer call. Buffers too big for the', | |
| '// host stack stage on its heap instead.', | |
| '// moduleMemory: late-bound accessor for the script module memory,', | |
| '// set after instantiation.', | |
| '// only: the one namespace to build, as a lane needs, or null for all.', | |
| '//', | |
| '// Names that cross to wasm are quoted so a closure compiled host', | |
| '// cannot rename them.', | |
| 'export function createRiveModuleImports(host, moduleMemory, only = null) {', | |
| ' const want = (module) => only === null || only === module;', | |
| ' const calls = host.calls;', | |
| ' const stackSave = host.stackSave;', | |
| ' const stackRestore = host.stackRestore;', | |
| ' const kStackStageLimit = 4096;', | |
| ' const counters = host.counters ?? { on: false };', | |
| ' // Rebuilt only when the module memory grows.', | |
| ' let moduleBytes = null;', | |
| ' const moduleU8 = () => {', | |
| ' const buffer = moduleMemory().buffer;', | |
| ' if (moduleBytes === null || moduleBytes.buffer !== buffer) {', | |
| ' moduleBytes = new Uint8Array(buffer);', | |
| ' }', | |
| ' return moduleBytes;', | |
| ' };', | |
| ' // Checked first: a range the module lacks throws before the alloc.', | |
| ' // Module addresses and lengths arrive as signed i32s.', | |
| ' const stageOut = (ptr, len) => {', | |
| ' if (len < 0 || (ptr >>> 0) + len > moduleU8().length) {', | |
| " throw new RangeError('module range out of bounds');", | |
| ' }', | |
| ' const dst =', | |
| ' len > kStackStageLimit ? host.malloc(len) : host.stackAlloc(len);', | |
| ' if (dst === 0) {', | |
| ' throw new Error(`could not stage ${len} bytes for the host`);', | |
| ' }', | |
| ' return dst;', | |
| ' };', | |
| ' const stageIn = (ptr, len) => {', | |
| ' const dst = stageOut(ptr, len);', | |
| ' ptr >>>= 0;', | |
| ' host.heapU8().set(moduleBytes.subarray(ptr, ptr + len), dst);', | |
| ' if (counters.on) counters.bytesStaged += len;', | |
| ' return dst;', | |
| ' };', | |
| ' const unstage = (dst, len) => {', | |
| ' if (dst !== 0 && len > kStackStageLimit) {', | |
| ' host.free(dst);', | |
| ' }', | |
| ' };', | |
| ' const copyOut = (src, ptr, len) => {', | |
| ' if (len === 0) {', | |
| ' return;', | |
| ' }', | |
| ' moduleU8().set(host.heapU8().subarray(src, src + len), ptr >>> 0);', | |
| ' if (counters.on) counters.bytesCopiedOut += len;', | |
| ' };', | |
| ' // The elements a size answer wrote: all of them when they fit.', | |
| ' const fitted = (size, capacity) =>', | |
| ' size >>> 0 <= capacity ? size >>> 0 : 0;', | |
| ' // Stages the managed string at text for fn(staged, byteLength). Null', | |
| ' // reads as empty and the rest outside module memory traps, as on WAMR.', | |
| ' const stageManaged = (text, fn) => {', | |
| ' const length = managedLength(moduleMemory().buffer, text);', | |
| ' const mark = stackSave();', | |
| ' let text_p = 0;', | |
| ' try {', | |
| ' text_p = stageIn(text, length);', | |
| ' fn(text_p, length);', | |
| ' } finally {', | |
| ' unstage(text_p, length);', | |
| ' stackRestore(mark);', | |
| ' }', | |
| ' };', | |
| ' // The std console, under the method codes rive_web_console takes.', | |
| ' const consoleCall = (method) => (text) =>', | |
| ' stageManaged(text, (text_p, length) =>', | |
| ' calls.rive_console(method, text_p, length)', | |
| ' );', | |
| ' return {', | |
| ' // Imported only by scripts that call Math.random, trace or console.', | |
| " 'env': {", | |
| " 'seed': () => Date.now() * Math.random(),", | |
| ' // Where modules baked before rive_rt_v1.now read the clocks.', | |
| " 'emscripten_get_now': () => performance.now(),", | |
| " 'emscripten_date_now': () => Date.now(),", | |
| " 'trace': (message, n, a0, a1, a2, a3, a4) =>", | |
| ' stageManaged(message, (text_p, length) =>', | |
| ' calls.rive_trace(text_p, length, n, a0, a1, a2, a3, a4)', | |
| ' ),', | |
| " 'console.log': consoleCall(0),", | |
| " 'console.debug': consoleCall(0),", | |
| " 'console.info': consoleCall(0),", | |
| " 'console.warn': consoleCall(0),", | |
| " 'console.error': consoleCall(0),", | |
| " 'console.time': consoleCall(1),", | |
| " 'console.timeLog': consoleCall(2),", | |
| " 'console.timeEnd': consoleCall(3),", | |
| ' },', | |
| ] | |
| for namespace in NAMESPACES: | |
| lines.append(" '%s': want('%s') && {" % ((namespace['module'],) * 2)) | |
| for op in namespace['ops']: | |
| lines.append(web_op(namespace, op)) | |
| lines.append(' },') | |
| lines += [ | |
| ' };', | |
| '}', | |
| '', | |
| '// host.calls for one VM. hostCall(index) is the librive function at', | |
| '// that index of its kRiveWebCalls table, undefined for a missing op.', | |
| '// ctx.vm is the VM', | |
| '// handle, zeroed when the VM goes away while its module is still', | |
| '// instantiating. ctx.raised holds a raiseModuleError message: it is', | |
| '// thrown once the host call has returned, so only module frames', | |
| '// unwind. Anything thrown from inside librive unwound its frames', | |
| '// without their destructors, so ctx.hostFault marks the VM done for.', | |
| '// ctx.counters, when given, tallies the calls into librive.', | |
| '// A fault in a nested script call unwinds the outer module frames too.', | |
| '// only binds just that namespace\'s calls, as in createRiveModuleImports.', | |
| 'export function bindRiveHostCalls(hostCall, ctx, only = null) {', | |
| ' const want = (module) => only === null || only === module;', | |
| ' const counters = ctx.counters ?? { on: false };', | |
| ' const raise = () => {', | |
| ' const error = new Error(ctx.raised);', | |
| ' ctx.raised = null;', | |
| ' throw error;', | |
| ' };', | |
| ' const fault = (error) => {', | |
| ' ctx.hostFault = true;', | |
| ' throw error;', | |
| ' };', | |
| ' const nestedFault = () => {', | |
| " throw new Error('librive faulted in a nested script call');", | |
| ' };', | |
| ' const released = () => {', | |
| " throw new Error('script VM was released');", | |
| ' };', | |
| ' return {', | |
| ] | |
| def host_call(module, name, argc, returns, index, unlinked=''): | |
| args = ', '.join('a%d' % i for i in range(argc)) | |
| call = 'fn(%s)' % ', '.join(filter(None, ['ctx.vm', args])) | |
| lines.append(" %s: want('%s') && ((fn) => %s(%s) => {" % | |
| (name, module, unlinked, args)) | |
| lines.append(' if (ctx.vm === 0) released();') | |
| lines.append(' if (counters.on) counters.hostCalls++;') | |
| if returns: | |
| lines.append(' let ret;') | |
| lines.append(' try { %s%s; } catch (error) { fault(error); }' % | |
| ('ret = ' if returns else '', call)) | |
| lines.append(' if (ctx.hostFault) nestedFault();') | |
| lines.append(' if (ctx.raised !== null) raise();') | |
| if returns: | |
| lines.append(' return ret;') | |
| lines.append(' })(hostCall(%d)),' % index) | |
| ops = web_host_ops() | |
| for index, (namespace, op) in enumerate(ops): | |
| # Missing outright, so a call traps rather than faulting. | |
| unlinked = ("fn === undefined ? unlinkedImport('%s', '%s') : " % | |
| (namespace['module'], op['name']) | |
| if op['guard'] else '') | |
| host_call(namespace['module'], c_symbol(namespace, op), | |
| len(host_params(op['params'])), op['ret'] is not None, | |
| index, unlinked) | |
| host_call('env', 'rive_console', 3, False, len(ops)) | |
| host_call('env', 'rive_trace', 8, False, len(ops) + 1) | |
| lines += [ | |
| ' };', | |
| '}', | |
| '', | |
| '// Stands in for an import the host lacks, so the module still links', | |
| '// and only a call to it traps, as on WAMR.', | |
| 'export function unlinkedImport(module, name) {', | |
| ' return () => {', | |
| ' throw new Error(', | |
| " 'failed to call unlinked import function ' + module + '.' + name", | |
| ' );', | |
| ' };', | |
| '}', | |
| '', | |
| ] | |
| return '\n'.join(lines) | |
| LAYOUT_NOTE = 'Generated by src/wasm/idl/generate.py from shared_layout.py.' \ | |
| ' Do not edit.' | |
| def layout_constants(declare, layout=SHARED_LAYOUT, indent='', keep=None): | |
| """Each group's constants after its comment; keep, when given, names the | |
| constants to emit.""" | |
| lines = [] | |
| for group, about, entries in layout: | |
| names = [('k' + group + name[:1].upper() + name[1:], value) | |
| for name, value in entries] | |
| names = [(name, value) for name, value in names | |
| if keep is None or name in keep] | |
| if not names: | |
| continue | |
| lines.append('') | |
| lines += [indent + '// ' + line | |
| for line in textwrap.wrap(about, 77 - len(indent))] | |
| for name, value in names: | |
| text = '0x%x' % value if value > 0xffff else str(value) | |
| lines.append(indent + declare % (name, text)) | |
| return lines | |
| def web_uses(): | |
| """The k-prefixed names the hand written web modules use.""" | |
| return set(re.findall(r'\bk[A-Z]\w*', ''.join( | |
| path.read_text() for path in WEB_SOURCE_PATHS))) | |
| def export_name(name): | |
| return 'k' + name[:1].upper() + name[1:] + 'Export' | |
| def text_name(name): | |
| return 'k' + name[:1].upper() + name[1:] | |
| def wait_name(name): | |
| return 'kWait' + name[:1].upper() + name[1:] | |
| def block_budget(max_fn, ceil_fn, equals='=='): | |
| return BLOCK_BUDGET_MS.replace('max(', max_fn + '(').replace( | |
| 'ceil(', ceil_fn + '(').replace('==', equals) | |
| def js_string(text): | |
| return "'%s'" % text.replace('\\', '\\\\').replace("'", "\\'") | |
| def js_string_lines(declare, text): | |
| """declare % a string expression, split into + joined lines that fit | |
| 100 columns as the web modules wrap.""" | |
| line = declare % js_string(text) | |
| if len(line) <= 100: | |
| return [line] | |
| pieces = [''] | |
| for word in text.split(' '): | |
| if len(js_string(pieces[-1] + word)) + 7 > 100: | |
| pieces.append('') | |
| pieces[-1] += word + ' ' | |
| pieces[-1] = pieces[-1].rstrip(' ') | |
| lines = [declare.split('%s')[0].rstrip()] | |
| for i, piece in enumerate(pieces): | |
| tail = ' +' if i + 1 < len(pieces) else ';' | |
| lines.append(' ' + js_string(piece) + tail) | |
| return lines | |
| def cpp_string(text): | |
| return '"%s"' % text.replace('\\', '\\\\').replace('"', '\\"') | |
| def check_lane_exports(): | |
| """Each lane export is declared by the shim lane_layout.py names.""" | |
| for _, export, shim in LANE_EXPORTS: | |
| if shim is None: | |
| continue | |
| text = (AS_STD_PATH / shim).read_text() | |
| if not re.search(r'^export (unsafe )?function %s\(' % export, text, | |
| flags=re.M): | |
| raise ValueError('std/rive/%s does not export %s, which' | |
| ' lane_layout.py names' % (shim, export)) | |
| def lane_enum_lines(name, about, members, ctype=None): | |
| """A C++ enum class, or an AS const enum when ctype is None, of members | |
| as (name, doc or None), numbered in order.""" | |
| lines = ['/// ' + about] | |
| if ctype is None: | |
| lines.append('export const enum %s {' % name) | |
| else: | |
| lines += ['enum class %s : %s' % (name, ctype), '{'] | |
| for value, (member, doc) in enumerate(members): | |
| if doc is not None: | |
| lines += [' /// ' + line for line in textwrap.wrap(doc, 72)] | |
| lines.append(' %s = %d,' % (member, value)) | |
| lines.append('}' if ctype is None else '};') | |
| return lines | |
| def emit_lane_layout_header(): | |
| sections = ''.join(' "%s",\n' % section for _, section, _ in LANE_KINDS) | |
| name_sections = ''.join(' "%s%s",\n' % (section, LANE_NAMES_SUFFIX) | |
| for _, section, _ in LANE_KINDS) | |
| prefixes = ''.join(' "%s",\n' % prefix for _, _, prefix in LANE_KINDS) | |
| names = [] | |
| for name, export, _ in LANE_EXPORTS: | |
| names.append('constexpr const char* %s = "%s";' % | |
| (export_name(name), export)) | |
| texts = ['constexpr const char* %s = %s;' % (text_name(name), | |
| cpp_string(text)) | |
| for name, text in LANE_TEXTS] | |
| waits = [] | |
| for name, value, about in WAIT_SETTLES + WAIT_ANSWERS: | |
| waits += ['// ' + line for line in textwrap.wrap(about, 77)] | |
| waits.append('constexpr double %s = %d;' % (wait_name(name), value)) | |
| return '\n'.join([ | |
| '// Generated by src/wasm/idl/generate.py from lane_layout.py.' | |
| ' Do not edit.', | |
| '//', | |
| '// The numbers lanes and their hosts agree on, which librive and the', | |
| '// bake both read.', | |
| '#ifndef _RIVE_WASM_LANE_LAYOUT_GEN_HPP_', | |
| '#define _RIVE_WASM_LANE_LAYOUT_GEN_HPP_', | |
| '', | |
| '#include <algorithm>', | |
| '#include <cmath>', | |
| '#include <cstdint>', | |
| '#include <string>', | |
| '#include <string_view>', | |
| '', | |
| '// clang-format off', | |
| 'namespace rive', | |
| '{', | |
| ] + lane_enum_lines('LaneKind', 'The rive_lanes_v1 lane kinds.', | |
| [(kind, None) for kind, _, _ in LANE_KINDS], | |
| 'uint32_t') + [''] + | |
| lane_enum_lines('LaneState', | |
| 'The rive_lanes_v1 states every lane moves through.', | |
| [(state, None) for state, _ in LANE_STATES], | |
| 'uint32_t') + [ | |
| '', | |
| '/// The main module section each kind\'s lane module rides in.', | |
| 'constexpr const char* kLaneSections[] = {', | |
| sections + '};', | |
| '', | |
| '/// The main module section beside each kind\'s lane module that names', | |
| '/// its entries in order: a count, then each name after its length.', | |
| 'constexpr const char* kLaneNameSections[] = {', | |
| name_sections + '};', | |
| '', | |
| '/// Each lane kind\'s name, by LaneKind.', | |
| 'constexpr const char* kLaneKindNames[] = {%s};' % | |
| ', '.join('"%s"' % kind for kind, _, _ in LANE_KINDS), | |
| '', | |
| '/// How messages name each kind\'s lane module, as the bake does.', | |
| 'constexpr const char* kLaneModuleNames[] = {', | |
| ''.join(' "%ss module",\n' % kind for kind, _, _ in LANE_KINDS) + | |
| '};', | |
| '', | |
| '/// What each kind\'s lane fails with when it trapped without a message.', | |
| 'constexpr const char* kLaneTrapTexts[] = {', | |
| ''.join(' "%s trapped",\n' % kind for kind, _, _ in LANE_KINDS) + | |
| '};', | |
| '', | |
| '/// The prefix each kind\'s entries export under, before their number.', | |
| 'constexpr const char* kLaneEntryPrefixes[] = {', | |
| prefixes + '};', | |
| '', | |
| 'namespace lane_layout', | |
| '{', | |
| ] + layout_constants('constexpr uint32_t %s = %s;', LANE_LAYOUT)[1:] + [ | |
| '', | |
| '// The exports a lane module may carry besides its entries.', | |
| ] + names + [''] + waits + [ | |
| '', | |
| '// What lanes report the same way on every host.', | |
| ] + texts + [ | |
| '', | |
| '// A processor block\'s budget in milliseconds, first for its first' | |
| ' block; 0 for none.', | |
| 'inline uint32_t processorBlockBudgetMs(uint32_t hardBlocks, uint32_t' | |
| ' frames, double sampleRate, bool first)', | |
| '{', | |
| ' // A rate of 0 or a huge hard limit must not overflow the cast.', | |
| ' return (uint32_t)std::min(4294967295.0, %s);' % | |
| block_budget('std::max', 'std::ceil'), | |
| '}', | |
| '', | |
| '// Why a lane module that imports module.name cannot load.', | |
| 'inline std::string laneImportError(std::string_view module,', | |
| ' std::string_view name)', | |
| '{', | |
| ' return "lane module imports " + std::string(module) + "." +' | |
| ' std::string(name) + ", which it cannot reach";', | |
| '}', | |
| '} // namespace lane_layout', | |
| '} // namespace rive', | |
| '', | |
| '#endif', | |
| '', | |
| ]) | |
| def emit_as_lane_codes(): | |
| codes = [entry for entry in LANE_LAYOUT + SHARED_LAYOUT | |
| if entry[0] in AS_LAYOUT_GROUPS] | |
| return '\n'.join([ | |
| '// Generated by src/wasm/idl/generate.py from lane_layout.py and', | |
| '// shared_layout.py. Do not edit.', | |
| '//', | |
| '// The numbers lanes and their hosts agree on.', | |
| '', | |
| ] + lane_enum_lines('LaneState', | |
| 'Where a job, worker or audio processor is in its' | |
| ' life.', LANE_STATES) + [ | |
| '', | |
| '// The rive_lanes_v1 lane kinds.', | |
| 'export const enum LaneKind {', | |
| ] + [' %s = %d,' % (kind, value) | |
| for value, (kind, _, _) in enumerate(LANE_KINDS)] + [ | |
| '}', | |
| ] + layout_constants('export const %s: u32 = %s;', codes) + [ | |
| '', | |
| '// What the std throws for a create\'s answer, after the API name.', | |
| ] + ['export const %s: string = %s;' % (text_name(name), cpp_string(text)) | |
| for name, text in SHARED_TEXTS] + [ | |
| line for name, value, about in WAIT_SETTLES for line in | |
| [''] + ['/// ' + text for text in textwrap.wrap(about, 76)] + | |
| ['export const %s: f64 = %d;' % (wait_name(name), value)]] + [ | |
| line for name, value, about in WAIT_ANSWERS for line in | |
| [''] + ['// ' + text for text in textwrap.wrap(about, 77)] + | |
| ['export const %s: f64 = %d;' % (wait_name(name), value)]] + ['']) | |
| def web_lane_layout_lines(): | |
| sections = ''.join(" ['%s', '%s'],\n" % (section, kind) | |
| for kind, section, _ in LANE_KINDS) | |
| prefixes = ''.join(" '%s': '%s',\n" % (kind, prefix) | |
| for kind, _, prefix in LANE_KINDS) | |
| states = ''.join(" '%s': %d,\n" % (state, value) | |
| for value, (state, _) in enumerate(LANE_STATES)) | |
| uses = web_uses() | |
| names = ['export const %s = \'%s\';' % (export_name(name), export) | |
| for name, export, _ in LANE_EXPORTS | |
| if export_name(name) in uses] | |
| texts = [line for name, text in LANE_TEXTS if text_name(name) in uses | |
| for line in js_string_lines( | |
| 'export const %s = %%s;' % text_name(name), text)] | |
| return [ | |
| '', | |
| '// [section, kind] for each lane kind, in rive_lanes_v1\'s order.', | |
| 'export const kLaneSections = [', | |
| sections + '];', | |
| '', | |
| '// The prefix each kind\'s entries export under, before their number.', | |
| 'export const kLaneEntryPrefixes = {', | |
| prefixes + '};', | |
| '', | |
| '// The rive_lanes_v1 states every lane moves through.', | |
| 'export const kLaneStates = {', | |
| states + '};', | |
| ] + layout_constants('export const %s = %s;', LANE_LAYOUT, | |
| keep=uses) + [ | |
| '', | |
| '// The exports a lane module may carry besides its entries.', | |
| ] + names + [ | |
| '', | |
| '// What lanes report the same way on every host.', | |
| ] + texts + [ | |
| '', | |
| '// A processor block\'s budget in milliseconds, first for its first' | |
| ' block; 0 for none.', | |
| 'export function processorBlockBudgetMs(hardBlocks, frames, sampleRate,' | |
| ' first) {', | |
| ' return %s;' % block_budget('Math.max', 'Math.ceil', '==='), | |
| '}', | |
| '', | |
| '// Why a lane module that imports module.name cannot load.', | |
| 'export function laneImportError(module, name) {', | |
| " return 'lane module imports ' + module + '.' + name + ', which it" | |
| " cannot reach';", | |
| '}', | |
| ] | |
| def emit_shared_layout_header(): | |
| return '\n'.join([ | |
| '// %s' % LAYOUT_NOTE, | |
| '//', | |
| '// The shared memory words librive and web/rive_shared_lane.mjs both', | |
| '// read, as wasm32 byte offsets.', | |
| '#ifndef _RIVE_WASM_SHARED_LAYOUT_GEN_HPP_', | |
| '#define _RIVE_WASM_SHARED_LAYOUT_GEN_HPP_', | |
| '', | |
| '#include <cstdint>', | |
| '', | |
| 'namespace rive', | |
| '{', | |
| 'namespace shared_layout', | |
| '{', | |
| ] + layout_constants('constexpr uint32_t %s = %s;')[1:] + [ | |
| '} // namespace shared_layout', | |
| '} // namespace rive', | |
| '', | |
| '#endif', | |
| '', | |
| ]) | |
| def emit_web_shared_layout(): | |
| return '\n'.join([ | |
| '// %s' % LAYOUT_NOTE, | |
| '//', | |
| '// The shared memory words librive and rive_shared_lane.mjs both read,', | |
| '// as byte offsets into librive\'s heap.', | |
| ] + layout_constants('export const %s = %s;', keep=web_uses()) + ['']) | |
| def lane_rules(): | |
| """(module, name or None for all of it, lane kinds, refusal or None), | |
| where the first match decides, so an op comes before its namespace.""" | |
| rules = [] | |
| for namespace in NAMESPACES: | |
| for op in namespace['ops']: | |
| if op['lanes'] is not None: | |
| rules.append((namespace['module'], op['name'], op['lanes'], | |
| op['lane_refusal'])) | |
| if namespace['lanes']: | |
| rules.append((namespace['module'], None, namespace['lanes'], None)) | |
| for lane_import in LANE_IMPORTS: | |
| rules.append((lane_import['module'], lane_import['name'], | |
| lane_import['lanes'], None)) | |
| for _, _, lanes, _ in rules: | |
| assert set(lanes) <= set(LANES), lanes | |
| return rules | |
| def c_string(text): | |
| return 'nullptr' if text is None else '"%s"' % text | |
| def emit_lane_imports_header(namespace, consumer, kinds): | |
| rule_lines = [] | |
| for module, name, lanes, refusal in lane_rules(): | |
| mask = sum(1 << LANES.index(lane) for lane in lanes) | |
| rule_lines.append(' {"%s", %s, 0x%x, %s},' % ( | |
| module, c_string(name), mask, c_string(refusal))) | |
| kind_lines = [] | |
| if kinds: | |
| names = ', '.join('"%s"' % lane for lane in LANES) | |
| kind_lines = lane_enum_lines( | |
| 'LaneKind', 'The rive_lanes_v1 lane kinds.', | |
| [(lane, None) for lane in LANES], 'uint8_t') + [ | |
| '', | |
| '/// Each lane kind\'s name, by LaneKind.', | |
| 'constexpr const char* kLaneKindNames[] = {%s};' % names, | |
| '', | |
| '/// The lane kinds as a sentence names them.', | |
| 'constexpr const char* kLaneKindList = "%s or %s";' % ( | |
| ', '.join(LANES[:-1]), LANES[-1]), | |
| '', | |
| '/// The export the compiler writes for a job module\'s host to reset' | |
| ' it.', | |
| ] + ['constexpr const char* %s = "%s";' % (export_name(name), export) | |
| for name, export, _ in LANE_EXPORTS if name == 'laneReset'] + [ | |
| '', | |
| ] | |
| return '\n'.join([ | |
| '// %s' % GENERATED_NOTE, | |
| '//', | |
| '// What each kind of lane module may import, which %s' % consumer, | |
| '// checks it against.', | |
| '', | |
| '#pragma once', | |
| '', | |
| '#include <cstdint>', | |
| '#include <string_view>', | |
| '', | |
| '// clang-format off', | |
| 'namespace %s' % namespace, | |
| '{', | |
| '', | |
| ] + kind_lines + [ | |
| '/// Why a lane module of a kind may not import module.name: null when it', | |
| '/// may, else the end of "<kind> code ...", empty when no rule says why.', | |
| 'inline const char* laneImportRefusal(uint32_t kind,', | |
| ' std::string_view module,', | |
| ' std::string_view name)', | |
| '{', | |
| ' struct Rule', | |
| ' {', | |
| ' const char* module;', | |
| ' // Null for every name in the module.', | |
| ' const char* name;', | |
| ' uint32_t kinds;', | |
| ' const char* refusal;', | |
| ' };', | |
| ' static constexpr Rule kRules[] = {', | |
| ] + rule_lines + [ | |
| ' };', | |
| ' for (const Rule& rule : kRules)', | |
| ' {', | |
| ' if (module == rule.module &&', | |
| ' (rule.name == nullptr || name == rule.name))', | |
| ' {', | |
| ' return ((rule.kinds >> kind) & 1) != 0 ? nullptr', | |
| ' : rule.refusal != nullptr ? rule.refusal', | |
| ' : "";', | |
| ' }', | |
| ' }', | |
| ' return "";', | |
| '}', | |
| '', | |
| '} // namespace %s' % namespace, | |
| '', | |
| ]) | |
| def emit_web_lane_imports(): | |
| rules = ',\n'.join( | |
| ' [%s, %s, [%s], %s]' % ( | |
| "'%s'" % module, | |
| 'null' if name is None else "'%s'" % name, | |
| ', '.join("'%s'" % lane for lane in lanes), | |
| 'null' if refusal is None else "'%s'" % refusal) | |
| for module, name, lanes, refusal in lane_rules()) | |
| console = ',\n'.join(" '%s'" % name for module, name, lanes, _ | |
| in lane_rules() | |
| if module == 'env' and set(lanes) == set(LANES)) | |
| return '\n'.join([ | |
| '// Generated by src/wasm/idl/generate.py from bindings.py and', | |
| '// lane_layout.py. Do not edit.', | |
| '//', | |
| '// What each kind of lane module may import, which the page runner', | |
| '// checks it against, and the numbers lanes and the page agree on.', | |
| '', | |
| '// The console imports every lane gets.', | |
| 'export const kLaneConsole = [', | |
| console + ',', | |
| '];', | |
| '', | |
| '// [module, name or null for all of it, kinds, refusal or null], the', | |
| '// first match deciding.', | |
| 'const kLaneImportRules = [', | |
| rules + ',', | |
| '];', | |
| '', | |
| '// Why a lane module of kind (\'job\', \'worker\' or \'processor\') may not', | |
| '// import module.name: null when it may, else the end of "<kind> code ...",', | |
| '// empty when no rule says why.', | |
| 'export function laneImportRefusal(kind, module, name) {', | |
| ' for (const [ruleModule, ruleName, kinds, refusal] of kLaneImportRules) {', | |
| ' if (module === ruleModule && (ruleName === null || name === ruleName)) {', | |
| " return kinds.includes(kind) ? null : refusal ?? '';", | |
| ' }', | |
| ' }', | |
| " return '';", | |
| '}', | |
| ] + web_lane_layout_lines() + ['']) | |
| def classic_script(text, name): | |
| """ES modules concatenated as one script body: imports dropped, exports | |
| made plain declarations.""" | |
| body = re.sub(r'^import \{[^}]*\} from [^;]*;\n', '', text, flags=re.M) | |
| body = body.replace('export function', 'function').replace( | |
| 'export const', 'const') | |
| if re.search(r'^(import|export)\b', body, flags=re.M): | |
| raise ValueError('module syntax left in the %s bundle' % name) | |
| return body | |
| def lane_sources(): | |
| generated = { | |
| WEB_LANE_IMPORTS_PATH: emit_web_lane_imports, | |
| WEB_SHARED_LAYOUT_PATH: emit_web_shared_layout, | |
| } | |
| return ''.join(generated[path]() if path in generated | |
| else path.read_text() for path in WEB_LANE_PATHS) | |
| def emit_web_pre_js(web_imports): | |
| """The import object and the hand written runner as one classic script | |
| for emscripten's --pre-js, which has no module syntax.""" | |
| body = classic_script(web_imports + lane_sources() + | |
| WEB_PARALLEL_PATH.read_text() + | |
| WEB_RUNNER_PATH.read_text(), 'pre-js') | |
| lines = [ | |
| '// %s' % GENERATED_NOTE, | |
| '//', | |
| '// rive_module_imports.mjs, the lane modules, rive_parallel.mjs and', | |
| '// rive_script_runner.mjs as one script.', | |
| '// Link it into an emscripten build of librive with --pre-js and the', | |
| '// script runner installs itself as Module.riveScripting.', | |
| '(function () {', | |
| body, | |
| 'installRiveScripting(Module);', | |
| '})();', | |
| '', | |
| ] | |
| return '\n'.join(lines) | |
| def emit_web_nrdp_worklet(): | |
| """NRDP's worklet thread runs a plain script, so the worklet glue and | |
| the lane modules it imports go in one.""" | |
| body = classic_script(lane_sources() + WEB_NRDP_WORKLET_PATH.read_text(), | |
| 'NRDP worklet') | |
| return '\n'.join([ | |
| '// %s' % GENERATED_NOTE, | |
| '//', | |
| '// rive_audio_nrdp_worklet.mjs and the lane modules it imports as one', | |
| '// script, for nrdp.thread.start({ url, source, audio }).', | |
| '(function () {', | |
| body, | |
| '})();', | |
| '', | |
| ]) | |
| def emit_web_worklet_pre_js(): | |
| """A browser worklet loads the lane modules it imports from beside it, so | |
| a build that serves no files of its own carries all of it as one script.""" | |
| script = '(function () {\n%s})();\n' % classic_script( | |
| lane_sources() + WEB_WORKLET_PATH.read_text(), 'browser worklet') | |
| return '\n'.join([ | |
| '// %s' % GENERATED_NOTE, | |
| '//', | |
| '// rive_audio_worklet.mjs and the lane modules it imports as one script,', | |
| '// which the runner starts from a blob when the host names no worklet.', | |
| '// Link it with --pre-js into builds with threads, the only ones that', | |
| '// play script audio in real time.', | |
| "Module['riveAudioWorkletSource'] = %s;" % json.dumps(script), | |
| '', | |
| ]) | |
| # Return the import surface as one digest: every op the namespaces declare, | |
| # with the parameter kinds and return kind that decide whether a wasm import | |
| # links. Sorted, so the order ops are written in never moves it. | |
| # | |
| # What it leaves out is everything that does not change linkage, such as an | |
| # op's stub behaviour or its web expression, so regenerating for one of those | |
| # does not tell anybody their build is out of date. | |
| def contract_digest(): | |
| signatures = [] | |
| for namespace in NAMESPACES: | |
| for op in namespace['ops']: | |
| kinds = ','.join(param['kind'] for param in op['params']) | |
| signatures.append( | |
| '%s.%s(%s)->%s' | |
| % (namespace['module'], op['name'], kinds, op['ret'] or 'void') | |
| ) | |
| signatures.sort() | |
| digest = hashlib.sha256('\n'.join(signatures).encode('utf-8')) | |
| return digest.hexdigest()[:16] | |
| # The same digest as a file, for build steps that have a shell rather than a | |
| # compiler: the host build copies it beside the module it produces. | |
| def emit_contract(): | |
| return contract_digest() + '\n' | |
| def main(): | |
| check_all_ops() | |
| check_lane_exports() | |
| check_features() | |
| web_imports = emit_web_imports() | |
| outputs = { | |
| HEADER_PATH: emit_header(), | |
| HOST_PATH: emit_host(), | |
| CONTRACT_PATH: emit_contract(), | |
| WEB_HOST_PATH: emit_web_host(), | |
| STUBS_PATH: emit_stubs(), | |
| WEB_PATH: web_imports, | |
| WEB_PRE_JS_PATH: emit_web_pre_js(web_imports), | |
| WEB_NRDP_BUNDLE_PATH: emit_web_nrdp_worklet(), | |
| WEB_WORKLET_PRE_JS_PATH: emit_web_worklet_pre_js(), | |
| AS_PATH: emit_as(), | |
| AS_NATIVES_PATH: emit_as_natives(), | |
| SHARED_LAYOUT_HEADER_PATH: emit_shared_layout_header(), | |
| WEB_SHARED_LAYOUT_PATH: emit_web_shared_layout(), | |
| LANE_LAYOUT_HEADER_PATH: emit_lane_layout_header(), | |
| AS_LANE_CODES_PATH: emit_as_lane_codes(), | |
| LANE_IMPORTS_HEADER_PATH: | |
| emit_lane_imports_header('rive', 'loadLaneModule', False), | |
| RASC_LANE_IMPORTS_PATH: | |
| emit_lane_imports_header('rasc', 'the compiler', True), | |
| WEB_LANE_IMPORTS_PATH: emit_web_lane_imports(), | |
| } | |
| 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()) |