├── .gitignore ├── example ├── example.mjs ├── build.zig.zon ├── src │ └── main.zig └── build.zig ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── README.md └── src └── napigen.zig /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-out/ 3 | *.node -------------------------------------------------------------------------------- /example/example.mjs: -------------------------------------------------------------------------------- 1 | // run with: 2 | // zig build && node example.mjs 3 | 4 | import { createRequire } from 'node:module' 5 | const require = createRequire(import.meta.url) 6 | const native = require('./zig-out/lib/example.node') 7 | 8 | console.log('1 + 2 =', native.add(1, 2)); 9 | -------------------------------------------------------------------------------- /example/build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .example, 3 | .fingerprint = 0x6eec9b9fb837cee4, 4 | .version = "0.0.1", 5 | 6 | .dependencies = .{ 7 | .napigen = .{ 8 | .path = "..", 9 | }, 10 | }, 11 | .paths = .{ 12 | "", 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /example/src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const napigen = @import("napigen"); 3 | 4 | export fn add(a: i32, b: i32) i32 { 5 | return a + b; 6 | } 7 | 8 | comptime { 9 | napigen.defineModule(initModule); 10 | } 11 | 12 | fn initModule(js: *napigen.JsContext, exports: napigen.napi_value) anyerror!napigen.napi_value { 13 | try js.setNamedProperty(exports, "add", try js.createFunction(add)); 14 | 15 | return exports; 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, macos-latest, windows-latest] 15 | fail-fast: false 16 | 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup Zig 24 | uses: goto-bus-stop/setup-zig@v2 25 | with: 26 | version: 0.15.2 27 | 28 | - name: Verify Zig installation 29 | run: zig version 30 | 31 | - name: Build 32 | run: zig build 33 | 34 | - name: Build example 35 | run: zig build 36 | working-directory: ./example 37 | -------------------------------------------------------------------------------- /example/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const napigen = @import("napigen"); 3 | 4 | pub fn build(b: *std.Build) void { 5 | const target = b.standardTargetOptions(.{}); 6 | const optimize = b.standardOptimizeOption(.{}); 7 | 8 | const lib = b.addLibrary(.{ 9 | .name = "example", 10 | .linkage = .dynamic, 11 | .root_module = b.createModule(.{ 12 | .root_source_file = b.path("src/main.zig"), 13 | .target = target, 14 | .optimize = optimize, 15 | }), 16 | }); 17 | 18 | // Add napigen 19 | napigen.setup(lib); 20 | 21 | // Build the lib 22 | b.installArtifact(lib); 23 | 24 | // Copy the result to a *.node file so we can require() it 25 | const copy_node_step = b.addInstallLibFile(lib.getEmittedBin(), "example.node"); 26 | b.getInstallStep().dependOn(©_node_step.step); 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2023, Kamil Tomšík [info@tomsik.cz] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-napigen 2 | 3 | Comptime N-API bindings for Zig. 4 | 5 | > Requires Zig 0.15.2 or later. 6 | > 7 | > See [ggml-js](https://github.com/cztomsik/ggml-js) for a complete, real-world 8 | > example. 9 | 10 | ## Features 11 | 12 | - Primitives, tuples, structs (value types), optionals 13 | - Strings (valid for the function scope) 14 | - Struct pointers (see below) 15 | - Functions (no classes, see below) 16 | - Common napi types like `napi_value` are re-exported for convenience 17 | - All napi functions and types are accessible via `napigen.napi.xxx` for lower-level control 18 | 19 | ## Limited scope 20 | 21 | The library provides a simple and thin API, supporting only basic types. This 22 | design choice is intentional, as it is often difficult to determine the ideal 23 | mapping for more complex types. The library allows users to hook into the 24 | mapping process or use the N-API directly for finer control. 25 | 26 | Specifically, there is no support for classes. 27 | 28 | ## Structs/tuples (value types) 29 | 30 | When returning a struct/tuple by value, it is mapped to an anonymous JavaScript 31 | object/array with all properties/elements mapped recursively. Similarly, when 32 | accepting a struct/tuple by value, it is mapped back from JavaScript to the 33 | respective native type. 34 | 35 | In both cases, a copy is created, so changes to the JS object are not reflected 36 | in the native part and vice versa. 37 | 38 | ## Struct pointers (\*T) 39 | 40 | When returning a pointer to a struct, an empty JavaScript object will be created 41 | with the pointer wrapped inside. If this JavaScript object is passed to a 42 | function that accepts a pointer, the pointer is unwrapped back. 43 | 44 | The same JavaScript object is obtained for the same pointer, unless it has 45 | already been collected. This is useful for attaching state to the JavaScript 46 | counterpart and accessing that data later. 47 | 48 | Changes to JavaScript objects are not reflected in the native part, but 49 | getters/setters can be provided in JavaScript and native functions can be called 50 | as necessary. 51 | 52 | ## Functions 53 | 54 | JavaScript functions can be created with ctx.createFunction(zig_fn) and then 55 | exported like any other value. Only comptime-known functions are supported. If 56 | an error is returned from a function call, an exception is thrown in JavaScript. 57 | 58 | ```zig 59 | fn add(a: i32, b: i32) i32 { 60 | return a + b; 61 | } 62 | 63 | // Somewhere where the JsContext is available 64 | const js_fun: napigen.napi_value = try js.createFunction(add); 65 | 66 | // Make the function accessible to JavaScript 67 | try js.setNamedProperty(exports, "add", js_fun); 68 | ``` 69 | 70 | Note that **the number of arguments must match exactly**. So if you need to 71 | support optional arguments, you will have to provide a wrapper function in JS, 72 | which calls the native function with the correct arguments. 73 | 74 | ## Callbacks, \*JsContext, napi_value 75 | 76 | Functions can also accept the current `*JsContext`, which is useful for calling 77 | the N-API directly or performing callbacks. To get a raw JavaScript value, 78 | simply use `napi_value` as an argument type. 79 | 80 | ```zig 81 | fn callMeBack(js: *napigen.JsContext, recv: napigen.napi_value, fun: napigen.napi_value) !void { 82 | try js.callFunction(recv, fun, .{ "Hello from Zig" }); 83 | } 84 | ``` 85 | 86 | And then 87 | 88 | ```javascript 89 | native.callMeBack(console, console.log) 90 | ``` 91 | 92 | If you need to store the callback for a longer period of time, you should create 93 | a ref. For now, you have to do that directly, using `napi_create_reference()`. 94 | 95 | ## defineModule(init_fn), exports 96 | 97 | N-API modules need to export a function which will also init & return the 98 | `exports` object. You could export `napi_register_module_v1` and call 99 | `JsContext.init()` yourself but there's also a shorthand using `comptime` block 100 | which will allow you to use `try` anywhere inside: 101 | 102 | ```zig 103 | comptime { napigen.defineModule(initModule) } 104 | 105 | fn initModule(js: *napigen.JsContext, exports: napigen.napi_value) anyerror!napigen.napi_value { 106 | try js.setNamedProperty(exports, ...); 107 | ... 108 | 109 | return exports; 110 | } 111 | ``` 112 | 113 | ## Hooks 114 | 115 | Whenever a value is passed from Zig to JS or vice versa, the library will call a 116 | hook function, if one is defined. This allows you to customize the mapping 117 | process. 118 | 119 | Hooks have to be defined in the root module, and they need to be named 120 | `napigenRead` and `napigenWrite` respectively. They must have the following 121 | signature: 122 | 123 | ```zig 124 | fn napigenRead(js: *napigen.JsContext, comptime T: type, value: napigen.napi_value) !T { 125 | return switch (T) { 126 | // we can easily customize the mapping for specific types 127 | // for example, we can allow passing regular JS strings anywhere where we expect an InternedString 128 | InternedString => InternedString.from(try js.read([]const u8)), 129 | 130 | // otherwise, just use the default mapping, note that this time 131 | // we call js.defaultRead() explicitly, to avoid infinite recursion 132 | else => js.defaultRead(T, value), 133 | } 134 | } 135 | 136 | pub fn napigenWrite(js: *napigen.JsContext, value: anytype) !napigen.napi_value { 137 | return switch (@TypeOf(value) { 138 | // convert InternedString to back to a JS string (hypothetically) 139 | InternedString => try js.write(value.ptr), 140 | 141 | // same thing here 142 | else => js.defaultWrite(value), 143 | } 144 | } 145 | ``` 146 | 147 | --- 148 | 149 | ## Complete example 150 | 151 | The repository includes a complete example in the `example` directory. Here's a quick walkthrough: 152 | 153 | **1. Create a new library** 154 | 155 | ```bash 156 | mkdir example 157 | cd example 158 | zig init-lib 159 | ``` 160 | 161 | **2. Add napigen as zig module.** 162 | 163 | ``` 164 | zig fetch --save git+https://github.com/cztomsik/napigen#main 165 | ``` 166 | 167 | **3. Update build.zig** 168 | 169 | Then, change your `build.zig` to something like this: 170 | 171 | ```zig 172 | const std = @import("std"); 173 | const napigen = @import("napigen"); 174 | 175 | pub fn build(b: *std.Build) void { 176 | const target = b.standardTargetOptions(.{}); 177 | const optimize = b.standardOptimizeOption(.{}); 178 | 179 | const lib = b.addLibrary(.{ 180 | .name = "example", 181 | .linkage = .dynamic, 182 | .root_module = b.createModule(.{ 183 | .root_source_file = b.path("src/main.zig"), 184 | .target = target, 185 | .optimize = optimize, 186 | }), 187 | }); 188 | 189 | // Add napigen 190 | napigen.setup(lib); 191 | 192 | // Build the lib 193 | b.installArtifact(lib); 194 | 195 | // Copy the result to a *.node file so we can require() it 196 | const copy_node_step = b.addInstallLibFile(lib.getEmittedBin(), "example.node"); 197 | b.getInstallStep().dependOn(©_node_step.step); 198 | } 199 | ``` 200 | 201 | **4. Define & export something useful** 202 | 203 | Next, define some functions and the N-API module itself in `src/main.zig` 204 | 205 | ```zig 206 | const std = @import("std"); 207 | const napigen = @import("napigen"); 208 | 209 | export fn add(a: i32, b: i32) i32 { 210 | return a + b; 211 | } 212 | 213 | comptime { 214 | napigen.defineModule(initModule); 215 | } 216 | 217 | fn initModule(js: *napigen.JsContext, exports: napigen.napi_value) anyerror!napigen.napi_value { 218 | try js.setNamedProperty(exports, "add", try js.createFunction(add)); 219 | 220 | return exports; 221 | } 222 | ``` 223 | 224 | **5. Use it from JS side** 225 | 226 | Finally, use it from JavaScript as expected: 227 | 228 | ```javascript 229 | import { createRequire } from 'node:module' 230 | const require = createRequire(import.meta.url) 231 | const native = require('./zig-out/lib/example.node') 232 | 233 | console.log('1 + 2 =', native.add(1, 2)) 234 | ``` 235 | 236 | To build the library and run the script: 237 | 238 | ``` 239 | > zig build && node example.js 240 | 1 + 2 = 3 241 | ``` 242 | 243 | ## License 244 | 245 | MIT 246 | -------------------------------------------------------------------------------- /src/napigen.zig: -------------------------------------------------------------------------------- 1 | const root = @import("root"); 2 | const std = @import("std"); 3 | 4 | pub const napi = @cImport({ 5 | @cInclude("node_api.h"); 6 | }); 7 | 8 | // Re-export commonly used napi types for convenience 9 | // Users can access all napi types/functions via napigen.napi.xxx 10 | pub const napi_value = napi.napi_value; 11 | pub const napi_env = napi.napi_env; 12 | pub const napi_status = napi.napi_status; 13 | pub const napi_ref = napi.napi_ref; 14 | pub const napi_callback_info = napi.napi_callback_info; 15 | pub const napi_valuetype = napi.napi_valuetype; 16 | 17 | // define error types 18 | pub const NapiError = error{ napi_invalid_arg, napi_object_expected, napi_string_expected, napi_name_expected, napi_function_expected, napi_number_expected, napi_boolean_expected, napi_array_expected, napi_generic_failure, napi_pending_exception, napi_cancelled, napi_escape_called_twice, napi_handle_scope_mismatch, napi_callback_scope_mismatch, napi_queue_full, napi_closing, napi_bigint_expected, napi_date_expected, napi_arraybuffer_expected, napi_detachable_arraybuffer_expected, napi_would_deadlock }; 19 | pub const Error = std.mem.Allocator.Error || error{InvalidArgumentCount} || NapiError; 20 | 21 | /// translate napi_status > 0 to NapiError with the same name 22 | pub fn check(status: napi.napi_status) Error!void { 23 | if (status != napi.napi_ok) { 24 | inline for (comptime std.meta.fieldNames(NapiError)) |f| { 25 | if (status == @field(napi, f)) return @field(NapiError, f); 26 | } else @panic("unknown napi err"); 27 | } 28 | } 29 | 30 | pub const allocator = std.heap.c_allocator; 31 | 32 | /// Convenience helper to define N-API module with a single function 33 | pub fn defineModule(comptime init_fn: fn (*JsContext, napi.napi_value) anyerror!napi.napi_value) void { 34 | const NapigenNapiModule = struct { 35 | fn register(env: napi.napi_env, exports: napi.napi_value) callconv(.c) napi.napi_value { 36 | var cx = JsContext.init(env) catch @panic("could not init JS context"); 37 | return init_fn(cx, exports) catch |e| cx.throw(e); 38 | } 39 | }; 40 | 41 | @export(&NapigenNapiModule.register, .{ .name = "napi_register_module_v1", .linkage = .strong }); 42 | } 43 | 44 | pub const JsContext = struct { 45 | env: napi.napi_env, 46 | arena: GenerationalArena, 47 | refs: std.AutoHashMapUnmanaged(usize, napi.napi_ref) = .{}, 48 | 49 | /// Init the JS context. 50 | pub fn init(env: napi.napi_env) Error!*JsContext { 51 | const self = try allocator.create(JsContext); 52 | try check(napi.napi_set_instance_data(env, self, finalize, null)); 53 | self.* = .{ 54 | .env = env, 55 | .arena = GenerationalArena.init(allocator), 56 | }; 57 | return self; 58 | } 59 | 60 | /// Deinit the JS context. 61 | pub fn deinit(self: *JsContext) void { 62 | self.arena.deinit(); 63 | allocator.destroy(self); 64 | } 65 | 66 | /// Retreive the JS context from the N-API environment. 67 | fn getInstance(env: napi.napi_env) *JsContext { 68 | var res: *JsContext = undefined; 69 | check(napi.napi_get_instance_data(env, @ptrCast(&res))) catch @panic("could not get JS context"); 70 | return res; 71 | } 72 | 73 | fn finalize(_: napi.napi_env, data: ?*anyopaque, _: ?*anyopaque) callconv(.c) void { 74 | // instance data might be already destroyed 75 | const self: *JsContext = @ptrCast(@alignCast(data)); 76 | self.deinit(); 77 | } 78 | 79 | /// Get the type of a JS value. 80 | pub fn typeOf(self: *JsContext, val: napi.napi_value) Error!napi.napi_valuetype { 81 | var res: napi.napi_valuetype = undefined; 82 | try check(napi.napi_typeof(self.env, val, &res)); 83 | return res; 84 | } 85 | 86 | /// Throw an error. 87 | pub fn throw(self: *JsContext, err: anyerror) napi.napi_value { 88 | const msg = @as([*c]const u8, @ptrCast(@errorName(err))); 89 | check(napi.napi_throw_error(self.env, null, msg)) catch |e| { 90 | if (e != error.napi_pending_exception) std.debug.panic("throw failed {s} {any}", .{ msg, e }); 91 | }; 92 | return self.undefined() catch @panic("throw return undefined"); 93 | } 94 | 95 | /// Get the JS `undefined` value. 96 | pub fn @"undefined"(self: *JsContext) Error!napi.napi_value { 97 | var res: napi.napi_value = undefined; 98 | try check(napi.napi_get_undefined(self.env, &res)); 99 | return res; 100 | } 101 | 102 | /// Get the JS `null` value. 103 | pub fn @"null"(self: *JsContext) Error!napi.napi_value { 104 | var res: napi.napi_value = undefined; 105 | try check(napi.napi_get_null(self.env, &res)); 106 | return res; 107 | } 108 | 109 | /// Create a JS boolean value. 110 | pub fn createBoolean(self: *JsContext, val: bool) Error!napi.napi_value { 111 | var res: napi.napi_value = undefined; 112 | try check(napi.napi_get_boolean(self.env, val, &res)); 113 | return res; 114 | } 115 | 116 | /// Read a native boolean from a JS value. 117 | pub fn readBoolean(self: *JsContext, val: napi.napi_value) Error!bool { 118 | var res: bool = undefined; 119 | try check(napi.napi_get_value_bool(self.env, val, &res)); 120 | return res; 121 | } 122 | 123 | /// Create a JS number value. 124 | pub fn createNumber(self: *JsContext, val: anytype) Error!napi.napi_value { 125 | var res: napi.napi_value = undefined; 126 | 127 | switch (@TypeOf(val)) { 128 | u8, u16, u32, c_uint => try check(napi.napi_create_uint32(self.env, val, &res)), 129 | u64, usize => try check(napi.napi_create_bigint_uint64(self.env, val, &res)), 130 | i8, i16, i32, c_int => try check(napi.napi_create_int32(self.env, val, &res)), 131 | i64, isize, @TypeOf(0) => try check(napi.napi_create_bigint_int64(self.env, val, &res)), 132 | f16, f32, f64, @TypeOf(0.0) => try check(napi.napi_create_double(self.env, val, &res)), 133 | else => |T| @compileError(@typeName(T) ++ " is not supported number"), 134 | } 135 | 136 | return res; 137 | } 138 | 139 | /// Read a native number from a JS value. 140 | pub fn readNumber(self: *JsContext, comptime T: type, val: napi.napi_value) Error!T { 141 | var res: T = undefined; 142 | var lossless: bool = undefined; // TODO: check overflow? 143 | 144 | switch (T) { 145 | u8, u16 => res = @as(T, @truncate(try self.read(u32, val))), 146 | u32, c_uint => try check(napi.napi_get_value_uint32(self.env, val, &res)), 147 | u64, usize => try check(napi.napi_get_value_bigint_uint64(self.env, val, &res, &lossless)), 148 | i8, i16 => res = @as(T, @truncate(self.read(i32, val))), 149 | i32, c_int => try check(napi.napi_get_value_int32(self.env, val, &res)), 150 | i64, isize => try check(napi.napi_get_value_bigint_int64(self.env, val, &res, &lossless)), 151 | f16, f32 => res = @as(T, @floatCast(try self.readNumber(f64, val))), 152 | f64 => try check(napi.napi_get_value_double(self.env, val, &res)), 153 | else => @compileError(@typeName(T) ++ " is not supported number"), 154 | } 155 | 156 | return res; 157 | } 158 | 159 | /// Create a JS string value. 160 | pub fn createString(self: *JsContext, val: []const u8) Error!napi.napi_value { 161 | var res: napi.napi_value = undefined; 162 | try check(napi.napi_create_string_utf8(self.env, @as([*c]const u8, @ptrCast(val)), val.len, &res)); 163 | return res; 164 | } 165 | 166 | /// Get the length of a JS string value. 167 | pub fn getStringLength(self: *JsContext, val: napi.napi_value) Error!usize { 168 | var res: usize = undefined; 169 | try check(napi.napi_get_value_string_utf8(self.env, val, null, 0, &res)); 170 | return res; 171 | } 172 | 173 | /// Read JS string into a temporary, arena-allocated buffer. 174 | pub fn readString(self: *JsContext, val: napi.napi_value) Error![]const u8 { 175 | var len: usize = try self.getStringLength(val); 176 | var buf = try self.arena.allocator().alloc(u8, len + 1); 177 | try check(napi.napi_get_value_string_utf8(self.env, val, @as([*c]u8, @ptrCast(buf)), buf.len, &len)); 178 | return buf[0..len]; 179 | } 180 | 181 | /// Create an empty JS array. 182 | pub fn createArray(self: *JsContext) Error!napi.napi_value { 183 | var res: napi.napi_value = undefined; 184 | try check(napi.napi_create_array(self.env, &res)); 185 | return res; 186 | } 187 | 188 | /// Create a JS array with a given length. 189 | pub fn createArrayWithLength(self: *JsContext, length: u32) Error!napi.napi_value { 190 | var res: napi.napi_value = undefined; 191 | try check(napi.napi_create_array_with_length(self.env, length, &res)); 192 | return res; 193 | } 194 | 195 | /// Create a JS array from a native array/slice. 196 | pub fn createArrayFrom(self: *JsContext, val: anytype) Error!napi.napi_value { 197 | const res = try self.createArrayWithLength(@as(u32, @truncate(val.len))); 198 | for (val, 0..) |v, i| { 199 | try self.setElement(res, @as(u32, @truncate(i)), try self.write(v)); 200 | } 201 | return res; 202 | } 203 | 204 | /// Get the length of a JS array. 205 | pub fn getArrayLength(self: *JsContext, array: napi.napi_value) Error!u32 { 206 | var res: u32 = undefined; 207 | try check(napi.napi_get_array_length(self.env, array, &res)); 208 | return res; 209 | } 210 | 211 | /// Read a native slice from a JS array. 212 | pub fn readArray(self: *JsContext, comptime T: type, array: napi.napi_value) Error![]T { 213 | const len: u32 = try self.getArrayLength(array); 214 | const res = try self.arena.allocator().alloc(T, len); 215 | for (res, 0..) |*v, i| { 216 | v.* = try self.read(T, try self.getElement(array, @as(u32, @intCast(i)))); 217 | } 218 | return res; 219 | } 220 | 221 | /// Read a native fixed-size array from a JS array. 222 | pub fn readArrayFixed(self: *JsContext, comptime T: type, comptime len: usize, array: napi.napi_value) Error![len]T { 223 | var res: [len]T = undefined; 224 | for (0..len) |i| { 225 | res[i] = try self.read(T, try self.getElement(array, @as(u32, @intCast(i)))); 226 | } 227 | return res; 228 | } 229 | 230 | /// Get a JS value from a JS array by index. 231 | pub fn getElement(self: *JsContext, array: napi.napi_value, index: u32) Error!napi.napi_value { 232 | var res: napi.napi_value = undefined; 233 | try check(napi.napi_get_element(self.env, array, index, &res)); 234 | return res; 235 | } 236 | 237 | /// Set a JS value to a JS array by index. 238 | pub fn setElement(self: *JsContext, array: napi.napi_value, index: u32, value: napi.napi_value) Error!void { 239 | try check(napi.napi_set_element(self.env, array, index, value)); 240 | } 241 | 242 | /// Create a JS array from a tuple. 243 | pub fn createTuple(self: *JsContext, val: anytype) Error!napi.napi_value { 244 | const fields = std.meta.fields(@TypeOf(val)); 245 | const res = try self.createArrayWithLength(fields.len); 246 | inline for (fields, 0..) |f, i| { 247 | const v = try self.write(@field(val, f.name)); 248 | try self.setElement(res, @as(u32, @truncate(i)), v); 249 | } 250 | return res; 251 | } 252 | 253 | /// Read a JS array into a tuple. 254 | pub fn readTuple(self: *JsContext, comptime T: type, val: napi.napi_value) Error!T { 255 | const fields = std.meta.fields(T); 256 | var res: T = undefined; 257 | inline for (fields, 0..) |f, i| { 258 | const v = try self.getElement(val, @as(u32, @truncate(i))); 259 | @field(res, f.name) = try self.read(f.type, v); 260 | } 261 | return res; 262 | } 263 | 264 | /// Create an empty JS object. 265 | pub fn createObject(self: *JsContext) Error!napi.napi_value { 266 | var res: napi.napi_value = undefined; 267 | try check(napi.napi_create_object(self.env, &res)); 268 | return res; 269 | } 270 | 271 | /// Create a JS object from a native value. 272 | pub fn createObjectFrom(self: *JsContext, val: anytype) Error!napi.napi_value { 273 | const res: napi.napi_value = try self.createObject(); 274 | inline for (std.meta.fields(@TypeOf(val))) |f| { 275 | const v = try self.write(@field(val, f.name)); 276 | try self.setNamedProperty(res, f.name ++ "", v); 277 | } 278 | return res; 279 | } 280 | 281 | /// Read a struct/tuple from a JS object. 282 | pub fn readObject(self: *JsContext, comptime T: type, val: napi.napi_value) Error!T { 283 | var res: T = undefined; 284 | inline for (std.meta.fields(T)) |f| { 285 | const v = try self.getNamedProperty(val, f.name ++ ""); 286 | @field(res, f.name) = try self.read(f.type, v); 287 | } 288 | return res; 289 | } 290 | 291 | /// Get the JS value of an object property by name. 292 | pub fn getNamedProperty(self: *JsContext, object: napi.napi_value, prop_name: [*:0]const u8) Error!napi.napi_value { 293 | var res: napi.napi_value = undefined; 294 | try check(napi.napi_get_named_property(self.env, object, prop_name, &res)); 295 | return res; 296 | } 297 | 298 | /// Set the JS value of an object property by name. 299 | pub fn setNamedProperty(self: *JsContext, object: napi.napi_value, prop_name: [*:0]const u8, value: napi.napi_value) Error!void { 300 | try check(napi.napi_set_named_property(self.env, object, prop_name, value)); 301 | } 302 | 303 | pub fn wrapPtr(self: *JsContext, val: anytype) Error!napi.napi_value { 304 | const info = @typeInfo(@TypeOf(val)); 305 | if (comptime info == .pointer and @typeInfo(info.pointer.child) == .@"fn") @compileError("use createFunction() to export functions"); 306 | 307 | var res: napi.napi_value = undefined; 308 | 309 | if (self.refs.get(@intFromPtr(val))) |ref| { 310 | if (napi.napi_get_reference_value(self.env, ref, &res) == napi.napi_ok and res != null) { 311 | return res; 312 | } else { 313 | _ = napi.napi_delete_reference(self.env, ref); 314 | } 315 | } 316 | 317 | var ref: napi.napi_ref = undefined; 318 | res = try self.createObject(); 319 | try check(napi.napi_wrap(self.env, res, @constCast(val), &deleteRef, @as(*anyopaque, @ptrCast(@constCast(val))), &ref)); 320 | try self.refs.put(allocator, @intFromPtr(val), ref); 321 | 322 | return res; 323 | } 324 | 325 | fn deleteRef(env: napi.napi_env, _: ?*anyopaque, ptr: ?*anyopaque) callconv(.c) void { 326 | var js = JsContext.getInstance(env); 327 | 328 | if (js.refs.get(@intFromPtr(ptr.?))) |ref| { 329 | // not sure if this is really needed but if we have a new ref and it's valid, we want to skip this 330 | var val: napi.napi_value = undefined; 331 | if (napi.napi_get_reference_value(env, ref, &val) == napi.napi_ok) return; 332 | 333 | _ = napi.napi_delete_reference(env, ref); 334 | _ = js.refs.remove(@intFromPtr(ptr.?)); 335 | } 336 | } 337 | 338 | /// Unwrap a pointer from a JS object. 339 | pub fn unwrap(self: *JsContext, comptime T: type, val: napi.napi_value) Error!*T { 340 | var res: *T = undefined; 341 | try check(napi.napi_unwrap(self.env, val, @as([*c]?*anyopaque, @ptrCast(&res)))); 342 | return res; 343 | } 344 | 345 | pub const read = if (@hasDecl(root, "napigenRead")) root.napigenRead else defaultRead; 346 | 347 | pub fn defaultRead(self: *JsContext, comptime T: type, val: napi.napi_value) Error!T { 348 | if (T == napi.napi_value) return val; 349 | if (comptime isString(T)) return self.readString(val); 350 | 351 | return switch (@typeInfo(T)) { 352 | .void => void{}, 353 | .null => null, 354 | .bool => self.readBoolean(val), 355 | .int, .comptime_int, .float, .comptime_float => self.readNumber(T, val), 356 | .@"enum" => std.meta.intToEnum(T, self.read(u32, val)), 357 | .@"struct" => if (isTuple(T)) self.readTuple(T, val) else self.readObject(T, val), 358 | .optional => |info| if (try self.typeOf(val) == napi.napi_null) null else try self.read(info.child, val), 359 | .pointer => |info| switch (info.size) { 360 | .one, .c => self.unwrap(info.child, val), 361 | .slice => self.readArray(info.child, val), 362 | else => @compileError("reading " ++ @tagName(@typeInfo(T)) ++ " " ++ @typeName(T) ++ " is not supported"), 363 | }, 364 | .array => |info| try self.readArrayFixed(info.child, info.len, val), 365 | else => @compileError("reading " ++ @tagName(@typeInfo(T)) ++ " " ++ @typeName(T) ++ " is not supported"), 366 | }; 367 | } 368 | 369 | pub const write = if (@hasDecl(root, "napigenWrite")) root.napigenWrite else defaultWrite; 370 | 371 | pub fn defaultWrite(self: *JsContext, val: anytype) Error!napi.napi_value { 372 | const T = @TypeOf(val); 373 | 374 | if (T == napi.napi_value) return val; 375 | if (comptime isString(T)) return self.createString(val); 376 | 377 | return switch (@typeInfo(T)) { 378 | .void => self.undefined(), 379 | .null => self.null(), 380 | .bool => self.createBoolean(val), 381 | .int, .comptime_int, .float, .comptime_float => self.createNumber(val), 382 | .@"enum" => self.createNumber(@as(u32, @intFromEnum(val))), 383 | .@"struct" => if (isTuple(T)) self.createTuple(val) else self.createObjectFrom(val), 384 | .optional => if (val) |v| self.write(v) else self.null(), 385 | .pointer => |info| switch (info.size) { 386 | .one, .c => self.wrapPtr(val), 387 | .slice => self.createArrayFrom(val), 388 | else => @compileError("writing " ++ @tagName(@typeInfo(T)) ++ " " ++ @typeName(T) ++ " is not supported"), 389 | }, 390 | .array => self.createArrayFrom(val), 391 | else => @compileError("writing " ++ @tagName(@typeInfo(T)) ++ " " ++ @typeName(T) ++ " is not supported"), 392 | }; 393 | } 394 | 395 | /// Create a JS function. 396 | pub fn createFunction(self: *JsContext, comptime fun: anytype) Error!napi.napi_value { 397 | return self.createNamedFunction("anonymous", fun); 398 | } 399 | 400 | /// Create a named JS function. 401 | pub fn createNamedFunction(self: *JsContext, comptime name: [*:0]const u8, comptime fun: anytype) Error!napi.napi_value { 402 | const F = @TypeOf(fun); 403 | const Args = std.meta.ArgsTuple(F); 404 | const Res = @typeInfo(F).@"fn".return_type.?; 405 | 406 | const Helper = struct { 407 | fn call(env: napi.napi_env, cb_info: napi.napi_callback_info) callconv(.c) napi.napi_value { 408 | var js = JsContext.getInstance(env); 409 | js.arena.inc(); 410 | defer js.arena.dec(); 411 | 412 | const args = readArgs(js, cb_info) catch |e| return js.throw(e); 413 | const res = @call(.auto, fun, args); 414 | 415 | if (comptime @typeInfo(Res) == .error_union) { 416 | return if (res) |r| js.write(r) catch |e| js.throw(e) else |e| js.throw(e); 417 | } else { 418 | return js.write(res) catch |e| js.throw(e); 419 | } 420 | } 421 | 422 | fn readArgs(js: *JsContext, cb_info: napi.napi_callback_info) Error!Args { 423 | var args: Args = undefined; 424 | var argc: usize = args.len; 425 | var argv: [args.len]napi.napi_value = undefined; 426 | try check(napi.napi_get_cb_info(js.env, cb_info, &argc, &argv, null, null)); 427 | 428 | var i: usize = 0; 429 | inline for (std.meta.fields(Args)) |f| { 430 | if (comptime f.type == *JsContext) { 431 | @field(args, f.name) = js; 432 | continue; 433 | } 434 | 435 | @field(args, f.name) = try js.read(f.type, argv[i]); 436 | i += 1; 437 | } 438 | 439 | if (i != argc) { 440 | std.debug.print("Expected {d} args\n", .{i}); 441 | return error.InvalidArgumentCount; 442 | } 443 | 444 | return args; 445 | } 446 | }; 447 | 448 | var res: napi.napi_value = undefined; 449 | try check(napi.napi_create_function(self.env, name, napi.NAPI_AUTO_LENGTH, &Helper.call, null, &res)); 450 | return res; 451 | } 452 | 453 | /// Call a JS function. 454 | pub fn callFunction(self: *JsContext, recv: napi.napi_value, fun: napi.napi_value, args: anytype) Error!napi.napi_value { 455 | const Args = @TypeOf(args); 456 | var argv: [std.meta.fields(Args).len]napi.napi_value = undefined; 457 | inline for (std.meta.fields(Args), 0..) |f, i| { 458 | argv[i] = try self.write(@field(args, f.name)); 459 | } 460 | 461 | var res: napi.napi_value = undefined; 462 | try check(napi.napi_call_function(self.env, recv, fun, argv.len, &argv, &res)); 463 | return res; 464 | } 465 | 466 | /// Export a single declaration. 467 | pub fn exportOne(self: *JsContext, exports: napi.napi_value, comptime name: []const u8, val: anytype) Error!void { 468 | const c_name = name ++ ""; 469 | 470 | if (comptime @typeInfo(@TypeOf(val)) == .@"fn") { 471 | try self.setNamedProperty(exports, c_name, try self.createNamedFunction(c_name, val)); 472 | } else { 473 | try self.setNamedProperty(exports, c_name, try self.write(val)); 474 | } 475 | } 476 | 477 | /// Export all public declarations from a module. 478 | pub fn exportAll(self: *JsContext, exports: napi.napi_value, comptime mod: anytype) Error!void { 479 | inline for (comptime std.meta.declarations(mod)) |d| { 480 | if (@TypeOf(@field(mod, d.name)) == type) continue; 481 | 482 | try self.exportOne(exports, d.name, @field(mod, d.name)); 483 | } 484 | } 485 | }; 486 | 487 | // To allow reading strings and other slices, we need to allocate memory 488 | // somewhere. Such data is only needed for a short time, so we can use a 489 | // generational arena to free the memory when it is no longer needed 490 | // - count is increased when a function is called and decreased when it returns 491 | // - when count reaches 0, the arena is reset (but not freed) 492 | const GenerationalArena = struct { 493 | count: u32 = 0, 494 | arena: std.heap.ArenaAllocator, 495 | 496 | pub fn init(child_allocator: std.mem.Allocator) GenerationalArena { 497 | return .{ 498 | .arena = std.heap.ArenaAllocator.init(child_allocator), 499 | }; 500 | } 501 | 502 | pub fn deinit(self: *GenerationalArena) void { 503 | self.arena.deinit(); 504 | } 505 | 506 | pub fn allocator(self: *GenerationalArena) std.mem.Allocator { 507 | return self.arena.allocator(); 508 | } 509 | 510 | pub fn inc(self: *GenerationalArena) void { 511 | self.count += 1; 512 | } 513 | 514 | pub fn dec(self: *GenerationalArena) void { 515 | self.count -= 1; 516 | if (self.count == 0) { 517 | _ = self.arena.reset(.retain_capacity); 518 | } 519 | } 520 | }; 521 | 522 | fn isString(comptime T: type) bool { 523 | return switch (@typeInfo(T)) { 524 | .pointer => |ptr| ptr.size == .slice and ptr.child == u8, 525 | else => return false, 526 | }; 527 | } 528 | 529 | fn isTuple(comptime T: type) bool { 530 | return switch (@typeInfo(T)) { 531 | .@"struct" => |s| s.is_tuple, 532 | else => return false, 533 | }; 534 | } 535 | --------------------------------------------------------------------------------