Skip to content

Commit 5ed537f

Browse files
authoredFeb 8, 2024
feat(vm): support wasm module (#5131)
1 parent 7a31a1a commit 5ed537f

File tree

12 files changed

+205
-13
lines changed

12 files changed

+205
-13
lines changed
 

‎eslint.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default antfu(
1111
'**/bench.json',
1212
'**/fixtures',
1313
'test/core/src/self',
14+
'test/core/src/wasm-bindgen-no-cyclic',
1415
'test/workspaces/results.json',
1516
'test/reporters/fixtures/with-syntax-error.test.js',
1617
'test/network-imports/public/slash@3.0.0.js',

‎packages/vitest/src/runtime/external-executor.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface ExternalModulesExecutorOptions {
2727
}
2828

2929
interface ModuleInformation {
30-
type: 'data' | 'builtin' | 'vite' | 'module' | 'commonjs'
30+
type: 'data' | 'builtin' | 'vite' | 'wasm' | 'module' | 'commonjs'
3131
url: string
3232
path: string
3333
}
@@ -165,7 +165,7 @@ export class ExternalModulesExecutor {
165165
const pathUrl = isFileUrl ? fileURLToPath(identifier.split('?')[0]) : identifier
166166
const fileUrl = isFileUrl ? identifier : pathToFileURL(pathUrl).toString()
167167

168-
let type: 'module' | 'commonjs' | 'vite'
168+
let type: 'module' | 'commonjs' | 'vite' | 'wasm'
169169
if (this.vite.canResolve(fileUrl)) {
170170
type = 'vite'
171171
}
@@ -175,6 +175,11 @@ export class ExternalModulesExecutor {
175175
else if (extension === '.cjs') {
176176
type = 'commonjs'
177177
}
178+
else if (extension === '.wasm') {
179+
// still experimental on NodeJS --experimental-wasm-modules
180+
// cf. ESM_FILE_FORMAT(url) in https://nodejs.org/docs/latest-v20.x/api/esm.html#resolution-algorithm
181+
type = 'wasm'
182+
}
178183
else {
179184
const pkgData = this.findNearestPackageData(normalize(pathUrl))
180185
type = pkgData.type === 'module' ? 'module' : 'commonjs'
@@ -188,7 +193,7 @@ export class ExternalModulesExecutor {
188193

189194
// create ERR_MODULE_NOT_FOUND on our own since latest NodeJS's import.meta.resolve doesn't throw on non-existing namespace or path
190195
// https://github.com/nodejs/node/pull/49038
191-
if ((type === 'module' || type === 'commonjs') && !existsSync(path)) {
196+
if ((type === 'module' || type === 'commonjs' || type === 'wasm') && !existsSync(path)) {
192197
const error = new Error(`Cannot find module '${path}'`)
193198
;(error as any).code = 'ERR_MODULE_NOT_FOUND'
194199
throw error
@@ -203,6 +208,8 @@ export class ExternalModulesExecutor {
203208
}
204209
case 'vite':
205210
return await this.vite.createViteModule(url)
211+
case 'wasm':
212+
return await this.esm.createWebAssemblyModule(url, this.fs.readBuffer(path))
206213
case 'module':
207214
return await this.esm.createEsModule(url, this.fs.readFile(path))
208215
case 'commonjs': {

‎packages/vitest/src/runtime/vm/esm-executor.ts

+13-6
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,15 @@ export class EsmExecutor {
7777
return m
7878
}
7979

80+
public async createWebAssemblyModule(fileUrl: string, code: Buffer) {
81+
const cached = this.moduleCache.get(fileUrl)
82+
if (cached)
83+
return cached
84+
const m = this.loadWebAssemblyModule(code, fileUrl)
85+
this.moduleCache.set(fileUrl, m)
86+
return m
87+
}
88+
8089
public async loadWebAssemblyModule(source: Buffer, identifier: string) {
8190
const cached = this.moduleCache.get(identifier)
8291
if (cached)
@@ -90,23 +99,21 @@ export class EsmExecutor {
9099
const moduleLookup: Record<string, VMModule> = {}
91100
for (const { module } of imports) {
92101
if (moduleLookup[module] === undefined) {
93-
const resolvedModule = await this.executor.resolveModule(
102+
moduleLookup[module] = await this.executor.resolveModule(
94103
module,
95104
identifier,
96105
)
97-
98-
moduleLookup[module] = await this.evaluateModule(resolvedModule)
99106
}
100107
}
101108

102109
const syntheticModule = new SyntheticModule(
103110
exports.map(({ name }) => name),
104-
() => {
111+
async () => {
105112
const importsObject: WebAssembly.Imports = {}
106113
for (const { module, name } of imports) {
107114
if (!importsObject[module])
108115
importsObject[module] = {}
109-
116+
await this.evaluateModule(moduleLookup[module])
110117
importsObject[module][name] = (moduleLookup[module].namespace as any)[name]
111118
}
112119
const wasmInstance = new WebAssembly.Instance(
@@ -150,7 +157,7 @@ export class EsmExecutor {
150157
if (encoding !== 'base64')
151158
throw new Error(`Invalid data URI encoding: ${encoding}`)
152159

153-
const module = await this.loadWebAssemblyModule(
160+
const module = this.loadWebAssemblyModule(
154161
Buffer.from(match.groups.code, 'base64'),
155162
identifier,
156163
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
The recent version of the wasm-bindgen bundler output does not use cyclic imports between wasm and js.
2+
3+
For this non-cyclic version to work, both `index_bg.js` and `index_bg.wasm` need to be externalized
4+
since otherwise a dual package hazard on `index_bg.js` would make it non-functional.
5+
6+
The code is copied from https://github.com/rustwasm/wasm-bindgen/tree/8198d2d25920e1f4fc593e9f8eb9d199e004d731/examples/hello_world
7+
8+
```sh
9+
npm i
10+
npm run build
11+
# then
12+
# 1. copy `examples/hello_world/pkg` to this directory
13+
# 2. add { "type": "module" } to `package.json`
14+
# (this will be automatically included after https://github.com/rustwasm/wasm-pack/pull/1061)
15+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* tslint:disable */
2+
/* eslint-disable */
3+
/**
4+
* @param {string} name
5+
*/
6+
export function greet(name: string): void;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import * as wasm from "./index_bg.wasm";
2+
import { __wbg_set_wasm } from "./index_bg.js";
3+
__wbg_set_wasm(wasm);
4+
export * from "./index_bg.js";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
let wasm;
2+
export function __wbg_set_wasm(val) {
3+
wasm = val;
4+
}
5+
6+
7+
const lTextDecoder = typeof TextDecoder === 'undefined' ? (0, module.require)('util').TextDecoder : TextDecoder;
8+
9+
let cachedTextDecoder = new lTextDecoder('utf-8', { ignoreBOM: true, fatal: true });
10+
11+
cachedTextDecoder.decode();
12+
13+
let cachedUint8Memory0 = null;
14+
15+
function getUint8Memory0() {
16+
if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) {
17+
cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);
18+
}
19+
return cachedUint8Memory0;
20+
}
21+
22+
function getStringFromWasm0(ptr, len) {
23+
ptr = ptr >>> 0;
24+
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
25+
}
26+
27+
function logError(f, args) {
28+
try {
29+
return f.apply(this, args);
30+
} catch (e) {
31+
let error = (function () {
32+
try {
33+
return e instanceof Error ? `${e.message}\n\nStack:\n${e.stack}` : e.toString();
34+
} catch(_) {
35+
return "<failed to stringify thrown value>";
36+
}
37+
}());
38+
console.error("wasm-bindgen: imported JS function that was not marked as `catch` threw an error:", error);
39+
throw e;
40+
}
41+
}
42+
43+
let WASM_VECTOR_LEN = 0;
44+
45+
const lTextEncoder = typeof TextEncoder === 'undefined' ? (0, module.require)('util').TextEncoder : TextEncoder;
46+
47+
let cachedTextEncoder = new lTextEncoder('utf-8');
48+
49+
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
50+
? function (arg, view) {
51+
return cachedTextEncoder.encodeInto(arg, view);
52+
}
53+
: function (arg, view) {
54+
const buf = cachedTextEncoder.encode(arg);
55+
view.set(buf);
56+
return {
57+
read: arg.length,
58+
written: buf.length
59+
};
60+
});
61+
62+
function passStringToWasm0(arg, malloc, realloc) {
63+
64+
if (typeof(arg) !== 'string') throw new Error('expected a string argument');
65+
66+
if (realloc === undefined) {
67+
const buf = cachedTextEncoder.encode(arg);
68+
const ptr = malloc(buf.length, 1) >>> 0;
69+
getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
70+
WASM_VECTOR_LEN = buf.length;
71+
return ptr;
72+
}
73+
74+
let len = arg.length;
75+
let ptr = malloc(len, 1) >>> 0;
76+
77+
const mem = getUint8Memory0();
78+
79+
let offset = 0;
80+
81+
for (; offset < len; offset++) {
82+
const code = arg.charCodeAt(offset);
83+
if (code > 0x7F) break;
84+
mem[ptr + offset] = code;
85+
}
86+
87+
if (offset !== len) {
88+
if (offset !== 0) {
89+
arg = arg.slice(offset);
90+
}
91+
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
92+
const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
93+
const ret = encodeString(arg, view);
94+
if (ret.read !== arg.length) throw new Error('failed to pass whole string');
95+
offset += ret.written;
96+
}
97+
98+
WASM_VECTOR_LEN = offset;
99+
return ptr;
100+
}
101+
/**
102+
* @param {string} name
103+
*/
104+
export function greet(name) {
105+
const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
106+
const len0 = WASM_VECTOR_LEN;
107+
wasm.greet(ptr0, len0);
108+
}
109+
110+
export function __wbg_alert_9ea5a791b0d4c7a3() { return logError(function (arg0, arg1) {
111+
alert(getStringFromWasm0(arg0, arg1));
112+
}, arguments) };
113+
114+
export function __wbindgen_throw(arg0, arg1) {
115+
throw new Error(getStringFromWasm0(arg0, arg1));
116+
};
117+
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* tslint:disable */
2+
/* eslint-disable */
3+
export const memory: WebAssembly.Memory;
4+
export function greet(a: number, b: number): void;
5+
export function __wbindgen_malloc(a: number, b: number): number;
6+
export function __wbindgen_realloc(a: number, b: number, c: number, d: number): number;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"type": "module",
3+
"name": "hello_world",
4+
"collaborators": [
5+
"The wasm-bindgen Developers"
6+
],
7+
"version": "0.1.0",
8+
"files": [
9+
"index_bg.wasm",
10+
"index.js",
11+
"index_bg.js",
12+
"index.d.ts"
13+
],
14+
"module": "index.js",
15+
"types": "index.d.ts",
16+
"sideEffects": [
17+
"./index.js",
18+
"./snippets/*"
19+
]
20+
}

‎test/core/test/vm-wasm.test.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { expect, test, vi } from 'vitest'
77
// @ts-expect-error wasm is not typed
88
import { add } from '../src/add.wasm'
99

10-
const wasmFileBuffer = readFileSync(resolve(__dirname, './src/add.wasm'))
10+
const wasmFileBuffer = readFileSync(resolve(__dirname, '../src/add.wasm'))
1111

1212
test('supports native wasm imports', () => {
1313
expect(add(1, 2)).toBe(3)
@@ -54,7 +54,7 @@ test('imports from "data:application/wasm" URI with invalid encoding fail', asyn
5454
).rejects.toThrow('Invalid data URI encoding: charset=utf-8')
5555
})
5656

57-
test('supports wasm files that import js resources (wasm-bindgen)', async () => {
57+
test('supports wasm/js cyclic import (old wasm-bindgen output)', async () => {
5858
globalThis.alert = vi.fn()
5959

6060
// @ts-expect-error not typed
@@ -63,3 +63,12 @@ test('supports wasm files that import js resources (wasm-bindgen)', async () =>
6363

6464
expect(globalThis.alert).toHaveBeenCalledWith('Hello, World!')
6565
})
66+
67+
test('supports wasm-bindgen', async () => {
68+
globalThis.alert = vi.fn()
69+
70+
const { greet } = await import('../src/wasm-bindgen-no-cyclic/index.js')
71+
greet('No Cyclic')
72+
73+
expect(globalThis.alert).toHaveBeenCalledWith('Hello, No Cyclic!')
74+
})

‎test/core/vite.config.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export default defineConfig({
4545
},
4646
test: {
4747
name: 'core',
48-
exclude: ['**/fixtures/**', '**/vm-wasm.test.ts', ...defaultExclude],
48+
exclude: ['**/fixtures/**', ...defaultExclude],
4949
slowTestThreshold: 1000,
5050
testTimeout: 2000,
5151
setupFiles: [
@@ -75,7 +75,7 @@ export default defineConfig({
7575
},
7676
server: {
7777
deps: {
78-
external: ['tinyspy', /src\/external/, /esm\/esm/, /\.wasm$/],
78+
external: ['tinyspy', /src\/external/, /esm\/esm/, /\.wasm$/, /\/wasm-bindgen-no-cyclic\/index_bg/],
7979
inline: ['inline-lib'],
8080
},
8181
},

0 commit comments

Comments
 (0)
Please sign in to comment.