├── .gitignore ├── LICENSING.md ├── .gitmodules ├── src ├── user_context.zig ├── test_runner.zig ├── internal_api.zig ├── console.zig ├── private_api.zig ├── tests │ ├── userctx_test.zig │ ├── global_test.zig │ ├── types_multiple_test.zig │ ├── types_object.zig │ ├── test_utils.zig │ ├── types_primitives_test.zig │ ├── cbk_test.zig │ ├── types_complex_test.zig │ ├── types_native_test.zig │ └── proto_test.zig ├── main_shell.zig ├── types.zig ├── refs.zig ├── native_context.zig ├── api.zig ├── engine.zig ├── bench.zig ├── main_bench.zig ├── generate.zig ├── engines │ └── v8 │ │ ├── types_primitives.zig │ │ └── callback.zig ├── run_tests.zig ├── shell.zig ├── pretty.zig └── loop.zig ├── .github ├── workflows │ ├── zig-fmt.yml │ ├── benchmark.yml │ └── zig-test.yml └── actions │ └── install │ └── action.yml ├── Makefile ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache 2 | zig-out 3 | /.zig-cache/ 4 | vendor/v8 5 | -------------------------------------------------------------------------------- /LICENSING.md: -------------------------------------------------------------------------------- 1 | # Licensing 2 | 3 | License names used in this document are as per [SPDX License 4 | List](https://spdx.org/licenses/). 5 | 6 | The default license for this project is [Apache-2.0](LICENSE). 7 | 8 | The following directories and their subdirectories are licensed under their 9 | original upstream licenses: 10 | 11 | ``` 12 | vendor/ 13 | ``` 14 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/zig-v8"] 2 | path = vendor/zig-v8 3 | url = https://github.com/lightpanda-io/zig-v8-fork.git/ 4 | branch = zig-0.14 5 | [submodule "vendor/tigerbeetle-io"] 6 | path = vendor/tigerbeetle-io 7 | url = https://github.com/lightpanda-io/tigerbeetle-io.git/ 8 | branch = zig-0.14 9 | [submodule "vendor/linenoise-mob"] 10 | path = vendor/linenoise-mob 11 | url = https://github.com/rain-1/linenoise-mob.git/ 12 | -------------------------------------------------------------------------------- /src/user_context.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | // UserContext is a type defined by the user optionally passed to the native 4 | // API. 5 | // The type is defined via a root declaration. 6 | // Request a UserContext parameter in your native implementation to get the 7 | // context. 8 | pub const UserContext = blk: { 9 | const root = @import("root"); 10 | if (@hasDecl(root, "UserContext")) { 11 | break :blk root.UserContext; 12 | } 13 | 14 | // when no declaration is given, UserContext is define with an empty struct. 15 | break :blk struct {}; 16 | }; 17 | -------------------------------------------------------------------------------- /src/test_runner.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const tests = @import("run_tests.zig"); 18 | 19 | pub const Types = tests.Types; 20 | pub const UserContext = tests.UserContext; 21 | 22 | pub fn main() !void { 23 | try tests.main(); 24 | } 25 | -------------------------------------------------------------------------------- /src/internal_api.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub const refs = @import("refs.zig"); 16 | pub const refl = @import("reflect.zig"); 17 | pub const gen = @import("generate.zig"); 18 | pub const eng = @import("engine.zig"); 19 | pub const NativeContext = @import("native_context.zig").NativeContext; 20 | -------------------------------------------------------------------------------- /src/console.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const log = std.log.scoped(.console); 18 | 19 | pub const Console = struct { 20 | // TODO: configurable writer 21 | 22 | pub fn _log(_: Console, str: []const u8) void { 23 | log.debug("{s}\n", .{str}); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/private_api.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const build_opts = @import("jsruntime_build_options"); 18 | 19 | // retrieve JS engine 20 | pub const Engine = switch (build_opts.engine) { 21 | .v8 => @import("engines/v8/v8.zig"), 22 | }; 23 | 24 | pub const API = Engine.API; 25 | 26 | // loadFn is a function which generates 27 | // the loading and binding of the native API into the JS engine 28 | pub const loadFn = Engine.loadFn; 29 | 30 | pub const Object = Engine.Object; 31 | -------------------------------------------------------------------------------- /src/tests/userctx_test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const public = @import("../api.zig"); 4 | const tests = public.test_utils; 5 | 6 | const Config = struct { 7 | use_proxy: bool, 8 | }; 9 | 10 | pub const UserContext = Config; 11 | 12 | const Request = struct { 13 | use_proxy: bool, 14 | 15 | pub fn constructor(ctx: Config) Request { 16 | return .{ 17 | .use_proxy = ctx.use_proxy, 18 | }; 19 | } 20 | 21 | pub fn get_proxy(self: *Request) bool { 22 | return self.use_proxy; 23 | } 24 | 25 | pub fn _configProxy(_: *Request, ctx: Config) bool { 26 | return ctx.use_proxy; 27 | } 28 | }; 29 | 30 | pub const Types = .{ 31 | Request, 32 | }; 33 | 34 | // exec tests 35 | pub fn exec( 36 | _: std.mem.Allocator, 37 | js_env: *public.Env, 38 | ) anyerror!void { 39 | try js_env.setUserContext(Config{ 40 | .use_proxy = true, 41 | }); 42 | 43 | // start JS env 44 | try js_env.start(); 45 | defer js_env.stop(); 46 | 47 | var tc = [_]tests.Case{ 48 | .{ .src = "const req = new Request();", .ex = "undefined" }, 49 | .{ .src = "req.proxy", .ex = "true" }, 50 | .{ .src = "req.configProxy()", .ex = "true" }, 51 | }; 52 | try tests.checkCases(js_env, &tc); 53 | } 54 | -------------------------------------------------------------------------------- /src/main_shell.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const public = @import("api.zig"); 18 | 19 | const WindowTypes = @import("tests/cbk_test.zig").Types; 20 | 21 | pub const Types = public.reflect(public.MergeTuple(.{ 22 | .{public.Console}, 23 | WindowTypes, 24 | })); 25 | 26 | pub fn main() !void { 27 | 28 | // create JS vm 29 | const vm = public.VM.init(); 30 | defer vm.deinit(); 31 | 32 | // alloc 33 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 34 | defer _ = gpa.deinit(); 35 | var arena = std.heap.ArenaAllocator.init(gpa.allocator()); 36 | defer arena.deinit(); 37 | 38 | // launch shell 39 | try public.shell(&arena, null, .{ .app_name = "zig-js-runtime-shell" }); 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/zig-fmt.yml: -------------------------------------------------------------------------------- 1 | name: zig-fmt 2 | 3 | env: 4 | ZIG_VERSION: 0.14.0 5 | 6 | on: 7 | pull_request: 8 | 9 | # By default GH trigger on types opened, synchronize and reopened. 10 | # see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request 11 | # Since we skip the job when the PR is in draft state, we want to force CI 12 | # running when the PR is marked ready_for_review w/o other change. 13 | # see https://github.com/orgs/community/discussions/25722#discussioncomment-3248917 14 | types: [opened, synchronize, reopened, ready_for_review] 15 | 16 | branches: 17 | - main 18 | paths: 19 | - "src/**/*.zig" 20 | - "src/*.zig" 21 | - "*.zig" 22 | # Allows you to run this workflow manually from the Actions tab 23 | workflow_dispatch: 24 | 25 | jobs: 26 | zig-fmt: 27 | name: zig fmt 28 | 29 | # Don't run the CI with draft PR. 30 | if: github.event.pull_request.draft == false 31 | 32 | runs-on: ubuntu-latest 33 | 34 | steps: 35 | - uses: mlugg/setup-zig@v1 36 | with: 37 | version: ${{ env.ZIG_VERSION }} 38 | 39 | - uses: actions/checkout@v4 40 | with: 41 | fetch-depth: 0 42 | 43 | - name: Run zig fmt 44 | id: fmt 45 | run: | 46 | zig fmt --check ./*.zig ./**/*.zig 2> zig-fmt.err > zig-fmt.err2 || echo "Failed" 47 | delimiter="$(openssl rand -hex 8)" 48 | echo "zig_fmt_errs<<${delimiter}" >> "${GITHUB_OUTPUT}" 49 | 50 | if [ -s zig-fmt.err ]; then 51 | echo "// The following errors occurred:" >> "${GITHUB_OUTPUT}" 52 | cat zig-fmt.err >> "${GITHUB_OUTPUT}" 53 | fi 54 | 55 | if [ -s zig-fmt.err2 ]; then 56 | echo "// The following files were not formatted:" >> "${GITHUB_OUTPUT}" 57 | cat zig-fmt.err2 >> "${GITHUB_OUTPUT}" 58 | fi 59 | 60 | echo "${delimiter}" >> "${GITHUB_OUTPUT}" 61 | - name: Fail the job 62 | if: steps.fmt.outputs.zig_fmt_errs != '' 63 | run: exit 1 64 | -------------------------------------------------------------------------------- /.github/actions/install/action.yml: -------------------------------------------------------------------------------- 1 | name: "V8 install" 2 | description: "Install deps for the project" 3 | 4 | inputs: 5 | zig: 6 | description: 'Zig version to install' 7 | required: false 8 | default: '0.14.0' 9 | arch: 10 | description: 'CPU arch used to select the v8 lib' 11 | required: false 12 | default: 'x86_64' 13 | os: 14 | description: 'OS used to select the v8 lib' 15 | required: false 16 | default: 'linux' 17 | zig-v8: 18 | description: 'zig v8 version to install' 19 | required: false 20 | default: 'v0.1.17' 21 | v8: 22 | description: 'v8 version to install' 23 | required: false 24 | default: '11.1.134' 25 | cache-dir: 26 | description: 'cache dir to use' 27 | required: false 28 | default: '~/.cache' 29 | 30 | runs: 31 | using: "composite" 32 | 33 | steps: 34 | - uses: mlugg/setup-zig@v1 35 | with: 36 | version: ${{ inputs.zig }} 37 | 38 | - name: Cache v8 39 | id: cache-v8 40 | uses: actions/cache@v4 41 | env: 42 | cache-name: cache-v8 43 | with: 44 | path: ${{ inputs.cache-dir }}/v8 45 | key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}_${{ inputs.zig-v8 }}.a 46 | 47 | - if: ${{ steps.cache-v8.outputs.cache-hit != 'true' }} 48 | shell: bash 49 | run: | 50 | mkdir -p ${{ inputs.cache-dir }}/v8 51 | 52 | wget -O ${{ inputs.cache-dir }}/v8/libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${{ inputs.zig-v8 }}/libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}.a 53 | 54 | - name: Install apt deps 55 | if: ${{ inputs.os == 'linux' }} 56 | run: sudo apt-get install -yq libglib2.0-dev 57 | shell: bash 58 | 59 | - name: install v8 60 | shell: bash 61 | run: | 62 | mkdir -p vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug 63 | ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug/libc_v8.a 64 | 65 | mkdir -p vendor/v8/${{inputs.arch}}-${{inputs.os}}/release 66 | ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/v8/${{inputs.arch}}-${{inputs.os}}/release/libc_v8.a 67 | -------------------------------------------------------------------------------- /src/tests/global_test.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const public = @import("../api.zig"); 18 | const tests = public.test_utils; 19 | 20 | const GlobalParent = struct { 21 | pub fn _parent(_: GlobalParent) bool { 22 | return true; 23 | } 24 | }; 25 | 26 | pub const Global = struct { 27 | pub const prototype = *GlobalParent; 28 | pub const global_type = true; 29 | 30 | proto: GlobalParent = .{}, 31 | 32 | pub fn _self(_: Global) bool { 33 | return true; 34 | } 35 | }; 36 | 37 | pub const Types = .{ 38 | GlobalParent, 39 | Global, 40 | }; 41 | 42 | // exec tests 43 | pub fn exec( 44 | _: std.mem.Allocator, 45 | js_env: *public.Env, 46 | ) anyerror!void { 47 | 48 | // start JS env 49 | try js_env.start(); 50 | defer js_env.stop(); 51 | 52 | // global 53 | const global = Global{}; 54 | try js_env.bindGlobal(global); 55 | try js_env.attachObject(try js_env.getGlobal(), "global", null); 56 | 57 | var globals = [_]tests.Case{ 58 | .{ .src = "Global.name", .ex = "Global" }, 59 | .{ .src = "GlobalParent.name", .ex = "GlobalParent" }, 60 | .{ .src = "self()", .ex = "true" }, 61 | .{ .src = "parent()", .ex = "true" }, 62 | .{ .src = "global.self()", .ex = "true" }, 63 | .{ .src = "global.parent()", .ex = "true" }, 64 | .{ .src = "global.foo = () => true; foo()", .ex = "true" }, 65 | .{ .src = "bar = () => true; global.bar()", .ex = "true" }, 66 | }; 67 | try tests.checkCases(js_env, &globals); 68 | } 69 | -------------------------------------------------------------------------------- /src/types.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub const i64Num = struct { 16 | value: i64, 17 | 18 | pub fn init(value: i64) i64Num { 19 | return .{ .value = value }; 20 | } 21 | 22 | pub fn get(self: i64Num) i64 { 23 | return self.value; 24 | } 25 | }; 26 | 27 | pub const u64Num = struct { 28 | value: u64, 29 | 30 | pub fn init(value: u64) u64Num { 31 | return .{ .value = value }; 32 | } 33 | 34 | pub fn get(self: u64Num) u64 { 35 | return self.value; 36 | } 37 | }; 38 | 39 | // TODO: we could avoid allocate on heap the Iterable instance 40 | // by removing the internal state (index) from the struct 41 | // and instead store it directly in the JS object as an internal field 42 | pub fn Iterable(comptime T: type) type { 43 | return struct { 44 | const Self = @This(); 45 | 46 | items: []T, 47 | index: usize = 0, 48 | 49 | pub fn init(items: []T) Self { 50 | return .{ .items = items }; 51 | } 52 | 53 | pub const Return = struct { 54 | value: ?T, 55 | done: bool, 56 | }; 57 | 58 | pub fn _next(self: *Self) Return { 59 | if (self.items.len > self.index) { 60 | const val = self.items[self.index]; 61 | self.index += 1; 62 | return .{ .value = val, .done = false }; 63 | } else { 64 | return .{ .value = null, .done = true }; 65 | } 66 | } 67 | }; 68 | } 69 | 70 | pub fn Variadic(comptime T: type) type { 71 | return struct { 72 | slice: []T, 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /src/refs.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const internal = @import("internal_api.zig"); 18 | const refl = internal.refl; 19 | 20 | // Map references all objects created in both JS and Native world 21 | // either from JS through a constructor template call 22 | // or from Native in an addObject call 23 | // - key is the adress of the object (as an int) 24 | // it will be store on the JS object as an internal field 25 | // - value is the index of API 26 | pub const Map = std.AutoHashMapUnmanaged(usize, usize); 27 | 28 | pub fn getObject(map: Map, comptime T: type, comptime types: []const refl.Struct, ptr: anytype) !*T { 29 | const key: usize = @intFromPtr(ptr); 30 | const T_index = map.get(key); 31 | if (T_index == null) { 32 | return error.NullReference; 33 | } 34 | 35 | // get the API corresponding to the API index 36 | // TODO: more efficient sorting? 37 | inline for (types) |T_refl| { 38 | if (T_refl.index == T_index.?) { 39 | if (!T_refl.isEmpty()) { // stage1: condition is needed for empty structs 40 | // go through the "proto" object chain 41 | // to retrieve the good object corresponding to T 42 | const target_ptr: *T_refl.Self() = @ptrFromInt(key); 43 | return try getRealObject(T, target_ptr); 44 | } 45 | } 46 | } 47 | return error.Reference; 48 | } 49 | 50 | fn getRealObject(comptime T: type, target_ptr: anytype) !*T { 51 | const T_target = @TypeOf(target_ptr.*); 52 | if (T_target == T) { 53 | return target_ptr; 54 | } 55 | if (@hasField(T_target, "proto")) { 56 | // here we retun the "right" pointer: &(field(...)) 57 | // ie. the direct pointer to the field 58 | // and not a pointer to a new const/var holding the field 59 | 60 | // TODO: and what if we have more than 2 types in the chain? 61 | return getRealObject(T, &(@field(target_ptr, "proto"))); 62 | } 63 | return error.Reference; 64 | } 65 | -------------------------------------------------------------------------------- /src/native_context.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const Loop = @import("api.zig").Loop; 18 | const UserContext = @import("api.zig").UserContext; 19 | const NatObjects = @import("internal_api.zig").refs.Map; 20 | 21 | pub const NativeContext = struct { 22 | alloc: std.mem.Allocator, 23 | loop: *Loop, 24 | userctx: ?UserContext, 25 | 26 | js_objs: JSObjects, 27 | nat_objs: NatObjects, 28 | 29 | // NOTE: DO NOT ACCESS DIRECTLY js_types 30 | // - use once loadTypes at startup to set them 31 | // - and then getType during execution to access them 32 | js_types: ?[]usize = null, 33 | 34 | pub const JSObjects = std.AutoHashMapUnmanaged(usize, usize); 35 | 36 | pub fn init(self: *NativeContext, alloc: std.mem.Allocator, loop: *Loop, userctx: ?UserContext) void { 37 | self.* = .{ 38 | .alloc = alloc, 39 | .loop = loop, 40 | .userctx = userctx, 41 | .js_objs = JSObjects{}, 42 | .nat_objs = NatObjects{}, 43 | }; 44 | } 45 | 46 | pub fn stop(self: *NativeContext) void { 47 | self.js_objs.clearAndFree(self.alloc); 48 | self.nat_objs.clearAndFree(self.alloc); 49 | } 50 | 51 | // loadTypes into the NativeContext 52 | // The caller holds the memory of the js_types slice, 53 | // no heap allocation is performed at the NativeContext level 54 | pub fn loadTypes(self: *NativeContext, js_types: []usize) void { 55 | std.debug.assert(self.js_types == null); 56 | self.js_types = js_types; 57 | } 58 | 59 | pub fn getType(self: *const NativeContext, comptime T: type, index: usize) *T { 60 | std.debug.assert(self.js_types != null); 61 | const t = self.js_types.?[index]; 62 | return @as(*T, @ptrFromInt(t)); 63 | } 64 | 65 | pub fn deinit(self: *NativeContext) void { 66 | self.stop(); 67 | self.js_objs.deinit(self.alloc); 68 | self.nat_objs.deinit(self.alloc); 69 | self.* = undefined; 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /src/tests/types_multiple_test.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const public = @import("../api.zig"); 18 | const tests = public.test_utils; 19 | 20 | const Windows = struct { 21 | const manufacturer = "Microsoft"; 22 | 23 | pub fn get_manufacturer(_: Windows) []const u8 { 24 | return manufacturer; 25 | } 26 | }; 27 | 28 | const MacOS = struct { 29 | const manufacturer = "Apple"; 30 | 31 | pub fn get_manufacturer(_: MacOS) []const u8 { 32 | return manufacturer; 33 | } 34 | }; 35 | 36 | const Linux = struct { 37 | const manufacturer = "Linux Foundation"; 38 | 39 | pub fn get_manufacturer(_: Linux) []const u8 { 40 | return manufacturer; 41 | } 42 | }; 43 | 44 | const OSTag = enum { 45 | windows, 46 | macos, 47 | linux, 48 | }; 49 | 50 | const OS = union(OSTag) { 51 | windows: Windows, 52 | macos: MacOS, 53 | linux: Linux, 54 | }; 55 | 56 | const Computer = struct { 57 | os: OS, 58 | 59 | pub fn constructor(os_name: []u8) Computer { 60 | var os: OS = undefined; 61 | if (std.mem.eql(u8, os_name, "macos")) { 62 | os = OS{ .macos = MacOS{} }; 63 | } else if (std.mem.eql(u8, os_name, "linux")) { 64 | os = OS{ .linux = Linux{} }; 65 | } else { 66 | os = OS{ .windows = Windows{} }; 67 | } 68 | return .{ .os = os }; 69 | } 70 | 71 | pub fn get_os(self: Computer) OS { 72 | return self.os; 73 | } 74 | }; 75 | 76 | pub const Types = .{ 77 | Windows, 78 | MacOS, 79 | Linux, 80 | Computer, 81 | }; 82 | 83 | // exec tests 84 | pub fn exec( 85 | _: std.mem.Allocator, 86 | js_env: *public.Env, 87 | ) anyerror!void { 88 | 89 | // start JS env 90 | try js_env.start(); 91 | defer js_env.stop(); 92 | 93 | var cases = [_]tests.Case{ 94 | .{ .src = "let linux_computer = new Computer('linux');", .ex = "undefined" }, 95 | .{ .src = "let os = linux_computer.os;", .ex = "undefined" }, 96 | .{ .src = "os.manufacturer", .ex = "Linux Foundation" }, 97 | }; 98 | try tests.checkCases(js_env, &cases); 99 | } 100 | -------------------------------------------------------------------------------- /src/api.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // ---------- 16 | // Public API 17 | // ---------- 18 | 19 | // only imports, no implementation code 20 | 21 | // Loader and Context 22 | // ------------------ 23 | 24 | const internal = @import("internal_api.zig"); 25 | 26 | pub const reflect = internal.gen.reflect; 27 | pub const loadEnv = internal.eng.loadEnv; 28 | pub const ContextExecFn = internal.eng.ContextExecFn; 29 | 30 | // Utils 31 | // ----- 32 | 33 | pub const MergeTuple = internal.gen.MergeTuple; 34 | 35 | pub const shell = @import("shell.zig").shell; 36 | pub const shellExec = @import("shell.zig").shellExec; 37 | 38 | pub const bench_allocator = @import("bench.zig").allocator; 39 | pub const test_utils = @import("tests/test_utils.zig"); 40 | 41 | // JS types 42 | // -------- 43 | 44 | pub const JSTypes = enum { 45 | object, 46 | function, 47 | string, 48 | number, 49 | boolean, 50 | bigint, 51 | null, 52 | undefined, 53 | }; 54 | 55 | const types = @import("types.zig"); 56 | pub const i64Num = types.i64Num; 57 | pub const u64Num = types.u64Num; 58 | 59 | pub const Iterable = types.Iterable; 60 | pub const Variadic = types.Variadic; 61 | 62 | pub const Loop = @import("loop.zig").SingleThreaded; 63 | pub const IO = @import("loop.zig").IO; 64 | pub const Console = @import("console.zig").Console; 65 | 66 | pub const UserContext = @import("user_context.zig").UserContext; 67 | 68 | // JS engine 69 | // --------- 70 | 71 | const Engine = @import("private_api.zig").Engine; 72 | 73 | pub const JSValue = Engine.JSValue; 74 | pub const JSObject = Engine.JSObject; 75 | pub const JSObjectID = Engine.JSObjectID; 76 | 77 | pub const Callback = Engine.Callback; 78 | pub const CallbackSync = Engine.CallbackSync; 79 | pub const CallbackArg = Engine.CallbackArg; 80 | pub const CallbackResult = Engine.CallbackResult; 81 | 82 | pub const TryCatch = Engine.TryCatch; 83 | pub const VM = Engine.VM; 84 | pub const Env = Engine.Env; 85 | 86 | pub const Inspector = Engine.Inspector; 87 | pub const InspectorOnResponseFn = *const fn (ctx: *anyopaque, call_id: u32, msg: []const u8) void; 88 | pub const InspectorOnEventFn = *const fn (ctx: *anyopaque, msg: []const u8) void; 89 | 90 | pub const Module = Engine.Module; 91 | pub const ModuleLoadFn = Engine.ModuleLoadFn; 92 | 93 | pub const EngineType = enum { 94 | v8, 95 | }; 96 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: benchmark 2 | 3 | env: 4 | AWS_ACCESS_KEY_ID: ${{ vars.LPD_PERF_AWS_ACCESS_KEY_ID }} 5 | AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }} 6 | AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }} 7 | AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }} 8 | 9 | on: 10 | push: 11 | branches: 12 | - main 13 | paths: 14 | - "src/**/*.zig" 15 | - "src/*.zig" 16 | pull_request: 17 | 18 | # By default GH trigger on types opened, synchronize and reopened. 19 | # see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request 20 | # Since we skip the job when the PR is in draft state, we want to force CI 21 | # running when the PR is marked ready_for_review w/o other change. 22 | # see https://github.com/orgs/community/discussions/25722#discussioncomment-3248917 23 | types: [opened, synchronize, reopened, ready_for_review] 24 | 25 | branches: 26 | - main 27 | paths: 28 | - "src/**/*.zig" 29 | - "src/*.zig" 30 | # Allows you to run this workflow manually from the Actions tab 31 | workflow_dispatch: 32 | 33 | jobs: 34 | benchmark: 35 | name: benchmark 36 | 37 | runs-on: ubuntu-latest 38 | 39 | # Don't run the CI with draft PR. 40 | if: github.event.pull_request.draft == false 41 | 42 | steps: 43 | - uses: actions/checkout@v4 44 | with: 45 | fetch-depth: 0 46 | submodules: true 47 | 48 | - uses: ./.github/actions/install 49 | 50 | - run: zig build -Doptimize=ReleaseSafe -Dengine=v8 51 | - name: run benchmark 52 | run: | 53 | ./zig-out/bin/zig-js-runtime-bench > benchmark.txt 54 | cat benchmark.txt 55 | 56 | - name: json output 57 | run: ./zig-out/bin/zig-js-runtime-bench --json > benchmark.json 58 | 59 | - name: write commit 60 | run: | 61 | echo "${{github.sha}}" > commit.txt 62 | 63 | - name: upload artifact 64 | uses: actions/upload-artifact@v4 65 | with: 66 | name: benchmark-results 67 | path: | 68 | benchmark.txt 69 | benchmark.json 70 | commit.txt 71 | 72 | # configure the retention policy: 10 days on PR and 150 on main. 73 | retention-days: ${{ github.event_name == 'pull_request' && 10 || 90 }} 74 | 75 | perf-fmt: 76 | name: perf-fmt 77 | needs: benchmark 78 | 79 | # Don't execute on PR 80 | if: github.event_name != 'pull_request' 81 | 82 | runs-on: ubuntu-latest 83 | container: 84 | image: ghcr.io/lightpanda-io/perf-fmt:latest 85 | credentials: 86 | username: ${{ github.actor }} 87 | password: ${{ secrets.GITHUB_TOKEN }} 88 | 89 | steps: 90 | - name: download artifact 91 | uses: actions/download-artifact@v4 92 | with: 93 | name: benchmark-results 94 | 95 | - name: format and send json result 96 | run: /perf-fmt bench-jsruntime ${{ github.sha }} benchmark.json 97 | -------------------------------------------------------------------------------- /src/engine.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | const builtin = @import("builtin"); 17 | 18 | const internal = @import("internal_api.zig"); 19 | const refs = internal.refs; 20 | const gen = internal.gen; 21 | const refl = internal.refl; 22 | const NativeContext = internal.NativeContext; 23 | 24 | const public = @import("api.zig"); 25 | const Env = public.Env; 26 | const Loop = public.Loop; 27 | const UserContext = public.UserContext; 28 | 29 | pub const ContextExecFn = (fn (std.mem.Allocator, *Env) anyerror!void); 30 | 31 | pub fn loadEnv( 32 | arena_alloc: *std.heap.ArenaAllocator, 33 | userctx: ?UserContext, 34 | comptime ctxExecFn: ContextExecFn, 35 | ) !void { 36 | const alloc = arena_alloc.allocator(); 37 | 38 | // create JS env 39 | var start: std.time.Instant = undefined; 40 | if (builtin.is_test) { 41 | start = try std.time.Instant.now(); 42 | } 43 | var loop = try Loop.init(alloc); 44 | defer loop.deinit(); 45 | var js_env: public.Env = undefined; 46 | Env.init(&js_env, alloc, &loop, userctx); 47 | defer js_env.deinit(); 48 | 49 | // load APIs in JS env 50 | var load_start: std.time.Instant = undefined; 51 | if (builtin.is_test) { 52 | load_start = try std.time.Instant.now(); 53 | } 54 | var js_types: [gen.Types.len]usize = undefined; 55 | try js_env.load(&js_types); 56 | 57 | // execute JS function 58 | var exec_start: std.time.Instant = undefined; 59 | if (builtin.is_test) { 60 | exec_start = try std.time.Instant.now(); 61 | } 62 | try ctxExecFn(alloc, &js_env); 63 | 64 | // Stats 65 | // ----- 66 | 67 | var exec_end: std.time.Instant = undefined; 68 | if (builtin.is_test) { 69 | exec_end = try std.time.Instant.now(); 70 | } 71 | 72 | if (builtin.is_test) { 73 | const us = std.time.ns_per_us; 74 | 75 | const create_time = std.time.Instant.since(load_start, start); 76 | const load_time = std.time.Instant.since(exec_start, load_start); 77 | const exec_time = std.time.Instant.since(exec_end, exec_start); 78 | const total_time = std.time.Instant.since(exec_end, start); 79 | 80 | const create_per = create_time * 100 / total_time; 81 | const load_per = load_time * 100 / total_time; 82 | const exec_per = exec_time * 100 / total_time; 83 | 84 | std.debug.print("\ncreation of env:\t{d}us\t{d}%\n", .{ create_time / us, create_per }); 85 | std.debug.print("load of apis:\t\t{d}us\t{d}%\n", .{ load_time / us, load_per }); 86 | std.debug.print("exec:\t\t\t{d}us\t{d}%\n", .{ exec_time / us, exec_per }); 87 | std.debug.print("Total:\t\t\t{d}us\n", .{total_time / us}); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Variables 2 | # --------- 3 | 4 | ZIG := zig 5 | 6 | # OS and ARCH 7 | UNAME_S := $(shell uname -s) 8 | ifeq ($(UNAME_S),Linux) 9 | OS := linux 10 | else ifeq ($(UNAME_S),Darwin) 11 | OS := macos 12 | else 13 | $(error "OS not supported") 14 | endif 15 | UNAME_M := $(shell uname -m) 16 | ifeq ($(UNAME_M),x86_64) 17 | ARCH := x86_64 18 | else ifeq ($(UNAME_M),aarch64) 19 | ARCH := aarch64 20 | else ifeq ($(UNAME_M),arm64) 21 | ARCH := aarch64 22 | else 23 | $(error "CPU not supported") 24 | endif 25 | ifeq ($(OS), macos && ($(ARCH), x86_64)) 26 | $(error "OS/CPU not supported") 27 | endif 28 | 29 | 30 | # Infos 31 | # ----- 32 | .PHONY: help 33 | 34 | ## Display this help screen 35 | help: 36 | @printf "\e[36m%-35s %s\e[0m\n" "Command" "Usage" 37 | @sed -n -e '/^## /{'\ 38 | -e 's/## //g;'\ 39 | -e 'h;'\ 40 | -e 'n;'\ 41 | -e 's/:.*//g;'\ 42 | -e 'G;'\ 43 | -e 's/\n/ /g;'\ 44 | -e 'p;}' Makefile | awk '{printf "\033[33m%-35s\033[0m%s\n", $$1, substr($$0,length($$1)+1)}' 45 | 46 | # Git commands 47 | git_clean := git diff --quiet; echo $$? 48 | git_current_branch := git branch --show-current 49 | git_last_commit_full := git log --pretty=format:'%cd_%h' -n 1 --date=format:'%Y-%m-%d_%H-%M' 50 | 51 | # List files 52 | tree: 53 | @tree -I zig-cache -I zig-out -I vendor -I questions -I benchmarks -I build -I "*~" 54 | 55 | # Install and build required dependencies commands 56 | # ------------ 57 | .PHONY: install-submodule 58 | .PHONY: _install-v8 install-v8-dev install-dev install-v8 install 59 | 60 | ## Install and build dependencies for release 61 | install: install-submodule install-v8 62 | 63 | ## Install and build dependencies for dev 64 | install-dev: install-submodule install-v8-dev 65 | 66 | ## Install and build v8 engine for release 67 | install-v8: _install-v8 68 | install-v8: mode=release 69 | install-v8: zig_opts=-Doptimize=ReleaseSafe 70 | 71 | ## Install and build v8 engine for dev 72 | install-v8-dev: _install-v8 73 | install-v8-dev: mode=debug 74 | 75 | _install-v8: 76 | @mkdir -p vendor/v8/$(ARCH)-$(OS)/$(mode) 77 | @cd vendor/zig-v8 && \ 78 | $(ZIG) build get-tools && \ 79 | $(ZIG) build get-v8 && \ 80 | $(ZIG) build $(zig_opts) && \ 81 | cd ../../ && \ 82 | cp vendor/zig-v8/v8-build/$(ARCH)-$(OS)/$(mode)/ninja/obj/zig/libc_v8.a vendor/v8/$(ARCH)-$(OS)/$(mode)/ 83 | 84 | ## Init and update git submodule 85 | install-submodule: 86 | @git submodule init && \ 87 | git submodule update 88 | 89 | # Zig commands 90 | # ------------ 91 | .PHONY: build build-release run run-release shell test bench 92 | 93 | ## Build in debug mode 94 | build: 95 | @printf "\e[36mBuilding (debug)...\e[0m\n" 96 | @$(ZIG) build bench -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;) 97 | @printf "\e[33mBuild OK\e[0m\n" 98 | 99 | build-release: 100 | @printf "\e[36mBuilding (release safe)...\e[0m\n" 101 | @$(ZIG) build -Doptimize=ReleaseSafe -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;) 102 | @printf "\e[33mBuild OK\e[0m\n" 103 | 104 | ## Run the benchmark in release-safe mode 105 | run: build-release 106 | @printf "\e[36mRunning...\e[0m\n" 107 | @./zig-out/bin/zig-js-runtime-bench || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;) 108 | @printf "\e[33mRun OK\e[0m\n" 109 | 110 | ## Run a JS shell in release-safe mode 111 | shell: 112 | @printf "\e[36mBuilding shell (debug)...\e[0m\n" 113 | @$(ZIG) build shell -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;) 114 | 115 | ## Test 116 | test: 117 | @printf "\e[36mTesting...\e[0m\n" 118 | @$(ZIG) build test -Dengine=v8 || (printf "\e[33mTest ERROR\e[0m\n"; exit 1;) 119 | @printf "\e[33mTest OK\e[0m\n" 120 | 121 | ## run + save results in benchmarks dir 122 | bench: build-release 123 | # Check repo is clean 124 | ifneq ($(shell $(git_clean)), 0) 125 | $(error repo is not clean) 126 | endif 127 | @mkdir -p benchmarks && \ 128 | ./zig-out/bin/zig-js-runtime-bench > benchmarks/$(shell $(git_last_commit_full))_$(shell $(git_current_branch)).txt 129 | -------------------------------------------------------------------------------- /.github/workflows/zig-test.yml: -------------------------------------------------------------------------------- 1 | name: zig-test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "src/**/*.zig" 9 | - "src/*.zig" 10 | - "*.zig" 11 | - ".github/**" 12 | - "vendor/**" 13 | pull_request: 14 | 15 | # By default GH trigger on types opened, synchronize and reopened. 16 | # see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request 17 | # Since we skip the job when the PR is in draft state, we want to force CI 18 | # running when the PR is marked ready_for_review w/o other change. 19 | # see https://github.com/orgs/community/discussions/25722#discussioncomment-3248917 20 | types: [opened, synchronize, reopened, ready_for_review] 21 | 22 | branches: 23 | - main 24 | paths: 25 | - "src/**/*.zig" 26 | - "src/*.zig" 27 | - "*.zig" 28 | - ".github/**" 29 | - "vendor/**" 30 | # Allows you to run this workflow manually from the Actions tab 31 | workflow_dispatch: 32 | 33 | jobs: 34 | zig-build-dev: 35 | name: zig build dev 36 | 37 | # Don't run the CI with draft PR. 38 | if: github.event.pull_request.draft == false 39 | 40 | runs-on: ubuntu-latest 41 | 42 | steps: 43 | - uses: actions/checkout@v4 44 | with: 45 | fetch-depth: 0 46 | submodules: true 47 | 48 | - uses: ./.github/actions/install 49 | 50 | - name: zig build debug 51 | run: zig build -Dengine=v8 52 | 53 | zig-build-release: 54 | name: zig build release 55 | 56 | # Don't run the CI with draft PR. 57 | if: github.event.pull_request.draft == false 58 | 59 | runs-on: ubuntu-latest 60 | 61 | steps: 62 | - uses: actions/checkout@v4 63 | with: 64 | fetch-depth: 0 65 | submodules: true 66 | 67 | - uses: ./.github/actions/install 68 | 69 | - name: zig build release 70 | run: zig build -Doptimize=ReleaseSafe -Dengine=v8 71 | 72 | zig-test: 73 | name: zig test 74 | 75 | # Don't run the CI with draft PR. 76 | if: github.event.pull_request.draft == false 77 | 78 | runs-on: ubuntu-latest 79 | 80 | steps: 81 | - uses: actions/checkout@v4 82 | with: 83 | fetch-depth: 0 84 | submodules: true 85 | 86 | - uses: ./.github/actions/install 87 | 88 | - name: zig build test 89 | run: zig build test -Dengine=v8 90 | 91 | zig-test-macos-x86_64: 92 | env: 93 | ARCH: x86_64 94 | OS: macos 95 | 96 | # Don't run the CI with draft PR. 97 | if: github.event.pull_request.draft == false 98 | 99 | runs-on: macos-13 100 | 101 | steps: 102 | - uses: actions/checkout@v4 103 | with: 104 | fetch-depth: 0 105 | submodules: true 106 | 107 | - uses: ./.github/actions/install 108 | with: 109 | os: ${{env.OS}} 110 | arch: ${{env.ARCH}} 111 | 112 | - name: zig build test 113 | run: zig build test -Dengine=v8 114 | 115 | zig-test-macos-aarch64: 116 | env: 117 | ARCH: aarch64 118 | OS: macos 119 | 120 | # Don't run the CI with draft PR. 121 | if: github.event.pull_request.draft == false 122 | 123 | runs-on: macos-latest 124 | 125 | steps: 126 | - uses: actions/checkout@v4 127 | with: 128 | fetch-depth: 0 129 | submodules: true 130 | 131 | - uses: ./.github/actions/install 132 | with: 133 | os: ${{env.OS}} 134 | arch: ${{env.ARCH}} 135 | 136 | - name: zig build test 137 | run: zig build test -Dengine=v8 138 | 139 | zig-test-linux-aarch64: 140 | env: 141 | ARCH: aarch64 142 | OS: linux 143 | 144 | # Don't run the CI with draft PR. 145 | if: github.event.pull_request.draft == false 146 | 147 | runs-on: ubuntu-24.04-arm 148 | 149 | steps: 150 | - uses: actions/checkout@v4 151 | with: 152 | fetch-depth: 0 153 | submodules: true 154 | 155 | - uses: ./.github/actions/install 156 | with: 157 | os: ${{env.OS}} 158 | arch: ${{env.ARCH}} 159 | 160 | - name: zig build test 161 | run: zig build test -Dengine=v8 162 | -------------------------------------------------------------------------------- /src/tests/types_object.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const public = @import("../api.zig"); 18 | const tests = public.test_utils; 19 | 20 | pub const Other = struct { 21 | val: u8, 22 | 23 | fn init(val: u8) Other { 24 | return .{ .val = val }; 25 | } 26 | 27 | pub fn _val(self: Other) u8 { 28 | return self.val; 29 | } 30 | }; 31 | 32 | pub const OtherUnion = union(enum) { 33 | Other: Other, 34 | Bool: bool, 35 | }; 36 | 37 | pub const MyObject = struct { 38 | val: bool, 39 | 40 | pub fn constructor(do_set: bool) MyObject { 41 | return .{ .val = do_set }; 42 | } 43 | 44 | pub fn postAttach(self: *MyObject, js_obj: public.JSObject, _: std.mem.Allocator) !void { 45 | if (self.val) try js_obj.set("a", @as(u8, 1)); 46 | } 47 | 48 | pub fn get_val(self: MyObject) bool { 49 | return self.val; 50 | } 51 | 52 | pub fn set_val(self: *MyObject, val: bool) void { 53 | self.val = val; 54 | } 55 | 56 | pub fn _other(_: MyObject, js_obj: public.JSObject, val: u8) !void { 57 | try js_obj.set("b", Other{ .val = val }); 58 | } 59 | 60 | pub fn _otherUnion(_: MyObject, js_obj: public.JSObject, val: ?u8) !void { 61 | if (val) |v| { 62 | const other = Other{ .val = v }; 63 | try js_obj.set("c", OtherUnion{ .Other = other }); 64 | } else { 65 | try js_obj.set("d", OtherUnion{ .Bool = true }); 66 | } 67 | } 68 | }; 69 | 70 | pub const MyAPI = struct { 71 | pub fn constructor() MyAPI { 72 | return .{}; 73 | } 74 | 75 | pub fn _obj(_: MyAPI, _: public.JSObject) !MyObject { 76 | return MyObject.constructor(true); 77 | } 78 | }; 79 | 80 | pub const Types = .{ 81 | Other, 82 | MyObject, 83 | MyAPI, 84 | }; 85 | 86 | // exec tests 87 | pub fn exec( 88 | alloc: std.mem.Allocator, 89 | js_env: *public.Env, 90 | ) anyerror!void { 91 | 92 | // start JS env 93 | try js_env.start(); 94 | defer js_env.stop(); 95 | 96 | // const o = Other{ .val = 4 }; 97 | // try js_env.addObject(apis, o, "other"); 98 | 99 | const ownBase = tests.engineOwnPropertiesDefault(); 100 | const ownBaseStr = tests.intToStr(alloc, ownBase); 101 | defer alloc.free(ownBaseStr); 102 | 103 | var direct = [_]tests.Case{ 104 | .{ .src = "Object.getOwnPropertyNames(MyObject).length;", .ex = ownBaseStr }, 105 | .{ .src = "let myObj = new MyObject(true);", .ex = "undefined" }, 106 | // check object property 107 | .{ .src = "myObj.a", .ex = "1" }, 108 | .{ .src = "Object.getOwnPropertyNames(myObj).length;", .ex = "1" }, 109 | // check if setter (pointer) still works 110 | .{ .src = "myObj.val", .ex = "true" }, 111 | .{ .src = "myObj.val = false", .ex = "false" }, 112 | .{ .src = "myObj.val", .ex = "false" }, 113 | // check other object, same type, has no property 114 | .{ .src = "let myObj2 = new MyObject(false);", .ex = "undefined" }, 115 | .{ .src = "myObj2.a", .ex = "undefined" }, 116 | .{ .src = "Object.getOwnPropertyNames(myObj2).length;", .ex = "0" }, 117 | // setting a user-defined object 118 | .{ .src = "myObj.other(3)", .ex = "undefined" }, 119 | .{ .src = "myObj.b.__proto__ === Other.prototype", .ex = "true" }, 120 | .{ .src = "myObj.b.val()", .ex = "3" }, 121 | // setting an union 122 | .{ .src = "myObj.otherUnion(4)", .ex = "undefined" }, 123 | .{ .src = "myObj.c.__proto__ === Other.prototype", .ex = "true" }, 124 | .{ .src = "myObj.c.val()", .ex = "4" }, 125 | .{ .src = "myObj.otherUnion()", .ex = "undefined" }, 126 | .{ .src = "myObj.d", .ex = "true" }, 127 | }; 128 | try tests.checkCases(js_env, &direct); 129 | 130 | var indirect = [_]tests.Case{ 131 | .{ .src = "let myAPI = new MyAPI();", .ex = "undefined" }, 132 | .{ .src = "let myObjIndirect = myAPI.obj();", .ex = "undefined" }, 133 | // check object property 134 | .{ .src = "myObjIndirect.a", .ex = "1" }, 135 | .{ .src = "Object.getOwnPropertyNames(myObjIndirect).length;", .ex = "1" }, 136 | }; 137 | try tests.checkCases(js_env, &indirect); 138 | } 139 | -------------------------------------------------------------------------------- /src/bench.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | pub const Result = struct { 18 | duration: u64, 19 | 20 | alloc_nb: usize, 21 | realloc_nb: usize, 22 | alloc_size: usize, 23 | }; 24 | 25 | pub fn call( 26 | func: anytype, 27 | args: anytype, 28 | comptime iter: comptime_int, 29 | comptime warmup: ?comptime_int, 30 | ) !u64 { 31 | var total: u64 = 0; 32 | var i: usize = 0; 33 | var is_error_union = false; 34 | 35 | while (i < iter) { 36 | var start: std.time.Instant = undefined; 37 | if (warmup != null and i > warmup.?) { 38 | start = try std.time.Instant.now(); 39 | } 40 | 41 | const res = @call(.auto, func, args); 42 | if (i == 0) { 43 | // TODO: handle more return cases 44 | const info = @typeInfo(@TypeOf(res)); 45 | if (info == .error_union) { 46 | is_error_union = true; 47 | } 48 | } 49 | if (is_error_union) { 50 | _ = try res; 51 | } 52 | 53 | if (warmup != null and i > warmup.?) { 54 | const end = try std.time.Instant.now(); 55 | const elapsed = std.time.Instant.since(end, start); 56 | total += elapsed; 57 | } 58 | i += 1; 59 | } 60 | var res: u64 = undefined; 61 | if (warmup != null) { 62 | res = total / (iter - warmup.?); 63 | } else { 64 | res = total / iter; 65 | } 66 | return total / iter; 67 | } 68 | 69 | pub const Allocator = struct { 70 | parent_allocator: std.mem.Allocator, 71 | 72 | alloc_nb: usize = 0, 73 | realloc_nb: usize = 0, 74 | free_nb: usize = 0, 75 | size: usize = 0, 76 | 77 | const Stats = struct { 78 | alloc_nb: usize, 79 | realloc_nb: usize, 80 | alloc_size: usize, 81 | }; 82 | 83 | fn init(parent_allocator: std.mem.Allocator) Allocator { 84 | return .{ 85 | .parent_allocator = parent_allocator, 86 | }; 87 | } 88 | 89 | pub fn stats(self: *Allocator) Stats { 90 | return .{ 91 | .alloc_nb = self.alloc_nb, 92 | .realloc_nb = self.realloc_nb, 93 | .alloc_size = self.size, 94 | }; 95 | } 96 | 97 | pub fn allocator(self: *Allocator) std.mem.Allocator { 98 | return std.mem.Allocator{ .ptr = self, .vtable = &.{ 99 | .alloc = alloc, 100 | .resize = resize, 101 | .free = free, 102 | .remap = remap, 103 | } }; 104 | } 105 | 106 | fn alloc( 107 | ctx: *anyopaque, 108 | len: usize, 109 | alignment: std.mem.Alignment, 110 | return_address: usize, 111 | ) ?[*]u8 { 112 | const self: *Allocator = @ptrCast(@alignCast(ctx)); 113 | const result = self.parent_allocator.rawAlloc(len, alignment, return_address); 114 | self.alloc_nb += 1; 115 | self.size += len; 116 | return result; 117 | } 118 | 119 | fn resize( 120 | ctx: *anyopaque, 121 | old_mem: []u8, 122 | alignment: std.mem.Alignment, 123 | new_len: usize, 124 | ra: usize, 125 | ) bool { 126 | const self: *Allocator = @ptrCast(@alignCast(ctx)); 127 | const result = self.parent_allocator.rawResize(old_mem, alignment, new_len, ra); 128 | self.realloc_nb += 1; // TODO: only if result is not null? 129 | return result; 130 | } 131 | 132 | fn free( 133 | ctx: *anyopaque, 134 | old_mem: []u8, 135 | alignment: std.mem.Alignment, 136 | ra: usize, 137 | ) void { 138 | const self: *Allocator = @ptrCast(@alignCast(ctx)); 139 | self.parent_allocator.rawFree(old_mem, alignment, ra); 140 | self.free_nb += 1; 141 | } 142 | 143 | fn remap( 144 | ctx: *anyopaque, 145 | memory: []u8, 146 | alignment: std.mem.Alignment, 147 | new_len: usize, 148 | ret_addr: usize, 149 | ) ?[*]u8 { 150 | const self: *Allocator = @ptrCast(@alignCast(ctx)); 151 | const result = self.parent_allocator.rawRemap(memory, alignment, new_len, ret_addr); 152 | self.realloc_nb += 1; // TODO: only if result is not null? 153 | return result; 154 | } 155 | }; 156 | 157 | pub fn allocator(parent_allocator: std.mem.Allocator) Allocator { 158 | return Allocator.init(parent_allocator); 159 | } 160 | -------------------------------------------------------------------------------- /src/main_bench.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | const io = @import("std").io; 17 | 18 | const public = @import("api.zig"); 19 | 20 | const bench = @import("bench.zig"); 21 | const pretty = @import("pretty.zig"); 22 | 23 | const proto = @import("tests/proto_test.zig"); 24 | 25 | const kb = 1024; 26 | const us = std.time.ns_per_us; 27 | 28 | pub const Types = public.reflect(proto.Types); 29 | 30 | fn benchWithIsolate( 31 | bench_alloc: *bench.Allocator, 32 | arena_alloc: *std.heap.ArenaAllocator, 33 | comptime ctxExecFn: public.ContextExecFn, 34 | comptime iter: comptime_int, 35 | comptime warmup: ?comptime_int, 36 | ) !bench.Result { 37 | const duration = try bench.call( 38 | public.loadEnv, 39 | .{ arena_alloc, null, ctxExecFn }, 40 | iter, 41 | warmup, 42 | ); 43 | const alloc_stats = bench_alloc.stats(); 44 | return bench.Result{ 45 | .duration = duration, 46 | .alloc_nb = alloc_stats.alloc_nb, 47 | .realloc_nb = alloc_stats.realloc_nb, 48 | .alloc_size = alloc_stats.alloc_size, 49 | }; 50 | } 51 | 52 | var duration_global: u64 = undefined; 53 | 54 | fn benchWithoutIsolate( 55 | bench_alloc: *bench.Allocator, 56 | arena_alloc: *std.heap.ArenaAllocator, 57 | comptime ctxExecFn: public.ContextExecFn, 58 | comptime iter: comptime_int, 59 | comptime warmup: ?comptime_int, 60 | ) !bench.Result { 61 | const s = struct { 62 | fn do( 63 | alloc_func: std.mem.Allocator, 64 | js_env: *public.Env, 65 | ) anyerror!void { 66 | const duration = try bench.call( 67 | ctxExecFn, 68 | .{ alloc_func, js_env }, 69 | iter, 70 | warmup, 71 | ); 72 | duration_global = duration; 73 | } 74 | }; 75 | try public.loadEnv(arena_alloc, null, s.do); 76 | const alloc_stats = bench_alloc.stats(); 77 | return bench.Result{ 78 | .duration = duration_global, 79 | .alloc_nb = alloc_stats.alloc_nb, 80 | .realloc_nb = alloc_stats.realloc_nb, 81 | .alloc_size = alloc_stats.alloc_size, 82 | }; 83 | } 84 | 85 | const usage = 86 | \\usage: {s} [options] 87 | \\ Run and display a jsruntime benchmark. 88 | \\ 89 | \\ -h, --help Print this help message and exit. 90 | \\ --json result is formatted in JSON. 91 | \\ 92 | ; 93 | 94 | pub fn main() !void { 95 | const allocator = std.heap.page_allocator; 96 | 97 | var args = try std.process.argsWithAllocator(allocator); 98 | defer args.deinit(); 99 | 100 | // get the exec name. 101 | const execname = args.next().?; 102 | 103 | var json = false; 104 | while (args.next()) |arg| { 105 | if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) { 106 | try io.getStdErr().writer().print(usage, .{execname}); 107 | std.posix.exit(0); 108 | } else if (std.mem.eql(u8, "--json", arg)) { 109 | json = true; 110 | } 111 | } 112 | 113 | // create JS vm 114 | const vm = public.VM.init(); 115 | defer vm.deinit(); 116 | 117 | // allocators 118 | var bench1 = bench.allocator(std.heap.page_allocator); 119 | var arena1 = std.heap.ArenaAllocator.init(bench1.allocator()); 120 | defer arena1.deinit(); 121 | var bench2 = bench.allocator(std.heap.page_allocator); 122 | var arena2 = std.heap.ArenaAllocator.init(bench2.allocator()); 123 | defer arena2.deinit(); 124 | 125 | // benchmark conf 126 | const iter = 100; 127 | const warmup = iter / 20; 128 | const title_fmt = "Benchmark jsengine 🚀 (~= {d} iters)"; 129 | var buf: [100]u8 = undefined; 130 | const title = try std.fmt.bufPrint(buf[0..], title_fmt, .{iter}); 131 | 132 | // benchmark funcs 133 | const res1 = try benchWithIsolate(&bench1, &arena1, proto.exec, iter, warmup); 134 | const res2 = try benchWithoutIsolate(&bench2, &arena2, proto.exec, iter, warmup); 135 | 136 | // generate a json output with the bench result. 137 | if (json) { 138 | const res = [_]struct { 139 | name: []const u8, 140 | bench: bench.Result, 141 | }{ 142 | .{ .name = "With Isolate", .bench = res1 }, 143 | .{ .name = "Without Isolate", .bench = res2 }, 144 | }; 145 | 146 | try std.json.stringify(res, .{ .whitespace = .indent_2 }, io.getStdOut().writer()); 147 | std.posix.exit(0); 148 | } 149 | 150 | // benchmark measures 151 | const dur1 = pretty.Measure{ .unit = "us", .value = res1.duration / us }; 152 | const dur2 = pretty.Measure{ .unit = "us", .value = res2.duration / us }; 153 | const size1 = pretty.Measure{ .unit = "kb", .value = res1.alloc_size / kb }; 154 | const size2 = pretty.Measure{ .unit = "kb", .value = res2.alloc_size / kb }; 155 | 156 | // benchmark table 157 | const row_shape = .{ 158 | []const u8, 159 | pretty.Measure, 160 | u64, 161 | u64, 162 | pretty.Measure, 163 | }; 164 | const table = try pretty.GenerateTable(2, row_shape, pretty.TableConf{ .margin_left = " " }); 165 | const header = .{ 166 | "FUNCTION", 167 | "DURATION (per iter)", 168 | "ALLOCATIONS (nb)", 169 | "RE-ALLOCATIONS (nb)", 170 | "HEAP SIZE", 171 | }; 172 | var t = table.init(title, header); 173 | try t.addRow(.{ "With Isolate", dur1, res1.alloc_nb, res1.realloc_nb, size1 }); 174 | try t.addRow(.{ "Without Isolate", dur2, res2.alloc_nb, res2.realloc_nb, size2 }); 175 | const out = std.io.getStdOut().writer(); 176 | try t.render(out); 177 | } 178 | -------------------------------------------------------------------------------- /src/tests/test_utils.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const public = @import("../api.zig"); 18 | 19 | const js_response_size = 220; 20 | 21 | fn isTypeError(expected: []const u8, msg: []const u8) bool { 22 | if (!std.mem.eql(u8, expected, "TypeError")) { 23 | return false; 24 | } 25 | if (std.mem.startsWith(u8, msg, "Uncaught TypeError: ")) { 26 | return true; 27 | } 28 | if (std.mem.startsWith(u8, msg, "TypeError: ")) { 29 | // TODO: why callback exception does not start with "Uncaught"? 30 | return true; 31 | } 32 | return false; 33 | } 34 | 35 | pub fn sleep(nanoseconds: u64) void { 36 | const s = nanoseconds / std.time.ns_per_s; 37 | const ns = nanoseconds % std.time.ns_per_s; 38 | std.posix.nanosleep(s, ns); 39 | } 40 | 41 | // result memory is owned by the caller 42 | pub fn intToStr(alloc: std.mem.Allocator, nb: u8) []const u8 { 43 | return std.fmt.allocPrint( 44 | alloc, 45 | "{d}", 46 | .{nb}, 47 | ) catch unreachable; 48 | } 49 | 50 | // engineOwnPropertiesDefault returns the number of own properties 51 | // by default for a current Type 52 | // result memory is owned by the caller 53 | pub fn engineOwnPropertiesDefault() u8 { 54 | return switch (public.Env.engine()) { 55 | .v8 => 5, 56 | }; 57 | } 58 | 59 | var test_case: usize = 0; 60 | 61 | fn caseError(src: []const u8, exp: []const u8, res: []const u8, stack: ?[]const u8) void { 62 | std.debug.print("\n\tcase: ", .{}); 63 | std.debug.print("\t\t{s}\n", .{src}); 64 | std.debug.print("\texpected: ", .{}); 65 | std.debug.print("\t{s}\n", .{exp}); 66 | std.debug.print("\tactual: ", .{}); 67 | std.debug.print("\t{s}\n", .{res}); 68 | if (stack != null) { 69 | std.debug.print("\tstack: \n{s}\n", .{stack.?}); 70 | } 71 | } 72 | 73 | pub fn checkCases(js_env: *public.Env, cases: []Case) !void { 74 | // res buf 75 | var res_buf: [js_response_size]u8 = undefined; 76 | var fba = std.heap.FixedBufferAllocator.init(&res_buf); 77 | const fba_alloc = fba.allocator(); 78 | 79 | try checkCasesAlloc(fba_alloc, js_env, cases); 80 | } 81 | 82 | pub fn checkCasesAlloc(allocator: std.mem.Allocator, js_env: *public.Env, cases: []Case) !void { 83 | var arena = std.heap.ArenaAllocator.init(allocator); 84 | const alloc = arena.allocator(); 85 | defer arena.deinit(); 86 | 87 | var has_error = false; 88 | 89 | var try_catch: public.TryCatch = undefined; 90 | try_catch.init(js_env); 91 | defer try_catch.deinit(); 92 | 93 | // cases 94 | for (cases, 0..) |case, i| { 95 | defer _ = arena.reset(.retain_capacity); 96 | test_case += 1; 97 | 98 | // prepare script execution 99 | var buf: [99]u8 = undefined; 100 | const name = try std.fmt.bufPrint(buf[0..], "test_{d}.js", .{test_case}); 101 | 102 | // run script error 103 | const res = js_env.execWait(case.src, name) catch |err| { 104 | 105 | // is it an intended error? 106 | const except = try try_catch.exception(alloc, js_env); 107 | if (except) |msg| { 108 | defer alloc.free(msg); 109 | if (isTypeError(case.ex, msg)) continue; 110 | } 111 | 112 | has_error = true; 113 | if (i == 0) { 114 | std.debug.print("\n", .{}); 115 | } 116 | 117 | const expected = switch (err) { 118 | error.JSExec => case.ex, 119 | error.JSExecCallback => case.cbk_ex, 120 | else => return err, 121 | }; 122 | if (try try_catch.stack(alloc, js_env)) |stack| { 123 | defer alloc.free(stack); 124 | caseError(case.src, expected, except.?, stack); 125 | } 126 | continue; 127 | }; 128 | 129 | // check if result is expected 130 | const res_string = try res.toString(alloc, js_env); 131 | defer alloc.free(res_string); 132 | const equal = std.mem.eql(u8, case.ex, res_string); 133 | if (!equal) { 134 | has_error = true; 135 | if (i == 0) { 136 | std.debug.print("\n", .{}); 137 | } 138 | caseError(case.src, case.ex, res_string, null); 139 | } 140 | } 141 | if (has_error) { 142 | std.debug.print("\n", .{}); 143 | return error.NotEqual; 144 | } 145 | } 146 | 147 | pub fn isCancelAvailable() bool { 148 | return switch (@import("builtin").target.os.tag) { 149 | .macos, .tvos, .watchos, .ios => false, 150 | else => true, 151 | }; 152 | } 153 | 154 | pub const Case = struct { 155 | src: []const u8, 156 | ex: []const u8, 157 | cbk_ex: []const u8 = "undefined", 158 | }; 159 | 160 | // a shorthand function to run a script within a JS env 161 | // with local TryCatch 162 | // - on success, do nothing 163 | // - on error, log error the JS result and JS stack if available 164 | pub fn runScript( 165 | js_env: *public.Env, 166 | alloc: std.mem.Allocator, 167 | script: []const u8, 168 | name: []const u8, 169 | ) !void { 170 | 171 | // local try catch 172 | var try_catch: public.TryCatch = undefined; 173 | try_catch.init(js_env); 174 | defer try_catch.deinit(); 175 | 176 | // check result 177 | _ = js_env.execWait(script, name) catch |err| { 178 | if (try try_catch.exception(alloc, js_env)) |msg| { 179 | defer alloc.free(msg); 180 | std.log.err("script {s} error: {s}\n", .{ name, msg }); 181 | } 182 | if (try try_catch.stack(alloc, js_env)) |msg| { 183 | defer alloc.free(msg); 184 | std.log.err("script {s} stack: {s}\n", .{ name, msg }); 185 | } 186 | return err; 187 | }; 188 | } 189 | -------------------------------------------------------------------------------- /src/generate.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const internal = @import("internal_api.zig"); 18 | const refl = internal.refl; 19 | const NativeContext = internal.NativeContext; 20 | 21 | const API = @import("private_api.zig").API; 22 | const loadFn = @import("private_api.zig").loadFn; 23 | 24 | // Compile and loading mechanism 25 | // ----------------------------- 26 | 27 | // NOTE: 28 | // The mechanism is based on 2 steps 29 | // 1. The compile step at comptime will produce a list of APIs 30 | // At this step we: 31 | // - reflect the native types to obtain type information (T_refl) 32 | // - generate a loading function containing corresponding JS callbacks functions 33 | // (constructor, getters, setters, methods) 34 | // 2. The loading step at runtime will produce a list of TPLs 35 | // At this step we call the loading function into the runtime v8 (Isolate and globals), 36 | // generating corresponding V8 functions and objects templates. 37 | 38 | // reflect the user-defined types to obtain type information (T_refl) 39 | // This function must be called at comptime by the root file of the project 40 | // and stored in a constant named `Types` 41 | pub fn reflect(comptime types: anytype) []const refl.Struct { 42 | std.debug.assert(@inComptime()); 43 | 44 | // call types reflection 45 | const structs: []const refl.Struct = refl.do(types) catch |e| @compileError(@errorName(e)); 46 | return structs; 47 | } 48 | 49 | // Import user-defined types 50 | pub const Types: []const refl.Struct = @import("root").Types; 51 | 52 | // retrieved the reflected type of a user-defined native type 53 | pub fn getType(comptime T: type) refl.Struct { 54 | std.debug.assert(@inComptime()); 55 | for (Types) |t| { 56 | if (T == t.Self() or T == *t.Self()) { 57 | return t; 58 | } 59 | } 60 | @compileError("NativeTypeNotHandled: " ++ @typeName(T)); 61 | } 62 | 63 | // generate APIs from reflected types 64 | // which can be later loaded in JS. 65 | fn generate(comptime types: []const refl.Struct) []API { 66 | std.debug.assert(@inComptime()); 67 | 68 | var apis: [types.len]API = undefined; 69 | inline for (types, 0..) |T_refl, i| { 70 | const loader = loadFn(T_refl); 71 | apis[i] = API{ .T_refl = T_refl, .load = loader }; 72 | } 73 | return &apis; 74 | } 75 | 76 | // Load user-defined native types into a JS sandbox 77 | // This function is called at runtime. 78 | pub fn load( 79 | nat_ctx: *NativeContext, 80 | js_sandbox: anytype, 81 | js_globals: anytype, 82 | comptime js_T: type, 83 | js_types: []js_T, 84 | ) !void { 85 | const apis = comptime generate(Types); 86 | 87 | inline for (Types, 0..) |T_refl, i| { 88 | if (T_refl.proto_index == null) { 89 | js_types[i] = try apis[i].load(nat_ctx, js_sandbox, js_globals, null); 90 | } else { 91 | const proto = js_types[T_refl.proto_index.?]; // safe because apis are ordered from parent to child 92 | js_types[i] = try apis[i].load(nat_ctx, js_sandbox, js_globals, proto); 93 | } 94 | } 95 | } 96 | 97 | // meta-programing 98 | 99 | fn itoa(comptime i: u8) []const u8 { 100 | var len: usize = undefined; 101 | if (i < 10) { 102 | len = 1; 103 | } else if (i < 100) { 104 | len = 2; 105 | } else { 106 | @compileError("too much members"); 107 | } 108 | var buf: [len]u8 = undefined; 109 | return std.fmt.bufPrint(buf[0..], "{d}", .{i}) catch unreachable; 110 | } 111 | 112 | // retrieve the number of elements in a tuple 113 | fn tupleNb(comptime tuple: anytype) usize { 114 | var nb = 0; 115 | for (@typeInfo(@TypeOf(tuple)).@"struct".fields) |member| { 116 | const member_info = @typeInfo(member.type); 117 | if (member_info != .@"struct" or (member_info == .@"struct" and !member_info.@"struct".is_tuple)) { 118 | @compileError("GenerateMemberNotTypeOrTuple"); 119 | } 120 | for (member_info.@"struct".fields) |field| { 121 | if (field.type != type) { 122 | @compileError("GenerateMemberTupleChildNotType"); 123 | } 124 | } 125 | nb += member_info.@"struct".fields.len; 126 | } 127 | return nb; 128 | } 129 | 130 | fn tupleTypes(comptime nb: usize, comptime tuple: anytype) [nb]type { 131 | var types: [nb]type = undefined; 132 | var i = 0; 133 | for (@typeInfo(@TypeOf(tuple)).@"struct".fields) |member| { 134 | const T = @field(tuple, member.name); 135 | const info = @typeInfo(@TypeOf(T)); 136 | for (info.@"struct".fields) |field| { 137 | types[i] = @field(T, field.name); 138 | i += 1; 139 | } 140 | } 141 | return types; 142 | } 143 | 144 | fn MergeTupleT(comptime value: anytype) type { 145 | const fields_nb = tupleNb(value); 146 | var fields: [fields_nb]std.builtin.Type.StructField = undefined; 147 | var i = 0; 148 | while (i < fields_nb) { 149 | fields[i] = .{ 150 | // StructField.name expect a null terminated string. 151 | // concatenate the `[]const u8` string with an empty string 152 | // literal (`name ++ ""`) to explicitly coerce it to `[:0]const 153 | // u8`. 154 | .name = itoa(i) ++ "", 155 | .type = type, 156 | .default_value_ptr = null, 157 | .is_comptime = false, 158 | .alignment = @alignOf(type), 159 | }; 160 | i += 1; 161 | } 162 | const decls: [0]std.builtin.Type.Declaration = undefined; 163 | const info = std.builtin.Type.Struct{ 164 | .layout = .auto, 165 | .fields = &fields, 166 | .decls = &decls, 167 | .is_tuple = true, 168 | }; 169 | return @Type(std.builtin.Type{ .@"struct" = info }); 170 | } 171 | 172 | pub fn MergeTuple(comptime value: anytype) MergeTupleT(value) { 173 | var t: MergeTupleT(value) = undefined; 174 | 175 | const fields_nb = tupleNb(value); 176 | const fields_types = tupleTypes(fields_nb, value); 177 | 178 | for (fields_types, 0..) |T, i| { 179 | const name = itoa(i); 180 | @field(t, name) = T; 181 | } 182 | return t; 183 | } 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-js-runtime 2 | 3 | A fast and easy library to add a Javascript runtime into your Zig project. 4 | 5 | With this library you can: 6 | 7 | - add Javascript as a scripting language for your Zig project (eg. plugin system, game scripting) 8 | - build a web browser (this library as been developed for the [Lightpanda headless browser](https://lightpanda.io)) 9 | - build a Javascript runtime (ie. a Node/Bun like) 10 | 11 | Features: 12 | 13 | - [x] Setup and configure the Javascript engine 14 | - [x] Expose Zig structs as Javascript functions and objects (at compile time) 15 | - [x] Bi-directional "link" between Zig structs and Javascript objects 16 | - [x] Support for inheritance (on Zig structs) and prototype chain (on Javascript objects) 17 | - [x] Support for Javascript asynchronous code (I/O event loop) 18 | 19 | Currently only v8 is supported as a Javascript engine, but other engines might be added in the future. 20 | 21 | This library is fully single-threaded to matches the nature of Javascript and avoid any cost of context switching for the Javascript engine. 22 | 23 | ## Rationale 24 | 25 | Integrate a Javascript engine into a Zig project is not just embeding an external library and making language bindings. 26 | You need to handle other stuffs: 27 | 28 | - the generation of your Zig structs as Javascript functions and objects (_ObjectTemplate_ and _FunctionTemplate_ in v8) 29 | - the callbacks of Javascript actions into your Zig functions (constructors, getters, setters, methods) 30 | - the memory management between the Javascript engine and your Zig code 31 | - the I/O event loop to support asynchronous Javascript code 32 | 33 | This library takes care of all this, with no overhead thanks to Zig awesome compile time capabilities. 34 | 35 | ## Getting started 36 | 37 | In your Zig project, let's say you have this basic struct that you want to expose in Javascript: 38 | 39 | ```zig 40 | const Person = struct { 41 | first_name: []u8, 42 | last_name: []u8, 43 | age: u32, 44 | 45 | // Constructor 46 | // if there is no 'constructor' defined 'new Person()' will raise a TypeError in JS 47 | pub fn constructor(first_name: []u8, last_name: []u8, age: u32) Person { 48 | return .{ 49 | .first_name = first_name, 50 | .last_name = last_name, 51 | .age = age, 52 | }; 53 | } 54 | 55 | // Getter, 'get_' 56 | pub fn get_age(self: Person) u32 { 57 | return self.age; 58 | } 59 | 60 | // Setter, 'set_' 61 | pub fn set_age(self: *Person, age: u32) void { 62 | self.age = age; 63 | } 64 | 65 | // Method, '_' 66 | pub fn _lastName(self: Person) []u8 { 67 | return self.last_name; 68 | } 69 | }; 70 | ``` 71 | 72 | You can generate the corresponding Javascript functions at comptime with: 73 | 74 | ```zig 75 | const jsruntime = @import("jsruntime"); 76 | pub const Types = jsruntime.reflect(.{Person}); 77 | ``` 78 | 79 | And then use it in a Javascript script: 80 | 81 | ```javascript 82 | // Creating a new instance of Person 83 | let p = new Person('John', 'Doe', 40); 84 | 85 | // Getter 86 | p.age; // => 40 87 | 88 | // Setter 89 | p.age = 41; 90 | p.age; // => 41 91 | 92 | // Method 93 | p.lastName(); // => 'Doe' 94 | ``` 95 | 96 | Let's add some inheritance (ie. prototype chain): 97 | 98 | ```zig 99 | const User = struct { 100 | proto: Person, 101 | role: u8, 102 | 103 | pub const prototype = *Person; 104 | 105 | pub fn constructor(first_name: []u8, last_name: []u8, age: u32, role: u8) User { 106 | const proto = Person.constructor(first_name, last_name, age); 107 | return .{ .proto = proto, .role = role }; 108 | } 109 | 110 | pub fn get_role(self: User) u8 { 111 | return self.role; 112 | } 113 | }; 114 | 115 | pub const Types = jsruntime.reflect(.{Person, User}); 116 | ``` 117 | 118 | And use it in a Javascript script: 119 | 120 | ```javascript 121 | // Creating a new instance of User 122 | let u = new User('Jane', 'Smith', 35, 1); // eg. 1 for admin 123 | 124 | // we can use the User getters/setters/methods 125 | u.role; // => 1 126 | 127 | // but also the Person getters/setters/methods 128 | u.age; // => 35 129 | u.age = 36; 130 | u.age; // => 36 131 | u.lastName(); // => 'Smith' 132 | 133 | // checking the prototype chain 134 | u instanceof User == true; 135 | u instanceof Person == true; 136 | User.prototype.__proto__ === Person.prototype; 137 | ``` 138 | 139 | ### Javascript shell 140 | 141 | A Javascript shell is provided as an example in `src/main_shell.zig`. 142 | 143 | ```sh 144 | $ make shell 145 | 146 | zig-js-runtime - Javascript Shell 147 | exit with Ctrl+D or "exit" 148 | 149 | > 150 | ``` 151 | 152 | ## Build 153 | 154 | ### Prerequisites 155 | 156 | zig-js-runtime is written with [Zig](https://ziglang.org/) `0.14.0`. You have to 157 | install it with the right version in order to build the project. 158 | 159 | To be able to build the v8 engine, you have to install some libs: 160 | 161 | For Debian/Ubuntu based Linux: 162 | ```sh 163 | sudo apt install xz-utils \ 164 | python3 ca-certificates git \ 165 | pkg-config libglib2.0-dev clang 166 | ``` 167 | 168 | For MacOS, you only need Python 3. 169 | 170 | ### Install and build dependencies 171 | 172 | The project uses git submodule for dependencies. 173 | The `make install-submodule` will init and update the submodules in the `vendor/` 174 | directory. 175 | 176 | ```sh 177 | make install-submodule 178 | ``` 179 | 180 | ### Build v8 181 | 182 | The command `make install-v8-dev` uses `zig-v8` dependency to build v8 engine lib. 183 | Be aware the build task is very long and cpu consuming. 184 | 185 | Build v8 engine for debug/dev version, it creates 186 | `vendor/v8/$ARCH/debug/libc_v8.a` file. 187 | 188 | ```sh 189 | make install-v8-dev 190 | ``` 191 | 192 | You should also build a release vesion of v8 with: 193 | 194 | ```sh 195 | make install-v8 196 | ``` 197 | 198 | ### All in one build 199 | 200 | You can run `make install` and `make install-dev` to install deps all in one. 201 | 202 | ## Development 203 | 204 | Some Javascript features are not supported yet: 205 | 206 | - [ ] [Promises](https://github.com/lightpanda-io/zig-js-runtime/issues/73) and [micro-tasks](https://github.com/lightpanda-io/zig-js-runtime/issues/56) 207 | - [ ] Some Javascript types, including [Arrays](https://github.com/lightpanda-io/zig-js-runtime/issues/52) 208 | - [ ] [Function overloading](https://github.com/lightpanda-io/zig-js-runtime/issues/54) 209 | - [ ] [Types static methods](https://github.com/lightpanda-io/zig-js-runtime/issues/127) 210 | - [ ] [Non-optional nullable types](https://github.com/lightpanda-io/zig-js-runtime/issues/72) 211 | 212 | ### Test 213 | 214 | You can test the zig-js-runtime library by running `make test`. 215 | 216 | ## Credits 217 | 218 | - [zig-v8](https://github.com/fubark/zig-v8/) for v8 bindings and build 219 | - [Tigerbeetle](https://github.com/tigerbeetledb/tigerbeetle/tree/main/src/io) for the IO loop based on _io\_uring_ 220 | - The v8 team for the [v8 Javascript engine](https://v8.dev/) 221 | - The Zig team for [Zig](https://ziglang.org/) 222 | -------------------------------------------------------------------------------- /src/tests/types_primitives_test.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const public = @import("../api.zig"); 18 | const tests = public.test_utils; 19 | 20 | // TODO: use functions instead of "fake" struct once we handle function API generation 21 | const Primitives = struct { 22 | const i64Num = @import("../types.zig").i64Num; 23 | const u64Num = @import("../types.zig").u64Num; 24 | 25 | const Self = @This(); 26 | 27 | pub fn constructor() Self { 28 | return .{}; 29 | } 30 | 31 | // List of bytes (string) 32 | pub fn _checkString(_: Self, v: []u8) []u8 { 33 | return v; 34 | } 35 | 36 | // Integers signed 37 | 38 | pub fn _checkI32(_: Self, v: i32) i32 { 39 | return v; 40 | } 41 | 42 | pub fn _checkI64(_: Self, v: i64) i64 { 43 | return v; 44 | } 45 | 46 | pub fn _checkI64Num(_: Self, v: i64Num) i64Num { 47 | return v; 48 | } 49 | 50 | // Integers unsigned 51 | 52 | pub fn _checkU32(_: Self, v: u32) u32 { 53 | return v; 54 | } 55 | 56 | pub fn _checkU64(_: Self, v: u64) u64 { 57 | return v; 58 | } 59 | 60 | pub fn _checkU64Num(_: Self, v: u64Num) u64Num { 61 | return v; 62 | } 63 | 64 | // Floats 65 | 66 | pub fn _checkF32(_: Self, v: f32) f32 { 67 | return v; 68 | } 69 | 70 | pub fn _checkF64(_: Self, v: f64) f64 { 71 | return v; 72 | } 73 | 74 | // Bool 75 | pub fn _checkBool(_: Self, v: bool) bool { 76 | return v; 77 | } 78 | 79 | // Undefined 80 | // TODO: there is a bug with this function 81 | // void paramater does not work => avoid for now 82 | // pub fn _checkUndefined(_: Self, v: void) void { 83 | // return v; 84 | // } 85 | 86 | // Null 87 | pub fn _checkNullEmpty(_: Self, v: ?u32) bool { 88 | return (v == null); 89 | } 90 | pub fn _checkNullNotEmpty(_: Self, v: ?u32) bool { 91 | return (v != null); 92 | } 93 | 94 | // Optionals 95 | pub fn _checkOptional(_: Self, _: ?u8, v: u8, _: ?u8, _: ?u8) u8 { 96 | return v; 97 | } 98 | pub fn _checkNonOptional(_: Self, v: u8) u8 { 99 | return v; 100 | } 101 | pub fn _checkOptionalReturn(_: Self) ?bool { 102 | return true; 103 | } 104 | pub fn _checkOptionalReturnNull(_: Self) ?bool { 105 | return null; 106 | } 107 | pub fn _checkOptionalReturnString(_: Self) ?[]const u8 { 108 | return "ok"; 109 | } 110 | }; 111 | 112 | pub const Types = .{ 113 | Primitives, 114 | }; 115 | 116 | // exec tests 117 | pub fn exec( 118 | _: std.mem.Allocator, 119 | js_env: *public.Env, 120 | ) anyerror!void { 121 | 122 | // start JS env 123 | try js_env.start(); 124 | defer js_env.stop(); 125 | 126 | // constructor 127 | var case_cstr = [_]tests.Case{ 128 | .{ .src = "let p = new Primitives();", .ex = "undefined" }, 129 | }; 130 | try tests.checkCases(js_env, &case_cstr); 131 | 132 | // JS <> Native translation of primitive types 133 | var cases = [_]tests.Case{ 134 | 135 | // String 136 | .{ .src = "p.checkString('ok ascii') === 'ok ascii';", .ex = "true" }, 137 | .{ .src = "p.checkString('ok emoji 🚀') === 'ok emoji 🚀';", .ex = "true" }, 138 | .{ .src = "p.checkString('ok chinese 鿍') === 'ok chinese 鿍';", .ex = "true" }, 139 | 140 | // String (JS liberal cases) 141 | .{ .src = "p.checkString(1) === '1';", .ex = "true" }, 142 | .{ .src = "p.checkString(null) === 'null';", .ex = "true" }, 143 | .{ .src = "p.checkString(undefined) === 'undefined';", .ex = "true" }, 144 | 145 | // Integers 146 | 147 | // signed 148 | .{ .src = "const min_i32 = -2147483648", .ex = "undefined" }, 149 | .{ .src = "p.checkI32(min_i32) === min_i32;", .ex = "true" }, 150 | .{ .src = "p.checkI32(min_i32-1) === min_i32-1;", .ex = "false" }, 151 | .{ .src = "try { p.checkI32(9007199254740995n) } catch(e) { e instanceof TypeError; }", .ex = "true" }, 152 | 153 | // unsigned 154 | .{ .src = "const max_u32 = 4294967295", .ex = "undefined" }, 155 | .{ .src = "p.checkU32(max_u32) === max_u32;", .ex = "true" }, 156 | .{ .src = "p.checkU32(max_u32+1) === max_u32+1;", .ex = "false" }, 157 | 158 | // int64 (with Number) 159 | .{ .src = "p.checkI64Num(min_i32-1) === min_i32-1;", .ex = "true" }, 160 | .{ .src = "p.checkU64Num(max_u32+1) === max_u32+1;", .ex = "true" }, 161 | 162 | // int64 (with BigInt) 163 | .{ .src = "const big_int = 9007199254740995n", .ex = "undefined" }, 164 | .{ .src = "p.checkI64(big_int) === big_int", .ex = "true" }, 165 | .{ .src = "p.checkU64(big_int) === big_int;", .ex = "true" }, 166 | .{ .src = "p.checkI64(0) === 0n;", .ex = "true" }, 167 | .{ .src = "p.checkI64(-1) === -1n;", .ex = "true" }, 168 | .{ .src = "p.checkU64(0) === 0n;", .ex = "true" }, 169 | 170 | // Floats 171 | // use round 2 decimals for float to ensure equality 172 | .{ .src = "const r = function(x) {return Math.round(x * 100) / 100};", .ex = "undefined" }, 173 | .{ .src = "const double = 10.02;", .ex = "undefined" }, 174 | .{ .src = "r(p.checkF32(double)) === double;", .ex = "true" }, 175 | .{ .src = "r(p.checkF64(double)) === double;", .ex = "true" }, 176 | 177 | // Bool 178 | .{ .src = "p.checkBool(true);", .ex = "true" }, 179 | .{ .src = "p.checkBool(false);", .ex = "false" }, 180 | .{ .src = "p.checkBool(0);", .ex = "false" }, 181 | .{ .src = "p.checkBool(1);", .ex = "true" }, 182 | 183 | // Bool (JS liberal cases) 184 | .{ .src = "p.checkBool(null);", .ex = "false" }, 185 | .{ .src = "p.checkBool(undefined);", .ex = "false" }, 186 | 187 | // Undefined 188 | // see TODO on Primitives.checkUndefined 189 | // .{ .src = "p.checkUndefined(undefined) === undefined;", .ex = "true" }, 190 | 191 | // Null 192 | .{ .src = "p.checkNullEmpty(null);", .ex = "true" }, 193 | .{ .src = "p.checkNullEmpty(undefined);", .ex = "true" }, 194 | .{ .src = "p.checkNullNotEmpty(1);", .ex = "true" }, 195 | 196 | // Optional 197 | .{ .src = "p.checkOptional(null, 3);", .ex = "3" }, 198 | .{ .src = "p.checkNonOptional();", .ex = "TypeError" }, 199 | .{ .src = "p.checkOptionalReturn() === true;", .ex = "true" }, 200 | .{ .src = "p.checkOptionalReturnNull() === null;", .ex = "true" }, 201 | .{ .src = "p.checkOptionalReturnString() === 'ok';", .ex = "true" }, 202 | }; 203 | try tests.checkCases(js_env, &cases); 204 | } 205 | -------------------------------------------------------------------------------- /src/engines/v8/types_primitives.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const v8 = @import("v8"); 18 | 19 | const internal = @import("../../internal_api.zig"); 20 | const refl = internal.refl; 21 | 22 | const public = @import("../../api.zig"); 23 | const i64Num = public.i64Num; 24 | const u64Num = public.u64Num; 25 | 26 | // Convert a Native value to a JS value 27 | pub fn nativeToJS( 28 | comptime T: type, 29 | val: T, 30 | isolate: v8.Isolate, 31 | ) !v8.Value { 32 | const js_val = switch (T) { 33 | 34 | // undefined 35 | void => v8.initUndefined(isolate), 36 | 37 | // list of bytes (ie. string) 38 | []u8, []const u8 => v8.String.initUtf8(isolate, val), 39 | 40 | // floats 41 | // TODO: what about precision, ie. 1.82 (native) -> 1.8200000524520874 (js) 42 | f32 => v8.Number.init(isolate, @as(f32, @floatCast(val))), 43 | f64 => v8.Number.init(isolate, val), 44 | 45 | // integers signed 46 | i8, i16 => v8.Integer.initI32(isolate, @as(i32, @intCast(val))), 47 | i32 => v8.Integer.initI32(isolate, val), 48 | i64Num => v8.Number.initBitCastedI64(isolate, val.get()), 49 | i64 => v8.BigInt.initI64(isolate, val), 50 | 51 | // integers unsigned 52 | u8, u16 => v8.Integer.initU32(isolate, @as(u32, @intCast(val))), 53 | u32 => v8.Integer.initU32(isolate, val), 54 | u64Num => v8.Number.initBitCastedU64(isolate, val.get()), 55 | u64 => v8.BigInt.initU64(isolate, val), 56 | 57 | // bool 58 | bool => v8.Boolean.init(isolate, val), 59 | 60 | else => return error.NativeTypeUnhandled, 61 | }; 62 | 63 | return v8.getValue(js_val); 64 | } 65 | 66 | // Convert a JS value to a Native value 67 | // allocator is only used if the JS value is a string, 68 | // in this case caller owns the memory 69 | pub fn jsToNative( 70 | alloc: std.mem.Allocator, 71 | comptime T: type, 72 | js_val: v8.Value, 73 | isolate: v8.Isolate, 74 | ctx: v8.Context, 75 | ) !T { 76 | 77 | // JS Undefined value for an Native void 78 | // not sure if this case make any sense (a void argument) 79 | // but let's support it for completeness 80 | if (js_val.isUndefined()) { 81 | // distinct comptime condition, otherwise compile error 82 | comptime { 83 | if (T == void) { 84 | return {}; 85 | } 86 | } 87 | } 88 | 89 | const info = @typeInfo(T); 90 | 91 | // JS Null or Undefined value 92 | if (js_val.isNull() or js_val.isUndefined()) { 93 | // if Native optional type return null 94 | if (comptime info == .optional) { 95 | return null; 96 | } 97 | // Here we should normally return an error 98 | // ie. a JS Null/Undefined value has been used for a non-optional Native type. 99 | // But JS is liberal, you can pass null/undefined to: 100 | // - string (=> 'null' and 'undefined') 101 | // - bool (=> false), 102 | // - numeric types (null => 0 value, undefined => NaN) 103 | // TODO: return JS NaN for JS Undefined on int/float Native types. 104 | } 105 | 106 | // unwrap Optional 107 | if (info == .optional) { 108 | return try jsToNative(alloc, info.optional.child, js_val, isolate, ctx); 109 | } 110 | 111 | // JS values 112 | // JS is liberal, you can pass: 113 | // - numeric value as string arg 114 | // - string value as numeric arg 115 | // - null as 0 numeric arg 116 | // - null and undefined as string or bool arg 117 | // Therefore we do not check the type of the JS value (ie. IsString, IsBool, etc.) 118 | // instead we directly try to return the corresponding Native value. 119 | 120 | switch (T) { 121 | 122 | // list of bytes (including strings) 123 | []u8, []const u8 => { 124 | return try valueToUtf8(alloc, js_val, isolate, ctx); 125 | }, 126 | 127 | // floats 128 | f32 => return js_val.toF32(ctx) catch return error.InvalidArgument, 129 | f64 => return js_val.toF64(ctx) catch return error.InvalidArgument, 130 | 131 | // integers signed 132 | i8, i16 => { 133 | const v = js_val.toI32(ctx) catch return error.InvalidArgument; 134 | return @as(T, @intCast(v)); 135 | }, 136 | i32 => return js_val.toI32(ctx) catch return error.InvalidArgument, 137 | i64Num => { 138 | const v = js_val.bitCastToI64(ctx) catch return error.InvalidArgument; 139 | return i64Num.init(v); 140 | }, 141 | i64 => { 142 | if (js_val.isBigInt()) { 143 | const v = js_val.castTo(v8.BigInt); 144 | return v.getInt64(); 145 | } 146 | return @intCast(js_val.toI32(ctx) catch return error.InvalidArgument); 147 | }, 148 | 149 | // integers unsigned 150 | u8, u16 => { 151 | const v = js_val.toU32(ctx) catch return error.InvalidArgument; 152 | return @as(T, @intCast(v)); 153 | }, 154 | u32 => return js_val.toU32(ctx) catch return error.InvalidArgument, 155 | u64Num, ?u64Num => { 156 | const v = js_val.bitCastToU64(ctx) catch return error.InvalidArgument; 157 | return u64Num.init(v); 158 | }, 159 | u64 => { 160 | if (js_val.isBigInt()) { 161 | const v = js_val.castTo(v8.BigInt); 162 | return v.getUint64(); 163 | } 164 | return @intCast(js_val.toU32(ctx) catch return error.InvalidArgument); 165 | }, 166 | 167 | // bool 168 | bool => return js_val.toBool(isolate), 169 | 170 | else => return error.JSTypeUnhandled, 171 | } 172 | } 173 | 174 | // Convert a JS value to a Native nested object 175 | pub fn jsToObject( 176 | alloc: std.mem.Allocator, 177 | comptime nested_T: refl.StructNested, 178 | comptime T: type, 179 | js_val: v8.Value, 180 | isolate: v8.Isolate, 181 | ctx: v8.Context, 182 | ) !T { 183 | const info = @typeInfo(T); 184 | 185 | // JS Null or Undefined value 186 | if (js_val.isNull() or js_val.isUndefined()) { 187 | // if Native optional type return null 188 | if (comptime info == .optional) { 189 | return null; 190 | } 191 | } 192 | 193 | // check it's a JS object 194 | if (!js_val.isObject()) { 195 | return error.JSNotObject; 196 | } 197 | 198 | // unwrap Optional 199 | if (comptime info == .optional) { 200 | return try jsToObject(alloc, nested_T, info.optional.child, js_val, isolate, ctx); 201 | } 202 | 203 | const js_obj = js_val.castTo(v8.Object); 204 | var obj: T = undefined; 205 | inline for (nested_T.fields, 0..) |field, i| { 206 | const name = field.name.?; 207 | const key = v8.String.initUtf8(isolate, name); 208 | if (js_obj.has(ctx, key.toValue())) { 209 | const field_js_val = try js_obj.getValue(ctx, key); 210 | const field_val = try jsToNative(alloc, field.T, field_js_val, isolate, ctx); 211 | @field(obj, name) = field_val; 212 | } else { 213 | if (comptime field.underOpt() != null) { 214 | @field(obj, name) = null; 215 | } else if (comptime !refl.hasDefaultValue(nested_T.T, i)) { 216 | return error.JSWrongObject; 217 | } 218 | } 219 | } 220 | // here we could handle pointer to JS object 221 | // (by allocating a pointer, setting it's value to obj and returning it) 222 | // but for this kind of use case a complete type API is preferable 223 | // over an anonymous JS object 224 | return obj; 225 | } 226 | 227 | pub fn valueToUtf8( 228 | alloc: std.mem.Allocator, 229 | value: v8.Value, 230 | isolate: v8.Isolate, 231 | ctx: v8.Context, 232 | ) ![]u8 { 233 | const str = try value.toString(ctx); 234 | const len = str.lenUtf8(isolate); 235 | const buf = try alloc.alloc(u8, len); 236 | _ = str.writeUtf8(isolate, buf); 237 | return buf; 238 | } 239 | -------------------------------------------------------------------------------- /src/tests/cbk_test.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const jsruntime = @import("../api.zig"); 18 | 19 | const u64Num = jsruntime.u64Num; 20 | const Callback = jsruntime.Callback; 21 | const CallbackSync = jsruntime.CallbackSync; 22 | const CallbackArg = jsruntime.CallbackArg; 23 | 24 | const tests = jsruntime.test_utils; 25 | 26 | pub const OtherCbk = struct { 27 | val: u8, 28 | 29 | pub fn get_val(self: OtherCbk) u8 { 30 | return self.val; 31 | } 32 | }; 33 | 34 | pub const Window = struct { 35 | 36 | // store a map between internal timeouts ids and pointers to uint. 37 | // the maximum number of possible timeouts is fixed. 38 | timeoutid: u32 = 0, 39 | timeoutids: [10]u64 = undefined, 40 | 41 | pub fn constructor() Window { 42 | return Window{}; 43 | } 44 | 45 | pub fn _cbkSyncWithoutArg(_: Window, _: CallbackSync) void { 46 | tests.sleep(1 * std.time.ns_per_ms); 47 | } 48 | 49 | pub fn _cbkSyncWithArg(_: Window, _: CallbackSync, _: CallbackArg) void { 50 | tests.sleep(1 * std.time.ns_per_ms); 51 | } 52 | 53 | pub fn _cbkAsync( 54 | self: *Window, 55 | loop: *jsruntime.Loop, 56 | callback: Callback, 57 | milliseconds: u32, 58 | ) !u32 { 59 | const n: u63 = @intCast(milliseconds); 60 | const id = try loop.timeout(n * std.time.ns_per_ms, callback); 61 | 62 | defer self.timeoutid += 1; 63 | self.timeoutids[self.timeoutid] = id; 64 | 65 | return self.timeoutid; 66 | } 67 | 68 | pub fn _cbkAsyncWithJSArg( 69 | self: *Window, 70 | loop: *jsruntime.Loop, 71 | callback: Callback, 72 | milliseconds: u32, 73 | _: CallbackArg, 74 | ) !u32 { 75 | const n: u63 = @intCast(milliseconds); 76 | const id = try loop.timeout(n * std.time.ns_per_ms, callback); 77 | 78 | defer self.timeoutid += 1; 79 | self.timeoutids[self.timeoutid] = id; 80 | 81 | return self.timeoutid; 82 | } 83 | 84 | pub fn _cancel(self: Window, loop: *jsruntime.Loop, id: u32) !void { 85 | if (id >= self.timeoutid) return; 86 | try loop.cancel(self.timeoutids[id], null); 87 | } 88 | 89 | pub fn _cbkAsyncWithNatArg(_: Window, callback: Callback) !void { 90 | const other = OtherCbk{ .val = 5 }; 91 | callback.call(.{other}) catch {}; 92 | // ignore the error to let the JS msg 93 | } 94 | 95 | pub fn get_cbk(_: Window) void {} 96 | 97 | pub fn set_cbk(_: *Window, callback: Callback) !void { 98 | callback.call(.{}) catch {}; 99 | } 100 | 101 | pub fn deinit(_: *Window, _: std.mem.Allocator) void {} 102 | }; 103 | 104 | pub const Types = .{ 105 | OtherCbk, 106 | Window, 107 | }; 108 | 109 | // exec tests 110 | pub fn exec( 111 | _: std.mem.Allocator, 112 | js_env: *jsruntime.Env, 113 | ) anyerror!void { 114 | 115 | // start JS env 116 | try js_env.start(); 117 | defer js_env.stop(); 118 | 119 | // constructor 120 | var case_cstr = [_]tests.Case{ 121 | .{ .src = "let window = new Window();", .ex = "undefined" }, 122 | }; 123 | try tests.checkCases(js_env, &case_cstr); 124 | 125 | // cbkSyncWithoutArg 126 | var cases_cbk_sync_without_arg = [_]tests.Case{ 127 | // traditional anonymous function 128 | .{ 129 | .src = 130 | \\let n = 1; 131 | \\function f() {n++}; 132 | \\window.cbkSyncWithoutArg(f); 133 | , 134 | .ex = "undefined", 135 | }, 136 | .{ .src = "n;", .ex = "2" }, 137 | // arrow function 138 | .{ 139 | .src = 140 | \\let m = 1; 141 | \\window.cbkSyncWithoutArg(() => m++); 142 | , 143 | .ex = "undefined", 144 | }, 145 | .{ .src = "m;", .ex = "2" }, 146 | }; 147 | try tests.checkCases(js_env, &cases_cbk_sync_without_arg); 148 | 149 | // cbkSyncWithArg 150 | var cases_cbk_sync_with_arg = [_]tests.Case{ 151 | // traditional anonymous function 152 | .{ 153 | .src = 154 | \\let x = 1; 155 | \\function f(a) {x = x + a}; 156 | \\window.cbkSyncWithArg(f, 2); 157 | , 158 | .ex = "undefined", 159 | }, 160 | .{ .src = "x;", .ex = "3" }, 161 | // arrow function 162 | .{ 163 | .src = 164 | \\let y = 1; 165 | \\window.cbkSyncWithArg((a) => y = y + a, 2); 166 | , 167 | .ex = "undefined", 168 | }, 169 | .{ .src = "y;", .ex = "3" }, 170 | }; 171 | try tests.checkCases(js_env, &cases_cbk_sync_with_arg); 172 | 173 | // cbkAsync 174 | var cases_cbk_async = [_]tests.Case{ 175 | // traditional anonymous function 176 | .{ 177 | .src = 178 | \\let o = 1; 179 | \\function f() { 180 | \\o++; 181 | \\if (o != 2) {throw Error('cases_cbk_async error: o is not equal to 2');} 182 | \\}; 183 | \\window.cbkAsync(f, 100); // 0.1 second 184 | , 185 | .ex = "0", 186 | }, 187 | // arrow functional 188 | .{ 189 | .src = 190 | \\let p = 1; 191 | \\window.cbkAsync(() => { 192 | \\p++; 193 | \\if (p != 2) {throw Error('cases_cbk_async error: p is not equal to 2');} 194 | \\}, 100); // 0.1 second 195 | , 196 | .ex = "1", 197 | }, 198 | }; 199 | try tests.checkCases(js_env, &cases_cbk_async); 200 | 201 | // cbkAsyncWithJSArg 202 | var cases_cbk_async_with_js_arg = [_]tests.Case{ 203 | // traditional anonymous function 204 | .{ 205 | .src = 206 | \\let i = 1; 207 | \\function f(a) { 208 | \\i = i + a; 209 | \\if (i != 3) {throw Error('i is not equal to 3');} 210 | \\}; 211 | \\window.cbkAsyncWithJSArg(f, 100, 2); // 0.1 second 212 | , 213 | .ex = "2", 214 | }, 215 | // arrow functional 216 | .{ 217 | .src = 218 | \\let j = 1; 219 | \\window.cbkAsyncWithJSArg((a) => { 220 | \\j = j + a; 221 | \\if (j != 3) {throw Error('j is not equal to 3');} 222 | \\}, 100, 2); // 0.1 second 223 | , 224 | .ex = "3", 225 | }, 226 | }; 227 | try tests.checkCases(js_env, &cases_cbk_async_with_js_arg); 228 | 229 | // cbkAsyncWithNatArg 230 | var cases_cbk_async_with_nat_arg = [_]tests.Case{ 231 | .{ .src = "let exp = 5", .ex = "undefined" }, 232 | 233 | // traditional anonymous function 234 | .{ 235 | .src = 236 | \\function f(other) { 237 | \\if (other.val != exp) {throw Error('other.val expected ' + exp + ', got ' + other.val);} 238 | \\}; 239 | \\window.cbkAsyncWithNatArg(f); 240 | , 241 | .ex = "undefined", 242 | }, 243 | // arrow functional 244 | .{ 245 | .src = 246 | \\window.cbkAsyncWithNatArg((other) => { 247 | \\if (other.val != exp) {throw Error('other.val expected ' + exp + ', got ' + other.val);} 248 | \\}); 249 | , 250 | .ex = "undefined", 251 | }, 252 | }; 253 | try tests.checkCases(js_env, &cases_cbk_async_with_nat_arg); 254 | 255 | // setter cbk 256 | var cases_cbk_setter_arg = [_]tests.Case{ 257 | .{ .src = "let v = 0", .ex = "undefined" }, 258 | .{ .src = "window.cbk = () => {v++};", .ex = "() => {v++}" }, 259 | .{ .src = "v", .ex = "1" }, 260 | }; 261 | try tests.checkCases(js_env, &cases_cbk_setter_arg); 262 | 263 | if (tests.isCancelAvailable()) { 264 | // cancel cbk 265 | var cases_cbk_cancel = [_]tests.Case{ 266 | .{ 267 | .src = 268 | \\let vv = 0; 269 | \\const id = window.cbkAsync(() => {vv += 1}, 100); 270 | \\window.cancel(id); 271 | , 272 | .ex = "undefined", 273 | }, 274 | .{ .src = "vv", .ex = "0" }, 275 | }; 276 | try tests.checkCases(js_env, &cases_cbk_cancel); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/tests/types_complex_test.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const public = @import("../api.zig"); 18 | const tests = public.test_utils; 19 | const MyIterable = public.Iterable(u8); 20 | const Variadic = public.Variadic; 21 | 22 | const MyList = struct { 23 | items: []u8, 24 | 25 | pub fn constructor(alloc: std.mem.Allocator, elem1: u8, elem2: u8, elem3: u8) MyList { 26 | var items = alloc.alloc(u8, 3) catch unreachable; 27 | items[0] = elem1; 28 | items[1] = elem2; 29 | items[2] = elem3; 30 | return .{ .items = items }; 31 | } 32 | 33 | pub fn _first(self: MyList) u8 { 34 | return self.items[0]; 35 | } 36 | 37 | pub fn _symbol_iterator(self: MyList) MyIterable { 38 | return MyIterable.init(self.items); 39 | } 40 | 41 | pub fn deinit(self: *MyList, alloc: std.mem.Allocator) void { 42 | alloc.free(self.items); 43 | } 44 | }; 45 | 46 | const MyVariadic = struct { 47 | member: u8, 48 | 49 | const VariadicBool = Variadic(bool); 50 | 51 | pub fn constructor() MyVariadic { 52 | return .{ .member = 0 }; 53 | } 54 | 55 | pub fn _len(_: MyVariadic, variadic: ?VariadicBool) u64 { 56 | return @as(u64, variadic.?.slice.len); 57 | } 58 | 59 | pub fn _first(_: MyVariadic, _: []const u8, variadic: ?VariadicBool) bool { 60 | return variadic.?.slice[0]; 61 | } 62 | 63 | pub fn _last(_: MyVariadic, _: std.mem.Allocator, variadic: ?VariadicBool) bool { 64 | return variadic.?.slice[variadic.?.slice.len - 1]; 65 | } 66 | 67 | pub fn _empty(_: MyVariadic, _: ?VariadicBool) bool { 68 | return true; 69 | } 70 | 71 | const VariadicList = Variadic(MyList); 72 | 73 | pub fn _myListLen(_: MyVariadic, variadic: ?VariadicList) u8 { 74 | return @as(u8, @intCast(variadic.?.slice.len)); 75 | } 76 | 77 | pub fn _myListFirst(_: MyVariadic, variadic: ?VariadicList) ?u8 { 78 | if (variadic.?.slice.len == 0) return null; 79 | return variadic.?.slice[0]._first(); 80 | } 81 | 82 | pub fn deinit(_: *MyVariadic, _: std.mem.Allocator) void {} 83 | }; 84 | 85 | const MyErrorUnion = struct { 86 | pub fn constructor(is_err: bool) !MyErrorUnion { 87 | if (is_err) return error.MyError; 88 | return .{}; 89 | } 90 | 91 | pub fn get_withoutError(_: MyErrorUnion) !u8 { 92 | return 0; 93 | } 94 | 95 | pub fn get_withError(_: MyErrorUnion) !u8 { 96 | return error.MyError; 97 | } 98 | 99 | pub fn set_withoutError(_: *MyErrorUnion, _: bool) !void {} 100 | 101 | pub fn set_withError(_: *MyErrorUnion, _: bool) !void { 102 | return error.MyError; 103 | } 104 | 105 | pub fn _funcWithoutError(_: MyErrorUnion) !void {} 106 | 107 | pub fn _funcWithError(_: MyErrorUnion) !void { 108 | return error.MyError; 109 | } 110 | }; 111 | 112 | pub const MyException = struct { 113 | err: ErrorSet, 114 | 115 | const errorNames = [_][]const u8{ 116 | "MyCustomError", 117 | }; 118 | const errorMsgs = [_][]const u8{ 119 | "Some custom message.", 120 | }; 121 | fn errorStrings(comptime i: usize) []const u8 { 122 | return errorNames[0] ++ ": " ++ errorMsgs[i]; 123 | } 124 | 125 | // interface definition 126 | 127 | pub const ErrorSet = error{ 128 | MyCustomError, 129 | }; 130 | 131 | pub fn init(_: std.mem.Allocator, err: anyerror, _: []const u8) anyerror!MyException { 132 | return .{ .err = @as(ErrorSet, @errorCast(err)) }; 133 | } 134 | 135 | pub fn get_name(self: MyException) []const u8 { 136 | return switch (self.err) { 137 | ErrorSet.MyCustomError => errorNames[0], 138 | }; 139 | } 140 | 141 | pub fn get_message(self: MyException) []const u8 { 142 | return switch (self.err) { 143 | ErrorSet.MyCustomError => errorMsgs[0], 144 | }; 145 | } 146 | 147 | pub fn _toString(self: MyException) []const u8 { 148 | return switch (self.err) { 149 | ErrorSet.MyCustomError => errorStrings(0), 150 | }; 151 | } 152 | 153 | pub fn deinit(_: *MyException, _: std.mem.Allocator) void {} 154 | }; 155 | 156 | const MyTypeWithException = struct { 157 | pub const Exception = MyException; 158 | 159 | pub fn constructor() MyTypeWithException { 160 | return .{}; 161 | } 162 | 163 | pub fn _withoutError(_: MyTypeWithException) MyException.ErrorSet!void {} 164 | 165 | pub fn _withError(_: MyTypeWithException) MyException.ErrorSet!void { 166 | return MyException.ErrorSet.MyCustomError; 167 | } 168 | 169 | pub fn _superSetError(_: MyTypeWithException) !void { 170 | return MyException.ErrorSet.MyCustomError; 171 | } 172 | 173 | pub fn _outOfMemory(_: MyTypeWithException) !void { 174 | return error.OutOfMemory; 175 | } 176 | }; 177 | 178 | pub const Types = .{ 179 | MyIterable, 180 | MyList, 181 | MyVariadic, 182 | MyErrorUnion, 183 | MyException, 184 | MyTypeWithException, 185 | }; 186 | 187 | // exec tests 188 | pub fn exec( 189 | _: std.mem.Allocator, 190 | js_env: *public.Env, 191 | ) anyerror!void { 192 | 193 | // start JS env 194 | try js_env.start(); 195 | defer js_env.stop(); 196 | 197 | var iter = [_]tests.Case{ 198 | .{ .src = "let myList = new MyList(1, 2, 3);", .ex = "undefined" }, 199 | .{ .src = "myList.first();", .ex = "1" }, 200 | .{ .src = "let iter = myList[Symbol.iterator]();", .ex = "undefined" }, 201 | .{ .src = "iter.next().value;", .ex = "1" }, 202 | .{ .src = "iter.next().value;", .ex = "2" }, 203 | .{ .src = "iter.next().value;", .ex = "3" }, 204 | .{ .src = "iter.next().done;", .ex = "true" }, 205 | .{ .src = "let arr = Array.from(myList);", .ex = "undefined" }, 206 | .{ .src = "arr.length;", .ex = "3" }, 207 | .{ .src = "arr[0];", .ex = "1" }, 208 | }; 209 | try tests.checkCases(js_env, &iter); 210 | 211 | var variadic = [_]tests.Case{ 212 | .{ .src = "let myVariadic = new MyVariadic();", .ex = "undefined" }, 213 | .{ .src = "myVariadic.len(true, false, true)", .ex = "3" }, 214 | .{ .src = "myVariadic.first('a_str', true, false, true, false)", .ex = "true" }, 215 | .{ .src = "myVariadic.last(true, false)", .ex = "false" }, 216 | .{ .src = "myVariadic.empty()", .ex = "true" }, 217 | .{ .src = "myVariadic.myListLen(myList)", .ex = "1" }, 218 | .{ .src = "myVariadic.myListFirst(myList)", .ex = "1" }, 219 | }; 220 | try tests.checkCases(js_env, &variadic); 221 | 222 | var error_union = [_]tests.Case{ 223 | .{ .src = "var myErrorCstr = ''; try {new MyErrorUnion(true)} catch (error) {myErrorCstr = error}; myErrorCstr", .ex = "Error: MyError" }, 224 | .{ .src = "let myErrorUnion = new MyErrorUnion(false);", .ex = "undefined" }, 225 | .{ .src = "myErrorUnion.withoutError", .ex = "0" }, 226 | .{ .src = "var myErrorGetter = ''; try {myErrorUnion.withError} catch (error) {myErrorGetter = error}; myErrorGetter", .ex = "Error: MyError" }, 227 | .{ .src = "myErrorUnion.withoutError = true", .ex = "true" }, 228 | .{ .src = "var myErrorSetter = ''; try {myErrorUnion.withError = true} catch (error) {myErrorSetter = error}; myErrorSetter", .ex = "Error: MyError" }, 229 | .{ .src = "myErrorUnion.funcWithoutError()", .ex = "undefined" }, 230 | .{ .src = "var myErrorFunc = ''; try {myErrorUnion.funcWithError()} catch (error) {myErrorFunc = error}; myErrorFunc", .ex = "Error: MyError" }, 231 | }; 232 | try tests.checkCases(js_env, &error_union); 233 | 234 | var exception = [_]tests.Case{ 235 | .{ .src = "MyException.prototype.__proto__ === Error.prototype", .ex = "true" }, 236 | .{ .src = "let myTypeWithException = new MyTypeWithException();", .ex = "undefined" }, 237 | .{ .src = "myTypeWithException.withoutError()", .ex = "undefined" }, 238 | .{ .src = "var myCustomError = ''; try {myTypeWithException.withError()} catch (error) {myCustomError = error}", .ex = "MyCustomError: Some custom message." }, 239 | .{ .src = "myCustomError instanceof MyException", .ex = "true" }, 240 | .{ .src = "myCustomError instanceof Error", .ex = "true" }, 241 | .{ .src = "var mySuperError = ''; try {myTypeWithException.superSetError()} catch (error) {mySuperError = error}", .ex = "MyCustomError: Some custom message." }, 242 | .{ .src = "var oomError = ''; try {myTypeWithException.outOfMemory()} catch (error) {oomError = error}; oomError", .ex = "Error: OutOfMemory" }, 243 | }; 244 | try tests.checkCases(js_env, &exception); 245 | } 246 | -------------------------------------------------------------------------------- /src/run_tests.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const eng = @import("engine.zig"); 18 | const ref = @import("reflect.zig"); 19 | const gen = @import("generate.zig"); 20 | 21 | const bench = @import("bench.zig"); 22 | const pretty = @import("pretty.zig"); 23 | 24 | const public = @import("api.zig"); 25 | const VM = public.VM; 26 | 27 | // tests imports 28 | const proto = @import("tests/proto_test.zig"); 29 | const primitive_types = @import("tests/types_primitives_test.zig"); 30 | const native_types = @import("tests/types_native_test.zig"); 31 | const complex_types = @import("tests/types_complex_test.zig"); 32 | const multiple_types = @import("tests/types_multiple_test.zig"); 33 | const object_types = @import("tests/types_object.zig"); 34 | const callback = @import("tests/cbk_test.zig"); 35 | const global = @import("tests/global_test.zig"); 36 | const userctx = @import("tests/userctx_test.zig"); 37 | 38 | // test to do 39 | const do_proto = true; 40 | const do_prim = true; 41 | const do_nat = true; 42 | const do_complex = true; 43 | const do_multi = true; 44 | const do_obj = true; 45 | const do_cbk = true; 46 | const do_global = true; 47 | const do_userctx = true; 48 | 49 | // tests nb 50 | const tests_nb = blk: { 51 | var nb = 0; 52 | if (do_proto) nb += 1; 53 | if (do_prim) nb += 1; 54 | if (do_nat) nb += 1; 55 | if (do_complex) nb += 1; 56 | if (do_multi) nb += 1; 57 | if (do_obj) nb += 1; 58 | if (do_cbk) nb += 1; 59 | if (do_global) nb += 1; 60 | if (do_userctx) nb += 1; 61 | break :blk nb; 62 | }; 63 | 64 | // Types 65 | pub const Types = gen.reflect(gen.MergeTuple(.{ 66 | proto.Types, 67 | primitive_types.Types, 68 | native_types.Types, 69 | complex_types.Types, 70 | multiple_types.Types, 71 | object_types.Types, 72 | callback.Types, 73 | global.Types, 74 | userctx.Types, 75 | })); 76 | 77 | pub const UserContext = userctx.UserContext; 78 | 79 | pub fn main() !void { 80 | std.debug.print("\n", .{}); 81 | 82 | // reflect tests 83 | try comptime ref.tests(); 84 | std.debug.print("Reflect tests: OK\n", .{}); 85 | 86 | if (tests_nb == 0) { 87 | std.debug.print("\nWARNING: No end to end tests.\n", .{}); 88 | return; 89 | } 90 | 91 | // create JS vm 92 | const vm = VM.init(); 93 | defer vm.deinit(); 94 | 95 | // base and prototype tests 96 | var proto_alloc: bench.Allocator = undefined; 97 | if (do_proto) { 98 | proto_alloc = bench.allocator(std.testing.allocator); 99 | var proto_arena = std.heap.ArenaAllocator.init(proto_alloc.allocator()); 100 | defer proto_arena.deinit(); 101 | _ = try eng.loadEnv(&proto_arena, null, proto.exec); 102 | } 103 | 104 | // primitive types tests 105 | var prim_alloc: bench.Allocator = undefined; 106 | if (do_prim) { 107 | prim_alloc = bench.allocator(std.testing.allocator); 108 | var prim_arena = std.heap.ArenaAllocator.init(prim_alloc.allocator()); 109 | defer prim_arena.deinit(); 110 | _ = try eng.loadEnv(&prim_arena, null, primitive_types.exec); 111 | } 112 | 113 | // native types tests 114 | var nat_alloc: bench.Allocator = undefined; 115 | if (do_nat) { 116 | nat_alloc = bench.allocator(std.testing.allocator); 117 | var nat_arena = std.heap.ArenaAllocator.init(nat_alloc.allocator()); 118 | defer nat_arena.deinit(); 119 | _ = try eng.loadEnv(&nat_arena, null, native_types.exec); 120 | } 121 | 122 | // complex types tests 123 | var complex_alloc: bench.Allocator = undefined; 124 | if (do_complex) { 125 | complex_alloc = bench.allocator(std.testing.allocator); 126 | var complex_arena = std.heap.ArenaAllocator.init(complex_alloc.allocator()); 127 | defer complex_arena.deinit(); 128 | _ = try eng.loadEnv(&complex_arena, null, complex_types.exec); 129 | } 130 | 131 | // multiple types tests 132 | var multi_alloc: bench.Allocator = undefined; 133 | if (do_multi) { 134 | multi_alloc = bench.allocator(std.testing.allocator); 135 | var multi_arena = std.heap.ArenaAllocator.init(multi_alloc.allocator()); 136 | defer multi_arena.deinit(); 137 | _ = try eng.loadEnv(&multi_arena, null, multiple_types.exec); 138 | } 139 | 140 | // object types tests 141 | var obj_alloc: bench.Allocator = undefined; 142 | if (do_obj) { 143 | obj_alloc = bench.allocator(std.testing.allocator); 144 | var obj_arena = std.heap.ArenaAllocator.init(obj_alloc.allocator()); 145 | defer obj_arena.deinit(); 146 | _ = try eng.loadEnv(&obj_arena, null, object_types.exec); 147 | } 148 | 149 | // callback tests 150 | var cbk_alloc: bench.Allocator = undefined; 151 | if (do_cbk) { 152 | cbk_alloc = bench.allocator(std.testing.allocator); 153 | var cbk_arena = std.heap.ArenaAllocator.init(cbk_alloc.allocator()); 154 | defer cbk_arena.deinit(); 155 | _ = try eng.loadEnv(&cbk_arena, null, callback.exec); 156 | } 157 | 158 | // global tests 159 | var global_alloc: bench.Allocator = undefined; 160 | if (do_global) { 161 | global_alloc = bench.allocator(std.testing.allocator); 162 | var global_arena = std.heap.ArenaAllocator.init(global_alloc.allocator()); 163 | defer global_arena.deinit(); 164 | _ = try eng.loadEnv(&global_arena, null, global.exec); 165 | } 166 | 167 | // user context tests 168 | var userctx_alloc: bench.Allocator = undefined; 169 | if (do_userctx) { 170 | userctx_alloc = bench.allocator(std.testing.allocator); 171 | var userctx_arena = std.heap.ArenaAllocator.init(userctx_alloc.allocator()); 172 | defer userctx_arena.deinit(); 173 | _ = try eng.loadEnv(&userctx_arena, null, userctx.exec); 174 | } 175 | 176 | if (tests_nb == 0) { 177 | return; 178 | } 179 | 180 | // benchmark table 181 | const row_shape = .{ 182 | []const u8, 183 | u64, 184 | pretty.Measure, 185 | }; 186 | const header = .{ 187 | "FUNCTION", 188 | "ALLOCATIONS", 189 | "HEAP SIZE", 190 | }; 191 | const table = try pretty.GenerateTable(tests_nb, row_shape, pretty.TableConf{ .margin_left = " " }); 192 | const title = "Test jsengine ✅"; 193 | var t = table.init(title, header); 194 | 195 | if (do_proto) { 196 | const proto_alloc_stats = proto_alloc.stats(); 197 | const proto_alloc_size = pretty.Measure{ 198 | .unit = "b", 199 | .value = proto_alloc_stats.alloc_size, 200 | }; 201 | try t.addRow(.{ "Prototype", proto_alloc.alloc_nb, proto_alloc_size }); 202 | } 203 | 204 | if (do_prim) { 205 | const prim_alloc_stats = prim_alloc.stats(); 206 | const prim_alloc_size = pretty.Measure{ 207 | .unit = "b", 208 | .value = prim_alloc_stats.alloc_size, 209 | }; 210 | try t.addRow(.{ "Primitives", prim_alloc.alloc_nb, prim_alloc_size }); 211 | } 212 | if (do_nat) { 213 | const nat_alloc_stats = nat_alloc.stats(); 214 | const nat_alloc_size = pretty.Measure{ 215 | .unit = "b", 216 | .value = nat_alloc_stats.alloc_size, 217 | }; 218 | try t.addRow(.{ "Natives", nat_alloc.alloc_nb, nat_alloc_size }); 219 | } 220 | 221 | if (do_complex) { 222 | const complex_alloc_stats = complex_alloc.stats(); 223 | const complex_alloc_size = pretty.Measure{ 224 | .unit = "b", 225 | .value = complex_alloc_stats.alloc_size, 226 | }; 227 | try t.addRow(.{ "Complexes", complex_alloc.alloc_nb, complex_alloc_size }); 228 | } 229 | 230 | if (do_multi) { 231 | const multi_alloc_stats = multi_alloc.stats(); 232 | const multi_alloc_size = pretty.Measure{ 233 | .unit = "b", 234 | .value = multi_alloc_stats.alloc_size, 235 | }; 236 | try t.addRow(.{ "Multiples", multi_alloc.alloc_nb, multi_alloc_size }); 237 | } 238 | 239 | if (do_obj) { 240 | const obj_alloc_stats = obj_alloc.stats(); 241 | const obj_alloc_size = pretty.Measure{ 242 | .unit = "b", 243 | .value = obj_alloc_stats.alloc_size, 244 | }; 245 | try t.addRow(.{ "Objects", obj_alloc.alloc_nb, obj_alloc_size }); 246 | } 247 | 248 | if (do_cbk) { 249 | const cbk_alloc_stats = cbk_alloc.stats(); 250 | const cbk_alloc_size = pretty.Measure{ 251 | .unit = "b", 252 | .value = cbk_alloc_stats.alloc_size, 253 | }; 254 | try t.addRow(.{ "Callbacks", cbk_alloc.alloc_nb, cbk_alloc_size }); 255 | } 256 | 257 | if (do_global) { 258 | const global_alloc_stats = global_alloc.stats(); 259 | const global_alloc_size = pretty.Measure{ 260 | .unit = "b", 261 | .value = global_alloc_stats.alloc_size, 262 | }; 263 | try t.addRow(.{ "Global", global_alloc.alloc_nb, global_alloc_size }); 264 | } 265 | 266 | if (do_userctx) { 267 | const userctx_alloc_stats = global_alloc.stats(); 268 | const userctx_alloc_size = pretty.Measure{ 269 | .unit = "b", 270 | .value = userctx_alloc_stats.alloc_size, 271 | }; 272 | try t.addRow(.{ "User context", userctx_alloc.alloc_nb, userctx_alloc_size }); 273 | } 274 | 275 | const out = std.io.getStdErr().writer(); 276 | try t.render(out); 277 | } 278 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/shell.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | const builtin = @import("builtin"); 17 | 18 | const c = @import("linenoise.zig"); 19 | 20 | const public = @import("api.zig"); 21 | 22 | const IO = @import("loop.zig").IO; 23 | 24 | // Global variables 25 | var socket_fd: std.posix.socket_t = undefined; 26 | var conf: Config = undefined; 27 | 28 | // Config 29 | pub const Config = struct { 30 | app_name: []const u8, 31 | 32 | // if not provided will be /tmp/{app_name}.sock 33 | socket_path: ?[]const u8 = null, 34 | p: []const u8 = undefined, 35 | 36 | history: bool = true, 37 | history_max: ?u8 = null, 38 | history_path: ?[]const u8 = null, 39 | 40 | const socket_path_default = "/tmp/{s}.sock"; // with app_name 41 | const history_max_default = 50; // if history is true 42 | const history_path_default = "{s}/.cache/{s}/history.txt"; // with $HOME, app_anme 43 | 44 | fn populate(self: *Config, socket_path_buf: []u8, history_path_buf: []u8) !void { 45 | if (self.socket_path == null) { 46 | self.socket_path = try std.fmt.bufPrint( 47 | socket_path_buf, 48 | socket_path_default, 49 | .{self.app_name}, 50 | ); 51 | } 52 | if (self.history) { 53 | if (self.history_max == null) { 54 | self.history_max = history_max_default; 55 | } 56 | if (self.history_path == null) { 57 | const home = std.posix.getenv("HOME").?; 58 | // NOTE: we are using bufPrintZ as we need a null-terminated slice 59 | // to translate as c char 60 | self.history_path = try std.fmt.bufPrintZ( 61 | history_path_buf, 62 | history_path_default, 63 | .{ home, self.app_name }, 64 | ); 65 | const f = try openOrCreateFile(self.history_path.?); 66 | f.close(); 67 | } 68 | } 69 | } 70 | }; 71 | 72 | // I/O connection context 73 | const ConnContext = struct { 74 | socket: std.posix.socket_t, 75 | 76 | cmdContext: *CmdContext, 77 | }; 78 | 79 | // I/O connection callback 80 | fn connCallback( 81 | ctx: *ConnContext, 82 | completion: *IO.Completion, 83 | result: IO.AcceptError!std.posix.socket_t, 84 | ) void { 85 | ctx.cmdContext.socket = result catch |err| @panic(@errorName(err)); 86 | 87 | // launch receving messages asynchronously 88 | ctx.cmdContext.js_env.nat_ctx.loop.io.recv( 89 | *CmdContext, 90 | ctx.cmdContext, 91 | cmdCallback, 92 | completion, 93 | ctx.cmdContext.socket, 94 | ctx.cmdContext.buf, 95 | ); 96 | } 97 | 98 | // I/O input command context 99 | const CmdContext = struct { 100 | alloc: std.mem.Allocator, 101 | js_env: *public.Env, 102 | socket: std.posix.socket_t, 103 | buf: []u8, 104 | close: bool = false, 105 | 106 | try_catch: *public.TryCatch, 107 | }; 108 | 109 | // I/O input command callback 110 | fn cmdCallback( 111 | ctx: *CmdContext, 112 | completion: *IO.Completion, 113 | result: IO.RecvError!usize, 114 | ) void { 115 | const size = result catch |err| { 116 | ctx.close = true; 117 | std.debug.print("recv error: {s}\n", .{@errorName(err)}); 118 | return; 119 | }; 120 | 121 | const input = ctx.buf[0..size]; 122 | 123 | // close on exit command 124 | if (std.mem.eql(u8, input, "exit")) { 125 | ctx.close = true; 126 | return; 127 | } 128 | 129 | defer { 130 | 131 | // acknowledge to repl result has been printed 132 | _ = std.posix.write(ctx.socket, "ok") catch unreachable; 133 | 134 | // continue receving messages asynchronously 135 | ctx.js_env.nat_ctx.loop.io.recv( 136 | *CmdContext, 137 | ctx, 138 | cmdCallback, 139 | completion, 140 | ctx.socket, 141 | ctx.buf, 142 | ); 143 | } 144 | 145 | // JS execute 146 | const res = ctx.js_env.exec( 147 | input, 148 | "shell.js", 149 | ) catch { 150 | const except = ctx.try_catch.exception(ctx.alloc, ctx.js_env) catch unreachable; 151 | if (except) |msg| { 152 | defer ctx.alloc.free(msg); 153 | printStdout("\x1b[38;5;242mUncaught {s}\x1b[0m\n", .{msg}); 154 | } 155 | return; 156 | }; 157 | 158 | // JS print result 159 | const s = res.toString(ctx.alloc, ctx.js_env) catch unreachable; 160 | defer ctx.alloc.free(s); 161 | if (std.mem.eql(u8, s, "undefined")) { 162 | printStdout("<- \x1b[38;5;242m{s}\x1b[0m\n", .{s}); 163 | } else { 164 | printStdout("<- \x1b[33m{s}\x1b[0m\n", .{s}); 165 | } 166 | } 167 | 168 | fn exec( 169 | alloc: std.mem.Allocator, 170 | js_env: *public.Env, 171 | ) anyerror!void { 172 | 173 | // start JS env 174 | try js_env.start(); 175 | defer js_env.stop(); 176 | 177 | try shellExec(alloc, js_env); 178 | } 179 | 180 | pub fn shellExec( 181 | alloc: std.mem.Allocator, 182 | js_env: *public.Env, 183 | ) !void { 184 | 185 | // alias global as self 186 | try js_env.attachObject(try js_env.getGlobal(), "self", null); 187 | 188 | // add console object 189 | const console = public.Console{}; 190 | try js_env.addObject(console, "console"); 191 | 192 | // JS try cache 193 | var try_catch: public.TryCatch = undefined; 194 | try_catch.init(js_env); 195 | defer try_catch.deinit(); 196 | 197 | // create I/O contexts and callbacks 198 | // for accepting connections and receving messages 199 | var input: [1024]u8 = undefined; 200 | var cmd_ctx = CmdContext{ 201 | .alloc = alloc, 202 | .js_env = js_env, 203 | .socket = undefined, 204 | .buf = &input, 205 | .try_catch = &try_catch, 206 | }; 207 | var conn_ctx = ConnContext{ 208 | .socket = socket_fd, 209 | .cmdContext = &cmd_ctx, 210 | }; 211 | var completion: IO.Completion = undefined; 212 | 213 | // launch accepting connection asynchronously on internal server 214 | const loop = js_env.nat_ctx.loop; 215 | loop.io.accept( 216 | *ConnContext, 217 | &conn_ctx, 218 | connCallback, 219 | &completion, 220 | socket_fd, 221 | ); 222 | 223 | // infinite loop on I/O events, either: 224 | // - user input command from repl 225 | // - JS callbacks events from scripts 226 | while (true) { 227 | try loop.io.run_for_ns(10 * std.time.ns_per_ms); 228 | if (loop.cbk_error) { 229 | if (try try_catch.exception(alloc, js_env)) |msg| { 230 | defer alloc.free(msg); 231 | printStdout("\x1b[38;5;242mUncaught {s}\x1b[0m\n", .{msg}); 232 | } 233 | loop.cbk_error = false; 234 | } 235 | if (cmd_ctx.close) { 236 | break; 237 | } 238 | } 239 | } 240 | 241 | pub fn shell( 242 | arena_alloc: *std.heap.ArenaAllocator, 243 | comptime ctxExecFn: ?public.ContextExecFn, 244 | comptime config: Config, 245 | ) !void { 246 | 247 | // set config 248 | var cf = config; 249 | var socket_path_buf: [100]u8 = undefined; 250 | var history_path_buf: [100]u8 = undefined; 251 | try cf.populate(&socket_path_buf, &history_path_buf); 252 | conf = cf; 253 | 254 | // remove socket file of internal server 255 | // reuse_address (SO_REUSEADDR flag) does not seems to work on unix socket 256 | // see: https://gavv.net/articles/unix-socket-reuse/ 257 | // TODO: use a lock file instead 258 | std.posix.unlink(conf.socket_path.?) catch |err| { 259 | if (err != error.FileNotFound) { 260 | return err; 261 | } 262 | }; 263 | 264 | // create internal server listening on a unix socket 265 | const addr = try std.net.Address.initUnix(conf.socket_path.?); 266 | var server = try addr.listen(.{ .reuse_address = true, .reuse_port = true }); 267 | defer server.deinit(); 268 | 269 | socket_fd = server.stream.handle; 270 | 271 | // launch repl in a separate detached thread 272 | var repl_thread = try std.Thread.spawn(.{}, repl, .{}); 273 | repl_thread.detach(); 274 | 275 | // load JS environement 276 | comptime var do_fn: public.ContextExecFn = exec; 277 | if (ctxExecFn) |func| { 278 | do_fn = func; 279 | } 280 | try public.loadEnv(arena_alloc, null, do_fn); 281 | } 282 | 283 | fn repl() !void { 284 | 285 | // greetings 286 | printStdout( 287 | \\ 288 | \\zig-js-runtime - Javascript Shell 289 | \\exit with Ctrl+D or "exit" 290 | \\ 291 | \\ 292 | , .{}); 293 | 294 | // create a socket client connected to the internal server 295 | const socket = try std.net.connectUnixSocket(conf.socket_path.?); 296 | 297 | var ack: [2]u8 = undefined; 298 | 299 | // history load 300 | if (conf.history) { 301 | if (c.linenoiseHistoryLoad(conf.history_path.?.ptr) != 0) { 302 | return error.LinenoiseHistoryLoad; 303 | } 304 | if (c.linenoiseHistorySetMaxLen(conf.history_max.?) != 1) { 305 | return error.LinenoiseHistorySetMaxLen; 306 | } 307 | } 308 | 309 | // infinite loop 310 | while (true) { 311 | 312 | // linenoise lib 313 | const line = c.linenoise("> "); 314 | 315 | if (line != null) { 316 | const input = std.mem.sliceTo(line.?, 0); 317 | 318 | // continue if input empty 319 | if (input.len == 0) { 320 | // free the line 321 | c.linenoiseFree(line); 322 | continue; 323 | } 324 | 325 | // stop loop on exit input 326 | if (std.mem.eql(u8, input, "exit") or 327 | std.mem.eql(u8, input, "exit;")) 328 | { 329 | // free the line 330 | c.linenoiseFree(line); 331 | break; 332 | } 333 | 334 | // add line in history 335 | if (conf.history) { 336 | if (c.linenoiseHistoryAdd(line) == 1) { 337 | // save only if line has been added 338 | // (ie. not on duplicated line) 339 | if (c.linenoiseHistorySave(conf.history_path.?.ptr) != 0) { 340 | return error.LinenoiseHistorySave; 341 | } 342 | } 343 | } 344 | 345 | // send the input command to the internal server 346 | _ = try socket.write(input); 347 | 348 | // free the line 349 | c.linenoiseFree(line); 350 | 351 | // aknowledge response from the internal server 352 | // before giving back the input to the user 353 | _ = socket.read(&ack) catch |err| { 354 | std.debug.print("ack error: {s}\n", .{@errorName(err)}); 355 | // stop loop on ack read error 356 | break; 357 | }; 358 | } else { 359 | 360 | // stop loop on Ctrl+D 361 | break; 362 | } 363 | } 364 | 365 | // send the exit command to the internal server 366 | _ = try socket.write("exit"); 367 | printStdout("Goodbye...\n", .{}); 368 | } 369 | 370 | fn printStdout(comptime format: []const u8, args: anytype) void { 371 | const stdout = std.io.getStdOut().writer(); 372 | stdout.print(format, args) catch unreachable; 373 | } 374 | 375 | // Utils 376 | // ----- 377 | 378 | fn openOrCreateFile(path: []const u8) !std.fs.File { 379 | var file: std.fs.File = undefined; 380 | if (std.fs.openFileAbsolute(path, .{})) |f| { 381 | file = f; 382 | } else |err| switch (err) { 383 | error.FileNotFound => { 384 | 385 | // file does not exists, let's check the dir 386 | const dir_path = std.fs.path.dirname(path); 387 | if (dir_path != null) { 388 | var dir: std.fs.Dir = undefined; 389 | if (std.fs.openDirAbsolute(dir_path.?, .{})) |d| { 390 | dir = d; 391 | dir.close(); 392 | } else |dir_err| switch (dir_err) { 393 | // create dir if not exists 394 | error.FileNotFound => { 395 | try std.fs.makeDirAbsolute(dir_path.?); 396 | }, 397 | else => return dir_err, 398 | } 399 | } 400 | 401 | // create the file 402 | file = try std.fs.createFileAbsolute(path, .{ .read = true }); 403 | }, 404 | else => return err, 405 | } 406 | return file; 407 | } 408 | -------------------------------------------------------------------------------- /src/engines/v8/callback.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const v8 = @import("v8"); // TODO: remove 18 | 19 | const internal = @import("../../internal_api.zig"); 20 | const refl = internal.refl; 21 | const gen = internal.gen; 22 | const NativeContext = internal.NativeContext; 23 | 24 | const JSObjectID = @import("v8.zig").JSObjectID; 25 | const setNativeType = @import("generate.zig").setNativeType; 26 | const CallbackInfo = @import("generate.zig").CallbackInfo; 27 | const getV8Object = @import("generate.zig").getV8Object; 28 | 29 | const valueToUtf8 = @import("types_primitives.zig").valueToUtf8; 30 | 31 | // TODO: Make this JS engine agnostic 32 | // by providing a common interface 33 | 34 | pub const Arg = struct { 35 | // TODO: it's required to have a non-empty struct 36 | // otherwise LLVM emits a warning 37 | // "stack frame size (x) exceeds limit (y)" 38 | // foo: bool = false, 39 | }; 40 | 41 | pub const Result = struct { 42 | alloc: std.mem.Allocator, 43 | success: bool = false, 44 | result: ?[]const u8 = null, 45 | stack: ?[]const u8 = null, 46 | 47 | pub fn init(alloc: std.mem.Allocator) Result { 48 | return .{ .alloc = alloc }; 49 | } 50 | 51 | pub fn deinit(self: Result) void { 52 | if (self.result) |res| self.alloc.free(res); 53 | if (self.stack) |stack| self.alloc.free(stack); 54 | } 55 | 56 | pub fn setError( 57 | self: *Result, 58 | isolate: v8.Isolate, 59 | js_ctx: v8.Context, 60 | try_catch: v8.TryCatch, 61 | ) !void { 62 | self.success = false; 63 | 64 | // exception 65 | if (try_catch.getException()) |except| { 66 | self.result = try valueToUtf8(self.alloc, except, isolate, js_ctx); 67 | } 68 | 69 | // stack 70 | if (try_catch.getStackTrace(js_ctx)) |stack| { 71 | self.stack = try valueToUtf8(self.alloc, stack, isolate, js_ctx); 72 | } 73 | } 74 | }; 75 | 76 | pub const FuncSync = struct { 77 | js_func: v8.Function, 78 | js_args: []v8.Value, 79 | 80 | nat_ctx: *NativeContext, 81 | isolate: v8.Isolate, 82 | 83 | thisArg: ?v8.Object = null, 84 | 85 | pub fn init( 86 | alloc: std.mem.Allocator, 87 | nat_ctx: *NativeContext, 88 | comptime func: refl.Func, 89 | raw_value: ?*const v8.C_Value, 90 | info: CallbackInfo, 91 | isolate: v8.Isolate, 92 | ) !FuncSync { 93 | 94 | // retrieve callback arguments indexes 95 | // TODO: Should we do that at reflection? 96 | comptime var js_args_indexes: [func.args_callback_nb]usize = undefined; 97 | comptime var x: usize = 0; 98 | inline for (func.args, 0..) |arg, i| { 99 | if (arg.T == Arg) { 100 | js_args_indexes[x] = i; 101 | x += 1; 102 | } 103 | } 104 | 105 | // retrieve callback arguments 106 | // var js_args: [func.args_callback_nb]v8.Value = undefined; 107 | var js_args = try alloc.alloc(v8.Value, func.args_callback_nb); 108 | for (js_args_indexes, 0..) |index, i| { 109 | js_args[i] = info.getArg(raw_value, index, func.index_offset) orelse unreachable; 110 | } 111 | 112 | var idx = func.callback_index.?; 113 | if (idx > 0) idx = idx - 1; // -1 because of self 114 | 115 | // retrieve callback function 116 | const js_func_val = info.getArg( 117 | raw_value, 118 | idx, 119 | func.index_offset, 120 | ) orelse unreachable; 121 | 122 | if (!js_func_val.isFunction()) { 123 | return error.JSWrongType; 124 | } 125 | const js_func = js_func_val.castTo(v8.Function); 126 | 127 | return FuncSync{ 128 | .js_func = js_func, 129 | .js_args = js_args, 130 | .nat_ctx = nat_ctx, 131 | .isolate = isolate, 132 | }; 133 | } 134 | 135 | pub fn setThisArg(self: *FuncSync, nat_obj_ptr: anytype) !void { 136 | self.thisArg = try getV8Object( 137 | self.nat_ctx, 138 | nat_obj_ptr, 139 | ) orelse return error.V8ObjectNotFound; 140 | } 141 | 142 | // call the function with a try catch to catch errors an report in res. 143 | pub fn trycall(self: FuncSync, alloc: std.mem.Allocator, res: *Result) anyerror!void { 144 | // JS try cache 145 | var try_catch: v8.TryCatch = undefined; 146 | try_catch.init(self.isolate); 147 | defer try_catch.deinit(); 148 | 149 | self.call(alloc) catch |e| { 150 | res.success = false; 151 | if (try_catch.hasCaught()) { 152 | // retrieve context 153 | // NOTE: match the Func.call implementation 154 | const ctx = self.isolate.getCurrentContext(); 155 | try res.setError(self.isolate, ctx, try_catch); 156 | } 157 | 158 | return e; 159 | }; 160 | 161 | res.success = true; 162 | } 163 | 164 | pub fn call(self: FuncSync, alloc: std.mem.Allocator) anyerror!void { 165 | 166 | // retrieve context 167 | // NOTE: match the Func.call implementation 168 | const ctx = self.isolate.getCurrentContext(); 169 | 170 | // Callbacks are typically called with a this value of undefined. 171 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#callbacks 172 | // TODO use undefined this instead of global. 173 | const this = self.thisArg orelse ctx.getGlobal(); 174 | 175 | // execute function 176 | _ = self.js_func.call(ctx, this, self.js_args); 177 | 178 | // free heap 179 | alloc.free(self.js_args); 180 | } 181 | }; 182 | 183 | const PersistentFunction = v8.Persistent(v8.Function); 184 | const PersistentValue = v8.Persistent(v8.Value); 185 | 186 | pub const Func = struct { 187 | _id: JSObjectID, 188 | 189 | // NOTE: we use persistent handles here 190 | // to ensure the references are not garbage collected 191 | // at the end of the JS calling function execution. 192 | js_func_pers: PersistentFunction, 193 | 194 | // TODO: as we know this information at comptime 195 | // we could change this to a generics function with JS args len as param 196 | // avoiding the need to allocate/free js_args_pers 197 | js_args_pers: []PersistentValue, 198 | 199 | nat_ctx: *NativeContext, 200 | isolate: v8.Isolate, 201 | 202 | thisArg: ?v8.Object = null, 203 | 204 | pub fn init( 205 | alloc: std.mem.Allocator, 206 | nat_ctx: *NativeContext, 207 | comptime func: refl.Func, 208 | raw_value: ?*const v8.C_Value, 209 | info: CallbackInfo, 210 | isolate: v8.Isolate, 211 | ) !Func { 212 | var idx = func.callback_index.?; 213 | if (idx > 0) idx = idx - 1; // -1 because of self 214 | 215 | // retrieve callback function 216 | const js_func_val = info.getArg( 217 | raw_value, 218 | idx, 219 | func.index_offset, 220 | ) orelse unreachable; 221 | if (!js_func_val.isFunction()) { 222 | return error.JSWrongType; 223 | } 224 | const js_func = js_func_val.castTo(v8.Function); 225 | const js_func_pers = PersistentFunction.init(isolate, js_func); 226 | 227 | // NOTE: we need to store the JS callback arguments on the heap 228 | // as the call method will be executed in another stack frame, 229 | // once the asynchronous operation will be fetched back from the kernel. 230 | var js_args_pers = try alloc.alloc(PersistentValue, func.args_callback_nb); 231 | 232 | // retrieve callback arguments indexes 233 | if (comptime func.args_callback_nb > 0) { 234 | 235 | // TODO: Should we do that at reflection? 236 | comptime var js_args_indexes: [func.args_callback_nb]usize = undefined; 237 | comptime { 238 | var x: usize = 0; 239 | for (func.args, 0..) |arg, i| { 240 | if (arg.T == Arg) { 241 | js_args_indexes[x] = i; 242 | x += 1; 243 | } 244 | } 245 | } 246 | 247 | // retrieve callback arguments 248 | for (js_args_indexes, 0..) |index, i| { 249 | const js_arg = info.getArg(raw_value, index, func.index_offset) orelse unreachable; 250 | const js_arg_pers = PersistentValue.init(isolate, js_arg); 251 | js_args_pers[i] = js_arg_pers; 252 | } 253 | } 254 | 255 | return Func{ 256 | ._id = JSObjectID.set(js_func_val.castTo(v8.Object)), 257 | .js_func_pers = js_func_pers, 258 | .js_args_pers = js_args_pers, 259 | .nat_ctx = nat_ctx, 260 | .isolate = isolate, 261 | }; 262 | } 263 | 264 | pub fn setThisArg(self: *Func, nat_obj_ptr: anytype) !void { 265 | self.thisArg = try getV8Object( 266 | self.nat_ctx, 267 | nat_obj_ptr, 268 | ) orelse return error.V8ObjectNotFound; 269 | } 270 | 271 | pub fn deinit(self: *Func, alloc: std.mem.Allocator) void { 272 | 273 | // cleanup persistent references in v8 274 | self.js_func_pers.deinit(); 275 | for (self.js_args_pers) |*arg| { 276 | arg.deinit(); 277 | } 278 | 279 | // free heap 280 | alloc.free(self.js_args_pers); 281 | } 282 | 283 | pub fn id(self: Func) usize { 284 | return self._id.get(); 285 | } 286 | 287 | // call the function with a try catch to catch errors an report in res. 288 | pub fn trycall(self: Func, nat_args: anytype, res: *Result) anyerror!void { 289 | // JS try cache 290 | var try_catch: v8.TryCatch = undefined; 291 | try_catch.init(self.isolate); 292 | defer try_catch.deinit(); 293 | 294 | self.call(nat_args) catch |e| { 295 | res.success = false; 296 | if (try_catch.hasCaught()) { 297 | // retrieve context 298 | // NOTE: match the Func.call implementation 299 | const ctx = self.isolate.getCurrentContext(); 300 | try res.setError(self.isolate, ctx, try_catch); 301 | } 302 | 303 | return e; 304 | }; 305 | 306 | res.success = true; 307 | } 308 | 309 | pub fn call(self: Func, nat_args: anytype) anyerror!void { 310 | // ensure Native args and JS args are not both provided 311 | const info = @typeInfo(@TypeOf(nat_args)); 312 | if (comptime info != .null) { 313 | // TODO: could be a compile error if we use generics for JS args 314 | std.debug.assert(self.js_args_pers.len == 0); 315 | } 316 | 317 | // retrieve context 318 | // TODO: should we instead store the original context in the Func object? 319 | // in this case we need to have a permanent handle (Global ?) on it. 320 | const js_ctx = self.isolate.getCurrentContext(); 321 | 322 | // retrieve JS function from persistent handle 323 | const js_func = self.js_func_pers.castToFunction(); 324 | 325 | // retrieve arguments 326 | var args = try self.nat_ctx.alloc.alloc(v8.Value, self.js_args_pers.len); 327 | defer self.nat_ctx.alloc.free(args); 328 | if (comptime info == .@"struct") { 329 | 330 | // - Native arguments provided on function call 331 | std.debug.assert(info.@"struct".is_tuple); 332 | args = try self.nat_ctx.alloc.alloc(v8.Value, info.@"struct".fields.len); 333 | comptime var i = 0; 334 | inline while (i < info.@"struct".fields.len) { 335 | comptime var ret: refl.Type = undefined; 336 | comptime { 337 | ret = try refl.Type.reflect(info.@"struct".fields[i].type, null); 338 | try ret.lookup(gen.Types); 339 | } 340 | args[i] = try setNativeType( 341 | self.nat_ctx.alloc, 342 | self.nat_ctx, 343 | ret, 344 | @field(nat_args, try refl.itoa(i)), 345 | js_ctx, 346 | self.isolate, 347 | ); 348 | i += 1; 349 | } 350 | } else if (self.js_args_pers.len > 0) { 351 | 352 | // - JS arguments set previously 353 | for (self.js_args_pers, 0..) |arg, i| { 354 | args[i] = arg.toValue(); 355 | } 356 | } 357 | // else -> no arguments 358 | 359 | // Callbacks are typically called with a this value of undefined. 360 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#callbacks 361 | // TODO use undefined this instead of global. 362 | const this = self.thisArg orelse js_ctx.getGlobal(); 363 | 364 | // execute function 365 | const result = js_func.call(js_ctx, this, args); 366 | if (result == null) { 367 | return error.JSExecCallback; 368 | } 369 | } 370 | }; 371 | -------------------------------------------------------------------------------- /src/pretty.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | fn checkArgs(args: anytype) void { 18 | comptime { 19 | const info = @typeInfo(@TypeOf(args)); 20 | if (info != .@"struct" or !info.@"struct".is_tuple) { 21 | @compileError("should be a tuple"); 22 | } 23 | } 24 | } 25 | 26 | pub const Measure = struct { 27 | unit: []const u8, 28 | value: u64, 29 | }; 30 | 31 | pub const TableConf = struct { 32 | max_row_length: usize = 50, 33 | margin_left: ?[]const u8 = " ", 34 | row_delimiter: []const u8 = "|", 35 | line_delimiter: []const u8 = "-", 36 | line_edge_delimiter: []const u8 = "+", 37 | }; 38 | 39 | pub fn GenerateTable( 40 | comptime rows_nb: usize, 41 | comptime row_shape: anytype, 42 | comptime table_conf: ?TableConf, 43 | ) !type { 44 | checkArgs(row_shape); 45 | 46 | const columns_nb = row_shape.len; 47 | if (columns_nb > 9) { 48 | @compileError("columns should have less then 10 rows"); 49 | } 50 | 51 | var fields: [columns_nb]std.builtin.Type.StructField = undefined; 52 | inline for (row_shape, 0..) |T, i| { 53 | if (@TypeOf(T) != type) { 54 | @compileError("columns elements should be of type 'type'"); 55 | } 56 | var buf: [1]u8 = undefined; 57 | const name = try std.fmt.bufPrint(buf[0..], "{d}", .{i}); 58 | fields[i] = std.builtin.Type.StructField{ 59 | // StructField.name expect a null terminated string. 60 | // concatenate the `[]const u8` string with an empty string 61 | // literal (`name ++ ""`) to explicitly coerce it to `[:0]const 62 | // u8`. 63 | .name = name ++ "", 64 | .type = T, 65 | .default_value_ptr = null, 66 | .is_comptime = false, 67 | .alignment = @alignOf(T), 68 | }; 69 | } 70 | const decls: [0]std.builtin.Type.Declaration = undefined; 71 | const shape_info = std.builtin.Type.Struct{ 72 | .layout = .auto, 73 | .fields = &fields, 74 | .decls = &decls, 75 | .is_tuple = true, 76 | }; 77 | const shape = @Type(std.builtin.Type{ .@"struct" = shape_info }); 78 | 79 | var table_c: TableConf = undefined; 80 | if (table_conf != null) { 81 | table_c = table_conf.?; 82 | } else { 83 | table_c = TableConf{}; 84 | } 85 | const conf = table_c; 86 | 87 | return struct { 88 | title: ?[]const u8, 89 | head: [columns_nb][]const u8 = undefined, 90 | rows: [rows_nb]shape = undefined, 91 | 92 | last_row: usize = 0, 93 | 94 | const Self = @This(); 95 | 96 | pub fn init(title: ?[]const u8, header: anytype) Self { 97 | checkArgs(header); 98 | if (header.len != columns_nb) { 99 | @compileError("header elements should be equal to table columns_nb"); 100 | } 101 | const self = Self{ .title = title, .head = header }; 102 | return self; 103 | } 104 | 105 | pub fn addRow(self: *Self, row: shape) !void { 106 | checkArgs(row); 107 | if (row.len != columns_nb) { 108 | @compileError("row elements should be equal to table columns_nb"); 109 | } 110 | if (self.last_row >= rows_nb) { 111 | return error.TableWrongRowNb; 112 | } 113 | self.rows[self.last_row] = row; 114 | self.last_row += 1; 115 | } 116 | 117 | // render the table on a writer 118 | pub fn render(self: Self, writer: anytype) !void { 119 | if (self.last_row < rows_nb) { 120 | return error.TableNotComplete; 121 | } 122 | 123 | // calc max size for each column 124 | // looking for size value in header and each row 125 | var max_sizes: [columns_nb]usize = undefined; 126 | for (self.head, 0..) |header, i| { 127 | max_sizes[i] = try utf8Size(header); 128 | } 129 | for (self.rows, 0..) |_, row_i| { 130 | comptime var col_i: usize = 0; 131 | inline while (col_i < columns_nb) { 132 | const arg = self.rows[row_i][col_i]; 133 | var buf: [conf.max_row_length]u8 = undefined; 134 | // stage1: we should catch err (or use try) 135 | // but compiler give us: 136 | // 'control flow attempts to use compile-time variable at runtime' 137 | const str = argStr(buf[0..], arg) catch unreachable; 138 | const size = utf8Size(str) catch unreachable; 139 | if (size > max_sizes[col_i]) { 140 | max_sizes[col_i] = size; 141 | } 142 | col_i += 1; 143 | } 144 | } 145 | 146 | // total size for a row 147 | var total: usize = 0; 148 | // we had 3 chars for each column: value 149 | const extra_per_row = 3; 150 | for (max_sizes) |size| { 151 | total += size + extra_per_row; 152 | } 153 | total += 1; // we had 1 char for the begining of the line: 154 | 155 | // buffered writer 156 | var buf = std.io.bufferedWriter(writer); 157 | const w = buf.writer(); 158 | 159 | // title 160 | try w.print("\n", .{}); 161 | if (self.title != null) { 162 | try drawLine(w, conf, total); 163 | if (conf.margin_left != null) { 164 | try w.print(conf.margin_left.?, .{}); 165 | } 166 | const title_len = try utf8Size(self.title.?); 167 | try w.print("{s} {s}", .{ conf.row_delimiter, self.title.? }); 168 | const title_extra = 3; // value 169 | const diff = total - title_extra - title_len; 170 | if (diff > 0) { 171 | try drawRepeat(w, " ", diff); 172 | } 173 | try w.print("{s}\n", .{conf.row_delimiter}); 174 | } 175 | 176 | // head 177 | try drawLine(w, conf, total); 178 | try drawRow(w, max_sizes, self.head, total); 179 | 180 | // rows 181 | for (self.rows) |row| { 182 | try drawRow(w, max_sizes, row, total); 183 | } 184 | 185 | try w.print("\n", .{}); 186 | 187 | try buf.flush(); 188 | } 189 | 190 | fn drawRow( 191 | w: anytype, 192 | max_sizes: [columns_nb]usize, 193 | row: anytype, 194 | total: usize, 195 | ) !void { 196 | 197 | // start of the row 198 | if (conf.margin_left != null) { 199 | try w.print(conf.margin_left.?, .{}); 200 | } 201 | 202 | comptime var i: usize = 0; 203 | inline while (i < row.len) { 204 | 205 | // left delimiter 206 | if (i == 0) { 207 | try w.print(conf.row_delimiter, .{}); 208 | } 209 | 210 | // string value 211 | const value = row[i]; 212 | var buf_str: [conf.max_row_length]u8 = undefined; 213 | // stage1: we should catch err (or use try) 214 | // but compiler give us an infinite loop 215 | const str = argStr(buf_str[0..], value) catch unreachable; 216 | 217 | // align string and print 218 | const str_len = try utf8Size(str); 219 | const diff = max_sizes[i] - str_len; 220 | switch (@TypeOf(value)) { 221 | // align left strings 222 | []u8, []const u8 => blk: { 223 | try w.print(" {s}", .{str}); 224 | if (diff > 0) { 225 | try drawRepeat(w, " ", diff); 226 | } 227 | break :blk; 228 | }, 229 | // otherwhise align right 230 | else => blk: { 231 | if (diff > 0) { 232 | try drawRepeat(w, " ", diff); 233 | } 234 | try w.print("{s} ", .{str}); 235 | break :blk; 236 | }, 237 | } 238 | 239 | // right delimiter 240 | try w.print(" {s}", .{conf.row_delimiter}); 241 | 242 | i += 1; 243 | } 244 | 245 | // end of the row 246 | try w.print("\n", .{}); 247 | try drawLine(w, conf, total); 248 | } 249 | }; 250 | } 251 | 252 | // Utils 253 | // ----- 254 | 255 | fn argStr(buf: []u8, arg: anytype) ![]const u8 { 256 | const T = @TypeOf(arg); 257 | return switch (T) { 258 | 259 | // slice of bytes, eg. string 260 | []u8, [:0]u8, []const u8, [:0]const u8 => arg, 261 | 262 | // int unsigned 263 | u8, u16, u32, u64, usize => try std.fmt.bufPrint(buf[0..], "{d}", .{arg}), 264 | 265 | // int signed 266 | i8, i16, i32, i64, isize, comptime_int => try std.fmt.bufPrint(buf[0..], "{d}", .{arg}), 267 | 268 | // float 269 | f16, f32, f64, comptime_float => try std.fmt.bufPrint(buf[0..], "{d:.2}", .{arg}), 270 | 271 | // bool 272 | bool => if (arg) "true" else "false", 273 | 274 | // measure 275 | Measure => try std.fmt.bufPrint(buf[0..], "{d}{s}", .{ arg.value, arg.unit }), 276 | 277 | else => try std.fmt.bufPrint(buf[0..], "{any}", .{arg}), 278 | }; 279 | } 280 | 281 | test "arg str" { 282 | const max = 50; 283 | 284 | // string 285 | const str: []const u8 = "ok"; 286 | var buf1: [max]u8 = undefined; 287 | try std.testing.expectEqualStrings(try argStr(&buf1, str), "ok"); 288 | 289 | // comptime int 290 | var buf2: [max]u8 = undefined; 291 | try std.testing.expectEqualStrings(try argStr(&buf2, 8), "8"); 292 | var buf3: [max]u8 = undefined; 293 | try std.testing.expectEqualStrings(try argStr(&buf3, -8), "-8"); 294 | 295 | // int unsigned 296 | const int_unsigned: u8 = 8; 297 | var buf4: [max]u8 = undefined; 298 | try std.testing.expectEqualStrings(try argStr(&buf4, int_unsigned), "8"); 299 | 300 | // int signed 301 | const int_signed: i32 = -8; 302 | var buf5: [max]u8 = undefined; 303 | try std.testing.expectEqualStrings(try argStr(&buf5, int_signed), "-8"); 304 | 305 | // float 306 | const f: f16 = 3.22; 307 | var buf6: [max]u8 = undefined; 308 | try std.testing.expectEqualStrings(try argStr(&buf6, f), "3.22"); 309 | 310 | // bool 311 | const b = true; 312 | var buf7: [max]u8 = undefined; 313 | try std.testing.expectEqualStrings(try argStr(&buf7, b), "true"); 314 | 315 | // measure 316 | const m = Measure{ .value = 972, .unit = "us" }; 317 | var buf_m: [max]u8 = undefined; 318 | try std.testing.expectEqualStrings("972us", try argStr(&buf_m, m)); 319 | 320 | // error, value too long 321 | var buf_e: [1]u8 = undefined; 322 | try std.testing.expectError(error.NoSpaceLeft, argStr(&buf_e, int_signed)); 323 | } 324 | 325 | fn utf8Size(s: []const u8) !usize { 326 | const points = try std.unicode.utf8CountCodepoints(s); 327 | if (points == s.len) { 328 | return s.len; 329 | } 330 | const view = try std.unicode.Utf8View.init(s); 331 | var iter = view.iterator(); 332 | var size: usize = 0; 333 | while (iter.nextCodepointSlice()) |next| { 334 | if (next.len == 1) { 335 | // ascii 336 | size += 1; 337 | } else { 338 | // non-ascii 339 | // TODO: list cases, this does not seems very solid 340 | if (next.len < 3) { 341 | size += 1; 342 | } else { 343 | size += 2; 344 | } 345 | } 346 | } 347 | return size; 348 | } 349 | 350 | test "utf8 size" { 351 | // stage 1: we can't but try inside equality 352 | 353 | const res1 = try utf8Size("test é"); 354 | try std.testing.expect(res1 == 6); // latin 355 | 356 | const res2 = try utf8Size("test 鿍"); 357 | try std.testing.expect(res2 == 7); // chinese 358 | 359 | const res3 = try utf8Size("test ✅"); 360 | try std.testing.expect(res3 == 7); // small emoji 361 | 362 | const res4 = try utf8Size("test 🚀"); 363 | try std.testing.expect(res4 == 7); // big emoji 364 | 365 | const res5 = try utf8Size("🚀 test ✅"); 366 | try std.testing.expect(res5 == 10); // mulitple utf-8 points 367 | } 368 | 369 | fn drawLine(w: anytype, comptime conf: TableConf, total: usize) !void { 370 | if (conf.margin_left != null) { 371 | try w.print(conf.margin_left.?, .{}); 372 | } 373 | try w.print(conf.line_edge_delimiter, .{}); 374 | try drawRepeat(w, conf.line_delimiter, total - 2); 375 | try w.print(conf.line_edge_delimiter, .{}); 376 | try w.print("\n", .{}); 377 | } 378 | 379 | fn drawRepeat(w: anytype, comptime fmt: []const u8, nb: usize) !void { 380 | var i: usize = 0; 381 | while (i < nb) { 382 | try w.print(fmt, .{}); 383 | i += 1; 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /src/tests/types_native_test.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const public = @import("../api.zig"); 18 | 19 | const tests = public.test_utils; 20 | 21 | // Native types with separate APIs 22 | // ------------------------------- 23 | 24 | const Brand = struct { 25 | name: []const u8, 26 | 27 | pub fn constructor(alloc: std.mem.Allocator, name: []const u8) Brand { 28 | const name_alloc = alloc.alloc(u8, name.len) catch unreachable; 29 | @memcpy(name_alloc, name); 30 | return .{ .name = name_alloc }; 31 | } 32 | 33 | pub fn get_name(self: Brand) []const u8 { 34 | return self.name; 35 | } 36 | 37 | pub fn set_name(self: *Brand, alloc: std.mem.Allocator, name: []u8) void { 38 | const name_alloc = alloc.alloc(u8, name.len) catch unreachable; 39 | @memcpy(name_alloc, name); 40 | self.name = name_alloc; 41 | } 42 | 43 | pub fn deinit(self: *Brand, alloc: std.mem.Allocator) void { 44 | alloc.free(self.name); 45 | } 46 | }; 47 | 48 | const Car = struct { 49 | brand: Brand, 50 | brand_ptr: *Brand, 51 | 52 | pub fn constructor(alloc: std.mem.Allocator) Car { 53 | const brand_name: []const u8 = "Renault"; 54 | const brand = Brand{ .name = brand_name }; 55 | const brand_ptr = alloc.create(Brand) catch unreachable; 56 | brand_ptr.* = Brand{ .name = brand_name }; 57 | return .{ .brand = brand, .brand_ptr = brand_ptr }; 58 | } 59 | 60 | // As argument 61 | // ----------- 62 | 63 | // accept in setter 64 | pub fn set_brand(self: *Car, brand: Brand) void { 65 | self.brand = brand; 66 | } 67 | 68 | // accept * in setter 69 | pub fn set_brandPtr(self: *Car, brand_ptr: *Brand) void { 70 | self.brand_ptr = brand_ptr; 71 | } 72 | 73 | // accept in method 74 | pub fn _changeBrand(self: *Car, brand: Brand) void { 75 | self.brand = brand; 76 | } 77 | 78 | // accept * in method 79 | pub fn _changeBrandPtr(self: *Car, brand_ptr: *Brand) void { 80 | self.brand_ptr = brand_ptr; 81 | } 82 | 83 | // accept ? in method 84 | pub fn _changeBrandOpt(self: *Car, brand: ?Brand) void { 85 | if (brand != null) { 86 | self.brand = brand.?; 87 | } 88 | } 89 | 90 | // accept ?* in method 91 | pub fn _changeBrandOptPtr(self: *Car, brand_ptr: ?*Brand) void { 92 | if (brand_ptr != null) { 93 | self.brand_ptr = brand_ptr.?; 94 | } 95 | } 96 | 97 | // As return value 98 | // --------------- 99 | 100 | // return in getter 101 | pub fn get_brand(self: Car) Brand { 102 | return self.brand; 103 | } 104 | 105 | // return * in getter 106 | pub fn get_brandPtr(self: Car) *Brand { 107 | return self.brand_ptr; 108 | } 109 | 110 | // return ? in getter 111 | pub fn get_brandOpt(self: Car) ?Brand { 112 | return self.brand; 113 | } 114 | 115 | // return ?* in getter 116 | pub fn get_brandPtrOpt(self: Car) ?*Brand { 117 | return self.brand_ptr; 118 | } 119 | 120 | // return ? null in getter 121 | pub fn get_brandOptNull(_: Car) ?Brand { 122 | return null; 123 | } 124 | 125 | // return ?* null in getter 126 | pub fn get_brandPtrOptNull(_: Car) ?*Brand { 127 | return null; 128 | } 129 | 130 | // return in method 131 | pub fn _getBrand(self: Car) Brand { 132 | return self.get_brand(); 133 | } 134 | 135 | // return * in method 136 | pub fn _getBrandPtr(self: Car) *Brand { 137 | return self.get_brandPtr(); 138 | } 139 | 140 | pub fn deinit(self: *Car, alloc: std.mem.Allocator) void { 141 | alloc.destroy(self.brand_ptr); 142 | } 143 | }; 144 | 145 | // Native types with nested APIs 146 | // ----------------------------- 147 | 148 | const Country = struct { 149 | stats: Stats, 150 | 151 | // Nested type 152 | // ----------- 153 | // NOTE: Nested types are objects litterals only supported as function argument, 154 | // typically for Javascript options. 155 | pub const Stats = struct { 156 | population: ?u32, 157 | pib: []const u8, 158 | }; 159 | 160 | // As argument 161 | // ----------- 162 | 163 | // in method arg 164 | pub fn constructor(stats: Stats) Country { 165 | return .{ .stats = stats }; 166 | } 167 | 168 | pub fn get_population(self: Country) ?u32 { 169 | return self.stats.population; 170 | } 171 | 172 | pub fn get_pib(self: Country) []const u8 { 173 | return self.stats.pib; 174 | } 175 | 176 | // ? optional in method arg 177 | pub fn _changeStats(self: *Country, stats: ?Stats) void { 178 | if (stats) |s| { 179 | self.stats = s; 180 | } 181 | } 182 | 183 | // * (ie. pointer) is not supported by design, 184 | // for a pointer use case, use a seperate Native API. 185 | 186 | // As return value 187 | // --------------- 188 | 189 | // return in getter 190 | pub fn get_stats(self: Country) Stats { 191 | return self.stats; 192 | } 193 | 194 | // return ? in method (null) 195 | pub fn _doStatsNull(_: Country) ?Stats { 196 | return null; 197 | } 198 | 199 | // return ? in method (non-null) 200 | pub fn _doStatsNotNull(self: Country) ?Stats { 201 | return self.stats; 202 | } 203 | }; 204 | 205 | const JSONGen = struct { 206 | jsobj: std.json.Parsed(std.json.Value), 207 | 208 | pub fn constructor(alloc: std.mem.Allocator) !JSONGen { 209 | return .{ 210 | .jsobj = try std.json.parseFromSlice(std.json.Value, alloc, 211 | \\{ 212 | \\ "str": "bar", 213 | \\ "int": 123, 214 | \\ "float": 123.456, 215 | \\ "array": [1,2,3], 216 | \\ "neg": -123, 217 | \\ "max": 1.7976931348623157e+308, 218 | \\ "min": 5e-324, 219 | \\ "max_safe_int": 9007199254740991, 220 | \\ "max_safe_int_over": 9007199254740992 221 | \\} 222 | , .{}), 223 | }; 224 | } 225 | 226 | pub fn _object(self: JSONGen) std.json.Value { 227 | return self.jsobj.value; 228 | } 229 | 230 | pub fn deinit(self: *JSONGen, _: std.mem.Allocator) void { 231 | self.jsobj.deinit(); 232 | } 233 | }; 234 | 235 | pub const Types = .{ 236 | Brand, 237 | Car, 238 | Country, 239 | JSONGen, 240 | }; 241 | 242 | // exec tests 243 | pub fn exec( 244 | _: std.mem.Allocator, 245 | js_env: *public.Env, 246 | ) anyerror!void { 247 | 248 | // start JS env 249 | try js_env.start(); 250 | defer js_env.stop(); 251 | 252 | var nested_arg = [_]tests.Case{ 253 | .{ .src = "let stats = {'pib': '322Mds', 'population': 80}; let country = new Country(stats);", .ex = "undefined" }, 254 | .{ .src = "country.population;", .ex = "80" }, 255 | .{ .src = "let stats_without_population = {'pib': '342Mds'}; country.changeStats(stats_without_population)", .ex = "undefined" }, 256 | .{ .src = "let stats2 = {'pib': '342Mds', 'population': 80}; country.changeStats(stats2);", .ex = "undefined" }, 257 | .{ .src = "country.pib;", .ex = "342Mds" }, 258 | .{ .src = "country.stats.pib;", .ex = "342Mds" }, 259 | .{ .src = "country.doStatsNull();", .ex = "null" }, 260 | .{ .src = "country.doStatsNotNull().pib;", .ex = "342Mds" }, 261 | }; 262 | try tests.checkCases(js_env, &nested_arg); 263 | 264 | var separate_cases = [_]tests.Case{ 265 | .{ .src = "let car = new Car();", .ex = "undefined" }, 266 | 267 | // basic tests for getter 268 | .{ .src = "let brand1 = car.brand", .ex = "undefined" }, 269 | .{ .src = "brand1.name", .ex = "Renault" }, 270 | .{ .src = "let brand1Ptr = car.brandPtr", .ex = "undefined" }, 271 | .{ .src = "brand1Ptr.name", .ex = "Renault" }, 272 | 273 | // basic test for method 274 | .{ .src = "let brand2 = car.getBrand()", .ex = "undefined" }, 275 | .{ .src = "brand2.name", .ex = "Renault" }, 276 | .{ .src = "brand2 !== brand1", .ex = "true" }, // return value, not equal 277 | .{ .src = "let brand2Ptr = car.getBrandPtr()", .ex = "undefined" }, 278 | .{ .src = "brand2Ptr.name", .ex = "Renault" }, 279 | .{ .src = "brand2Ptr === brand1Ptr", .ex = "true" }, // return pointer, strict equal 280 | 281 | // additional call for pointer, to ensure persistent 282 | .{ .src = "let brand2BisPtr = car.getBrandPtr()", .ex = "undefined" }, 283 | .{ .src = "brand2BisPtr.name", .ex = "Renault" }, 284 | .{ .src = "brand2BisPtr === brand1Ptr", .ex = "true" }, // return pointer, strict equal 285 | .{ .src = "brand2BisPtr === brand2Ptr", .ex = "true" }, // return pointer, strict equal 286 | 287 | // successive calls for getter value 288 | // check the set of a new name on brand1 (value) has no impact 289 | .{ .src = "brand1.name = 'Peugot'", .ex = "Peugot" }, 290 | .{ .src = "let brand1_again = car.brand", .ex = "undefined" }, 291 | .{ .src = "brand1_again.name", .ex = "Renault" }, 292 | // check the set of a new name on brand1Ptr (pointer) has impact 293 | // ie. successive calls return the same pointer 294 | .{ .src = "brand1Ptr.name = 'Peugot'", .ex = "Peugot" }, 295 | .{ .src = "let brand1Ptr_again = car.brandPtr", .ex = "undefined" }, 296 | .{ .src = "brand1Ptr_again.name", .ex = "Peugot" }, 297 | // and check back the set of a new name on brand1Ptr_agin in brand1Ptr 298 | .{ .src = "brand1Ptr_again.name = 'Citroën'", .ex = "Citroën" }, 299 | .{ .src = "brand1Ptr.name", .ex = "Citroën" }, 300 | 301 | // null test 302 | .{ .src = "let brand_opt = car.brandOpt", .ex = "undefined" }, 303 | .{ .src = "brand_opt.name", .ex = "Renault" }, 304 | .{ .src = "let brand_ptr_opt = car.brandPtrOpt", .ex = "undefined" }, 305 | .{ .src = "brand_ptr_opt.name", .ex = "Citroën" }, 306 | .{ .src = "car.brandOptNull", .ex = "null" }, 307 | .{ .src = "car.brandPtrOptNull", .ex = "null" }, 308 | 309 | // as argumemnt for setter 310 | .{ .src = "let brand3 = new Brand('Audi')", .ex = "undefined" }, 311 | .{ .src = "var _ = (car.brand = brand3)", .ex = "undefined" }, 312 | .{ .src = "car.brand.name === 'Audi'", .ex = "true" }, 313 | .{ .src = "var _ = (car.brandPtr = brand3)", .ex = "undefined" }, 314 | .{ .src = "car.brandPtr.name === 'Audi'", .ex = "true" }, 315 | 316 | // as argumemnt for methods 317 | .{ .src = "let brand4 = new Brand('Tesla')", .ex = "undefined" }, 318 | .{ .src = "car.changeBrand(brand4)", .ex = "undefined" }, 319 | .{ .src = "car.brand.name === 'Tesla'", .ex = "true" }, 320 | .{ .src = "car.changeBrandPtr(brand4)", .ex = "undefined" }, 321 | .{ .src = "car.brandPtr.name === 'Tesla'", .ex = "true" }, 322 | 323 | .{ .src = "let brand5 = new Brand('Audi')", .ex = "undefined" }, 324 | .{ .src = "car.changeBrandOpt(brand5)", .ex = "undefined" }, 325 | .{ .src = "car.brand.name === 'Audi'", .ex = "true" }, 326 | .{ .src = "car.changeBrandOpt(null)", .ex = "undefined" }, 327 | .{ .src = "car.brand.name === 'Audi'", .ex = "true" }, 328 | 329 | .{ .src = "let brand6 = new Brand('Ford')", .ex = "undefined" }, 330 | .{ .src = "car.changeBrandOptPtr(brand6)", .ex = "undefined" }, 331 | .{ .src = "car.brandPtr.name === 'Ford'", .ex = "true" }, 332 | .{ .src = "car.changeBrandOptPtr(null)", .ex = "undefined" }, 333 | .{ .src = "car.brandPtr.name === 'Ford'", .ex = "true" }, 334 | }; 335 | try tests.checkCases(js_env, &separate_cases); 336 | 337 | var bug_native_obj = [_]tests.Case{ 338 | // Test for the bug #185: native func expects a object but the js value is 339 | // not. 340 | // https://github.com/lightpanda-io/jsruntime-lib/issues/185 341 | .{ .src = "try { car.changeBrand('foo'); false; } catch(e) { e instanceof TypeError; }", .ex = "true" }, 342 | // Test for the bug #187: native func expects a native object but the js value is 343 | // not. 344 | // https://github.com/lightpanda-io/jsruntime-lib/issues/187 345 | .{ .src = "try { car.changeBrand({'foo': 'bar'}); false; } catch(e) { e instanceof TypeError; }", .ex = "true" }, 346 | }; 347 | try tests.checkCases(js_env, &bug_native_obj); 348 | 349 | var json_native = [_]tests.Case{ 350 | .{ .src = "let json = (new JSONGen()).object()", .ex = "undefined" }, 351 | .{ .src = "json.str", .ex = "bar" }, 352 | .{ .src = "json.int", .ex = "123" }, 353 | .{ .src = "json.float", .ex = "123.456" }, 354 | .{ .src = "json.neg", .ex = "-123" }, 355 | .{ .src = "json.min", .ex = "5e-324" }, 356 | .{ .src = "json.max", .ex = "1.7976931348623157e+308" }, 357 | 358 | .{ .src = "json.max_safe_int", .ex = "9007199254740991" }, 359 | .{ .src = "json.max_safe_int_over", .ex = "9007199254740992" }, 360 | 361 | .{ .src = "typeof(json.int)", .ex = "number" }, 362 | .{ .src = "typeof(json.float)", .ex = "number" }, 363 | .{ .src = "typeof(json.neg)", .ex = "number" }, 364 | .{ .src = "typeof(json.max)", .ex = "number" }, 365 | .{ .src = "typeof(json.min)", .ex = "number" }, 366 | 367 | // TODO these tests should pass, but we've got bigint instead. 368 | //.{ .src = "typeof(json.max_safe_int)", .ex = "number" }, 369 | //.{ .src = "typeof(json.max_safe_int_over)", .ex = "number" }, 370 | 371 | .{ .src = "json.array.length", .ex = "3" }, 372 | .{ .src = "json.array[0]", .ex = "1" }, 373 | }; 374 | try tests.checkCases(js_env, &json_native); 375 | } 376 | -------------------------------------------------------------------------------- /src/tests/proto_test.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | 17 | const public = @import("../api.zig"); 18 | const tests = public.test_utils; 19 | 20 | // TODO: handle memory allocation in the data struct itself. 21 | // Each struct should have a deinit method to free internal memory and destroy object itself. 22 | // Each setter should accept an alloc arg in order to free pre-existing internal allocation. 23 | // Each method should accept an alloc arg in order to handle internal allocation (alloc or free). 24 | // Is it worth it with an Arena like allocator? 25 | // see the balance between memory size and cost of free 26 | 27 | const Entity = struct {}; 28 | 29 | const Person = struct { 30 | first_name: []u8, 31 | last_name: []u8, 32 | age: u32, 33 | 34 | // static attributes 35 | pub const _AGE_MIN = 18; 36 | pub const _NATIONALITY = "French"; 37 | 38 | pub fn constructor(alloc: std.mem.Allocator, first_name: []u8, last_name: []u8, age: u32) Person { 39 | 40 | // alloc last_name slice to keep them after function returns 41 | // NOTE: we do not alloc first_name on purpose to check freeArgs 42 | const last_name_alloc = alloc.alloc(u8, last_name.len) catch unreachable; 43 | @memcpy(last_name_alloc, last_name); 44 | 45 | return .{ 46 | .first_name = first_name, 47 | .last_name = last_name_alloc, 48 | .age = age, 49 | }; 50 | } 51 | 52 | pub fn get_age(self: Person) u32 { 53 | return self.age; 54 | } 55 | 56 | fn allocTest(alloc: std.mem.Allocator) !void { 57 | const v = try alloc.alloc(u8, 10); 58 | defer alloc.free(v); 59 | } 60 | 61 | pub fn get_allocator(_: Person, alloc: std.mem.Allocator) !bool { 62 | try Person.allocTest(alloc); 63 | return true; 64 | } 65 | 66 | pub fn get_UPPER(_: Person) bool { 67 | return true; 68 | } 69 | 70 | pub fn _UPPERMETHOD(_: Person) bool { 71 | return true; 72 | } 73 | 74 | pub fn set_allocator(_: *Person, alloc: std.mem.Allocator, _: bool) void { 75 | Person.allocTest(alloc) catch unreachable; 76 | } 77 | 78 | pub fn get_nonAllocFirstName(self: Person) []const u8 { 79 | return self.first_name; 80 | } 81 | 82 | pub fn set_age(self: *Person, age: u32) void { 83 | self.age = age; 84 | } 85 | 86 | pub fn _fullName(self: *const Person) []u8 { 87 | return self.last_name; 88 | } 89 | 90 | pub fn _setAgeMethod(self: *Person, age: u32) void { 91 | self.age = age; 92 | } 93 | 94 | pub fn _say(_: *Person, _: ?[]const u8) void {} 95 | 96 | // TODO: should be a static function 97 | // see https://github.com/Browsercore/jsruntime-lib/issues/127 98 | pub fn get_symbol_toStringTag(_: Person) []const u8 { 99 | return "MyPerson"; 100 | } 101 | 102 | pub fn deinit(self: *Person, alloc: std.mem.Allocator) void { 103 | alloc.free(self.last_name); 104 | } 105 | }; 106 | 107 | const User = struct { 108 | proto: Person, 109 | role: u8, 110 | 111 | pub const prototype = *Person; 112 | 113 | pub fn constructor( 114 | alloc: std.mem.Allocator, 115 | first_name: []u8, 116 | last_name: []u8, 117 | age: u32, 118 | ) User { 119 | const proto = Person.constructor(alloc, first_name, last_name, age); 120 | return .{ .proto = proto, .role = 1 }; 121 | } 122 | 123 | pub fn get_role(self: User) u8 { 124 | return self.role; 125 | } 126 | 127 | pub fn deinit(self: *User, alloc: std.mem.Allocator) void { 128 | self.proto.deinit(alloc); 129 | } 130 | }; 131 | 132 | const PersonPtr = struct { 133 | name: []u8, 134 | 135 | pub fn constructor(alloc: std.mem.Allocator, name: []u8) *PersonPtr { 136 | const name_alloc = alloc.alloc(u8, name.len) catch unreachable; 137 | @memcpy(name_alloc, name); 138 | 139 | const person_ptr = alloc.create(PersonPtr) catch unreachable; 140 | person_ptr.* = .{ .name = name_alloc }; 141 | return person_ptr; 142 | } 143 | 144 | pub fn get_name(self: *const PersonPtr) []u8 { 145 | return self.name; 146 | } 147 | 148 | pub fn set_name(self: *PersonPtr, alloc: std.mem.Allocator, name: []u8) void { 149 | const name_alloc = alloc.alloc(u8, name.len) catch unreachable; 150 | @memcpy(name_alloc, name); 151 | self.name = name_alloc; 152 | } 153 | 154 | pub fn deinit(self: *PersonPtr, alloc: std.mem.Allocator) void { 155 | alloc.free(self.name); 156 | } 157 | }; 158 | 159 | const UserForContainer = struct { 160 | proto: Person, 161 | role: u8, 162 | 163 | pub const prototype = *Person; 164 | 165 | pub fn constructor( 166 | alloc: std.mem.Allocator, 167 | first_name: []u8, 168 | last_name: []u8, 169 | age: u32, 170 | ) UserForContainer { 171 | const proto = Person.constructor(alloc, first_name, last_name, age); 172 | return .{ .proto = proto, .role = 1 }; 173 | } 174 | 175 | pub fn get_role(self: UserForContainer) u8 { 176 | return self.role; 177 | } 178 | }; 179 | 180 | const UserContainer = struct { 181 | pub const Self = UserForContainer; 182 | pub const prototype = *Person; 183 | 184 | pub fn constructor( 185 | alloc: std.mem.Allocator, 186 | first_name: []u8, 187 | last_name: []u8, 188 | age: u32, 189 | ) User { 190 | const proto = Person.constructor(alloc, first_name, last_name, age); 191 | return .{ .proto = proto, .role = 1 }; 192 | } 193 | 194 | pub fn get_role_container(self: UserForContainer) u8 { 195 | return self.role; 196 | } 197 | 198 | pub fn set_role_container(self: *UserForContainer, role: u8) void { 199 | self.role = role; 200 | } 201 | 202 | pub fn _roleVal(self: UserForContainer) u8 { 203 | return self.role; 204 | } 205 | 206 | pub fn deinit(self: *UserForContainer, alloc: std.mem.Allocator) void { 207 | self.proto.deinit(alloc); 208 | } 209 | }; 210 | 211 | const PersonProtoCast = struct { 212 | first_name: []const u8, 213 | 214 | pub fn protoCast(child_ptr: anytype) *PersonProtoCast { 215 | return @ptrCast(child_ptr); 216 | } 217 | 218 | pub fn constructor(alloc: std.mem.Allocator, first_name: []u8) PersonProtoCast { 219 | const first_name_alloc = alloc.alloc(u8, first_name.len) catch unreachable; 220 | @memcpy(first_name_alloc, first_name); 221 | return .{ .first_name = first_name_alloc }; 222 | } 223 | 224 | pub fn get_name(self: PersonProtoCast) []const u8 { 225 | return self.first_name; 226 | } 227 | 228 | pub fn deinit(self: *PersonProtoCast, alloc: std.mem.Allocator) void { 229 | alloc.free(self.first_name); 230 | } 231 | }; 232 | 233 | const UserProtoCast = struct { 234 | not_proto: PersonProtoCast, 235 | 236 | pub const prototype = *PersonProtoCast; 237 | 238 | pub fn constructor(alloc: std.mem.Allocator, first_name: []u8) UserProtoCast { 239 | return .{ .not_proto = PersonProtoCast.constructor(alloc, first_name) }; 240 | } 241 | 242 | pub fn deinit(self: *UserProtoCast, alloc: std.mem.Allocator) void { 243 | self.not_proto.deinit(alloc); 244 | } 245 | }; 246 | 247 | pub const Types = .{ 248 | User, 249 | Person, 250 | PersonPtr, 251 | Entity, 252 | UserContainer, 253 | PersonProtoCast, 254 | UserProtoCast, 255 | }; 256 | 257 | // exec tests 258 | pub fn exec( 259 | alloc: std.mem.Allocator, 260 | js_env: *public.Env, 261 | ) anyerror!void { 262 | 263 | // start JS env 264 | try js_env.start(); 265 | defer js_env.stop(); 266 | 267 | const ownBase = tests.engineOwnPropertiesDefault(); 268 | const ownBaseStr = tests.intToStr(alloc, ownBase); 269 | defer alloc.free(ownBaseStr); 270 | 271 | // global 272 | try js_env.attachObject(try js_env.getGlobal(), "self", null); 273 | 274 | var global = [_]tests.Case{ 275 | .{ .src = "self.foo = function() {} !== undefined", .ex = "true" }, 276 | .{ .src = "foo !== undefined", .ex = "true" }, 277 | .{ .src = "self.foo === foo", .ex = "true" }, 278 | .{ .src = "var bar = function() {}", .ex = "undefined" }, 279 | .{ .src = "self.bar !== undefined", .ex = "true" }, 280 | .{ .src = "self.bar === bar", .ex = "true" }, 281 | .{ .src = "let not_self = 0", .ex = "undefined" }, 282 | .{ .src = "self.not_self === undefined", .ex = "true" }, 283 | }; 284 | try tests.checkCases(js_env, &global); 285 | 286 | // 1. constructor 287 | var cases1 = [_]tests.Case{ 288 | .{ .src = "let p = new Person('Francis', 'Bouvier', 40);", .ex = "undefined" }, 289 | .{ .src = "p.__proto__ === Person.prototype", .ex = "true" }, 290 | .{ .src = "typeof(p.constructor) === 'function'", .ex = "true" }, 291 | .{ .src = "p[Symbol.toStringTag] === 'MyPerson';", .ex = "true" }, // custom string tag 292 | .{ .src = "new Person('Francis', 40)", .ex = "TypeError" }, // arg is missing (last_name) 293 | .{ .src = "new Entity()", .ex = "TypeError" }, // illegal constructor 294 | }; 295 | try tests.checkCases(js_env, &cases1); 296 | 297 | // 2. getter 298 | var cases2 = [_]tests.Case{ 299 | .{ .src = "p.age === 40", .ex = "true" }, 300 | .{ .src = "p.allocator", .ex = "true" }, 301 | .{ .src = "p.UPPER", .ex = "true" }, 302 | .{ .src = "p.UPPERMETHOD()", .ex = "true" }, 303 | // first name has not been allocated, so it's a normal behavior 304 | // here we check that freeArgs works well 305 | .{ .src = "p.nonAllocFirstName !== 'Francis'", .ex = "true" }, 306 | }; 307 | try tests.checkCases(js_env, &cases2); 308 | 309 | // 3. setter 310 | var cases3 = [_]tests.Case{ 311 | .{ .src = "p.age = 41;", .ex = "41" }, 312 | .{ .src = "p.age", .ex = "41" }, 313 | .{ .src = "p.allocator = true", .ex = "true" }, 314 | }; 315 | try tests.checkCases(js_env, &cases3); 316 | 317 | // 4. method 318 | var cases4 = [_]tests.Case{ 319 | .{ .src = "p.fullName() === 'Bouvier';", .ex = "true" }, 320 | .{ .src = "p.fullName('unused arg') === 'Bouvier';", .ex = "true" }, 321 | .{ .src = "p.setAgeMethod(42); p.age", .ex = "42" }, 322 | }; 323 | try tests.checkCases(js_env, &cases4); 324 | 325 | // static attr 326 | const ownPersonStr = intToStr(alloc, ownBase + 2); 327 | defer alloc.free(ownPersonStr); 328 | var cases_static = [_]tests.Case{ 329 | // basic static case 330 | .{ .src = "Person.AGE_MIN === 18", .ex = "true" }, 331 | .{ .src = "Person.NATIONALITY === 'French'", .ex = "true" }, 332 | // static attributes are own properties 333 | .{ .src = "let ownPerson = Object.getOwnPropertyNames(Person)", .ex = "undefined" }, 334 | .{ .src = "ownPerson.length", .ex = ownPersonStr }, 335 | // static attributes are also available on instances 336 | .{ .src = "p.AGE_MIN === 18", .ex = "true" }, 337 | .{ .src = "p.NATIONALITY === 'French'", .ex = "true" }, 338 | }; 339 | try tests.checkCases(js_env, &cases_static); 340 | 341 | // prototype chain, constructor level 342 | var cases_proto_constructor = [_]tests.Case{ 343 | // template level (load) FunctionTemplate.inherit 344 | .{ .src = "User.prototype.__proto__ === Person.prototype", .ex = "true" }, 345 | // object level (context started) FunctionTemplate.getFunction.setPrototype 346 | .{ .src = "User.__proto__ === Person", .ex = "true" }, 347 | // static attributes inherited on constructor 348 | .{ .src = "User.AGE_MIN === 18", .ex = "true" }, 349 | .{ .src = "User.NATIONALITY === 'French'", .ex = "true" }, 350 | // static attributes inherited are NOT own properties 351 | .{ .src = "let ownUser = Object.getOwnPropertyNames(User)", .ex = "undefined" }, 352 | .{ .src = "ownUser.length", .ex = ownBaseStr }, 353 | }; 354 | try tests.checkCases(js_env, &cases_proto_constructor); 355 | 356 | // prototype chain, instance level 357 | var cases_proto_instance = [_]tests.Case{ 358 | .{ .src = "let u = new User('Francis', 'Englund', 42);", .ex = "undefined" }, 359 | .{ .src = "u.__proto__ === User.prototype", .ex = "true" }, 360 | .{ .src = "u.__proto__.__proto__ === Person.prototype", .ex = "true" }, 361 | .{ .src = "u[Symbol.toStringTag] === 'User';", .ex = "true" }, // generic string tag 362 | .{ .src = "u.fullName();", .ex = "Englund" }, 363 | .{ .src = "u.age;", .ex = "42" }, 364 | .{ .src = "u.age = 43;", .ex = "43" }, 365 | .{ .src = "u.role = 2;", .ex = "2" }, 366 | .{ .src = "u.age;", .ex = "43" }, 367 | // static attributes inherited are also available on instances 368 | .{ .src = "u.AGE_MIN === 18", .ex = "true" }, 369 | .{ .src = "u.NATIONALITY === 'French'", .ex = "true" }, 370 | }; 371 | try tests.checkCases(js_env, &cases_proto_instance); 372 | 373 | // constructor returning pointer 374 | var casesPtr = [_]tests.Case{ 375 | .{ .src = "let pptr = new PersonPtr('Francis');", .ex = "undefined" }, 376 | .{ .src = "pptr.name = 'Bouvier'; pptr.name === 'Bouvier'", .ex = "true" }, 377 | }; 378 | try tests.checkCases(js_env, &casesPtr); 379 | 380 | // container 381 | var casesContainer = [_]tests.Case{ 382 | .{ .src = "let uc = new UserContainer('Francis', 'Bouvier', 40);", .ex = "undefined" }, 383 | .{ .src = "uc.role_container === 1", .ex = "true" }, 384 | .{ .src = "uc.role_container = 2; uc.role_container === 2", .ex = "true" }, 385 | .{ .src = "uc.roleVal() === 2", .ex = "true" }, 386 | .{ .src = "uc.age === 40", .ex = "true" }, 387 | }; 388 | try tests.checkCases(js_env, &casesContainer); 389 | 390 | // protoCast func 391 | var casesProtoCast = [_]tests.Case{ 392 | .{ .src = "let ppc = new PersonProtoCast('Bouvier');", .ex = "undefined" }, 393 | .{ .src = "ppc.name === 'Bouvier'", .ex = "true" }, 394 | .{ .src = "let upc = new UserProtoCast('Francis');", .ex = "undefined" }, 395 | .{ .src = "upc.name === 'Francis'", .ex = "true" }, 396 | }; 397 | try tests.checkCases(js_env, &casesProtoCast); 398 | 399 | // free func arguments 400 | var casesFreeArguments = [_]tests.Case{ 401 | .{ .src = "let dt = new Person('Deep', 'Thought', 7500000);", .ex = "undefined" }, 402 | .{ .src = "dt.say('42')", .ex = "undefined" }, 403 | .{ .src = "dt.say(null)", .ex = "undefined" }, 404 | }; 405 | try tests.checkCases(js_env, &casesFreeArguments); 406 | 407 | var strict_const = [_]tests.Case{ 408 | .{ .src = 409 | \\"use strict"; 410 | \\var cp = new Person('John', 'Doe', 25); 411 | \\cp.age = 35; 412 | , .ex = "35" }, 413 | }; 414 | try tests.checkCases(js_env, &strict_const); 415 | } 416 | 417 | fn intToStr(alloc: std.mem.Allocator, nb: u8) []const u8 { 418 | return std.fmt.allocPrint( 419 | alloc, 420 | "{d}", 421 | .{nb}, 422 | ) catch unreachable; 423 | } 424 | -------------------------------------------------------------------------------- /src/loop.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Lightpanda (Selecy SAS) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const std = @import("std"); 16 | const builtin = @import("builtin"); 17 | const MemoryPool = std.heap.MemoryPool; 18 | 19 | pub const IO = @import("tigerbeetle-io").IO; 20 | 21 | const public = @import("api.zig"); 22 | const JSCallback = public.Callback; 23 | 24 | const log = std.log.scoped(.loop); 25 | 26 | // SingleThreaded I/O Loop based on Tigerbeetle io_uring loop. 27 | // On Linux it's using io_uring. 28 | // On MacOS and Windows it's using kqueue/IOCP with a ring design. 29 | // This is a thread-unsafe version without any lock on shared resources, 30 | // use it only on a single thread. 31 | // The loop provides I/O APIs based on callbacks. 32 | // I/O APIs based on async/await might be added in the future. 33 | pub const SingleThreaded = struct { 34 | alloc: std.mem.Allocator, // TODO: unmanaged version ? 35 | io: IO, 36 | 37 | // both events_nb are used to track how many callbacks are to be called. 38 | // We use these counters to wait until all the events are finished. 39 | js_events_nb: usize, 40 | zig_events_nb: usize, 41 | 42 | cbk_error: bool = false, 43 | 44 | // js_ctx_id is incremented each time the loop is reset for JS. 45 | // All JS callbacks store an initial js_ctx_id and compare before execution. 46 | // If a ctx is outdated, the callback is ignored. 47 | // This is a weak way to cancel all future JS callbacks. 48 | js_ctx_id: u32 = 0, 49 | 50 | // zig_ctx_id is incremented each time the loop is reset for Zig. 51 | // All Zig callbacks store an initial zig_ctx_id and compare before execution. 52 | // If a ctx is outdated, the callback is ignored. 53 | // This is a weak way to cancel all future Zig callbacks. 54 | zig_ctx_id: u32 = 0, 55 | 56 | // The MacOS event loop doesn't support cancellation. We use this to track 57 | // cancellation ids and, on the timeout callback, we can can check here 58 | // to see if it's been cancelled. 59 | cancelled: std.AutoHashMapUnmanaged(usize, void), 60 | 61 | cancel_pool: MemoryPool(ContextCancel), 62 | timeout_pool: MemoryPool(ContextTimeout), 63 | event_callback_pool: MemoryPool(EventCallbackContext), 64 | 65 | const Self = @This(); 66 | pub const Completion = IO.Completion; 67 | 68 | pub const ConnectError = IO.ConnectError; 69 | pub const RecvError = IO.RecvError; 70 | pub const SendError = IO.SendError; 71 | 72 | pub fn init(alloc: std.mem.Allocator) !Self { 73 | return Self{ 74 | .alloc = alloc, 75 | .cancelled = .{}, 76 | .io = try IO.init(32, 0), 77 | .js_events_nb = 0, 78 | .zig_events_nb = 0, 79 | .cancel_pool = MemoryPool(ContextCancel).init(alloc), 80 | .timeout_pool = MemoryPool(ContextTimeout).init(alloc), 81 | .event_callback_pool = MemoryPool(EventCallbackContext).init(alloc), 82 | }; 83 | } 84 | 85 | pub fn deinit(self: *Self) void { 86 | // first disable callbacks for existing events. 87 | // We don't want a callback re-create a setTimeout, it could create an 88 | // infinite loop on wait for events. 89 | self.resetJS(); 90 | self.resetZig(); 91 | 92 | // run tail events. We do run the tail events to ensure all the 93 | // contexts are correcly free. 94 | while (self.eventsNb(.js) > 0 or self.eventsNb(.zig) > 0) { 95 | self.io.run_for_ns(10 * std.time.ns_per_ms) catch |err| { 96 | log.err("deinit run tail events: {any}", .{err}); 97 | break; 98 | }; 99 | } 100 | if (comptime CANCEL_SUPPORTED) { 101 | self.io.cancel_all(); 102 | } 103 | self.io.deinit(); 104 | self.cancel_pool.deinit(); 105 | self.timeout_pool.deinit(); 106 | self.event_callback_pool.deinit(); 107 | self.cancelled.deinit(self.alloc); 108 | } 109 | 110 | // Retrieve all registred I/O events completed by OS kernel, 111 | // and execute sequentially their callbacks. 112 | // Stops when there is no more I/O events registered on the loop. 113 | // Note that I/O events callbacks might register more I/O events 114 | // on the go when they are executed (ie. nested I/O events). 115 | pub fn run(self: *Self) !void { 116 | while (self.eventsNb(.js) > 0) { 117 | try self.io.run_for_ns(10 * std.time.ns_per_ms); 118 | // at each iteration we might have new events registred by previous callbacks 119 | } 120 | // TODO: return instead immediatly on the first JS callback error 121 | // and let the caller decide what to do next 122 | // (typically retrieve the exception through the TryCatch and 123 | // continue the execution of callbacks with a new call to loop.run) 124 | if (self.cbk_error) { 125 | return error.JSExecCallback; 126 | } 127 | } 128 | 129 | const Event = enum { js, zig }; 130 | 131 | fn eventsPtr(self: *Self, comptime event: Event) *usize { 132 | return switch (event) { 133 | .zig => &self.zig_events_nb, 134 | .js => &self.js_events_nb, 135 | }; 136 | } 137 | 138 | // Register events atomically 139 | // - add 1 event and return previous value 140 | fn addEvent(self: *Self, comptime event: Event) void { 141 | _ = @atomicRmw(usize, self.eventsPtr(event), .Add, 1, .acq_rel); 142 | } 143 | // - remove 1 event and return previous value 144 | fn removeEvent(self: *Self, comptime event: Event) void { 145 | _ = @atomicRmw(usize, self.eventsPtr(event), .Sub, 1, .acq_rel); 146 | } 147 | // - get the number of current events 148 | fn eventsNb(self: *Self, comptime event: Event) usize { 149 | return @atomicLoad(usize, self.eventsPtr(event), .seq_cst); 150 | } 151 | 152 | // JS callbacks APIs 153 | // ----------------- 154 | 155 | // Timeout 156 | 157 | const ContextTimeout = struct { 158 | loop: *Self, 159 | js_cbk: ?JSCallback, 160 | js_ctx_id: u32, 161 | }; 162 | 163 | fn timeoutCallback( 164 | ctx: *ContextTimeout, 165 | completion: *IO.Completion, 166 | result: IO.TimeoutError!void, 167 | ) void { 168 | const loop = ctx.loop; 169 | defer { 170 | loop.removeEvent(.js); 171 | loop.timeout_pool.destroy(ctx); 172 | loop.alloc.destroy(completion); 173 | } 174 | 175 | if (comptime CANCEL_SUPPORTED == false) { 176 | if (loop.cancelled.remove(@intFromPtr(completion))) { 177 | return; 178 | } 179 | } 180 | 181 | // If the loop's context id has changed, don't call the js callback 182 | // function. The callback's memory has already be cleaned and the 183 | // events nb reset. 184 | if (ctx.js_ctx_id != loop.js_ctx_id) return; 185 | 186 | // TODO: return the error to the callback 187 | result catch |err| { 188 | switch (err) { 189 | error.Canceled => {}, 190 | else => log.err("timeout callback: {any}", .{err}), 191 | } 192 | return; 193 | }; 194 | 195 | // js callback 196 | if (ctx.js_cbk) |*js_cbk| { 197 | defer js_cbk.deinit(loop.alloc); 198 | js_cbk.call(null) catch { 199 | loop.cbk_error = true; 200 | }; 201 | } 202 | } 203 | 204 | pub fn timeout(self: *Self, nanoseconds: u63, js_cbk: ?JSCallback) !usize { 205 | const completion = try self.alloc.create(Completion); 206 | errdefer self.alloc.destroy(completion); 207 | completion.* = undefined; 208 | 209 | const ctx = try self.timeout_pool.create(); 210 | errdefer self.timeout_pool.destroy(ctx); 211 | ctx.* = ContextTimeout{ 212 | .loop = self, 213 | .js_cbk = js_cbk, 214 | .js_ctx_id = self.js_ctx_id, 215 | }; 216 | 217 | self.addEvent(.js); 218 | self.io.timeout(*ContextTimeout, ctx, timeoutCallback, completion, nanoseconds); 219 | return @intFromPtr(completion); 220 | } 221 | 222 | const ContextCancel = struct { 223 | loop: *Self, 224 | js_cbk: ?JSCallback, 225 | js_ctx_id: u32, 226 | }; 227 | 228 | fn cancelCallback( 229 | ctx: *ContextCancel, 230 | completion: *IO.Completion, 231 | result: IO.CancelOneError!void, 232 | ) void { 233 | const loop = ctx.loop; 234 | 235 | defer { 236 | loop.removeEvent(.js); 237 | loop.cancel_pool.destroy(ctx); 238 | loop.alloc.destroy(completion); 239 | } 240 | 241 | // If the loop's context id has changed, don't call the js callback 242 | // function. The callback's memory has already be cleaned and the 243 | // events nb reset. 244 | if (ctx.js_ctx_id != loop.js_ctx_id) return; 245 | 246 | // TODO: return the error to the callback 247 | result catch |err| { 248 | switch (err) { 249 | error.NotFound => log.debug("cancel callback: {any}", .{err}), 250 | else => log.err("cancel callback: {any}", .{err}), 251 | } 252 | return; 253 | }; 254 | 255 | // js callback 256 | if (ctx.js_cbk) |*js_cbk| { 257 | defer js_cbk.deinit(loop.alloc); 258 | js_cbk.call(null) catch { 259 | loop.cbk_error = true; 260 | }; 261 | } 262 | } 263 | 264 | pub fn cancel(self: *Self, id: usize, js_cbk: ?JSCallback) !void { 265 | const alloc = self.alloc; 266 | if (comptime CANCEL_SUPPORTED == false) { 267 | try self.cancelled.put(alloc, id, {}); 268 | if (js_cbk) |cbk| { 269 | var vcbk = cbk; 270 | defer vcbk.deinit(alloc); 271 | vcbk.call(null) catch { 272 | self.cbk_error = true; 273 | }; 274 | } 275 | return; 276 | } 277 | const comp_cancel: *IO.Completion = @ptrFromInt(id); 278 | 279 | const completion = try alloc.create(Completion); 280 | errdefer alloc.destroy(completion); 281 | completion.* = undefined; 282 | 283 | const ctx = self.alloc.create(ContextCancel) catch unreachable; 284 | ctx.* = ContextCancel{ 285 | .loop = self, 286 | .js_cbk = js_cbk, 287 | .js_ctx_id = self.js_ctx_id, 288 | }; 289 | 290 | self.addEvent(.js); 291 | self.io.cancel_one(*ContextCancel, ctx, cancelCallback, completion, comp_cancel); 292 | } 293 | 294 | // Reset all existing JS callbacks. 295 | // The existing events will happen and their memory will be cleanup but the 296 | // corresponding callbacks will not be called. 297 | pub fn resetJS(self: *Self) void { 298 | self.js_ctx_id += 1; 299 | self.cancelled.clearRetainingCapacity(); 300 | } 301 | 302 | // Reset all existing Zig callbacks. 303 | // The existing events will happen and their memory will be cleanup but the 304 | // corresponding callbacks will not be called. 305 | pub fn resetZig(self: *Self) void { 306 | self.zig_ctx_id += 1; 307 | } 308 | 309 | // IO callbacks APIs 310 | // ----------------- 311 | 312 | // Connect 313 | 314 | pub fn connect( 315 | self: *Self, 316 | comptime Ctx: type, 317 | ctx: *Ctx, 318 | completion: *Completion, 319 | comptime cbk: fn (ctx: *Ctx, _: *Completion, res: ConnectError!void) void, 320 | socket: std.posix.socket_t, 321 | address: std.net.Address, 322 | ) !void { 323 | const onConnect = struct { 324 | fn onConnect(callback: *EventCallbackContext, completion_: *Completion, res: ConnectError!void) void { 325 | defer callback.loop.event_callback_pool.destroy(callback); 326 | callback.loop.removeEvent(.js); 327 | cbk(@alignCast(@ptrCast(callback.ctx)), completion_, res); 328 | } 329 | }.onConnect; 330 | 331 | const callback = try self.event_callback_pool.create(); 332 | errdefer self.event_callback_pool.destroy(callback); 333 | callback.* = .{ .loop = self, .ctx = ctx }; 334 | 335 | self.addEvent(.js); 336 | self.io.connect(*EventCallbackContext, callback, onConnect, completion, socket, address); 337 | } 338 | 339 | // Send 340 | 341 | pub fn send( 342 | self: *Self, 343 | comptime Ctx: type, 344 | ctx: *Ctx, 345 | completion: *Completion, 346 | comptime cbk: fn (ctx: *Ctx, completion: *Completion, res: SendError!usize) void, 347 | socket: std.posix.socket_t, 348 | buf: []const u8, 349 | ) !void { 350 | const onSend = struct { 351 | fn onSend(callback: *EventCallbackContext, completion_: *Completion, res: SendError!usize) void { 352 | defer callback.loop.event_callback_pool.destroy(callback); 353 | callback.loop.removeEvent(.js); 354 | cbk(@alignCast(@ptrCast(callback.ctx)), completion_, res); 355 | } 356 | }.onSend; 357 | 358 | const callback = try self.event_callback_pool.create(); 359 | errdefer self.event_callback_pool.destroy(callback); 360 | callback.* = .{ .loop = self, .ctx = ctx }; 361 | 362 | self.addEvent(.js); 363 | self.io.send(*EventCallbackContext, callback, onSend, completion, socket, buf); 364 | } 365 | 366 | // Recv 367 | 368 | pub fn recv( 369 | self: *Self, 370 | comptime Ctx: type, 371 | ctx: *Ctx, 372 | completion: *Completion, 373 | comptime cbk: fn (ctx: *Ctx, completion: *Completion, res: RecvError!usize) void, 374 | socket: std.posix.socket_t, 375 | buf: []u8, 376 | ) !void { 377 | const onRecv = struct { 378 | fn onRecv(callback: *EventCallbackContext, completion_: *Completion, res: RecvError!usize) void { 379 | defer callback.loop.event_callback_pool.destroy(callback); 380 | callback.loop.removeEvent(.js); 381 | cbk(@alignCast(@ptrCast(callback.ctx)), completion_, res); 382 | } 383 | }.onRecv; 384 | 385 | const callback = try self.event_callback_pool.create(); 386 | errdefer self.event_callback_pool.destroy(callback); 387 | callback.* = .{ .loop = self, .ctx = ctx }; 388 | 389 | self.addEvent(.js); 390 | self.io.recv(*EventCallbackContext, callback, onRecv, completion, socket, buf); 391 | } 392 | 393 | // Zig timeout 394 | 395 | const ContextZigTimeout = struct { 396 | loop: *Self, 397 | zig_ctx_id: u32, 398 | 399 | context: *anyopaque, 400 | callback: *const fn ( 401 | context: ?*anyopaque, 402 | ) void, 403 | }; 404 | 405 | fn zigTimeoutCallback( 406 | ctx: *ContextZigTimeout, 407 | completion: *IO.Completion, 408 | result: IO.TimeoutError!void, 409 | ) void { 410 | const loop = ctx.loop; 411 | defer { 412 | loop.removeEvent(.zig); 413 | loop.alloc.destroy(ctx); 414 | loop.alloc.destroy(completion); 415 | } 416 | 417 | // If the loop's context id has changed, don't call the js callback 418 | // function. The callback's memory has already be cleaned and the 419 | // events nb reset. 420 | if (ctx.zig_ctx_id != loop.zig_ctx_id) return; 421 | 422 | result catch |err| { 423 | switch (err) { 424 | error.Canceled => {}, 425 | else => log.err("zig timeout callback: {any}", .{err}), 426 | } 427 | return; 428 | }; 429 | 430 | // callback 431 | ctx.callback(ctx.context); 432 | } 433 | 434 | // zigTimeout performs a timeout but the callback is a zig function. 435 | pub fn zigTimeout( 436 | self: *Self, 437 | nanoseconds: u63, 438 | comptime Context: type, 439 | context: Context, 440 | comptime callback: fn (context: Context) void, 441 | ) void { 442 | const completion = self.alloc.create(IO.Completion) catch unreachable; 443 | completion.* = undefined; 444 | const ctxtimeout = self.alloc.create(ContextZigTimeout) catch unreachable; 445 | ctxtimeout.* = ContextZigTimeout{ 446 | .loop = self, 447 | .zig_ctx_id = self.zig_ctx_id, 448 | .context = context, 449 | .callback = struct { 450 | fn wrapper(ctx: ?*anyopaque) void { 451 | callback(@ptrCast(@alignCast(ctx))); 452 | } 453 | }.wrapper, 454 | }; 455 | 456 | self.addEvent(.zig); 457 | self.io.timeout(*ContextZigTimeout, ctxtimeout, zigTimeoutCallback, completion, nanoseconds); 458 | } 459 | }; 460 | 461 | const EventCallbackContext = struct { 462 | ctx: *anyopaque, 463 | loop: *SingleThreaded, 464 | }; 465 | 466 | const CANCEL_SUPPORTED = switch (builtin.target.os.tag) { 467 | .linux => true, 468 | .macos, .tvos, .watchos, .ios => false, 469 | else => @compileError("IO is not supported for platform"), 470 | }; 471 | --------------------------------------------------------------------------------