Skip to content

Commit 9192e84

Browse files
committed
port addFunction and removeFunction for non-emscripten target
1 parent 80a2a60 commit 9192e84

File tree

3 files changed

+208
-0
lines changed

3 files changed

+208
-0
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/* eslint-disable @stylistic/indent */
2+
3+
/**
4+
* @license
5+
* Copyright 2020 The Emscripten Authors
6+
* SPDX-License-Identifier: MIT
7+
*/
8+
9+
// port from emscripten src/lib/libaddfunction.js
10+
11+
import { from64, to64 } from 'emscripten:parse-tools'
12+
import { wasmTable, getWasmTableEntry, setWasmTableEntry } from 'emscripten:runtime'
13+
14+
// This gives correct answers for everything less than 2^{14} = 16384
15+
// I hope nobody is contemplating functions with 16384 arguments...
16+
function uleb128EncodeWithLen (arr: number[]): number[] {
17+
const n = arr.length
18+
// Note: this LEB128 length encoding produces extra byte for n < 128,
19+
// but we don't care as it's only used in a temporary representation.
20+
return [(n % 128) | 128, n >> 7, ...arr]
21+
}
22+
23+
// Converts a signature like 'vii' into a description of the wasm types, like
24+
// { parameters: ['i32', 'i32'], results: [] }.
25+
function sigToWasmTypes (sig: string): { parameters: string[]; results: string[] } {
26+
const typeNames: Record<string, string> = {
27+
i: 'i32',
28+
j: 'i64',
29+
f: 'f32',
30+
d: 'f64',
31+
e: 'externref',
32+
p: 'i32',
33+
}
34+
// #if MEMORY64
35+
typeNames.p = 'i64'
36+
// #endif
37+
const type = {
38+
parameters: [] as string[],
39+
results: sig[0] === 'v' ? [] : [typeNames[sig[0]]]
40+
}
41+
for (let i = 1; i < sig.length; ++i) {
42+
type.parameters.push(typeNames[sig[i]])
43+
}
44+
return type
45+
}
46+
47+
// Note: using template literal here instead of plain object
48+
// because jsify serializes objects w/o quotes and Closure will then
49+
// incorrectly mangle the properties.
50+
const wasmTypeCodes = (function () {
51+
const map = {
52+
i: 0x7f, // i32
53+
p: 0x7f,
54+
j: 0x7e, // i64
55+
f: 0x7d, // f32
56+
d: 0x7c, // f64
57+
e: 0x6f, // externref
58+
}
59+
// #if MEMORY64
60+
map.p = 0x7e
61+
// #endif
62+
return map
63+
})()
64+
65+
function generateTypePack (types: string): number[] {
66+
return uleb128EncodeWithLen(Array.from(types, (type) => {
67+
const code = (wasmTypeCodes as any)[type]
68+
return code
69+
}))
70+
}
71+
72+
function convertJsFunctionToWasm (func: Function, sig: string): Function {
73+
if ((WebAssembly as any).Function) {
74+
return new (WebAssembly as any).Function(sigToWasmTypes(sig), func)
75+
}
76+
77+
// Rest of the module is static
78+
const bytes = Uint8Array.of(
79+
0x00, 0x61, 0x73, 0x6d, // magic ("\0asm")
80+
0x01, 0x00, 0x00, 0x00, // version: 1
81+
0x01, // Type section code
82+
// The module is static, with the exception of the type section, which is
83+
// generated based on the signature passed in.
84+
...uleb128EncodeWithLen([
85+
0x01, // count: 1
86+
0x60 /* form: func */,
87+
// param types
88+
...generateTypePack(sig.slice(1)),
89+
// return types (for now only supporting [] if `void` and single [T] otherwise)
90+
...generateTypePack(sig[0] === 'v' ? '' : sig[0])
91+
]),
92+
// The rest of the module is static
93+
0x02, 0x07, // import section
94+
// (import "e" "f" (func 0 (type 0)))
95+
0x01, 0x01, 0x65, 0x01, 0x66, 0x00, 0x00,
96+
0x07, 0x05, // export section
97+
// (export "f" (func 0 (type 0)))
98+
0x01, 0x01, 0x66, 0x00, 0x00
99+
)
100+
101+
// We can compile this wasm module synchronously because it is very small.
102+
// This accepts an import (at "e.f"), that it reroutes to an export (at "f")
103+
const module = new WebAssembly.Module(bytes)
104+
const instance = new WebAssembly.Instance(module, { e: { f: func } })
105+
const wrappedFunc = instance.exports['f'] as Function
106+
return wrappedFunc
107+
}
108+
109+
const freeTableIndexes: number[] = []
110+
111+
// Weak map of functions in the table to their indexes, created on first use.
112+
let functionsInTableMap: WeakMap<Function, number> | undefined
113+
114+
function getEmptyTableSlot (): number {
115+
// Reuse a free index if there is one, otherwise grow.
116+
if (freeTableIndexes.length) {
117+
return freeTableIndexes.pop()!
118+
}
119+
try {
120+
// Grow the table
121+
return wasmTable.grow(to64('1') as number)
122+
} catch (err) {
123+
if (!(err instanceof RangeError)) {
124+
throw err
125+
}
126+
throw new Error('Unable to grow wasm table. Specify `--growable-table` for wasm-ld.')
127+
}
128+
}
129+
130+
function updateTableMap (
131+
offset: number,
132+
count: number
133+
): void {
134+
if (functionsInTableMap) {
135+
for (let i = offset; i < offset + count; i++) {
136+
const item = getWasmTableEntry(i)
137+
// Ignore null values.
138+
if (item) {
139+
functionsInTableMap.set(item, i)
140+
}
141+
}
142+
}
143+
}
144+
145+
function getFunctionAddress (func: Function): number {
146+
// First, create the map if this is the first use.
147+
if (!functionsInTableMap) {
148+
functionsInTableMap = new WeakMap()
149+
let count = wasmTable.length
150+
from64('count')
151+
updateTableMap(0, count)
152+
}
153+
return functionsInTableMap.get(func) || 0
154+
}
155+
156+
/**
157+
* Add a function to the table.
158+
* 'sig' parameter is required if the function being added is a JS function.
159+
*/
160+
export function addFunction (func: Function, sig?: string): number {
161+
// Check if the function is already in the table, to ensure each function
162+
// gets a unique index.
163+
let rtn = getFunctionAddress(func)
164+
if (rtn) {
165+
return rtn
166+
}
167+
168+
// It's not in the table, add it now.
169+
170+
const ret = getEmptyTableSlot()
171+
172+
// Set the new value.
173+
try {
174+
// Attempting to call this with JS function will cause table.set() to fail
175+
setWasmTableEntry(ret, func)
176+
} catch (err) {
177+
if (!(err instanceof TypeError)) {
178+
throw err
179+
}
180+
if (!sig) {
181+
throw new Error('Missing signature argument to addFunction: ' + func)
182+
}
183+
const wrapped = convertJsFunctionToWasm(func, sig)
184+
setWasmTableEntry(ret, wrapped)
185+
}
186+
187+
functionsInTableMap!.set(func, ret)
188+
189+
return ret
190+
}
191+
192+
export function removeFunction (index: number): void {
193+
if (!functionsInTableMap) return
194+
functionsInTableMap.delete(getWasmTableEntry(index)!)
195+
setWasmTableEntry(index, null)
196+
freeTableIndexes.push(index)
197+
}

packages/emnapi/src/core/init.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ export function runtimeKeepalivePush (): void {}
5757

5858
export function runtimeKeepalivePop (): void {}
5959

60+
export function getWasmTableEntry (index: number): Function | null {
61+
return wasmTable.get(index) as Function | null
62+
}
63+
64+
export function setWasmTableEntry (index: number, func: Function | null): void {
65+
wasmTable.set(index, func)
66+
}
67+
6068
export var napiModule: INapiModule = {
6169
imports: {
6270
env: {},
@@ -292,3 +300,5 @@ export var PThread = new ThreadManager(
292300
)
293301

294302
napiModule.PThread = PThread
303+
304+
export * from './addfunction'

packages/emnapi/src/core/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
}
88
},
99
"include": [
10+
"./addfunction.ts",
1011
"./index.ts",
1112
"./pthread.ts",
1213
"./init.ts",

0 commit comments

Comments
 (0)