@thi.ng/wasm-api 0.4.0 → 0.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/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Change Log
2
2
 
3
- - **Last updated**: 2022-08-07T15:28:01Z
3
+ - **Last updated**: 2022-08-08T22:36:17Z
4
4
  - **Generator**: [thi.ng/monopub](https://thi.ng/monopub)
5
5
 
6
6
  All notable changes to this project will be documented in this file.
@@ -9,6 +9,17 @@ See [Conventional Commits](https://conventionalcommits.org/) for commit guidelin
9
9
  **Note:** Unlisted _patch_ versions only involve non-code or otherwise excluded changes
10
10
  and/or version bumps of transitive dependencies.
11
11
 
12
+ ## [0.5.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/wasm-api@0.5.0) (2022-08-08)
13
+
14
+ #### 🚀 Features
15
+
16
+ - add memory allocation ([980c1f2](https://github.com/thi-ng/umbrella/commit/980c1f2))
17
+ - add WasmBridge.allocate()/free()
18
+ - add WasmBridge.growMemory()
19
+ - extract WasmBridge.ensureMemory()
20
+ - update WasmExports
21
+ - update Zig bindings (configurable allocator, GPA as default)
22
+
12
23
  ## [0.4.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/wasm-api@0.4.0) (2022-08-07)
13
24
 
14
25
  #### 🚀 Features
package/api.d.ts CHANGED
@@ -27,7 +27,8 @@ export interface IWasmAPI<T extends WasmExports = WasmExports> {
27
27
  }
28
28
  /**
29
29
  * Base interface of exports declared by the WASM module. At the very least, the
30
- * module needs to export its memory.
30
+ * module needs to export its memory and the functions defined in this
31
+ * interface.
31
32
  *
32
33
  * @remarks
33
34
  * This interface is supposed to be extended with the concrete exports defined
@@ -39,9 +40,32 @@ export interface IWasmAPI<T extends WasmExports = WasmExports> {
39
40
  export interface WasmExports {
40
41
  /**
41
42
  * The WASM module's linear memory buffer. The `WasmBridge` automatically
42
- * creates various typed views of that memory.
43
+ * creates various typed views of that memory (i.e. u8, u16, u32, f32 etc.)
43
44
  */
44
45
  memory: WebAssembly.Memory;
46
+ /**
47
+ * Implementation specific memory allocation function (likely heap-based).
48
+ * If successful returns address of new memory block, or zero if
49
+ * unsuccessful.
50
+ *
51
+ * @remarks
52
+ * In the supplied Zig bindings (see `/zig/core.zig`), by default this is
53
+ * using the `std.heap.GeneralPurposeAllocator` (which also automatically
54
+ * handles growing the WASM memory), however as mentioned the underlying
55
+ * mechanism is purposefully left to the actual WASM-side implementation. In
56
+ * a C program, this would likely use `malloc()` or similar...
57
+ */
58
+ _wasm_allocate(numBytes: number): number;
59
+ /**
60
+ * Implementation specific function to free a previously allocated chunk of
61
+ * of WASM memory (allocated via {@link WasmExports._wasm_allocate}).
62
+ *
63
+ * @remarks
64
+ * In the supplied Zig bindings (/zig/core.zig) this is a no-op (currently).
65
+ *
66
+ * @param addr
67
+ */
68
+ _wasm_free(addr: number): void;
45
69
  }
46
70
  /**
47
71
  * Core API of WASM imports defined by the {@link WasmBridge}. The same
package/bridge.d.ts CHANGED
@@ -1,6 +1,17 @@
1
+ /// <reference types="node" />
1
2
  import type { NumericArray } from "@thi.ng/api";
2
3
  import type { ILogger } from "@thi.ng/logger";
3
4
  import type { BigIntArray, CoreAPI, IWasmAPI, WasmExports } from "./api.js";
5
+ export declare const OutOfMemoryError: {
6
+ new (msg?: string | undefined): {
7
+ name: string;
8
+ message: string;
9
+ stack?: string | undefined;
10
+ };
11
+ captureStackTrace(targetObject: object, constructorOpt?: Function | undefined): void;
12
+ prepareStackTrace?: ((err: Error, stackTraces: NodeJS.CallSite[]) => any) | undefined;
13
+ stackTraceLimit: number;
14
+ };
4
15
  /**
5
16
  * The main interop API bridge between the JS host environment and a WebAssembly
6
17
  * module. This class provides a small core API with various typed accessors and
@@ -58,6 +69,11 @@ export declare class WasmBridge<T extends WasmExports = WasmExports> {
58
69
  * @param exports
59
70
  */
60
71
  init(exports: T): Promise<boolean>;
72
+ /**
73
+ * Called automatically. Initializes and/or updates the various typed WASM
74
+ * memory views (e.g. after growing the WASM memory).
75
+ */
76
+ ensureMemory(): void;
61
77
  /**
62
78
  * Required use for WASM module instantiation to provide JS imports to the
63
79
  * module. Returns an object of all WASM imports declared by the bridge core
@@ -65,8 +81,9 @@ export declare class WasmBridge<T extends WasmExports = WasmExports> {
65
81
  *
66
82
  * @remarks
67
83
  * Since v0.4.0 each API module's imports will be in their own WASM import
68
- * table, named using the same key which was assigned to the module when
69
- * creating the WASM bridge.
84
+ * object, named using the same key which was assigned to the module when
85
+ * creating the WASM bridge. The bridge's core API will be named `core` and
86
+ * is reserved.
70
87
  *
71
88
  * @example
72
89
  * The following creates a bridge with a fictional `custom` API module:
@@ -92,6 +109,27 @@ export declare class WasmBridge<T extends WasmExports = WasmExports> {
92
109
  * ```
93
110
  */
94
111
  getImports(): WebAssembly.Imports;
112
+ /**
113
+ * Attempts to grow the WASM memory by an additional `numPages` (64KB/page)
114
+ * and if successful updates all typed memory views to use the new
115
+ * underlying buffer.
116
+ *
117
+ * @param numPages
118
+ */
119
+ growMemory(numPages: number): void;
120
+ /**
121
+ * Attempts to allocate `numBytes` using the exported WASM core API function
122
+ * {@link WasmExports._wasm_allocate} (implementation specific) and returns
123
+ * start address of the new memory block. If unsuccessful, throws an
124
+ * {@link OutOfMemoryError}.
125
+ *
126
+ * @remarks
127
+ * See {@link WasmExports._wasm_allocate} docs for further details.
128
+ *
129
+ * @param numBytes
130
+ */
131
+ allocate(numBytes: number): number;
132
+ free(addr: number): void;
95
133
  getI8(addr: number): number;
96
134
  getU8(addr: number): number;
97
135
  getI16(addr: number): number;
package/bridge.js CHANGED
@@ -1,7 +1,9 @@
1
+ import { defError } from "@thi.ng/errors/deferror";
1
2
  import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
2
3
  import { U16, U32, U64HL, U8 } from "@thi.ng/hex";
3
4
  import { ConsoleLogger } from "@thi.ng/logger/console";
4
5
  const B32 = BigInt(32);
6
+ export const OutOfMemoryError = defError(() => "Out of memory");
5
7
  /**
6
8
  * The main interop API bridge between the JS host environment and a WebAssembly
7
9
  * module. This class provides a small core API with various typed accessors and
@@ -85,7 +87,23 @@ export class WasmBridge {
85
87
  */
86
88
  async init(exports) {
87
89
  this.exports = exports;
88
- const buf = exports.memory.buffer;
90
+ this.ensureMemory();
91
+ for (let id in this.modules) {
92
+ this.logger.debug(`initializing API module: ${id}`);
93
+ const status = await this.modules[id].init(this);
94
+ if (!status)
95
+ return false;
96
+ }
97
+ return true;
98
+ }
99
+ /**
100
+ * Called automatically. Initializes and/or updates the various typed WASM
101
+ * memory views (e.g. after growing the WASM memory).
102
+ */
103
+ ensureMemory() {
104
+ const buf = this.exports.memory.buffer;
105
+ if (this.u8 && this.u8.buffer === buf)
106
+ return;
89
107
  this.i8 = new Int8Array(buf);
90
108
  this.u8 = new Uint8Array(buf);
91
109
  this.i16 = new Int16Array(buf);
@@ -96,13 +114,6 @@ export class WasmBridge {
96
114
  this.u64 = new BigUint64Array(buf);
97
115
  this.f32 = new Float32Array(buf);
98
116
  this.f64 = new Float64Array(buf);
99
- for (let id in this.modules) {
100
- this.logger.debug(`initializing API module: ${id}`);
101
- const status = await this.modules[id].init(this);
102
- if (!status)
103
- return false;
104
- }
105
- return true;
106
117
  }
107
118
  /**
108
119
  * Required use for WASM module instantiation to provide JS imports to the
@@ -111,8 +122,9 @@ export class WasmBridge {
111
122
  *
112
123
  * @remarks
113
124
  * Since v0.4.0 each API module's imports will be in their own WASM import
114
- * table, named using the same key which was assigned to the module when
115
- * creating the WASM bridge.
125
+ * object, named using the same key which was assigned to the module when
126
+ * creating the WASM bridge. The bridge's core API will be named `core` and
127
+ * is reserved.
116
128
  *
117
129
  * @example
118
130
  * The following creates a bridge with a fictional `custom` API module:
@@ -149,6 +161,40 @@ export class WasmBridge {
149
161
  }
150
162
  return this.imports;
151
163
  }
164
+ /**
165
+ * Attempts to grow the WASM memory by an additional `numPages` (64KB/page)
166
+ * and if successful updates all typed memory views to use the new
167
+ * underlying buffer.
168
+ *
169
+ * @param numPages
170
+ */
171
+ growMemory(numPages) {
172
+ this.exports.memory.grow(numPages);
173
+ this.ensureMemory();
174
+ }
175
+ /**
176
+ * Attempts to allocate `numBytes` using the exported WASM core API function
177
+ * {@link WasmExports._wasm_allocate} (implementation specific) and returns
178
+ * start address of the new memory block. If unsuccessful, throws an
179
+ * {@link OutOfMemoryError}.
180
+ *
181
+ * @remarks
182
+ * See {@link WasmExports._wasm_allocate} docs for further details.
183
+ *
184
+ * @param numBytes
185
+ */
186
+ allocate(numBytes) {
187
+ const addr = this.exports._wasm_allocate(numBytes);
188
+ if (!addr)
189
+ throw new OutOfMemoryError(`unable to allocate: ${numBytes}`);
190
+ this.logger.debug(`allocated ${numBytes} bytes @ 0x${U32(addr)}`);
191
+ this.ensureMemory();
192
+ return addr;
193
+ }
194
+ free(addr) {
195
+ this.logger.debug(`freeing memory @ 0x${U32(addr)}`);
196
+ this.exports._wasm_free(addr);
197
+ }
152
198
  getI8(addr) {
153
199
  return this.i8[addr];
154
200
  }
@@ -303,7 +349,7 @@ export class WasmBridge {
303
349
  setString(str, addr, maxBytes, terminate = true) {
304
350
  maxBytes = Math.min(maxBytes, this.u8.length - addr);
305
351
  const len = this.utf8Encoder.encodeInto(str, this.u8.subarray(addr, addr + maxBytes)).written;
306
- if (len != null && len < maxBytes + (terminate ? 0 : 1)) {
352
+ if (len == null || len >= maxBytes + (terminate ? 0 : 1)) {
307
353
  illegalArgs(`error writing string to 0x${U32(addr)}`);
308
354
  }
309
355
  if (terminate) {
package/dev/hello.zig CHANGED
@@ -3,6 +3,11 @@
3
3
  /// import externals
4
4
  /// see build command for configuration
5
5
  const js = @import("wasmapi");
6
+ const std = @import("std");
7
+
8
+ // var buf: [1024]u8 = undefined;
9
+ // var fba = std.heap.FixedBufferAllocator.init(&buf);
10
+ pub const WASM_ALLOCATOR: ?std.mem.Allocator = null; //fba.allocator();
6
11
 
7
12
  export fn start() void {
8
13
  js.printStr("hello world!");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thi.ng/wasm-api",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Modular, extensible API bridge and generic glue code between JS & WebAssembly",
5
5
  "type": "module",
6
6
  "module": "./index.js",
@@ -31,7 +31,8 @@
31
31
  "doc:readme": "yarn doc:stats && tools:readme",
32
32
  "doc:stats": "tools:module-stats",
33
33
  "pub": "yarn npm publish --access public",
34
- "test": "testament test"
34
+ "test": "testament test",
35
+ "test:build-zig": "zig build-lib -O ReleaseSmall -target wasm32-freestanding -dynamic --strip --pkg-begin wasmapi zig/core.zig --pkg-end test/custom.zig && wasm-dis -o custom.wast custom.wasm && cp custom.wasm test"
35
36
  },
36
37
  "dependencies": {
37
38
  "@thi.ng/api": "^8.3.9",
@@ -92,5 +93,5 @@
92
93
  "status": "alpha",
93
94
  "year": 2022
94
95
  },
95
- "gitHead": "0eeb5054111cea51f4714b013dda8700ade3cd54\n"
96
+ "gitHead": "e579cb171fc720cbf0b71d3a5f4adfacccdaf214\n"
96
97
  }
package/test/custom.zig CHANGED
@@ -1,7 +1,11 @@
1
1
  // Import JS core API
2
2
  const js = @import("wasmapi");
3
+ const std = @import("std");
4
+
5
+ pub const WASM_ALLOCATOR: ?std.mem.Allocator = null;
3
6
 
4
7
  /// Fill vec2 with random values
8
+ /// Associate this function with the "custom" import section
5
9
  extern "custom" fn setVec2(addr: usize) void;
6
10
 
7
11
  export fn test_setVec2() void {
@@ -0,0 +1,39 @@
1
+ const std = @import("std");
2
+ /// Zig version. When writing code that supports multiple versions of Zig, prefer
3
+ /// feature detection (i.e. with `@hasDecl` or `@hasField`) over version checks.
4
+ pub const zig_version = std.SemanticVersion.parse("0.10.0-dev.3034+6fab6c3e4") catch unreachable;
5
+ pub const zig_backend = std.builtin.CompilerBackend.stage1;
6
+ /// Temporary until self-hosted supports the `cpu.arch` value.
7
+ pub const stage2_arch: std.Target.Cpu.Arch = .wasm32;
8
+
9
+ pub const output_mode = std.builtin.OutputMode.Lib;
10
+ pub const link_mode = std.builtin.LinkMode.Dynamic;
11
+ pub const is_test = false;
12
+ pub const single_threaded = true;
13
+ pub const abi = std.Target.Abi.musl;
14
+ pub const cpu: std.Target.Cpu = .{
15
+ .arch = .wasm32,
16
+ .model = &std.Target.wasm.cpu.generic,
17
+ .features = std.Target.wasm.featureSet(&[_]std.Target.wasm.Feature{
18
+ }),
19
+ };
20
+ pub const os = std.Target.Os{
21
+ .tag = .freestanding,
22
+ .version_range = .{ .none = {} },
23
+ };
24
+ pub const target = std.Target{
25
+ .cpu = cpu,
26
+ .os = os,
27
+ .abi = abi,
28
+ };
29
+ pub const object_format = std.Target.ObjectFormat.wasm;
30
+ pub const mode = std.builtin.Mode.ReleaseSmall;
31
+ pub const link_libc = false;
32
+ pub const link_libcpp = false;
33
+ pub const have_error_return_tracing = false;
34
+ pub const valgrind_support = false;
35
+ pub const sanitize_thread = false;
36
+ pub const position_independent_code = true;
37
+ pub const position_independent_executable = false;
38
+ pub const strip_debug_info = true;
39
+ pub const code_model = std.builtin.CodeModel.default;
package/zig/core.zig CHANGED
@@ -1,5 +1,42 @@
1
1
  //! JavaScript externals for https://thi.ng/wasm-api
2
2
 
3
+ const std = @import("std");
4
+ const root = @import("root");
5
+
6
+ /// Initialize the allocator to be exposed to the WASM host env
7
+ /// (via `_wasm_allocate()` and `_wasm_free()`).
8
+ /// If the user defines a public `WASM_ALLOCATOR` in their root file
9
+ /// then this allocator will be used, otherwise the implementation
10
+ /// falls back to using GPA.
11
+ /// Note: The type for this var is purposefully chosen as an optional,
12
+ /// effectively disabling allocations from the WASM host side if
13
+ /// `WASM_ALLOCATOR` is set to null.
14
+ pub const allocator: ?std.mem.Allocator = alloc: {
15
+ if (@hasDecl(root, "WASM_ALLOCATOR")) {
16
+ break :alloc root.WASM_ALLOCATOR;
17
+ } else {
18
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
19
+ break :alloc gpa.allocator();
20
+ }
21
+ };
22
+
23
+ /// Attempts to allocate memory using configured `allocator` and if
24
+ /// successful returns address of new chunk or zero if failed
25
+ /// Note: For SIMD compatibility all allocations are aligned to 16 bytes
26
+ pub export fn _wasm_allocate(numBytes: usize) usize {
27
+ if (allocator) |a| {
28
+ var buf = a.alignedAlloc(u8, 16, numBytes) catch return 0;
29
+ return @ptrToInt(buf.ptr);
30
+ }
31
+ return 0;
32
+ }
33
+
34
+ /// Frees chunk of heap memory (previously allocated using `_wasm_allocate()`)
35
+ /// starting at given address. Note: This is a no-op currently.
36
+ pub export fn _wasm_free(addr: usize) void {
37
+ _ = addr;
38
+ }
39
+
3
40
  /// Prints number using configured JS logger
4
41
  pub extern "core" fn printI8(x: i8) void;
5
42
  /// Prints number using configured JS logger