@thi.ng/wasm-api 2.4.32 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,7 @@
20
20
  - [About](#about)
21
21
  - [Polyglot bindings generator](#polyglot-bindings-generator)
22
22
  - [Custom API modules](#custom-api-modules)
23
+ - [Async handling](#async-handling)
23
24
  - [String handling](#string-handling)
24
25
  - [Memory allocations](#memory-allocations)
25
26
  - [API module auto-initialization](#api-module-auto-initialization)
@@ -123,6 +124,13 @@ export const CustomModule: WasmModuleSpec = {
123
124
  // only happens at a later point via WasmBridge.instantiate() or
124
125
  // WasmBridge.init() and each module's own init() method...
125
126
  factory: () => new CustomAPI(),
127
+ opts: {
128
+ // List of exported symbols which should be auto-wrapped using
129
+ // `WebAssembly.promising`. The counterpart, async function WASM imports,
130
+ // are also auto-wrapped using `WebAssembly.Suspend`.
131
+ // Also see section "Async handling" in readme
132
+ asyncExports: [/* ... */]
133
+ }
126
134
  };
127
135
 
128
136
  // Optional declarations for JS-side functions which can be used from the WASM side.
@@ -172,6 +180,9 @@ export class CustomAPI implements IWasmAPI<CustomWasmExports> {
172
180
  *
173
181
  * Each module's imports will be grouped by its declared module ID, which
174
182
  * also needs to be used to declare extern functions on the WASM side.
183
+ *
184
+ * Async functions given as WASM imports are will be auto-wrapped using
185
+ * `WebAssembly.Suspend`.
175
186
  */
176
187
  getImports(): CustomImports {
177
188
  return {
@@ -230,6 +241,25 @@ export fn test_randomVec4() void {
230
241
  }
231
242
  ```
232
243
 
244
+ ## Async handling
245
+
246
+ If the targeted WASM runtime supports
247
+ [`WebAssembly.promising`](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/promising_static)
248
+ and
249
+ [`WebAssembly.Suspending`](https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Suspending),
250
+ then async functions will be automatically wrapped as follows:
251
+
252
+ - Async functions declared in a module's WASM imports will be auto-wrapped using
253
+ `WebAssembly.Suspending`
254
+ - WASM exports declared via
255
+ [`WasmModuleOpts.asyncExports`](https://docs.thi.ng/umbrella/wasm-api/interfaces/WasmModuleOpts.html#asyncexports)
256
+ will be auto-wrapped using `WebAssembly.promising`
257
+
258
+ (Also see above code examples...)
259
+
260
+ If either of these are present, but the WASM runtime doesn't support these
261
+ features, an error will be thrown during instantiation/init.
262
+
233
263
  ## String handling
234
264
 
235
265
  Most low-level languages deal with strings very differently and alas there's no
@@ -469,7 +499,7 @@ Browser ESM import:
469
499
 
470
500
  [JSDelivr documentation](https://www.jsdelivr.com/)
471
501
 
472
- Package sizes (brotli'd, pre-treeshake): ESM: 3.15 KB
502
+ Package sizes (brotli'd, pre-treeshake): ESM: 3.37 KB
473
503
 
474
504
  ## Dependencies
475
505
 
package/api.d.ts CHANGED
@@ -96,6 +96,26 @@ export interface WasmModuleSpec<T extends WasmExports = WasmExports> {
96
96
  * modules own {@link IWasmAPI.init} method.
97
97
  */
98
98
  factory: Fn<WasmBridge<T>, IWasmAPI<T>>;
99
+ /**
100
+ * Module options to configure advanced WASM features
101
+ */
102
+ opts?: WasmModuleOpts;
103
+ }
104
+ export interface WasmModuleOpts {
105
+ /**
106
+ * List of export symbol names which should be automatically wrapped
107
+ * using `WebAssembly.promising()`.
108
+ *
109
+ * @remarks
110
+ * Note: Async functions provided as module imports are automatically
111
+ * wrapped via `WebAssembly.Suspending()`, however for exports no
112
+ * auto-detection is possible and hence needs to be provided here.
113
+ *
114
+ * Reference:
115
+ * - https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/promising_static
116
+ * - https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Suspending
117
+ */
118
+ asyncExports?: string[];
99
119
  }
100
120
  /**
101
121
  * Base interface of exports declared by the WASM module. At the very least, a
package/bridge.d.ts CHANGED
@@ -59,6 +59,7 @@ export declare class WasmBridge<T extends WasmExports = WasmExports> implements
59
59
  imports: WebAssembly.Imports;
60
60
  exports: T;
61
61
  api: CoreAPI;
62
+ moduleSpecs: IObjectOf<WasmModuleSpec<T>>;
62
63
  modules: IObjectOf<IWasmAPI<T>>;
63
64
  order: string[];
64
65
  constructor(modules?: WasmModuleSpec<T>[], logger?: ILogger);
@@ -85,10 +86,17 @@ export declare class WasmBridge<T extends WasmExports = WasmExports> implements
85
86
  * will be instantiated via `WebAssembly.instantiateStreaming()`, otherwise
86
87
  * the non-streaming version will be used.
87
88
  *
89
+ * Any async functions defined in given `imports` will be automatically
90
+ * wrapped using `WebAssembly.Suspend`. An error will be thrown if async
91
+ * function imports are provided, but if the suspend feature is not
92
+ * supported by the WASM runtime. See
93
+ * {@link WasmModuleSpec.opts} for more details and counterparts.
94
+ *
88
95
  * @param src
89
96
  * @param imports
90
97
  */
91
98
  instantiate(src: Response | BufferSource | PromiseLike<Response | BufferSource>, imports?: WebAssembly.Imports): Promise<boolean>;
99
+ protected _prepareImports(imports: WebAssembly.Imports): WebAssembly.Imports;
92
100
  /**
93
101
  * Receives the WASM module's combined exports, stores them for future
94
102
  * reference and then initializes all declared bridge child API modules in
@@ -102,6 +110,7 @@ export declare class WasmBridge<T extends WasmExports = WasmExports> implements
102
110
  * @param exports
103
111
  */
104
112
  init(exports: T): Promise<boolean>;
113
+ protected _prepareExports(exports: T): T;
105
114
  /**
106
115
  * Called automatically during initialization and from other memory
107
116
  * accessors. Initializes and/or updates the various typed WASM memory views
package/bridge.js CHANGED
@@ -10,8 +10,10 @@ var __decorateClass = (decorators, target, key, kind) => {
10
10
  };
11
11
  import { INotifyMixin } from "@thi.ng/api/mixins/inotify";
12
12
  import { topoSort } from "@thi.ng/arrays/topo-sort";
13
+ import { isAsyncFunction } from "@thi.ng/checks/is-async-function";
13
14
  import { assert } from "@thi.ng/errors/assert";
14
15
  import { defError } from "@thi.ng/errors/deferror";
16
+ import { unsupportedFeature } from "@thi.ng/errors/unsupported";
15
17
  import { U16, U32, U64BIG, U8, hexdumpLines } from "@thi.ng/hex";
16
18
  import { ConsoleLogger } from "@thi.ng/logger/console";
17
19
  import {
@@ -89,6 +91,7 @@ let WasmBridge = class {
89
91
  imports;
90
92
  exports;
91
93
  api;
94
+ moduleSpecs;
92
95
  modules;
93
96
  order;
94
97
  /**
@@ -126,6 +129,13 @@ let WasmBridge = class {
126
129
  {}
127
130
  );
128
131
  this.order = topoSort(graph, (mod) => mod.deps?.map((x) => x.id));
132
+ this.moduleSpecs = this.order.reduce(
133
+ (acc, id) => {
134
+ acc[id] = graph[id];
135
+ return acc;
136
+ },
137
+ {}
138
+ );
129
139
  this.modules = this.order.reduce(
130
140
  (acc, id) => {
131
141
  acc[id] = graph[id].factory(this);
@@ -144,15 +154,41 @@ let WasmBridge = class {
144
154
  * will be instantiated via `WebAssembly.instantiateStreaming()`, otherwise
145
155
  * the non-streaming version will be used.
146
156
  *
157
+ * Any async functions defined in given `imports` will be automatically
158
+ * wrapped using `WebAssembly.Suspend`. An error will be thrown if async
159
+ * function imports are provided, but if the suspend feature is not
160
+ * supported by the WASM runtime. See
161
+ * {@link WasmModuleSpec.opts} for more details and counterparts.
162
+ *
147
163
  * @param src
148
164
  * @param imports
149
165
  */
150
166
  async instantiate(src, imports) {
151
167
  const $src = await src;
152
- const $imports = { ...this.getImports(), ...imports };
168
+ const $imports = this._prepareImports({
169
+ ...this.getImports(),
170
+ ...imports
171
+ });
153
172
  const wasm = await ($src instanceof Response ? WebAssembly.instantiateStreaming($src, $imports) : WebAssembly.instantiate($src, $imports));
154
173
  return this.init(wasm.instance.exports);
155
174
  }
175
+ _prepareImports(imports) {
176
+ for (let modID in imports) {
177
+ const moduleImports = imports[modID];
178
+ for (let id in moduleImports) {
179
+ if (isAsyncFunction(moduleImports[id])) {
180
+ if (!("Suspending" in WebAssembly))
181
+ unsupportedFeature(
182
+ "async function in WASM module imports"
183
+ );
184
+ moduleImports[id] = new WebAssembly["Suspending"](
185
+ moduleImports[id]
186
+ );
187
+ }
188
+ }
189
+ }
190
+ return imports;
191
+ }
156
192
  /**
157
193
  * Receives the WASM module's combined exports, stores them for future
158
194
  * reference and then initializes all declared bridge child API modules in
@@ -166,7 +202,7 @@ let WasmBridge = class {
166
202
  * @param exports
167
203
  */
168
204
  async init(exports) {
169
- this.exports = exports;
205
+ this.exports = this._prepareExports(exports);
170
206
  this.ensureMemory(false);
171
207
  for (const id of this.order) {
172
208
  this.logger.debug(`initializing API module: ${id}`);
@@ -176,6 +212,30 @@ let WasmBridge = class {
176
212
  this.notify({ id: EVENT_MEMORY_CHANGED, value: this.exports.memory });
177
213
  return true;
178
214
  }
215
+ _prepareExports(exports) {
216
+ for (let modID in this.modules) {
217
+ const opts = this.moduleSpecs[modID].opts;
218
+ if (!opts?.asyncExports) continue;
219
+ for (let id of opts.asyncExports) {
220
+ const item = exports[id];
221
+ if (item != null) {
222
+ if (!("promising" in WebAssembly))
223
+ unsupportedFeature(
224
+ "async function in WASM module exports"
225
+ );
226
+ exports = {
227
+ ...exports,
228
+ [id]: WebAssembly.promising(item)
229
+ };
230
+ } else {
231
+ this.logger.warn(
232
+ `WASM export ${id} declared as async, but missing...`
233
+ );
234
+ }
235
+ }
236
+ }
237
+ return exports;
238
+ }
179
239
  /**
180
240
  * Called automatically during initialization and from other memory
181
241
  * accessors. Initializes and/or updates the various typed WASM memory views
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thi.ng/wasm-api",
3
- "version": "2.4.32",
3
+ "version": "2.5.0",
4
4
  "description": "Generic, modular, extensible API bridge and infrastructure for hybrid JS & WebAssembly projects",
5
5
  "type": "module",
6
6
  "module": "./index.js",
@@ -43,22 +43,23 @@
43
43
  "tool:tangle": "../../node_modules/.bin/tangle src/**/*.ts"
44
44
  },
45
45
  "dependencies": {
46
- "@thi.ng/api": "^8.12.26",
47
- "@thi.ng/arrays": "^2.14.23",
48
- "@thi.ng/checks": "^3.10.0",
49
- "@thi.ng/errors": "^2.6.15",
50
- "@thi.ng/hex": "^2.4.18",
51
- "@thi.ng/idgen": "^2.2.100",
52
- "@thi.ng/logger": "^3.3.9"
46
+ "@thi.ng/api": "^8.12.27",
47
+ "@thi.ng/arrays": "^2.14.25",
48
+ "@thi.ng/checks": "^3.11.0",
49
+ "@thi.ng/errors": "^2.6.16",
50
+ "@thi.ng/hex": "^2.4.19",
51
+ "@thi.ng/idgen": "^2.2.101",
52
+ "@thi.ng/logger": "^3.3.10"
53
53
  },
54
54
  "devDependencies": {
55
- "esbuild": "^0.28.0",
55
+ "esbuild": "^0.28.1",
56
56
  "typedoc": "^0.28.19",
57
57
  "typescript": "^6.0.3"
58
58
  },
59
59
  "keywords": [
60
60
  "allocator",
61
61
  "api",
62
+ "async",
62
63
  "bigint",
63
64
  "bindings",
64
65
  "browser",
@@ -127,5 +128,5 @@
127
128
  "tag": "wasm",
128
129
  "year": 2022
129
130
  },
130
- "gitHead": "c47c56420bcae2e0477d252a497be44f08ca185a"
131
+ "gitHead": "6a4e7c564ebc476770d213d98a2aa6257a41ac15"
131
132
  }