Skip to content

Commit 8b0592d

Browse files
authored
feat: create env in wasm (#196)
1 parent ce2c162 commit 8b0592d

23 files changed

Lines changed: 509 additions & 145 deletions

File tree

.github/workflows/main.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ permissions:
3232
contents: write
3333

3434
env:
35-
WASI_VERSION: '27'
36-
WASI_VERSION_FULL: '27.0'
37-
WASI_SDK_PATH: './wasi-sdk-27.0'
38-
EM_VERSION: '4.0.1'
35+
WASI_VERSION: '29'
36+
WASI_VERSION_FULL: '29.0'
37+
WASI_SDK_PATH: './wasi-sdk-29.0'
38+
EM_VERSION: '4.0.11'
3939
EM_CACHE_FOLDER: 'emsdk-cache'
4040
NODE_VERSION: '24.11.1'
4141

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export default tseslint.config(
5555
'@typescript-eslint/naming-convention': 'off',
5656
'@typescript-eslint/no-dynamic-delete': 'off',
5757
'@typescript-eslint/method-signature-style': 'off',
58+
'@typescript-eslint/no-duplicate-enum-values': 'off',
5859
'@typescript-eslint/prefer-includes': 'off',
5960
'@typescript-eslint/ban-ts-comment': 'off',
6061
'@typescript-eslint/ban-types': 'off',

packages/emnapi/include/node/js_native_api_types.h

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,12 +238,24 @@ typedef struct {
238238
#endif // NAPI_VERSION >= 8
239239

240240
#ifndef EMNAPI_UNMODIFIED_UPSTREAM
241-
struct napi_env__ {
242-
void* reserved;
243-
uint64_t sentinel; // Should be NODE_API_VT_SENTINEL
244-
const struct node_api_js_vtable* js_vtable;
241+
// Sentinel format: "NODE_VT" (7 bytes) + marker byte.
242+
// Marker byte = (version << 1) | 1
243+
// - Bit 0 is always 1: ensures the sentinel can never match a C++ vtable
244+
// pointer (which is always pointer-aligned, thus bit 0 = 0).
245+
// - Bits 1-7: struct version number (0-127).
246+
#define NODE_API_VT_SENTINEL_VERSION 0
247+
#define NODE_API_VT_SENTINEL_MAKE(version) \
248+
(0x4E4F44455F565400ULL | (((version) << 1) | 1))
249+
#define NODE_API_VT_SENTINEL \
250+
NODE_API_VT_SENTINEL_MAKE(NODE_API_VT_SENTINEL_VERSION)
251+
252+
#define EMNAPI_NAPI_ENV_FIELDS \
253+
uint64_t sentinel; \
254+
const struct node_api_js_vtable* js_vtable; \
245255
const struct node_api_module_vtable* module_vtable;
246-
napi_extended_error_info last_error;
256+
257+
struct napi_env__ {
258+
EMNAPI_NAPI_ENV_FIELDS
247259
};
248260
#endif
249261

packages/emnapi/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
},
1515
"scripts": {
1616
"build": "node ./script/build.js",
17-
"version": "node ./script/version.js"
17+
"version": "node ./script/version.js",
18+
"generate-struct-info-wasm32": "node -e \"fs.mkdirSync('script/out',{recursive:true})\" && emcc -Iinclude/node script/generate_struct_info.c -sNODERAWFS -o script/out/generate_struct_info_wasm32.js && node ./script/out/generate_struct_info_wasm32.js",
19+
"generate-struct-info-wasm64": "node -e \"fs.mkdirSync('script/out',{recursive:true})\" && emcc -Iinclude/node script/generate_struct_info.c -sNODERAWFS -sMEMORY64 -o script/out/generate_struct_info_wasm64.js && node ./script/out/generate_struct_info_wasm64.js",
20+
"generate-struct-info": "npm run generate-struct-info-wasm32 && npm run generate-struct-info-wasm64"
1821
},
1922
"repository": {
2023
"type": "git",

packages/emnapi/script/build.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
const fs = require('fs')
22
const path = require('path')
33
const { createRequire } = require('module')
4+
const { spawnSync } = require('child_process')
45
const ts = require('typescript')
56
const rollupTypescript = require('@rollup/plugin-typescript').default
67
const rollupAlias = require('@rollup/plugin-alias').default
8+
const rollupReplace = require('@rollup/plugin-replace').default
79
const { rollup } = require('rollup')
810
const { rollupRegion } = require('@emnapi/shared')
911

@@ -13,6 +15,33 @@ const sharedModuleSpecifier = 'emnapi:shared'
1315
const runtimeRequire = createRequire(path.join(__dirname, '../../runtime/index.js'))
1416
const ctxVarName = 'emnapiPluginCtx'
1517

18+
const temp = path.join(__dirname, '../../../temp')
19+
const out = path.join(temp, 'runtime.wasm')
20+
let __EMNAPI_RUNTIME_BINARY__ = ''
21+
22+
async function buildRuntimeBinary () {
23+
fs.mkdirSync(temp, { recursive: true })
24+
spawnSync(path.join(process.env.WASI_SDK_PATH, 'bin', 'clang'), [
25+
'--target=wasm32-unknown-unknown',
26+
'-matomics',
27+
'-mbulk-memory',
28+
'-nostdlib',
29+
'-shared',
30+
'-fPIC',
31+
'-I', path.join(__dirname, '../include/node'),
32+
'-O3',
33+
'-Wl,--no-entry,--import-undefined,--export-dynamic,--import-memory,--shared-memory,--experimental-pic',
34+
path.join(__dirname, '../src/js_native_api.c'),
35+
'-o', out
36+
], { stdio: 'inherit' })
37+
const size = fs.statSync(out).size
38+
console.log(out, size)
39+
if (size > 4096) {
40+
console.log(`Built runtime binary: ${fs.statSync(out).size} bytes`)
41+
}
42+
__EMNAPI_RUNTIME_BINARY__ = `(new Uint8Array([${Array.from(fs.readFileSync(out)).join(',')}]))`
43+
}
44+
1645
async function buildEmscriptenMain (tsconfigPath, libOut) {
1746
const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, 'utf8'))
1847
const include = tsconfig.include.map(s => path.join(path.dirname(tsconfigPath), s))
@@ -23,6 +52,10 @@ async function buildEmscriptenMain (tsconfigPath, libOut) {
2352
treeshake: false,
2453
plugins: [
2554
rollupRegion(),
55+
rollupReplace({
56+
preventAssignment: true,
57+
__EMNAPI_RUNTIME_BINARY__
58+
}),
2659
rollupTypescript({
2760
tsconfig: tsconfigPath,
2861
tslib: path.join(
@@ -72,6 +105,10 @@ async function buildNonEmscriptenMain (tsconfigPath, outputDir) {
72105
treeshake: false,
73106
plugins: [
74107
rollupRegion(),
108+
rollupReplace({
109+
preventAssignment: true,
110+
__EMNAPI_RUNTIME_BINARY__
111+
}),
75112
rollupTypescript({
76113
tsconfig: tsconfigPath,
77114
tslib: path.join(
@@ -278,6 +315,7 @@ ${exportModules.map(modName => {
278315
}
279316

280317
async function build () {
318+
// await buildRuntimeBinary()
281319
const libTsconfigPath = path.join(__dirname, '../tsconfig.json')
282320
const v8TsconfigPath = path.join(__dirname, '../src/v8/tsconfig.json')
283321

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#include <stdio.h>
2+
#include "../src/emnapi_internal.h"
3+
4+
#ifdef __wasm64__
5+
#define WASM_BIT "64"
6+
#else
7+
#define WASM_BIT "32"
8+
#endif
9+
10+
int main() {
11+
FILE* f = fopen("src/typings/struct-wasm" WASM_BIT ".d.ts", "w");
12+
if (f == NULL) {
13+
fprintf(stderr, "Failed to open file\n");
14+
return 1;
15+
}
16+
17+
fprintf(f, "declare const enum NapiEnvOffset" WASM_BIT " {\n");
18+
fprintf(f, " __size__ = %zu,\n", sizeof(node_api_base_env__));
19+
fprintf(f, " vptr = %zu,\n", offsetof(node_api_base_env__, vptr));
20+
fprintf(f, " sentinel = %zu,\n", offsetof(node_api_base_env__, sentinel));
21+
fprintf(f, " js_vtable = %zu,\n", offsetof(node_api_base_env__, js_vtable));
22+
fprintf(f, " module_vtable = %zu,\n", offsetof(node_api_base_env__, module_vtable));
23+
fprintf(f, " id = %zu,\n", offsetof(node_api_base_env__, id));
24+
fprintf(f, " last_error = %zu,\n", offsetof(node_api_base_env__, last_error));
25+
fprintf(f, " last_error___size__ = %zu,\n", sizeof(napi_extended_error_info));
26+
fprintf(f, " last_error_error_message = %zu,\n", offsetof(node_api_base_env__, last_error) + offsetof(napi_extended_error_info, error_message));
27+
fprintf(f, " last_error_engine_reserved = %zu,\n", offsetof(node_api_base_env__, last_error) + offsetof(napi_extended_error_info, engine_reserved));
28+
fprintf(f, " last_error_engine_error_code = %zu,\n", offsetof(node_api_base_env__, last_error) + offsetof(napi_extended_error_info, engine_error_code));
29+
fprintf(f, " last_error_error_code = %zu,\n", offsetof(node_api_base_env__, last_error) + offsetof(napi_extended_error_info, error_code));
30+
fprintf(f, "}\n");
31+
32+
fclose(f);
33+
return 0;
34+
}

packages/emnapi/src/core/init.ts

Lines changed: 29 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export var wasmTable: WebAssembly.Table
4646

4747
export var _malloc: (size: number | bigint) => void_p
4848
export var _free: (ptr: void_p) => void
49+
export var _emnapi_create_env: () => void_p
50+
export var _emnapi_delete_env: (ptr: void_p) => void
4951

5052
export function abort (msg?: string): never {
5153
if (typeof WebAssembly.RuntimeError === 'function') {
@@ -66,6 +68,22 @@ export function setWasmTableEntry (index: number, func: Function | null): void {
6668
wasmTable.set(index, func)
6769
}
6870

71+
function setLastError (env: napi_env, error_code: napi_status, engine_error_code: uint32_t, engine_reserved: number) {
72+
from64('env')
73+
;(env as number) -= 8
74+
from64('engine_reserved')
75+
// #if MEMORY64
76+
makeSetValue('env', NapiEnvOffset64.last_error_error_code, 'error_code', 'i32')
77+
makeSetValue('env', NapiEnvOffset64.last_error_engine_error_code, 'engine_error_code', 'u32')
78+
makeSetValue('env', NapiEnvOffset64.last_error_engine_reserved, 'engine_reserved', '*')
79+
// #else
80+
makeSetValue('env', NapiEnvOffset32.last_error_error_code, 'error_code', 'i32')
81+
makeSetValue('env', NapiEnvOffset32.last_error_engine_error_code, 'engine_error_code', 'u32')
82+
makeSetValue('env', NapiEnvOffset32.last_error_engine_reserved, 'engine_reserved', '*')
83+
// #endif
84+
return error_code
85+
}
86+
6987
export var napiModule: INapiModule = {
7088
imports: {
7189
env: {},
@@ -104,6 +122,8 @@ export var napiModule: INapiModule = {
104122
if (typeof exports.free !== 'function') throw new TypeError('free is not exported')
105123
_malloc = exports.malloc as (size: number | bigint) => void_p
106124
_free = exports.free as (ptr: void_p) => void
125+
_emnapi_create_env = exports.emnapi_create_env as () => void_p
126+
_emnapi_delete_env = exports.emnapi_delete_env as (ptr: void_p) => void
107127

108128
if (!napiModule.childThread) {
109129
// main thread only
@@ -144,64 +164,28 @@ export var napiModule: INapiModule = {
144164
}
145165

146166
const envObject = napiModule.envObject || (() => {
147-
let struct_napi_env__: SNapiEnv
148-
// #if MEMORY64
149-
struct_napi_env__ = {
150-
__size__: 56,
151-
reserved: 0,
152-
sentinel: 8,
153-
js_vtable: 16,
154-
module_vtable: 24,
155-
last_error: {
156-
__size__: 24,
157-
error_message: 32,
158-
engine_reserved: 40,
159-
engine_error_code: 48,
160-
error_code: 52
161-
}
162-
}
163-
// #else
164-
struct_napi_env__ = {
165-
__size__: 40,
166-
reserved: 0,
167-
sentinel: 8,
168-
js_vtable: 16,
169-
module_vtable: 20,
170-
last_error: {
171-
__size__: 16,
172-
error_message: 24,
173-
engine_reserved: 28,
174-
engine_error_code: 32,
175-
error_code: 36
176-
}
177-
}
178-
// #endif
179-
180-
let address = _malloc(to64('struct_napi_env__.__size__')) as number
181-
const errorCodeOffset = struct_napi_env__.last_error.error_code
182-
const engineErrorCodeOffset = struct_napi_env__.last_error.engine_error_code
183-
const engineReservedOffset = struct_napi_env__.last_error.engine_reserved
167+
let address = _emnapi_create_env() as number
184168
from64('address')
185169
const envObject = napiModule.envObject = emnapiEnv = emnapiCtx.createEnv(
186170
napiModule.filename,
187171
moduleApiVersion,
188172
{
189173
address,
190-
free: (ptr) => {
191-
_free(to64('ptr'))
192-
},
193-
setLastError (env, error_code, engine_error_code, engine_reserved) {
194-
from64('engine_reserved')
195-
makeSetValue('env', 'errorCodeOffset', 'error_code', 'i32')
196-
makeSetValue('env', 'engineErrorCodeOffset', 'engine_error_code', 'u32')
197-
makeSetValue('env', 'engineReservedOffset', 'engine_reserved', '*')
174+
deleteEnv: (ptr) => {
175+
_emnapi_delete_env((to64('ptr') as number) - (to64('8') as number))
198176
},
177+
setLastError,
199178
makeDynCall_vppp: (cb: Ptr) => makeDynCall('vppp', 'cb'),
200179
makeDynCall_vp: (cb: Ptr) => makeDynCall('vp', 'cb'),
201180
abort,
202181
},
203182
emnapiNodeBinding
204183
)
184+
// #if MEMORY64
185+
makeSetValue('address - 8', NapiEnvOffset64.id, 'envObject.id', 'u32')
186+
// #else
187+
makeSetValue('address - 8', NapiEnvOffset32.id, 'envObject.id', 'u32')
188+
// #endif
205189
return envObject
206190
})()
207191

packages/emnapi/src/emnapi_internal.h

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,6 @@ EXTERN_C_END
7272

7373
EXTERN_C_START
7474

75-
EMNAPI_INTERNAL_EXTERN napi_status napi_set_last_error(node_api_basic_env env,
76-
napi_status error_code,
77-
uint32_t engine_error_code,
78-
void* engine_reserved);
79-
EMNAPI_INTERNAL_EXTERN napi_status napi_clear_last_error(node_api_basic_env env);
80-
8175
#ifdef __EMSCRIPTEN__
8276
#if __EMSCRIPTEN_major__ * 10000 + __EMSCRIPTEN_minor__ * 100 + __EMSCRIPTEN_tiny__ >= 30114 // NOLINT
8377
#define EMNAPI_KEEPALIVE_PUSH emscripten_runtime_keepalive_push
@@ -186,3 +180,5 @@ void _emnapi_env_check_gc_access(napi_env env);
186180
} while (0)
187181

188182
EXTERN_C_END
183+
184+
#include "js_native_api_internal.h"

0 commit comments

Comments
 (0)