blob: 46ea58c8badd0d0517f8600b665edfefcc9df12b [file] [edit]
#!/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())