diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 779d03b..74990cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,9 +7,9 @@ on: branches: [ main ] jobs: - test: + build: runs-on: ubuntu-latest - name: Test on ubuntu-latest + name: Build on ubuntu-latest steps: - name: Checkout @@ -30,4 +30,37 @@ jobs: - name: Build run: | pushd examples/basic && zig build && popd - pushd examples/init && zig build && popd \ No newline at end of file + pushd examples/init && zig build && popd + + arkvm-tests: + runs-on: ubuntu-latest + name: ArkTS N-API tests + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Zig for OpenHarmony + uses: openharmony-zig/setup-zig-ohos@v0.1.0 + with: + tag: '0.16.0' + + - name: Setup arkvm + id: setup-arkvm + uses: harmony-contrib/arkts-vm@v2.0.0 + with: + cache: true + + - name: Validate Ark host bundle + env: + ARK_HOST_TOOLS_DIR: ${{ steps.setup-arkvm.outputs.arkvm-path }} + run: | + set -euo pipefail + test -x "${ARK_HOST_TOOLS_DIR}/ark_js_napi_cli" + test -x "${ARK_HOST_TOOLS_DIR}/es2abc" + test -f "${ARK_HOST_TOOLS_DIR}/libace_napi.so" + + - name: Run ArkTS tests + env: + ARK_HOST_TOOLS_DIR: ${{ steps.setup-arkvm.outputs.arkvm-path }} + run: bash ./scripts/arkvm/run_arkvm_tests.sh diff --git a/.github/workflows/memory-leak-detect.yml b/.github/workflows/memory-leak-detect.yml new file mode 100644 index 0000000..cb6d6aa --- /dev/null +++ b/.github/workflows/memory-leak-detect.yml @@ -0,0 +1,45 @@ +name: Memory Leak Detect + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + arkvm-memory: + name: ArkVM N-API memory + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Zig + uses: openharmony-zig/setup-zig-ohos@v0.1.0 + with: + tag: '0.16.0' + + - name: Setup ArkVM + id: setup-arkvm + uses: harmony-contrib/arkts-vm@v2.0.0 + with: + cache: true + + - name: Validate Ark host bundle + env: + ARK_HOST_TOOLS_DIR: ${{ steps.setup-arkvm.outputs.arkvm-path }} + run: | + set -euo pipefail + echo "Expect host bundle at: ${ARK_HOST_TOOLS_DIR}" + test -x "${ARK_HOST_TOOLS_DIR}/ark_js_napi_cli" + test -x "${ARK_HOST_TOOLS_DIR}/es2abc" + test -f "${ARK_HOST_TOOLS_DIR}/libace_napi.so" + test -f "${ARK_HOST_TOOLS_DIR}/libets_interop_js_napi.so" + + - name: Run ArkVM memory tests + shell: bash + env: + ARK_HOST_TOOLS_DIR: ${{ steps.setup-arkvm.outputs.arkvm-path }} + KEEP_WORKDIR: "1" + run: bash scripts/arkvm/run_arkvm_memory_tests.sh diff --git a/.gitignore b/.gitignore index 24439b6..9727803 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .DS_Store .zig-cache -zig-out \ No newline at end of file +zig-out +.tmp_arkvm_runner +.tmp_arkvm_memory_runner diff --git a/build.zig b/build.zig index 099f570..c743d51 100644 --- a/build.zig +++ b/build.zig @@ -10,8 +10,11 @@ pub fn build(b: *std.Build) !void { const napi = b.addModule("napi", .{ .root_source_file = b.path("src/napi.zig"), }); + const build_options = b.addOptions(); + build_options.addOption(bool, "napi_tsgen", false); napi.addImport("napi-sys", napi_sys); + napi.addOptions("build_options", build_options); napi.addIncludePath(b.path("src/sys/header")); napi_sys.addIncludePath(b.path("src/sys/header")); diff --git a/examples/basic/build.zig b/examples/basic/build.zig index fbb53d7..72e261c 100644 --- a/examples/basic/build.zig +++ b/examples/basic/build.zig @@ -20,15 +20,21 @@ pub fn build(b: *std.Build) !void { if (result.arm64) |arm64| { arm64.root_module.addImport("napi", napi); - arm64.root_module.linkSystemLibrary("hilog_ndk.z", .{}); + if (arm64.rootModuleTarget().abi.isOpenHarmony()) { + arm64.root_module.linkSystemLibrary("hilog_ndk.z", .{}); + } } if (result.arm) |arm| { arm.root_module.addImport("napi", napi); - arm.root_module.linkSystemLibrary("hilog_ndk.z", .{}); + if (arm.rootModuleTarget().abi.isOpenHarmony()) { + arm.root_module.linkSystemLibrary("hilog_ndk.z", .{}); + } } if (result.x64) |x64| { x64.root_module.addImport("napi", napi); - x64.root_module.linkSystemLibrary("hilog_ndk.z", .{}); + if (x64.rootModuleTarget().abi.isOpenHarmony()) { + x64.root_module.linkSystemLibrary("hilog_ndk.z", .{}); + } } const dts = try napi_build.generateTypeDefinition(b, .{ diff --git a/examples/basic/src/hello.zig b/examples/basic/src/hello.zig index 7cf6047..40cb4d5 100644 --- a/examples/basic/src/hello.zig +++ b/examples/basic/src/hello.zig @@ -10,7 +10,10 @@ const object = @import("object.zig"); const function = @import("function.zig"); const thread_safe_function = @import("thread_safe_function.zig"); const class = @import("class.zig"); -const log = @import("log/log.zig"); +const builtin = @import("builtin"); +const log = if (builtin.target.abi.isOpenHarmony()) @import("log/log.zig") else struct { + pub fn test_hilog() void {} +}; const buffer = @import("buffer.zig"); const arraybuffer = @import("arraybuffer.zig"); const typedarray = @import("typedarray.zig"); diff --git a/examples/basic/src/string.zig b/examples/basic/src/string.zig index 73848b5..6c4af84 100644 --- a/examples/basic/src/string.zig +++ b/examples/basic/src/string.zig @@ -1,13 +1,13 @@ const std = @import("std"); const napi = @import("napi"); -pub fn hello(name: []u8) []u8 { +pub fn hello(env: napi.Env, name: []u8) napi.String { const allocator = std.heap.page_allocator; const message = std.fmt.allocPrint(allocator, "Hello, {s}!", .{name}) catch @panic("OOM"); defer allocator.free(message); - return message; + return napi.String.New(env, message); } pub const text = "Hello World"; diff --git a/examples/basic/src/thread_safe_function.zig b/examples/basic/src/thread_safe_function.zig index 59f4075..669f0aa 100644 --- a/examples/basic/src/thread_safe_function.zig +++ b/examples/basic/src/thread_safe_function.zig @@ -5,11 +5,8 @@ const Args = struct { i32, i32 }; const Return = i32; fn sleepForFiveSeconds() void { - var req = std.c.timespec{ - .sec = 5, - .nsec = 0, - }; - _ = std.c.nanosleep(&req, null); + // Keep this hook separate so TSFN examples can add scheduling delay when needed + // without making the type-generation build depend on libc sleep symbols. } fn execute_thread_safe_function(tsfn: *napi.ThreadSafeFunction(Args, Return, true, 0)) void { diff --git a/examples/init/src/hello.zig b/examples/init/src/hello.zig index e1cd4e8..4cc0521 100644 --- a/examples/init/src/hello.zig +++ b/examples/init/src/hello.zig @@ -27,13 +27,13 @@ fn add(left: f64, right: f64) f64 { return result; } -fn hello(name: []u8) []u8 { +fn hello(env: napi.Env, name: []u8) napi.String { const allocator = std.heap.page_allocator; const message = std.fmt.allocPrint(allocator, "Hello, {s}!", .{name}) catch @panic("OOM"); defer allocator.free(message); - return message; + return napi.String.New(env, message); } fn fib(env: napi.Env, n: f64) void { diff --git a/examples/memory/build.zig b/examples/memory/build.zig new file mode 100644 index 0000000..d3e3db9 --- /dev/null +++ b/examples/memory/build.zig @@ -0,0 +1,29 @@ +const std = @import("std"); +const napi_build = @import("zig-napi").napi_build; + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const zig_napi = b.dependency("zig-napi", .{}); + const napi = zig_napi.module("napi"); + + const result = try napi_build.nativeAddonBuild(b, .{ + .name = "hello", + .root_module_options = .{ + .root_source_file = b.path("./src/hello.zig"), + .target = target, + .optimize = optimize, + }, + }); + + if (result.arm64) |arm64| { + arm64.root_module.addImport("napi", napi); + } + if (result.arm) |arm| { + arm.root_module.addImport("napi", napi); + } + if (result.x64) |x64| { + x64.root_module.addImport("napi", napi); + } +} diff --git a/examples/memory/build.zig.zon b/examples/memory/build.zig.zon new file mode 100644 index 0000000..fd77b9f --- /dev/null +++ b/examples/memory/build.zig.zon @@ -0,0 +1,8 @@ +.{ + .name = .memory, + .version = "0.0.1", + .minimum_zig_version = "0.16.0", + .fingerprint = 0xea6d34357876ddeb, + .dependencies = .{ .@"zig-napi" = .{ .path = "../.." } }, + .paths = .{ "build.zig", "build.zig.zon", "src" }, +} diff --git a/examples/memory/src/async.zig b/examples/memory/src/async.zig new file mode 100644 index 0000000..23cbef5 --- /dev/null +++ b/examples/memory/src/async.zig @@ -0,0 +1,134 @@ +const std = @import("std"); +const napi = @import("napi"); + +const AsyncInput = struct { + label: []u8, + values: []f32, +}; + +const AsyncSummary = struct { + label: []u8, + count: usize, + total: f64, +}; + +const CountProgress = struct { + current: u32, + total: u32, +}; + +const FnArgs = struct { i32, i32 }; +const FnReturn = i32; + +fn async_summary_execute(ctx: napi.AsyncContext(void), input: AsyncInput) !AsyncSummary { + var total: f64 = 0; + for (input.values) |value| { + total += value; + } + const label = try ctx.allocator.dupe(u8, input.label); + return .{ .label = label, .count = input.values.len, .total = total }; +} + +fn async_void_execute(_: []u8) void {} + +fn async_fail_execute(message: []u8) !void { + return napi.Error.fromReason(message); +} + +fn count_with_progress_execute(ctx: napi.AsyncContext(CountProgress), total: u32) !u32 { + var current: u32 = 0; + while (current <= total) : (current += 1) { + try ctx.emit(.{ .current = current, .total = total }); + } + return total; +} + +fn abortable_count_execute(ctx: napi.AsyncContext(void), total: u32) !u32 { + var current: u32 = 0; + while (current < total) : (current += 1) { + if (current % 256 == 0) { + try ctx.checkCancelled(); + } + } + try ctx.checkCancelled(); + return total; +} + +fn abortable_slow_count_execute(ctx: napi.AsyncContext(void), total: u32) !u32 { + var current: u32 = 0; + while (current < total) : (current += 1) { + if (current % 16 == 0) { + for (0..10000) |_| { + std.atomic.spinLoopHint(); + } + try ctx.checkCancelled(); + } + } + try ctx.checkCancelled(); + return total; +} + +pub fn memory_async_summary(input: AsyncInput) napi.Async(AsyncSummary, .thread) { + return napi.Async(AsyncSummary, .thread).from(input, async_summary_execute); +} + +pub fn memory_async_summary_single(input: AsyncInput) napi.Async(AsyncSummary, .single) { + return napi.Async(AsyncSummary, .single).from(input, async_summary_execute); +} + +pub fn memory_async_void(label: []u8) napi.Async(void, .thread) { + return napi.Async(void, .thread).from(label, async_void_execute); +} + +pub fn memory_async_fail(message: []u8) napi.Async(void, .thread) { + return napi.Async(void, .thread).from(message, async_fail_execute); +} + +pub fn memory_async_progress(total: u32) napi.AsyncWithEvents(u32, CountProgress, .thread) { + return napi.AsyncWithEvents(u32, CountProgress, .thread).from(total, count_with_progress_execute); +} + +pub fn memory_event_mode_progress(total: u32) napi.AsyncWithEvents(u32, CountProgress, .event) { + return napi.AsyncWithEvents(u32, CountProgress, .event).from(total, count_with_progress_execute); +} + +pub fn memory_abortable_count(total: u32, signal: napi.AbortSignal) napi.Async(u32, .thread) { + _ = signal; + return napi.Async(u32, .thread).from(total, abortable_count_execute); +} + +pub fn memory_abortable_slow_count(total: u32, signal: napi.AbortSignal) napi.Async(u32, .thread) { + _ = signal; + return napi.Async(u32, .thread).from(total, abortable_slow_count_execute); +} + +fn worker_execute(value: u32) u32 { + return value + 1; +} + +pub fn memory_worker(env: napi.Env, value: u32) napi.Promise { + const worker = napi.Worker(env, .{ .data = value, .Execute = worker_execute }); + return worker.AsyncQueue(); +} + +fn execute_thread_safe_function(tsfn: *napi.ThreadSafeFunction(FnArgs, FnReturn, true, 0)) void { + defer tsfn.release(.Release) catch {}; + tsfn.Ok(.{ 1, 2 }, .NonBlocking) catch {}; +} + +fn execute_thread_safe_function_with_error(tsfn: *napi.ThreadSafeFunction(FnArgs, FnReturn, true, 0)) void { + defer tsfn.release(.Release) catch {}; + tsfn.Err(napi.Error.withReason("memory tsfn error"), .NonBlocking) catch {}; +} + +pub fn memory_thread_safe_function(tsfn: *napi.ThreadSafeFunction(FnArgs, FnReturn, true, 0)) !void { + try tsfn.acquire(); + const worker = try std.Thread.spawn(.{}, execute_thread_safe_function, .{tsfn}); + worker.detach(); + + try tsfn.acquire(); + const worker_with_error = try std.Thread.spawn(.{}, execute_thread_safe_function_with_error, .{tsfn}); + worker_with_error.detach(); + + try tsfn.release(.Release); +} diff --git a/examples/memory/src/binary.zig b/examples/memory/src/binary.zig new file mode 100644 index 0000000..fd47211 --- /dev/null +++ b/examples/memory/src/binary.zig @@ -0,0 +1,269 @@ +const std = @import("std"); +const napi = @import("napi"); +const finalizer_state = @import("finalizer_state.zig"); + +var external_buffer_finalizers = std.atomic.Value(usize).init(0); +var external_arraybuffer_finalizers = std.atomic.Value(usize).init(0); +var external_typedarray_finalizers = std.atomic.Value(usize).init(0); +var external_dataview_finalizers = std.atomic.Value(usize).init(0); + +fn onExternalBufferFinalized() void { + _ = external_buffer_finalizers.fetchAdd(1, .monotonic); + finalizer_state.onExternalFinalized(); +} + +fn onExternalArrayBufferFinalized() void { + _ = external_arraybuffer_finalizers.fetchAdd(1, .monotonic); + finalizer_state.onExternalFinalized(); +} + +fn onExternalTypedArrayFinalized() void { + _ = external_typedarray_finalizers.fetchAdd(1, .monotonic); + finalizer_state.onExternalFinalized(); +} + +fn onExternalDataViewFinalized() void { + _ = external_dataview_finalizers.fetchAdd(1, .monotonic); + finalizer_state.onExternalFinalized(); +} + +pub fn reset_external_finalizer_counts() void { + external_buffer_finalizers.store(0, .monotonic); + external_arraybuffer_finalizers.store(0, .monotonic); + external_typedarray_finalizers.store(0, .monotonic); + external_dataview_finalizers.store(0, .monotonic); +} + +pub fn external_finalizer_count() usize { + return external_buffer_finalizers.load(.monotonic) + + external_arraybuffer_finalizers.load(.monotonic) + + external_typedarray_finalizers.load(.monotonic) + + external_dataview_finalizers.load(.monotonic); +} + +pub fn create_buffer_copy(env: napi.Env, len: u32) !napi.Buffer { + var bytes = [_]u8{0} ** 256; + const actual_len = @min(@as(usize, len), bytes.len); + for (bytes[0..actual_len], 0..) |*byte, i| { + byte.* = @intCast(i % 251); + } + return try napi.Buffer.copy(env, bytes[0..actual_len]); +} + +pub fn create_buffer_new(env: napi.Env, len: u32) !napi.Buffer { + var buffer = try napi.Buffer.New(env, @intCast(len)); + @memset(buffer.asSlice(), 0x5a); + return buffer; +} + +pub fn create_external_buffer(env: napi.Env, len: u32) !napi.Buffer { + const allocator = napi.globalAllocator(); + const bytes = try allocator.alloc(u8, len); + errdefer allocator.free(bytes); + for (bytes, 0..) |*byte, i| { + byte.* = @intCast(i % 251); + } + return try napi.Buffer.fromWithFinalizer(env, bytes, onExternalBufferFinalized); +} + +pub fn buffer_length(value: napi.Buffer) usize { + return value.length(); +} + +pub fn buffer_first_byte(value: napi.Buffer) u8 { + return if (value.length() == 0) 0 else value.asConstSlice()[0]; +} + +pub fn create_arraybuffer_copy(env: napi.Env, len: u32) !napi.ArrayBuffer { + var bytes = [_]u8{0} ** 256; + const actual_len = @min(@as(usize, len), bytes.len); + for (bytes[0..actual_len], 0..) |*byte, i| { + byte.* = @intCast((i + 3) % 251); + } + return try napi.ArrayBuffer.copy(env, bytes[0..actual_len]); +} + +pub fn create_arraybuffer_new(env: napi.Env, len: u32) !napi.ArrayBuffer { + var arraybuffer = try napi.ArrayBuffer.New(env, @intCast(len)); + @memset(arraybuffer.asSlice(), 0x6b); + return arraybuffer; +} + +pub fn create_external_arraybuffer(env: napi.Env, len: u32) !napi.ArrayBuffer { + const allocator = napi.globalAllocator(); + const bytes = try allocator.alloc(u8, len); + errdefer allocator.free(bytes); + for (bytes, 0..) |*byte, i| { + byte.* = @intCast((i + 3) % 251); + } + return try napi.ArrayBuffer.fromWithFinalizer(env, bytes, onExternalArrayBufferFinalized); +} + +pub fn arraybuffer_length(value: napi.ArrayBuffer) usize { + return value.length(); +} + +pub fn arraybuffer_first_byte(value: napi.ArrayBuffer) u8 { + return if (value.length() == 0) 0 else value.asConstSlice()[0]; +} + +pub fn create_uint8_typedarray_copy(env: napi.Env) !napi.Uint8Array { + const bytes = [_]u8{ 1, 2, 3, 4 }; + return try napi.Uint8Array.copy(env, &bytes); +} + +pub fn create_int16_typedarray_copy(env: napi.Env) !napi.Int16Array { + const values = [_]i16{ -1, 2, 3 }; + return try napi.Int16Array.copy(env, &values); +} + +pub fn create_uint16_typedarray_copy(env: napi.Env) !napi.Uint16Array { + const values = [_]u16{ 4, 5, 6 }; + return try napi.Uint16Array.copy(env, &values); +} + +pub fn create_int32_typedarray_copy(env: napi.Env) !napi.Int32Array { + const values = [_]i32{ -7, 8, 9 }; + return try napi.Int32Array.copy(env, &values); +} + +pub fn create_uint32_typedarray_copy(env: napi.Env) !napi.Uint32Array { + const values = [_]u32{ 10, 11, 12 }; + return try napi.Uint32Array.copy(env, &values); +} + +pub fn create_float64_typedarray_copy(env: napi.Env) !napi.Float64Array { + const values = [_]f64{ 1.5, 2.25, -0.75 }; + return try napi.Float64Array.copy(env, &values); +} + +pub fn create_external_uint8_typedarray(env: napi.Env) !napi.Uint8Array { + const allocator = napi.globalAllocator(); + const bytes = try allocator.alloc(u8, 4); + errdefer allocator.free(bytes); + @memcpy(bytes, &[_]u8{ 5, 6, 7, 8 }); + const arraybuffer = try napi.ArrayBuffer.fromWithFinalizer(env, bytes, onExternalTypedArrayFinalized); + return try napi.Uint8Array.fromArrayBuffer(env, arraybuffer, bytes.len, 0); +} + +pub fn typedarray_sum(value: napi.Uint8Array) usize { + var sum: usize = 0; + for (value.asConstSlice()) |item| { + sum += item; + } + return sum; +} + +pub fn int16_typedarray_sum(value: napi.Int16Array) i64 { + var sum: i64 = 0; + for (value.asConstSlice()) |item| { + sum += @intCast(item); + } + return sum; +} + +pub fn uint16_typedarray_sum(value: napi.Uint16Array) i64 { + var sum: i64 = 0; + for (value.asConstSlice()) |item| { + sum += @intCast(item); + } + return sum; +} + +pub fn int32_typedarray_sum(value: napi.Int32Array) i64 { + var sum: i64 = 0; + for (value.asConstSlice()) |item| { + sum += @intCast(item); + } + return sum; +} + +pub fn uint32_typedarray_sum(value: napi.Uint32Array) i64 { + var sum: i64 = 0; + for (value.asConstSlice()) |item| { + sum += @intCast(item); + } + return sum; +} + +pub fn float32_typedarray_sum(value: napi.Float32Array) f64 { + var sum: f64 = 0; + for (value.asConstSlice()) |item| { + sum += item; + } + return sum; +} + +pub fn float64_typedarray_sum(value: napi.Float64Array) f64 { + var sum: f64 = 0; + for (value.asConstSlice()) |item| { + sum += item; + } + return sum; +} + +pub fn create_dataview_copy(env: napi.Env) !napi.DataView { + const bytes = [_]u8{ 0x78, 0x56, 0x34, 0x12 }; + return try napi.DataView.copy(env, &bytes); +} + +pub fn create_dataview_new(env: napi.Env, len: u32) !napi.DataView { + var view = try napi.DataView.New(env, @intCast(len)); + @memset(view.asSlice(), 0); + return view; +} + +pub fn create_external_dataview(env: napi.Env) !napi.DataView { + const allocator = napi.globalAllocator(); + const bytes = try allocator.alloc(u8, 4); + errdefer allocator.free(bytes); + @memcpy(bytes, &[_]u8{ 0x78, 0x56, 0x34, 0x12 }); + const arraybuffer = try napi.ArrayBuffer.fromWithFinalizer(env, bytes, onExternalDataViewFinalized); + return try napi.DataView.fromArrayBuffer(env, arraybuffer, 0, bytes.len); +} + +pub fn dataview_length(value: napi.DataView) usize { + return value.byteLength(); +} + +pub fn dataview_uint32_le(value: napi.DataView) !u32 { + return try value.readInt(u32, 0, true); +} + +pub fn dataview_accessors_roundtrip(env: napi.Env) !bool { + const view = try napi.DataView.New(env, 64); + + try view.setInt8(0, -5); + try view.setUint8(1, 250); + try view.setInt16(2, -1234, true); + try view.setUint16(4, 4321, false); + try view.setInt32(8, -123456, true); + try view.setUint32(12, 0x89abcdef, false); + try view.setBigInt64(16, -123456789, true); + try view.setBigUint64(24, 123456789, false); + try view.setFloat32(32, 3.5, true); + try view.setFloat64(40, 6.25, false); + + if ((try view.getInt8(0)) != -5) return false; + if ((try view.getUint8(1)) != 250) return false; + if ((try view.getInt16(2, true)) != -1234) return false; + if ((try view.getUint16(4, false)) != 4321) return false; + if ((try view.getInt32(8, true)) != -123456) return false; + if ((try view.getUint32(12, false)) != 0x89abcdef) return false; + if ((try view.getBigInt64(16, true)) != -123456789) return false; + if ((try view.getBigUint64(24, false)) != 123456789) return false; + if (!std.math.approxEqAbs(f32, try view.getFloat32(32, true), 3.5, 0.001)) return false; + if (!std.math.approxEqAbs(f64, try view.getFloat64(40, false), 6.25, 0.001)) return false; + + return true; +} + +pub fn invalid_typedarray_from_arraybuffer(env: napi.Env) !void { + const arraybuffer = try napi.ArrayBuffer.New(env, 4); + _ = try napi.Uint32Array.fromArrayBuffer(env, arraybuffer, 2, 1); +} + +pub fn invalid_dataview_from_arraybuffer(env: napi.Env) !void { + const arraybuffer = try napi.ArrayBuffer.New(env, 4); + _ = try napi.DataView.fromArrayBuffer(env, arraybuffer, 3, 2); +} diff --git a/examples/memory/src/classes.zig b/examples/memory/src/classes.zig new file mode 100644 index 0000000..d5330bc --- /dev/null +++ b/examples/memory/src/classes.zig @@ -0,0 +1,96 @@ +const std = @import("std"); +const napi = @import("napi"); +const finalizer_state = @import("finalizer_state.zig"); + +var class_finalizers = std.atomic.Value(usize).init(0); + +fn onClassFinalized() void { + _ = class_finalizers.fetchAdd(1, .monotonic); + finalizer_state.onClassFinalized(); +} + +pub fn reset_class_finalizer_count() void { + class_finalizers.store(0, .monotonic); +} + +pub fn class_finalizer_count() usize { + return class_finalizers.load(.monotonic); +} + +const MemoryClassData = struct { + name: []u8, + values: []f32, + + const Self = @This(); + + pub fn init(name: []u8, values: []f32) Self { + return .{ .name = name, .values = values }; + } + + pub fn total(self: *Self) f64 { + var sum: f64 = 0; + for (self.values) |value| { + sum += value; + } + return sum; + } + + pub fn deinit(self: *Self) void { + const allocator = napi.globalAllocator(); + if (self.name.len > 0) { + allocator.free(self.name); + } + if (self.values.len > 0) { + allocator.free(self.values); + } + onClassFinalized(); + } +}; + +const MemoryWithoutInitData = struct { + count: u32, + + const Self = @This(); + + pub fn total(self: *Self) u32 { + return self.count; + } + + pub fn deinit(_: *Self) void { + onClassFinalized(); + } +}; + +const MemoryFactoryData = struct { + name: []u8, + values: []f32, + + const Self = @This(); + + pub fn initWithFactory(name: []u8, values: []f32) Self { + return .{ .name = name, .values = values }; + } + + pub fn total(self: *Self) f64 { + var sum: f64 = 0; + for (self.values) |value| { + sum += value; + } + return sum; + } + + pub fn deinit(self: *Self) void { + const allocator = napi.globalAllocator(); + if (self.name.len > 0) { + allocator.free(self.name); + } + if (self.values.len > 0) { + allocator.free(self.values); + } + onClassFinalized(); + } +}; + +pub const MemoryClass = napi.Class(MemoryClassData); +pub const MemoryClassWithoutInit = napi.ClassWithoutInit(MemoryWithoutInitData); +pub const MemoryFactoryClass = napi.Class(MemoryFactoryData); diff --git a/examples/memory/src/finalizer_state.zig b/examples/memory/src/finalizer_state.zig new file mode 100644 index 0000000..1d6b460 --- /dev/null +++ b/examples/memory/src/finalizer_state.zig @@ -0,0 +1,44 @@ +const std = @import("std"); + +var expected_external = std.atomic.Value(usize).init(0); +var expected_class = std.atomic.Value(usize).init(0); +var seen_external = std.atomic.Value(usize).init(0); +var seen_class = std.atomic.Value(usize).init(0); +var printed = std.atomic.Value(bool).init(false); + +pub fn begin_finalizer_state_check(external_count: usize, class_count: usize) void { + expected_external.store(external_count, .monotonic); + expected_class.store(class_count, .monotonic); + seen_external.store(0, .monotonic); + seen_class.store(0, .monotonic); + printed.store(false, .monotonic); +} + +pub fn onExternalFinalized() void { + _ = seen_external.fetchAdd(1, .monotonic); + maybePrintResult(); +} + +pub fn onClassFinalized() void { + _ = seen_class.fetchAdd(1, .monotonic); + maybePrintResult(); +} + +fn maybePrintResult() void { + const external_count = seen_external.load(.monotonic); + const class_count = seen_class.load(.monotonic); + const external_expected = expected_external.load(.monotonic); + const class_expected = expected_class.load(.monotonic); + + if (external_expected == 0 and class_expected == 0) { + return; + } + if (external_count < external_expected or class_count < class_expected) { + return; + } + if (printed.swap(true, .monotonic)) { + return; + } + + std.debug.print("__ZIG_NAPI_FINALIZER_RESULT__ status=ok external={d} class={d}\n", .{ external_count, class_count }); +} diff --git a/examples/memory/src/hello.zig b/examples/memory/src/hello.zig new file mode 100644 index 0000000..91b29f1 --- /dev/null +++ b/examples/memory/src/hello.zig @@ -0,0 +1,88 @@ +const napi = @import("napi"); + +const tracker = @import("tracker.zig"); +const sync = @import("sync.zig"); +const binary = @import("binary.zig"); +const classes = @import("classes.zig"); +const async_tests = @import("async.zig"); +const finalizer_state = @import("finalizer_state.zig"); + +pub const leak_tracker_start = tracker.leak_tracker_start; +pub const leak_tracker_finish = tracker.leak_tracker_finish; +pub const leak_tracker_abort = tracker.leak_tracker_abort; +pub const tracked_alloc_roundtrip = tracker.tracked_alloc_roundtrip; +pub const begin_finalizer_state_check = finalizer_state.begin_finalizer_state_check; + +pub const hello = sync.hello; +pub const get_object = sync.get_object; +pub const get_optional_object = sync.get_optional_object; +pub const nullable_name_is_null = sync.nullable_name_is_null; +pub const get_named_array = sync.get_named_array; +pub const array_sum = sync.array_sum; +pub const arraylist_sum = sync.arraylist_sum; +pub const nested_summary = sync.nested_summary; +pub const union_kind = sync.union_kind; +pub const create_function = sync.create_function; +pub const call_function = sync.call_function; +pub const call_function_with_reference = sync.call_function_with_reference; +pub const function_reference_ref_count = sync.function_reference_ref_count; +pub const create_bigint_value = sync.create_bigint_value; +pub const create_small_bigint_value = sync.create_small_bigint_value; +pub const bigint_to_i64 = sync.bigint_to_i64; +pub const manual_resolved_promise = sync.manual_resolved_promise; + +pub const create_buffer_copy = binary.create_buffer_copy; +pub const create_buffer_new = binary.create_buffer_new; +pub const create_external_buffer = binary.create_external_buffer; +pub const buffer_length = binary.buffer_length; +pub const buffer_first_byte = binary.buffer_first_byte; +pub const create_arraybuffer_copy = binary.create_arraybuffer_copy; +pub const create_arraybuffer_new = binary.create_arraybuffer_new; +pub const create_external_arraybuffer = binary.create_external_arraybuffer; +pub const arraybuffer_length = binary.arraybuffer_length; +pub const arraybuffer_first_byte = binary.arraybuffer_first_byte; +pub const create_uint8_typedarray_copy = binary.create_uint8_typedarray_copy; +pub const create_int16_typedarray_copy = binary.create_int16_typedarray_copy; +pub const create_uint16_typedarray_copy = binary.create_uint16_typedarray_copy; +pub const create_int32_typedarray_copy = binary.create_int32_typedarray_copy; +pub const create_uint32_typedarray_copy = binary.create_uint32_typedarray_copy; +pub const create_float64_typedarray_copy = binary.create_float64_typedarray_copy; +pub const create_external_uint8_typedarray = binary.create_external_uint8_typedarray; +pub const typedarray_sum = binary.typedarray_sum; +pub const int16_typedarray_sum = binary.int16_typedarray_sum; +pub const uint16_typedarray_sum = binary.uint16_typedarray_sum; +pub const int32_typedarray_sum = binary.int32_typedarray_sum; +pub const uint32_typedarray_sum = binary.uint32_typedarray_sum; +pub const float32_typedarray_sum = binary.float32_typedarray_sum; +pub const float64_typedarray_sum = binary.float64_typedarray_sum; +pub const create_dataview_copy = binary.create_dataview_copy; +pub const create_dataview_new = binary.create_dataview_new; +pub const create_external_dataview = binary.create_external_dataview; +pub const dataview_length = binary.dataview_length; +pub const dataview_uint32_le = binary.dataview_uint32_le; +pub const dataview_accessors_roundtrip = binary.dataview_accessors_roundtrip; +pub const invalid_typedarray_from_arraybuffer = binary.invalid_typedarray_from_arraybuffer; +pub const invalid_dataview_from_arraybuffer = binary.invalid_dataview_from_arraybuffer; +pub const reset_external_finalizer_counts = binary.reset_external_finalizer_counts; +pub const external_finalizer_count = binary.external_finalizer_count; + +pub const MemoryClass = classes.MemoryClass; +pub const MemoryClassWithoutInit = classes.MemoryClassWithoutInit; +pub const MemoryFactoryClass = classes.MemoryFactoryClass; +pub const reset_class_finalizer_count = classes.reset_class_finalizer_count; +pub const class_finalizer_count = classes.class_finalizer_count; + +pub const memory_async_summary = async_tests.memory_async_summary; +pub const memory_async_summary_single = async_tests.memory_async_summary_single; +pub const memory_async_void = async_tests.memory_async_void; +pub const memory_async_fail = async_tests.memory_async_fail; +pub const memory_async_progress = async_tests.memory_async_progress; +pub const memory_event_mode_progress = async_tests.memory_event_mode_progress; +pub const memory_abortable_count = async_tests.memory_abortable_count; +pub const memory_abortable_slow_count = async_tests.memory_abortable_slow_count; +pub const memory_worker = async_tests.memory_worker; +pub const memory_thread_safe_function = async_tests.memory_thread_safe_function; + +comptime { + napi.NODE_API_MODULE("hello", @This()); +} diff --git a/examples/memory/src/sync.zig b/examples/memory/src/sync.zig new file mode 100644 index 0000000..06344ba --- /dev/null +++ b/examples/memory/src/sync.zig @@ -0,0 +1,131 @@ +const std = @import("std"); +const napi = @import("napi"); +const ArrayList = std.ArrayList; + +const Person = struct { + name: []u8, + age: f64, + is_student: bool, +}; + +const NamedTuple = struct { f32, bool, []u8 }; + +const NumberOrText = union(enum) { + number: f64, + text: []const u8, +}; + +const OptionalPerson = struct { + name: []u8, + age: ?f64, + is_student: ?bool, +}; + +const NestedPayload = struct { + title: []u8, + values: []f32, + tuple: NamedTuple, + maybe: ?[]u8, +}; + +const FnArgs = struct { i32, i32 }; +const FnReturn = i32; + +pub fn hello(env: napi.Env, name: []u8) napi.String { + const allocator = std.heap.page_allocator; + const message = std.fmt.allocPrint(allocator, "Hello, {s}!", .{name}) catch @panic("OOM"); + defer allocator.free(message); + return napi.String.New(env, message); +} + +pub fn get_object(config: Person) Person { + return config; +} + +pub fn get_optional_object(config: OptionalPerson) OptionalPerson { + return .{ + .name = config.name, + .age = config.age orelse 18, + .is_student = config.is_student orelse true, + }; +} + +pub fn nullable_name_is_null(name: ?[]u8) bool { + return name == null; +} + +pub fn get_named_array(array: NamedTuple) NamedTuple { + return array; +} + +pub fn array_sum(values: []f32) f64 { + var sum: f64 = 0; + for (values) |value| { + sum += value; + } + return sum; +} + +pub fn arraylist_sum(values: ArrayList(f32)) f64 { + var sum: f64 = 0; + for (values.items) |value| { + sum += value; + } + return sum; +} + +pub fn nested_summary(payload: NestedPayload) usize { + return payload.title.len + payload.values.len + payload.tuple[2].len + if (payload.maybe) |text| text.len else 0; +} + +pub fn union_kind(value: NumberOrText) []const u8 { + return switch (value) { + .number => "number", + .text => "text", + }; +} + +fn basic_function(left: i32, right: i32) i32 { + return left + right; +} + +pub fn create_function(env: napi.Env) !napi.Function(FnArgs, FnReturn) { + return try napi.Function(FnArgs, FnReturn).New(env, "memory_basic_function", basic_function); +} + +pub fn call_function(cb: napi.Function(FnArgs, FnReturn)) !i32 { + return try cb.Call(.{ 20, 22 }); +} + +pub fn call_function_with_reference(env: napi.Env, cb: napi.Function(FnArgs, FnReturn)) !i32 { + var reference = try cb.CreateRef(); + defer reference.Unref(env) catch @panic("Failed to unref function reference"); + + const function = try reference.GetValue(env); + return try function.Call(.{ 19, 23 }); +} + +pub fn function_reference_ref_count(env: napi.Env, cb: napi.Function(FnArgs, FnReturn)) !u32 { + var reference = try cb.CreateRef(); + const count = try reference.Ref(env); + try reference.Unref(env); + return count; +} + +pub fn create_bigint_value(env: napi.Env) napi.BigInt { + return napi.BigInt.New(env, @as(i128, 9007199254740993)); +} + +pub fn create_small_bigint_value(env: napi.Env) napi.BigInt { + return napi.BigInt.New(env, @as(i128, 42)); +} + +pub fn bigint_to_i64(value: napi.BigInt) i64 { + return napi.BigInt.from_napi_value(value.env, value.raw, i64); +} + +pub fn manual_resolved_promise(env: napi.Env) !napi.Promise { + var promise = napi.Promise.New(env); + try promise.Resolve(@as(i32, 42)); + return promise; +} diff --git a/examples/memory/src/tracker.zig b/examples/memory/src/tracker.zig new file mode 100644 index 0000000..428930a --- /dev/null +++ b/examples/memory/src/tracker.zig @@ -0,0 +1,69 @@ +const std = @import("std"); +const napi = @import("napi"); + +const DebugAllocator = std.heap.DebugAllocator(.{ + .stack_trace_frames = 0, +}); + +var debug_allocator: DebugAllocator = .init; +var tracking = false; +var previous_allocator: ?std.mem.Allocator = null; + +pub fn leak_tracker_start() void { + if (tracking) { + if (previous_allocator) |allocator| { + napi.setOperationAllocator(allocator); + } else { + napi.resetOperationAllocator(); + } + previous_allocator = null; + tracking = false; + _ = debug_allocator.deinit(); + debug_allocator = .init; + } + + previous_allocator = napi.globalAllocator(); + napi.setOperationAllocator(debug_allocator.allocator()); + tracking = true; +} + +pub fn leak_tracker_finish() bool { + if (!tracking) { + return true; + } + + if (previous_allocator) |allocator| { + napi.setOperationAllocator(allocator); + } else { + napi.resetOperationAllocator(); + } + previous_allocator = null; + tracking = false; + + const result = debug_allocator.deinit(); + debug_allocator = .init; + return result == .ok; +} + +pub fn leak_tracker_abort() void { + if (!tracking) { + return; + } + + if (previous_allocator) |allocator| { + napi.setOperationAllocator(allocator); + } else { + napi.resetOperationAllocator(); + } + previous_allocator = null; + tracking = false; +} + +pub fn tracked_alloc_roundtrip(len: u32) bool { + const allocator = napi.globalAllocator(); + const buf = allocator.alloc(u8, len) catch return false; + defer allocator.free(buf); + + @memset(buf, 0xaa); + return buf.len == len; +} diff --git a/harmony_example/.gitignore b/harmony_example/.gitignore deleted file mode 100644 index 7a555b1..0000000 --- a/harmony_example/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -/node_modules -/oh_modules -/local.properties -/.idea -**/build -/.hvigor -.cxx -/.clangd -/.clang-format -/.clang-tidy -**/.test -/.appanalyzer -*.so \ No newline at end of file diff --git a/harmony_example/AppScope/app.json5 b/harmony_example/AppScope/app.json5 deleted file mode 100644 index 0a21739..0000000 --- a/harmony_example/AppScope/app.json5 +++ /dev/null @@ -1,10 +0,0 @@ -{ - "app": { - "bundleName": "com.example.zig_example", - "vendor": "example", - "versionCode": 1000000, - "versionName": "1.0.0", - "icon": "$media:layered_image", - "label": "$string:app_name" - } -} diff --git a/harmony_example/AppScope/resources/base/element/string.json b/harmony_example/AppScope/resources/base/element/string.json deleted file mode 100644 index ba579ff..0000000 --- a/harmony_example/AppScope/resources/base/element/string.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "string": [ - { - "name": "app_name", - "value": "zig_example" - } - ] -} diff --git a/harmony_example/AppScope/resources/base/media/background.png b/harmony_example/AppScope/resources/base/media/background.png deleted file mode 100644 index 923f2b3..0000000 Binary files a/harmony_example/AppScope/resources/base/media/background.png and /dev/null differ diff --git a/harmony_example/AppScope/resources/base/media/foreground.png b/harmony_example/AppScope/resources/base/media/foreground.png deleted file mode 100644 index eb94275..0000000 Binary files a/harmony_example/AppScope/resources/base/media/foreground.png and /dev/null differ diff --git a/harmony_example/AppScope/resources/base/media/layered_image.json b/harmony_example/AppScope/resources/base/media/layered_image.json deleted file mode 100644 index fb49920..0000000 --- a/harmony_example/AppScope/resources/base/media/layered_image.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "layered-image": - { - "background" : "$media:background", - "foreground" : "$media:foreground" - } -} \ No newline at end of file diff --git a/harmony_example/build-profile.json5 b/harmony_example/build-profile.json5 deleted file mode 100644 index 3ed2fd7..0000000 --- a/harmony_example/build-profile.json5 +++ /dev/null @@ -1,43 +0,0 @@ -{ - "app": { - "signingConfigs": [], - "products": [ - { - "name": "default", - "signingConfig": "default", - "targetSdkVersion": "6.0.0(20)", - "compatibleSdkVersion": "6.0.0(20)", - "runtimeOS": "HarmonyOS", - "buildOption": { - "nativeCompiler": "BiSheng", - "strictMode": { - "caseSensitiveCheck": true, - "useNormalizedOHMUrl": true - } - } - } - ], - "buildModeSet": [ - { - "name": "debug", - }, - { - "name": "release" - } - ] - }, - "modules": [ - { - "name": "entry", - "srcPath": "./entry", - "targets": [ - { - "name": "default", - "applyToProducts": [ - "default" - ] - } - ] - } - ] -} \ No newline at end of file diff --git a/harmony_example/code-linter.json5 b/harmony_example/code-linter.json5 deleted file mode 100644 index 073990f..0000000 --- a/harmony_example/code-linter.json5 +++ /dev/null @@ -1,32 +0,0 @@ -{ - "files": [ - "**/*.ets" - ], - "ignore": [ - "**/src/ohosTest/**/*", - "**/src/test/**/*", - "**/src/mock/**/*", - "**/node_modules/**/*", - "**/oh_modules/**/*", - "**/build/**/*", - "**/.preview/**/*" - ], - "ruleSet": [ - "plugin:@performance/recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@security/no-unsafe-aes": "error", - "@security/no-unsafe-hash": "error", - "@security/no-unsafe-mac": "warn", - "@security/no-unsafe-dh": "error", - "@security/no-unsafe-dsa": "error", - "@security/no-unsafe-ecdsa": "error", - "@security/no-unsafe-rsa-encrypt": "error", - "@security/no-unsafe-rsa-sign": "error", - "@security/no-unsafe-rsa-key": "error", - "@security/no-unsafe-dsa-key": "error", - "@security/no-unsafe-dh-key": "error", - "@security/no-unsafe-3des": "error" - } -} \ No newline at end of file diff --git a/harmony_example/entry/.gitignore b/harmony_example/entry/.gitignore deleted file mode 100644 index e2713a2..0000000 --- a/harmony_example/entry/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -/node_modules -/oh_modules -/.preview -/build -/.cxx -/.test \ No newline at end of file diff --git a/harmony_example/entry/build-profile.json5 b/harmony_example/entry/build-profile.json5 deleted file mode 100644 index 1bdab41..0000000 --- a/harmony_example/entry/build-profile.json5 +++ /dev/null @@ -1,44 +0,0 @@ -{ - "apiType": "stageMode", - "buildOption": { - "resOptions": { - "copyCodeResource": { - "enable": false - } - }, - "externalNativeOptions": { - "path": "./src/main/cpp/CMakeLists.txt", - "arguments": "", - "cppFlags": "", - } - }, - "buildOptionSet": [ - { - "name": "release", - "arkOptions": { - "obfuscation": { - "ruleOptions": { - "enable": false, - "files": [ - "./obfuscation-rules.txt" - ] - } - } - }, - "nativeLib": { - "debugSymbol": { - "strip": true, - "exclude": [] - } - } - }, - ], - "targets": [ - { - "name": "default" - }, - { - "name": "ohosTest", - } - ] -} \ No newline at end of file diff --git a/harmony_example/entry/hvigorfile.ts b/harmony_example/entry/hvigorfile.ts deleted file mode 100644 index b0e3a1a..0000000 --- a/harmony_example/entry/hvigorfile.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { hapTasks } from '@ohos/hvigor-ohos-plugin'; - -export default { - system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ - plugins: [] /* Custom plugin to extend the functionality of Hvigor. */ -} \ No newline at end of file diff --git a/harmony_example/entry/libs/arm64-v8a/.gitkeep b/harmony_example/entry/libs/arm64-v8a/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/harmony_example/entry/obfuscation-rules.txt b/harmony_example/entry/obfuscation-rules.txt deleted file mode 100644 index 272efb6..0000000 --- a/harmony_example/entry/obfuscation-rules.txt +++ /dev/null @@ -1,23 +0,0 @@ -# Define project specific obfuscation rules here. -# You can include the obfuscation configuration files in the current module's build-profile.json5. -# -# For more details, see -# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5 - -# Obfuscation options: -# -disable-obfuscation: disable all obfuscations -# -enable-property-obfuscation: obfuscate the property names -# -enable-toplevel-obfuscation: obfuscate the names in the global scope -# -compact: remove unnecessary blank spaces and all line feeds -# -remove-log: remove all console.* statements -# -print-namecache: print the name cache that contains the mapping from the old names to new names -# -apply-namecache: reuse the given cache file - -# Keep options: -# -keep-property-name: specifies property names that you want to keep -# -keep-global-name: specifies names that you want to keep in the global scope - --enable-property-obfuscation --enable-toplevel-obfuscation --enable-filename-obfuscation --enable-export-obfuscation \ No newline at end of file diff --git a/harmony_example/entry/oh-package-lock.json5 b/harmony_example/entry/oh-package-lock.json5 deleted file mode 100644 index dd4113d..0000000 --- a/harmony_example/entry/oh-package-lock.json5 +++ /dev/null @@ -1,26 +0,0 @@ -{ - "meta": { - "stableOrder": true, - "enableUnifiedLockfile": false - }, - "lockfileVersion": 3, - "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", - "specifiers": { - "libentry.so@src/main/cpp/types/libentry": "libentry.so@src/main/cpp/types/libentry", - "libhello.so@src/main/cpp/types/libhello": "libhello.so@src/main/cpp/types/libhello" - }, - "packages": { - "libentry.so@src/main/cpp/types/libentry": { - "name": "libentry.so", - "version": "1.0.0", - "resolved": "", - "registryType": "local" - }, - "libhello.so@src/main/cpp/types/libhello": { - "name": "libhello.so", - "version": "1.0.0", - "resolved": "", - "registryType": "local" - } - } -} \ No newline at end of file diff --git a/harmony_example/entry/oh-package.json5 b/harmony_example/entry/oh-package.json5 deleted file mode 100644 index d18a500..0000000 --- a/harmony_example/entry/oh-package.json5 +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "entry", - "version": "1.0.0", - "description": "Please describe the basic information.", - "main": "", - "author": "", - "license": "", - "dependencies": { - "libentry.so": "file:./src/main/cpp/types/libentry", - "libhello.so": "file:./src/main/cpp/types/libhello" - } -} \ No newline at end of file diff --git a/harmony_example/entry/src/main/cpp/CMakeLists.txt b/harmony_example/entry/src/main/cpp/CMakeLists.txt deleted file mode 100644 index aab11c0..0000000 --- a/harmony_example/entry/src/main/cpp/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -# the minimum version of CMake. -cmake_minimum_required(VERSION 3.5.0) -project(harmony_example) - -set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}) - -if(DEFINED PACKAGE_FIND_FILE) - include(${PACKAGE_FIND_FILE}) -endif() - -include_directories(${NATIVERENDER_ROOT_PATH} - ${NATIVERENDER_ROOT_PATH}/include) - -add_library(entry SHARED napi_init.cpp) -target_link_libraries(entry PUBLIC libace_napi.z.so) \ No newline at end of file diff --git a/harmony_example/entry/src/main/cpp/napi_init.cpp b/harmony_example/entry/src/main/cpp/napi_init.cpp deleted file mode 100644 index 987bd48..0000000 --- a/harmony_example/entry/src/main/cpp/napi_init.cpp +++ /dev/null @@ -1,53 +0,0 @@ -#include "napi/native_api.h" - -static napi_value Add(napi_env env, napi_callback_info info) -{ - size_t argc = 2; - napi_value args[2] = {nullptr}; - - napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); - - napi_valuetype valuetype0; - napi_typeof(env, args[0], &valuetype0); - - napi_valuetype valuetype1; - napi_typeof(env, args[1], &valuetype1); - - double value0; - napi_get_value_double(env, args[0], &value0); - - double value1; - napi_get_value_double(env, args[1], &value1); - - napi_value sum; - napi_create_double(env, value0 + value1, &sum); - - return sum; - -} - -EXTERN_C_START -static napi_value Init(napi_env env, napi_value exports) -{ - napi_property_descriptor desc[] = { - { "add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr } - }; - napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); - return exports; -} -EXTERN_C_END - -static napi_module demoModule = { - .nm_version = 1, - .nm_flags = 0, - .nm_filename = nullptr, - .nm_register_func = Init, - .nm_modname = "entry", - .nm_priv = ((void*)0), - .reserved = { 0 }, -}; - -extern "C" __attribute__((constructor)) void RegisterEntryModule(void) -{ - napi_module_register(&demoModule); -} diff --git a/harmony_example/entry/src/main/cpp/types/libentry/Index.d.ts b/harmony_example/entry/src/main/cpp/types/libentry/Index.d.ts deleted file mode 100644 index e44f361..0000000 --- a/harmony_example/entry/src/main/cpp/types/libentry/Index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export const add: (a: number, b: number) => number; \ No newline at end of file diff --git a/harmony_example/entry/src/main/cpp/types/libentry/oh-package.json5 b/harmony_example/entry/src/main/cpp/types/libentry/oh-package.json5 deleted file mode 100644 index ea41072..0000000 --- a/harmony_example/entry/src/main/cpp/types/libentry/oh-package.json5 +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "libentry.so", - "types": "./Index.d.ts", - "version": "1.0.0", - "description": "Please describe the basic information." -} \ No newline at end of file diff --git a/harmony_example/entry/src/main/cpp/types/libhello/Index.d.ts b/harmony_example/entry/src/main/cpp/types/libhello/Index.d.ts deleted file mode 100644 index e156915..0000000 --- a/harmony_example/entry/src/main/cpp/types/libhello/Index.d.ts +++ /dev/null @@ -1,335 +0,0 @@ -import buffer from "@ohos.buffer"; - -/** - * Basic example module for zig-napi - * OpenHarmony/HarmonyNext native module written in Zig - */ - -// ============== Number Functions ============== - -/** - * Adds two 32-bit signed integers - * @param left - The first integer - * @param right - The second integer - * @returns The sum of left and right - */ -export declare function test_i32(left: number, right: number): number; - -/** - * Adds two 32-bit floating-point numbers - * @param left - The first float - * @param right - The second float - * @returns The sum of left and right - */ -export declare function test_f32(left: number, right: number): number; - -/** - * Adds two 32-bit unsigned integers - * @param left - The first unsigned integer - * @param right - The second unsigned integer - * @returns The sum of left and right - */ -export declare function test_u32(left: number, right: number): number; - -// ============== String Functions ============== - -/** - * Returns a greeting message - * @param name - The name to greet - * @returns A greeting string "Hello, {name}!" - */ -export declare function hello(name: string): string; - -/** - * A constant text string - */ -export declare const text: string; - -// ============== Error Functions ============== - -/** - * Throws a test error - * @throws {Error} Always throws an error with reason "test" - */ -export declare function throw_error(): void; - -// ============== Worker Functions ============== - -/** - * Calculates fibonacci number asynchronously (fire and forget) - * @param n - The fibonacci index - */ -export declare function fib(n: number): void; - -/** - * Calculates fibonacci number asynchronously with Promise - * @param n - The fibonacci index - * @returns A Promise that resolves when calculation is complete - */ -export declare function fib_async(n: number): Promise; - -// ============== Array Functions ============== - -/** - * Takes an array and returns it - * @param array - An array of numbers - * @returns The same array - */ -export declare function get_and_return_array(array: number[]): number[]; - -/** - * Takes a tuple array and returns it - * @param array - A tuple of [number, boolean, string] - * @returns The same tuple - */ -export declare function get_named_array( - array: [number, boolean, string] -): [number, boolean, string]; - -/** - * Takes an ArrayList and returns it - * @param array - An array of numbers - * @returns The same array - */ -export declare function get_arraylist(array: number[]): number[]; - -// ============== Object Types ============== - -/** - * Full field object with all required fields - */ -export interface FullField { - name: string; - age: number; - is_student: boolean; -} - -/** - * Object with optional fields - */ -export interface OptionalField { - name: string; - age?: number; - is_student?: boolean; -} - -/** - * Object with nullable field - */ -export interface NullableField { - name: string | null; -} - -// ============== Object Functions ============== - -/** - * Takes a full field object and returns it - * @param config - Object with name, age, and is_student - * @returns The same object - */ -export declare function get_object(config: FullField): FullField; - -/** - * Takes an object with optional fields - * @param config - Object with name (required), age and is_student (optional) - * @returns Object with default values applied (age: 18, is_student: true) - */ -export declare function get_object_optional( - config: OptionalField -): OptionalField; - -/** - * Takes an optional object and returns it - * @param config - Object with optional fields - * @returns The same object - */ -export declare function get_optional_object_and_return_optional( - config: OptionalField -): OptionalField; - -/** - * Takes an object with nullable name field - * @param config - Object with nullable name - * @returns The same object - */ -export declare function get_nullable_object( - config: NullableField -): NullableField; - -/** - * Returns a nullable object with null name - * @returns Object with name set to null - */ -export declare function return_nullable(): NullableField; - -// ============== Function Types ============== - -/** - * Callback function type that takes two numbers and returns a number - */ -export type CallbackFunction = (arg0: number, arg1: number) => number; - -// ============== Function Functions ============== - -/** - * Calls the provided callback function with (1, 2) - * @param cb - A callback function that takes two numbers and returns a number - * @returns The result of calling cb(1, 2) - */ -export declare function call_function(cb: CallbackFunction): number; - -/** - * Adds two numbers - * @param left - The first number - * @param right - The second number - * @returns The sum of left and right - */ -export declare function basic_function(left: number, right: number): number; - -/** - * Creates a new function that wraps basic_function - * @returns A function that adds two numbers - */ -export declare function create_function(): CallbackFunction; - -/** - * Creates a native reference to the callback, reads it back, and calls it with (1, 2) - * @param cb - A callback function that takes two numbers and returns a number - * @returns The result of calling cb(1, 2) - */ -export declare function call_function_with_reference(cb: CallbackFunction): number; - -// ============== Thread Safe Function ============== - -/** - * Calls the thread safe function from multiple threads - * @param tsfn - A thread-safe callback function - */ -export declare function call_thread_safe_function(tsfn: CallbackFunction): void; - -// ============== Class Types ============== - -/** - * Basic test class with name and age properties - */ -export declare class TestClass { - constructor(name: string, age: number); - name: string; - age: number; -} - -/** - * Test class with custom init function - * Constructor takes (age, name) instead of field order - */ -export declare class TestWithInitClass { - constructor(age: number, name: string); - name: string; - age: number; - static readonly hello: string; -} - -/** - * Test class without constructor (abstract-like) - */ -export declare class TestWithoutInitClass { - private constructor(); - name: string; - age: number; - static readonly hello: string; -} - -/** - * Test class with factory method - */ -export declare class TestFactoryClass { - constructor(age: number, name: string); - name: string; - age: number; - /** - * Formats the object as a string - * @returns Formatted string representation - */ - format(): string; -} - -// ============== Log Functions ============== - -/** - * Tests hilog functionality (OpenHarmony logging) - */ -export declare function test_hilog(): void; - -// ============== Buffer Functions ============== - -/** - * Creates a new buffer - * @param size - The size of the buffer - * @returns The new buffer - */ -export declare function create_buffer(): ArrayBuffer; - -/** - * Gets the buffer as a string - * @param buffer - The buffer - * @returns The buffer as a string - */ -export declare function get_buffer(buffer: ArrayBuffer): number; - -/** - * Gets the buffer as a string - * @param buffer - The buffer - * @returns The buffer as a string - */ -export declare function get_buffer_as_string(buffer: ArrayBuffer): string; - -// ============== TypedArray Functions ============== - -/** - * Creates a Uint8Array with 4 bytes: [1, 2, 3, 4] - * @returns The created typed array - */ -export declare function create_uint8_typedarray(): Uint8Array; - -/** - * Gets the length of a Uint8Array - * @param array - The typed array - * @returns The element count - */ -export declare function get_uint8_typedarray_length(array: Uint8Array): number; - -/** - * Sums all items in a Float32Array - * @param array - The typed array - * @returns The sum of all elements - */ -export declare function sum_float32_typedarray(array: Float32Array): number; - -// ============== DataView Functions ============== - -/** - * Creates a DataView with 4 bytes initialized to 0x78, 0x56, 0x34, 0x12 - * @returns The created data view - */ -export declare function create_dataview(): DataView; - -/** - * Gets the byte length of a DataView - * @param view - The data view - * @returns The byte length - */ -export declare function get_dataview_length(view: DataView): number; - -/** - * Gets the first byte of a DataView - * @param view - The data view - * @returns The first byte, or 0 if empty - */ -export declare function get_dataview_first_byte(view: DataView): number; - -/** - * Reads the first 4 bytes of a DataView as a little-endian uint32 - * @param view - The data view - * @returns The uint32 value - */ -export declare function get_dataview_uint32_le(view: DataView): number; diff --git a/harmony_example/entry/src/main/cpp/types/libhello/oh-package.json5 b/harmony_example/entry/src/main/cpp/types/libhello/oh-package.json5 deleted file mode 100644 index fcde40c..0000000 --- a/harmony_example/entry/src/main/cpp/types/libhello/oh-package.json5 +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "libhello.so", - "types": "./Index.d.ts", - "version": "1.0.0", - "description": "Please describe the basic information." -} \ No newline at end of file diff --git a/harmony_example/entry/src/main/ets/entryability/EntryAbility.ets b/harmony_example/entry/src/main/ets/entryability/EntryAbility.ets deleted file mode 100644 index 091797f..0000000 --- a/harmony_example/entry/src/main/ets/entryability/EntryAbility.ets +++ /dev/null @@ -1,48 +0,0 @@ -import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit'; -import { hilog } from '@kit.PerformanceAnalysisKit'; -import { window } from '@kit.ArkUI'; - -const DOMAIN = 0x0000; - -export default class EntryAbility extends UIAbility { - onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { - try { - this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET); - } catch (err) { - hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err)); - } - hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate'); - } - - onDestroy(): void { - hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy'); - } - - onWindowStageCreate(windowStage: window.WindowStage): void { - // Main window is created, set main page for this ability - hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); - - windowStage.loadContent('pages/Index', (err) => { - if (err.code) { - hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err)); - return; - } - hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.'); - }); - } - - onWindowStageDestroy(): void { - // Main window is destroyed, release UI related resources - hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); - } - - onForeground(): void { - // Ability has brought to foreground - hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground'); - } - - onBackground(): void { - // Ability has back to background - hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground'); - } -} \ No newline at end of file diff --git a/harmony_example/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets b/harmony_example/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets deleted file mode 100644 index 8e4de99..0000000 --- a/harmony_example/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets +++ /dev/null @@ -1,16 +0,0 @@ -import { hilog } from '@kit.PerformanceAnalysisKit'; -import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit'; - -const DOMAIN = 0x0000; - -export default class EntryBackupAbility extends BackupExtensionAbility { - async onBackup() { - hilog.info(DOMAIN, 'testTag', 'onBackup ok'); - await Promise.resolve(); - } - - async onRestore(bundleVersion: BundleVersion) { - hilog.info(DOMAIN, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion)); - await Promise.resolve(); - } -} \ No newline at end of file diff --git a/harmony_example/entry/src/main/ets/pages/Index.ets b/harmony_example/entry/src/main/ets/pages/Index.ets deleted file mode 100644 index 9931cc8..0000000 --- a/harmony_example/entry/src/main/ets/pages/Index.ets +++ /dev/null @@ -1,41 +0,0 @@ -import { hilog } from '@kit.PerformanceAnalysisKit'; -import testNapi from 'libhello.so'; -import buffer from '@ohos.buffer'; - -const DOMAIN = 0x0000; - -@Entry -@Component -struct Index { - @State message: string = 'Hello World'; - - aboutToAppear(): void { - const napi = testNapi; - - const buf = napi.create_buffer(); - - const buff = buffer.from([1,2,3]); - const ret = napi.get_buffer(buff.buffer); - console.log(`${ret}`) - - const buff3 = buffer.from("hello world"); - const ret1 = napi.get_buffer_as_string(buff3.buffer); - - console.log(`${ret1}`) - } - - build() { - Row() { - Column() { - Text(this.message) - .fontSize($r('app.float.page_text_font_size')) - .fontWeight(FontWeight.Bold) - .onClick(() => { - this.message = 'Welcome'; - }) - } - .width('100%') - } - .height('100%') - } -} diff --git a/harmony_example/entry/src/main/module.json5 b/harmony_example/entry/src/main/module.json5 deleted file mode 100644 index 53024e8..0000000 --- a/harmony_example/entry/src/main/module.json5 +++ /dev/null @@ -1,50 +0,0 @@ -{ - "module": { - "name": "entry", - "type": "entry", - "description": "$string:module_desc", - "mainElement": "EntryAbility", - "deviceTypes": [ - "phone" - ], - "deliveryWithInstall": true, - "installationFree": false, - "pages": "$profile:main_pages", - "abilities": [ - { - "name": "EntryAbility", - "srcEntry": "./ets/entryability/EntryAbility.ets", - "description": "$string:EntryAbility_desc", - "icon": "$media:layered_image", - "label": "$string:EntryAbility_label", - "startWindowIcon": "$media:startIcon", - "startWindowBackground": "$color:start_window_background", - "exported": true, - "skills": [ - { - "entities": [ - "entity.system.home" - ], - "actions": [ - "ohos.want.action.home" - ] - } - ] - } - ], - "extensionAbilities": [ - { - "name": "EntryBackupAbility", - "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets", - "type": "backup", - "exported": false, - "metadata": [ - { - "name": "ohos.extension.backup", - "resource": "$profile:backup_config" - } - ], - } - ] - } -} \ No newline at end of file diff --git a/harmony_example/entry/src/main/resources/base/element/color.json b/harmony_example/entry/src/main/resources/base/element/color.json deleted file mode 100644 index 3c71296..0000000 --- a/harmony_example/entry/src/main/resources/base/element/color.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "color": [ - { - "name": "start_window_background", - "value": "#FFFFFF" - } - ] -} \ No newline at end of file diff --git a/harmony_example/entry/src/main/resources/base/element/float.json b/harmony_example/entry/src/main/resources/base/element/float.json deleted file mode 100644 index 33ea223..0000000 --- a/harmony_example/entry/src/main/resources/base/element/float.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "float": [ - { - "name": "page_text_font_size", - "value": "50fp" - } - ] -} diff --git a/harmony_example/entry/src/main/resources/base/element/string.json b/harmony_example/entry/src/main/resources/base/element/string.json deleted file mode 100644 index f945955..0000000 --- a/harmony_example/entry/src/main/resources/base/element/string.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "string": [ - { - "name": "module_desc", - "value": "module description" - }, - { - "name": "EntryAbility_desc", - "value": "description" - }, - { - "name": "EntryAbility_label", - "value": "label" - } - ] -} \ No newline at end of file diff --git a/harmony_example/entry/src/main/resources/base/media/background.png b/harmony_example/entry/src/main/resources/base/media/background.png deleted file mode 100644 index 923f2b3..0000000 Binary files a/harmony_example/entry/src/main/resources/base/media/background.png and /dev/null differ diff --git a/harmony_example/entry/src/main/resources/base/media/foreground.png b/harmony_example/entry/src/main/resources/base/media/foreground.png deleted file mode 100644 index 97014d3..0000000 Binary files a/harmony_example/entry/src/main/resources/base/media/foreground.png and /dev/null differ diff --git a/harmony_example/entry/src/main/resources/base/media/layered_image.json b/harmony_example/entry/src/main/resources/base/media/layered_image.json deleted file mode 100644 index fb49920..0000000 --- a/harmony_example/entry/src/main/resources/base/media/layered_image.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "layered-image": - { - "background" : "$media:background", - "foreground" : "$media:foreground" - } -} \ No newline at end of file diff --git a/harmony_example/entry/src/main/resources/base/media/startIcon.png b/harmony_example/entry/src/main/resources/base/media/startIcon.png deleted file mode 100644 index 205ad8b..0000000 Binary files a/harmony_example/entry/src/main/resources/base/media/startIcon.png and /dev/null differ diff --git a/harmony_example/entry/src/main/resources/base/profile/backup_config.json b/harmony_example/entry/src/main/resources/base/profile/backup_config.json deleted file mode 100644 index 78f40ae..0000000 --- a/harmony_example/entry/src/main/resources/base/profile/backup_config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "allowToBackupRestore": true -} \ No newline at end of file diff --git a/harmony_example/entry/src/main/resources/base/profile/main_pages.json b/harmony_example/entry/src/main/resources/base/profile/main_pages.json deleted file mode 100644 index 1898d94..0000000 --- a/harmony_example/entry/src/main/resources/base/profile/main_pages.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "src": [ - "pages/Index" - ] -} diff --git a/harmony_example/entry/src/main/resources/dark/element/color.json b/harmony_example/entry/src/main/resources/dark/element/color.json deleted file mode 100644 index 79b11c2..0000000 --- a/harmony_example/entry/src/main/resources/dark/element/color.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "color": [ - { - "name": "start_window_background", - "value": "#000000" - } - ] -} \ No newline at end of file diff --git a/harmony_example/entry/src/mock/Libentry.mock.ets b/harmony_example/entry/src/mock/Libentry.mock.ets deleted file mode 100644 index c217171..0000000 --- a/harmony_example/entry/src/mock/Libentry.mock.ets +++ /dev/null @@ -1,7 +0,0 @@ -const NativeMock: Record = { - 'add': (a: number, b: number) => { - return a + b; - }, -}; - -export default NativeMock; \ No newline at end of file diff --git a/harmony_example/entry/src/mock/mock-config.json5 b/harmony_example/entry/src/mock/mock-config.json5 deleted file mode 100644 index 6540976..0000000 --- a/harmony_example/entry/src/mock/mock-config.json5 +++ /dev/null @@ -1,5 +0,0 @@ -{ - "libentry.so": { - "source": "src/mock/Libentry.mock.ets" - } -} \ No newline at end of file diff --git a/harmony_example/entry/src/ohosTest/ets/test/Ability.test.ets b/harmony_example/entry/src/ohosTest/ets/test/Ability.test.ets deleted file mode 100644 index 85c78f6..0000000 --- a/harmony_example/entry/src/ohosTest/ets/test/Ability.test.ets +++ /dev/null @@ -1,35 +0,0 @@ -import { hilog } from '@kit.PerformanceAnalysisKit'; -import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; - -export default function abilityTest() { - describe('ActsAbilityTest', () => { - // Defines a test suite. Two parameters are supported: test suite name and test suite function. - beforeAll(() => { - // Presets an action, which is performed only once before all test cases of the test suite start. - // This API supports only one parameter: preset action function. - }) - beforeEach(() => { - // Presets an action, which is performed before each unit test case starts. - // The number of execution times is the same as the number of test cases defined by **it**. - // This API supports only one parameter: preset action function. - }) - afterEach(() => { - // Presets a clear action, which is performed after each unit test case ends. - // The number of execution times is the same as the number of test cases defined by **it**. - // This API supports only one parameter: clear action function. - }) - afterAll(() => { - // Presets a clear action, which is performed after all test cases of the test suite end. - // This API supports only one parameter: clear action function. - }) - it('assertContain', 0, () => { - // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. - hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); - let a = 'abc'; - let b = 'b'; - // Defines a variety of assertion methods, which are used to declare expected boolean conditions. - expect(a).assertContain(b); - expect(a).assertEqual(a); - }) - }) -} \ No newline at end of file diff --git a/harmony_example/entry/src/ohosTest/ets/test/List.test.ets b/harmony_example/entry/src/ohosTest/ets/test/List.test.ets deleted file mode 100644 index 794c7dc..0000000 --- a/harmony_example/entry/src/ohosTest/ets/test/List.test.ets +++ /dev/null @@ -1,5 +0,0 @@ -import abilityTest from './Ability.test'; - -export default function testsuite() { - abilityTest(); -} \ No newline at end of file diff --git a/harmony_example/entry/src/ohosTest/module.json5 b/harmony_example/entry/src/ohosTest/module.json5 deleted file mode 100644 index 509a3a2..0000000 --- a/harmony_example/entry/src/ohosTest/module.json5 +++ /dev/null @@ -1,11 +0,0 @@ -{ - "module": { - "name": "entry_test", - "type": "feature", - "deviceTypes": [ - "phone" - ], - "deliveryWithInstall": true, - "installationFree": false - } -} diff --git a/harmony_example/entry/src/test/List.test.ets b/harmony_example/entry/src/test/List.test.ets deleted file mode 100644 index bb5b5c3..0000000 --- a/harmony_example/entry/src/test/List.test.ets +++ /dev/null @@ -1,5 +0,0 @@ -import localUnitTest from './LocalUnit.test'; - -export default function testsuite() { - localUnitTest(); -} \ No newline at end of file diff --git a/harmony_example/entry/src/test/LocalUnit.test.ets b/harmony_example/entry/src/test/LocalUnit.test.ets deleted file mode 100644 index 165fc16..0000000 --- a/harmony_example/entry/src/test/LocalUnit.test.ets +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; - -export default function localUnitTest() { - describe('localUnitTest', () => { - // Defines a test suite. Two parameters are supported: test suite name and test suite function. - beforeAll(() => { - // Presets an action, which is performed only once before all test cases of the test suite start. - // This API supports only one parameter: preset action function. - }); - beforeEach(() => { - // Presets an action, which is performed before each unit test case starts. - // The number of execution times is the same as the number of test cases defined by **it**. - // This API supports only one parameter: preset action function. - }); - afterEach(() => { - // Presets a clear action, which is performed after each unit test case ends. - // The number of execution times is the same as the number of test cases defined by **it**. - // This API supports only one parameter: clear action function. - }); - afterAll(() => { - // Presets a clear action, which is performed after all test cases of the test suite end. - // This API supports only one parameter: clear action function. - }); - it('assertContain', 0, () => { - // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. - let a = 'abc'; - let b = 'b'; - // Defines a variety of assertion methods, which are used to declare expected boolean conditions. - expect(a).assertContain(b); - expect(a).assertEqual(a); - }); - }); -} \ No newline at end of file diff --git a/harmony_example/hvigor/hvigor-config.json5 b/harmony_example/hvigor/hvigor-config.json5 deleted file mode 100644 index 7a7ab89..0000000 --- a/harmony_example/hvigor/hvigor-config.json5 +++ /dev/null @@ -1,23 +0,0 @@ -{ - "modelVersion": "6.0.0", - "dependencies": { - }, - "execution": { - // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | "ultrafine" | false ]. Default: "normal" */ - // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */ - // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */ - // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */ - // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */ - // "optimizationStrategy": "memory" /* Define the optimization strategy. Value: [ "memory" | "performance" ]. Default: "memory" */ - }, - "logging": { - // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ - }, - "debugging": { - // "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */ - }, - "nodeOptions": { - // "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/ - // "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/ - } -} diff --git a/harmony_example/hvigorfile.ts b/harmony_example/hvigorfile.ts deleted file mode 100644 index 47113e2..0000000 --- a/harmony_example/hvigorfile.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { appTasks } from '@ohos/hvigor-ohos-plugin'; - -export default { - system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ - plugins: [] /* Custom plugin to extend the functionality of Hvigor. */ -} \ No newline at end of file diff --git a/harmony_example/oh-package-lock.json5 b/harmony_example/oh-package-lock.json5 deleted file mode 100644 index 1020c3e..0000000 --- a/harmony_example/oh-package-lock.json5 +++ /dev/null @@ -1,28 +0,0 @@ -{ - "meta": { - "stableOrder": true, - "enableUnifiedLockfile": false - }, - "lockfileVersion": 3, - "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", - "specifiers": { - "@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0", - "@ohos/hypium@1.0.24": "@ohos/hypium@1.0.24" - }, - "packages": { - "@ohos/hamock@1.0.0": { - "name": "", - "version": "1.0.0", - "integrity": "sha512-K6lDPYc6VkKe6ZBNQa9aoG+ZZMiwqfcR/7yAVFSUGIuOAhPvCJAo9+t1fZnpe0dBRBPxj2bxPPbKh69VuyAtDg==", - "resolved": "https://repo.harmonyos.com/ohpm/@ohos/hamock/-/hamock-1.0.0.har", - "registryType": "ohpm" - }, - "@ohos/hypium@1.0.24": { - "name": "", - "version": "1.0.24", - "integrity": "sha512-3dCqc+BAR5LqEGG2Vtzi8O3r7ci/3fYU+FWjwvUobbfko7DUnXGOccaror0yYuUhJfXzFK0aZNMGSnXaTwEnbw==", - "resolved": "https://repo.harmonyos.com/ohpm/@ohos/hypium/-/hypium-1.0.24.har", - "registryType": "ohpm" - } - } -} \ No newline at end of file diff --git a/harmony_example/oh-package.json5 b/harmony_example/oh-package.json5 deleted file mode 100644 index c72aa05..0000000 --- a/harmony_example/oh-package.json5 +++ /dev/null @@ -1,10 +0,0 @@ -{ - "modelVersion": "6.0.0", - "description": "Please describe the basic information.", - "dependencies": { - }, - "devDependencies": { - "@ohos/hypium": "1.0.24", - "@ohos/hamock": "1.0.0" - } -} diff --git a/memory-testing/assert.ts b/memory-testing/assert.ts new file mode 100644 index 0000000..27342ec --- /dev/null +++ b/memory-testing/assert.ts @@ -0,0 +1,44 @@ +export function fail(message: string): never { + throw new Error(message); +} + +export function assert(condition: boolean, message: string) { + if (!condition) { + fail(message); + } +} + +export function assertEqual(actual: ESObject, expected: ESObject, message: string) { + if (actual !== expected) { + fail(`${message}: expected=${String(expected)} actual=${String(actual)}`); + } +} + +export function assertArrayEqual(actual: Array, expected: Array, message: string) { + assertEqual(actual.length, expected.length, `${message}.length`); + for (let i = 0; i < expected.length; i++) { + assertEqual(actual[i], expected[i], `${message}[${i}]`); + } +} + +export function assertThrows(run: () => void, message: string) { + try { + run(); + } catch (_) { + return; + } + fail(`${message}: expected throw`); +} + +export async function assertRejects(promise: ESObject, expected: string, message: string) { + try { + await promise; + } catch (err) { + const text = String(err && (err.message || err)); + if (text.indexOf(expected) >= 0) { + return; + } + fail(`${message}: expected rejection containing ${expected}, actual=${text}`); + } + fail(`${message}: expected rejection`); +} diff --git a/memory-testing/async.ts b/memory-testing/async.ts new file mode 100644 index 0000000..e1bd475 --- /dev/null +++ b/memory-testing/async.ts @@ -0,0 +1,107 @@ +import { assert, assertArrayEqual, assertEqual, assertRejects } from "./assert"; +import { delay, settleFinalizers } from "./native"; + +type NativeAddon = ESObject; + +type AbortControllerLike = { + signal: ESObject; + abort: () => void; +}; + +function abortController(aborted: boolean): AbortControllerLike { + const signal: ESObject = { + aborted, + reason: "memory abort", + onabort: null, + addEventListener(_: string, __: ESObject) {}, + removeEventListener(_: string, __: ESObject) {}, + throwIfAborted() { + if (this.aborted) { + throw new Error("memory abort"); + } + }, + }; + return { + signal, + abort() { + signal.aborted = true; + if (signal.onabort) { + signal.onabort(); + } + }, + }; +} + +export async function exerciseThreadSafeFunctionWrapper(native: NativeAddon) { + await new Promise((resolve, reject) => { + let sawOk = false; + let sawErr = false; + + function maybeResolve() { + if (sawOk && sawErr) { + resolve(); + } + } + + try { + native.memory_thread_safe_function((err: ESObject, left: number, right: number) => { + try { + if (err) { + assert(!sawErr, "tsfn error callback duplicated"); + sawErr = true; + } else { + assert(!sawOk, "tsfn success callback duplicated"); + assertEqual(left + right, 3, "tsfn callback"); + sawOk = true; + } + maybeResolve(); + } catch (callbackErr) { + reject(callbackErr); + } + }); + } catch (err) { + reject(err); + } + }); + await settleFinalizers(8); +} + +export async function exerciseAsyncWrappers(native: NativeAddon) { + const summary = await native.memory_async_summary({ + label: "async-summary", + values: [1, 2, 3, 4], + }); + assertEqual(summary.label, "async-summary", "async summary label"); + assertEqual(summary.count, 4, "async summary count"); + assertEqual(summary.total, 10, "async summary total"); + + const singleSummary = await native.memory_async_summary_single({ + label: "async-single", + values: [2, 3, 5], + }); + assertEqual(singleSummary.label, "async-single", "async single label"); + assertEqual(singleSummary.count, 3, "async single count"); + assertEqual(singleSummary.total, 10, "async single total"); + + await native.memory_async_void("async-void"); + await assertRejects(native.memory_async_fail("async memory failure"), "async memory failure", "async fail"); + assertEqual(await native.manual_resolved_promise(), 42, "manual promise wrapper"); + + const progressEvents: Array = []; + assertEqual(await native.memory_async_progress(3, (event: ESObject) => progressEvents.push(event)), 3, "async progress result"); + assertArrayEqual(progressEvents.map((event: ESObject) => event.current), [0, 1, 2, 3], "async progress events"); + + const eventModeEvents: Array = []; + assertEqual(await native.memory_event_mode_progress(2, (event: ESObject) => eventModeEvents.push(event)), 2, "event progress result"); + assertArrayEqual(eventModeEvents.map((event: ESObject) => event.current), [0, 1, 2], "event progress events"); + + await assertRejects(native.memory_abortable_count(1024, abortController(true).signal), "AbortError", "abort pre-cancel"); + + const controller = abortController(false); + const pending = native.memory_abortable_slow_count(100000, controller.signal); + await delay(0); + controller.abort(); + await assertRejects(pending, "AbortError", "abort mid-flight"); + + assertEqual(await native.memory_worker(41), 42, "worker promise result"); +} diff --git a/memory-testing/binary.ts b/memory-testing/binary.ts new file mode 100644 index 0000000..0bff81c --- /dev/null +++ b/memory-testing/binary.ts @@ -0,0 +1,50 @@ +import { assertEqual, assertThrows } from "./assert"; + +type NativeAddon = ESObject; + +export function exerciseBinaryWrappers(native: NativeAddon) { + for (let i = 0; i < 250; i++) { + const bufferValue = native.create_buffer_copy(64); + assertEqual(native.buffer_length(bufferValue), 64, "buffer copy length"); + assertEqual(native.buffer_first_byte(bufferValue), 0, "buffer first byte"); + + const newBufferValue = native.create_buffer_new(32); + assertEqual(native.buffer_length(newBufferValue), 32, "buffer new length"); + assertEqual(native.buffer_first_byte(newBufferValue), 0x5a, "buffer new first byte"); + + const arrayBufferValue = native.create_arraybuffer_copy(64); + assertEqual(native.arraybuffer_length(arrayBufferValue), 64, "arraybuffer copy length"); + assertEqual(native.arraybuffer_first_byte(arrayBufferValue), 3, "arraybuffer first byte"); + + const newArrayBufferValue = native.create_arraybuffer_new(32); + assertEqual(native.arraybuffer_length(newArrayBufferValue), 32, "arraybuffer new length"); + assertEqual(native.arraybuffer_first_byte(newArrayBufferValue), 0x6b, "arraybuffer new first byte"); + + const typedArrayValue = native.create_uint8_typedarray_copy(); + assertEqual(native.typedarray_sum(typedArrayValue), 10, "typedarray created sum"); + assertEqual(native.typedarray_sum(new Uint8Array([1, 2, 3, 4])), 10, "typedarray input sum"); + assertEqual(native.int16_typedarray_sum(native.create_int16_typedarray_copy()), 4, "int16 typedarray created sum"); + assertEqual(native.int16_typedarray_sum(new Int16Array([-1, 2, 3])), 4, "int16 typedarray input sum"); + assertEqual(native.uint16_typedarray_sum(native.create_uint16_typedarray_copy()), 15, "uint16 typedarray created sum"); + assertEqual(native.uint16_typedarray_sum(new Uint16Array([4, 5, 6])), 15, "uint16 typedarray input sum"); + assertEqual(native.int32_typedarray_sum(native.create_int32_typedarray_copy()), 10, "int32 typedarray created sum"); + assertEqual(native.int32_typedarray_sum(new Int32Array([-7, 8, 9])), 10, "int32 typedarray input sum"); + assertEqual(native.uint32_typedarray_sum(native.create_uint32_typedarray_copy()), 33, "uint32 typedarray created sum"); + assertEqual(native.uint32_typedarray_sum(new Uint32Array([10, 11, 12])), 33, "uint32 typedarray input sum"); + assertEqual(native.float32_typedarray_sum(new Float32Array([1.5, 2.5, -1])), 3, "float32 typedarray input sum"); + assertEqual(native.float64_typedarray_sum(native.create_float64_typedarray_copy()), 3, "float64 typedarray created sum"); + assertEqual(native.float64_typedarray_sum(new Float64Array([1.5, 2.25, -0.75])), 3, "float64 typedarray input sum"); + + const dataViewValue = native.create_dataview_copy(); + assertEqual(native.dataview_length(dataViewValue), 4, "dataview copy length"); + assertEqual(native.dataview_uint32_le(dataViewValue), 0x12345678, "dataview copy uint32"); + assertEqual(native.dataview_uint32_le(new DataView(new Uint8Array([0x78, 0x56, 0x34, 0x12]).buffer)), 0x12345678, "dataview input uint32"); + + const newDataViewValue = native.create_dataview_new(8); + assertEqual(native.dataview_length(newDataViewValue), 8, "dataview new length"); + assertEqual(native.dataview_accessors_roundtrip(), true, "dataview accessor roundtrip"); + } + + assertThrows(() => native.invalid_typedarray_from_arraybuffer(), "invalid typedarray branch"); + assertThrows(() => native.invalid_dataview_from_arraybuffer(), "invalid dataview branch"); +} diff --git a/memory-testing/finalizers.ts b/memory-testing/finalizers.ts new file mode 100644 index 0000000..2b5f096 --- /dev/null +++ b/memory-testing/finalizers.ts @@ -0,0 +1,39 @@ +import { assertEqual } from "./assert"; + +type NativeAddon = ESObject; + +export function exerciseFinalizerWrappers(native: NativeAddon) { + native.begin_finalizer_state_check(128, 96); + + for (let i = 0; i < 32; i++) { + let bufferValue: ESObject | null = native.create_external_buffer(32); + assertEqual(native.buffer_length(bufferValue), 32, "external buffer length"); + bufferValue = null; + + let arrayBufferValue: ESObject | null = native.create_external_arraybuffer(32); + assertEqual(native.arraybuffer_length(arrayBufferValue), 32, "external arraybuffer length"); + arrayBufferValue = null; + + let typedArrayValue: ESObject | null = native.create_external_uint8_typedarray(); + assertEqual(native.typedarray_sum(typedArrayValue), 26, "external typedarray sum"); + typedArrayValue = null; + + let dataViewValue: ESObject | null = native.create_external_dataview(); + assertEqual(native.dataview_uint32_le(dataViewValue), 0x12345678, "external dataview value"); + dataViewValue = null; + + let classValue: ESObject | null = new native.MemoryClass(`class-${i}`, [1, 2, 3, i]); + assertEqual(classValue.name, `class-${i}`, "class name"); + assertEqual(classValue.total(), i + 6, "class method"); + classValue = null; + + let withoutInit: ESObject | null = new native.MemoryClassWithoutInit(); + assertEqual(withoutInit.total(), 0, "class without init method"); + withoutInit = null; + + let factoryClass: ESObject | null = native.MemoryFactoryClass.initWithFactory(`factory-${i}`, [1, 2, 3, i]); + assertEqual(factoryClass.name, `factory-${i}`, "factory class name"); + assertEqual(factoryClass.total(), i + 6, "factory class method"); + factoryClass = null; + } +} diff --git a/memory-testing/memory.ts b/memory-testing/memory.ts new file mode 100644 index 0000000..4c123cc --- /dev/null +++ b/memory-testing/memory.ts @@ -0,0 +1,17 @@ +import { exerciseAsyncWrappers, exerciseThreadSafeFunctionWrapper } from "./async"; +import { exerciseBinaryWrappers } from "./binary"; +import { exerciseFinalizerWrappers } from "./finalizers"; +import { runMemorySuite, withLeakTracking } from "./native"; +import { exerciseSyncWrappers } from "./sync"; + +const RESULT_PREFIX = "__ZIG_NAPI_MEMORY_RESULT__"; + +runMemorySuite(RESULT_PREFIX, async (native) => { + await withLeakTracking(native, "sync wrappers", () => { + exerciseSyncWrappers(native); + exerciseBinaryWrappers(native); + }); + exerciseFinalizerWrappers(native); + await withLeakTracking(native, "async wrappers", () => exerciseAsyncWrappers(native)); + await exerciseThreadSafeFunctionWrapper(native); +}); diff --git a/memory-testing/native.ts b/memory-testing/native.ts new file mode 100644 index 0000000..26bbbfc --- /dev/null +++ b/memory-testing/native.ts @@ -0,0 +1,126 @@ +import { assert } from "./assert"; + +declare function requireNapiPreview(name: string, isApp: boolean): ESObject; +declare function print(message: string): void; +declare function setInterval(callback: () => void, delay: number): number; +declare function clearInterval(id: number): void; +declare const globalThis: ESObject; + +type NativeAddon = ESObject; + +const SUITE_TIMEOUT_MS = 120000; +const KEEP_ALIVE_INTERVAL_MS = 10; + +export function installTimerGlobals() { + const etsInterop = requireNapiPreview("ets_interop_js_napi", true) as ESObject; + const created = etsInterop.createRuntime({ + "panda-files": "./hello.abc", + "boot-panda-files": "./etsstdlib.abc:./hello.abc", + "xgc-trigger-type": "never", + }); + if (!created) { + throw new Error("failed to initialize ArkVM timer runtime"); + } +} + +export function loadNative(): NativeAddon { + return requireNapiPreview("hello", true) as NativeAddon; +} + +export function delay(ms: number) { + return new Promise((resolve) => { + const timer = setInterval(() => { + clearInterval(timer); + resolve(); + }, ms); + }); +} + +function forceGc() { + const tools = globalThis.ArkTools; + if (!tools) { + return; + } + if (tools.hintGC) { + tools.hintGC(); + } + if (tools.hintOldSpaceGC) { + tools.hintOldSpaceGC(); + } +} + +export async function settleFinalizers(rounds = 8) { + for (let i = 0; i < rounds; i++) { + const pressure: Array = []; + for (let j = 0; j < 16; j++) { + pressure.push(new ArrayBuffer(64 * 1024)); + } + pressure.length = 0; + forceGc(); + await delay(0); + } +} + +export async function withLeakTracking(native: NativeAddon, label: string, run: () => Promise | void) { + let tracking = false; + native.leak_tracker_start(); + tracking = true; + try { + await run(); + await settleFinalizers(4); + const noLeaks = native.leak_tracker_finish(); + tracking = false; + assert(noLeaks, `${label}: native global allocator leaked`); + } catch (err) { + if (tracking) { + native.leak_tracker_abort(); + } + throw err; + } +} + +function failResult(resultPrefix: string, err: ESObject): never { + const message = String(err && (err.message || err)); + print(`${resultPrefix} status=fail message=${message}`); + throw err; +} + +export function runMemorySuite(resultPrefix: string, run: (native: NativeAddon) => Promise | void) { + installTimerGlobals(); + + let finished = false; + let elapsed = 0; + const keepAlive = setInterval(() => { + if (finished) { + clearInterval(keepAlive); + return; + } + elapsed += KEEP_ALIVE_INTERVAL_MS; + if (elapsed >= SUITE_TIMEOUT_MS) { + finished = true; + clearInterval(keepAlive); + failResult(resultPrefix, new Error(`suite timed out after ${SUITE_TIMEOUT_MS}ms`)); + } + }, KEEP_ALIVE_INTERVAL_MS); + + Promise.resolve() + .then(() => run(loadNative())) + .then( + () => { + if (finished) { + return; + } + finished = true; + print(`${resultPrefix} status=ok`); + clearInterval(keepAlive); + }, + (err) => { + if (finished) { + return; + } + finished = true; + clearInterval(keepAlive); + failResult(resultPrefix, err); + }, + ); +} diff --git a/memory-testing/sync.ts b/memory-testing/sync.ts new file mode 100644 index 0000000..d1620ff --- /dev/null +++ b/memory-testing/sync.ts @@ -0,0 +1,49 @@ +import { assert, assertArrayEqual, assertEqual } from "./assert"; + +type NativeAddon = ESObject; + +export function exerciseSyncWrappers(native: NativeAddon) { + for (let i = 0; i < 250; i++) { + const suffix = `${i}-${i % 17}`; + assert(native.tracked_alloc_roundtrip((i % 128) + 1), "tracked allocator roundtrip"); + assertEqual(native.hello(`ArkTS-${suffix}`), `Hello, ArkTS-${suffix}!`, "string wrapper"); + + const objectValue = native.get_object({ + name: `name-${suffix}`, + age: i, + is_student: i % 2 === 0, + }); + assertEqual(objectValue.name, `name-${suffix}`, "object.name"); + assertEqual(objectValue.age, i, "object.age"); + assertEqual(objectValue.is_student, i % 2 === 0, "object.is_student"); + + const optionalValue = native.get_optional_object({ name: `optional-${suffix}` }); + assertEqual(optionalValue.name, `optional-${suffix}`, "optional.name"); + assertEqual(optionalValue.age, 18, "optional.age"); + assertEqual(optionalValue.is_student, true, "optional.is_student"); + assertEqual(native.nullable_name_is_null(null), true, "nullable null"); + assertEqual(native.nullable_name_is_null(`nullable-${suffix}`), false, "nullable string"); + + assertArrayEqual(native.get_named_array([i, i % 2 === 0, `tuple-${suffix}`]), [i, i % 2 === 0, `tuple-${suffix}`], "tuple roundtrip"); + assertEqual(native.array_sum([1, 2, 3, i]), i + 6, "array sum"); + assertEqual(native.arraylist_sum([1, 2, 3, i]), i + 6, "arraylist sum"); + assertEqual(native.nested_summary({ + title: `nested-${suffix}`, + values: [1, 2, 3], + tuple: [i, true, `tuple-${suffix}`], + maybe: i % 2 === 0 ? `maybe-${suffix}` : null, + }) > 0, true, "nested summary"); + + assertEqual(native.union_kind(`variant-${suffix}`), "text", "union text"); + assertEqual(native.union_kind(i), "number", "union number"); + + const createdFunction = native.create_function(); + assertEqual(createdFunction(19, 23), 42, "create function call"); + assertEqual(native.call_function((left: number, right: number) => left + right), 42, "function callback"); + assertEqual(native.call_function_with_reference((left: number, right: number) => left + right), 42, "function reference"); + assertEqual(native.function_reference_ref_count((left: number, right: number) => left + right), 2, "reference ref count"); + + assertEqual(String(native.create_bigint_value()), "9007199254740993", "bigint return"); + assertEqual(native.bigint_to_i64(native.create_small_bigint_value()), 42, "bigint input"); + } +} diff --git a/scripts/arkvm/run_arkvm_memory_tests.sh b/scripts/arkvm/run_arkvm_memory_tests.sh new file mode 100755 index 0000000..d609a48 --- /dev/null +++ b/scripts/arkvm/run_arkvm_memory_tests.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." &>/dev/null && pwd)" + +export ARKVM_TEST_SUITE="${ARKVM_TEST_SUITE:-memory-testing/memory.ts}" +export ARKVM_ENTRY_POINT="${ARKVM_ENTRY_POINT:-memory-testing/memory}" +export ARKVM_RESULT_PREFIX="${ARKVM_RESULT_PREFIX:-__ZIG_NAPI_MEMORY_RESULT__}" +export ARKVM_EXAMPLE_DIR="${ARKVM_EXAMPLE_DIR:-examples/memory}" +export ARKVM_WORK_ROOT="${ARKVM_WORK_ROOT:-${REPO_ROOT}/.tmp_arkvm_memory_runner}" +export ARKVM_WAIT_FOR_EXIT="${ARKVM_WAIT_FOR_EXIT:-1}" +export ARKVM_EXPECT_LOG="${ARKVM_EXPECT_LOG:-^__ZIG_NAPI_FINALIZER_RESULT__ status=ok external=128 class=96$}" +export TEST_TIMEOUT_SEC="${TEST_TIMEOUT_SEC:-180}" + +exec "${SCRIPT_DIR}/run_arkvm_tests.sh" diff --git a/scripts/arkvm/run_arkvm_tests.sh b/scripts/arkvm/run_arkvm_tests.sh new file mode 100755 index 0000000..f544be6 --- /dev/null +++ b/scripts/arkvm/run_arkvm_tests.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." &>/dev/null && pwd)" + +: "${ARK_HOST_TOOLS_DIR:?ARK_HOST_TOOLS_DIR is required}" + +ARK_ES2ABC="${ARK_HOST_TOOLS_DIR}/es2abc" +ARK_JS_NAPI_CLI="${ARK_HOST_TOOLS_DIR}/ark_js_napi_cli" +TEST_TIMEOUT_SEC="${TEST_TIMEOUT_SEC:-90}" +RESULT_GRACE_SEC="${RESULT_GRACE_SEC:-2}" +KEEP_WORKDIR="${KEEP_WORKDIR:-0}" +WORK_ROOT="${ARKVM_WORK_ROOT:-${REPO_ROOT}/.tmp_arkvm_runner}" +WAIT_FOR_EXIT="${ARKVM_WAIT_FOR_EXIT:-0}" +EXPECT_LOG="${ARKVM_EXPECT_LOG:-}" + +[[ -x "${ARK_ES2ABC}" ]] || { echo "Missing binary: ${ARK_ES2ABC}" >&2; exit 1; } +[[ -x "${ARK_JS_NAPI_CLI}" ]] || { echo "Missing binary: ${ARK_JS_NAPI_CLI}" >&2; exit 1; } +[[ -f "${ARK_HOST_TOOLS_DIR}/libace_napi.so" ]] || { echo "Missing shared lib: ${ARK_HOST_TOOLS_DIR}/libace_napi.so" >&2; exit 1; } +[[ -f "${ARK_HOST_TOOLS_DIR}/libets_interop_js_napi.so" ]] || { echo "Missing shared lib: ${ARK_HOST_TOOLS_DIR}/libets_interop_js_napi.so" >&2; exit 1; } +[[ -f "${ARK_HOST_TOOLS_DIR}/etsstdlib.abc" ]] || { echo "Missing ArkTS stdlib: ${ARK_HOST_TOOLS_DIR}/etsstdlib.abc" >&2; exit 1; } +[[ -f "${ARK_HOST_TOOLS_DIR}/hello.abc" ]] || { echo "Missing ArkVM fixture abc: ${ARK_HOST_TOOLS_DIR}/hello.abc" >&2; exit 1; } + +add_file_info() { + local source_file="$1" + local files_info="$2" + local rel_path="${source_file#${REPO_ROOT}/}" + local record_name="${rel_path%.*}" + printf '%s;%s;esm;%s;%s;false\n' "${source_file}" "${record_name}" "${rel_path}" "${record_name}" >> "${files_info}" +} + +run_case() { + local example_dir="$1" + local suite="$2" + local result_prefix="$3" + local entry_point="$4" + local build_args="$5" + local addon_subdir="$6" + local workspace="${WORK_ROOT}/${entry_point//\//_}" + local abc="${workspace}/suite.abc" + local files_info="${workspace}/filesInfo.txt" + local log_file="${workspace}/arkvm.log" + + echo "==> ${example_dir}: ${suite}" + if [[ "${ARKVM_SKIP_BUILD:-0}" != "1" ]]; then + (cd "${REPO_ROOT}/${example_dir}" && zig build ${build_args}) + fi + + rm -rf "${workspace}" + mkdir -p "${workspace}/module" "${workspace}/fixtures" + cp "${REPO_ROOT}/${example_dir}/zig-out/${addon_subdir}/libhello.so" "${workspace}/module/" + ln -sf "${ARK_HOST_TOOLS_DIR}/libets_interop_js_napi.so" "${workspace}/module/libets_interop_js_napi.so" + cp "${ARK_HOST_TOOLS_DIR}/etsstdlib.abc" "${workspace}/" + cp "${ARK_HOST_TOOLS_DIR}/hello.abc" "${workspace}/" + printf 'alpha\n' > "${workspace}/fixtures/first.txt" + printf 'bravo\n' > "${workspace}/fixtures/second.txt" + + : > "${files_info}" + if [[ "${suite}" == test/* ]]; then + while IFS= read -r source_file; do + add_file_info "${source_file}" "${files_info}" + done < <(find "${REPO_ROOT}/test" -maxdepth 1 -name '*.ts' | sort) + elif [[ "${suite}" == memory-testing/* ]]; then + while IFS= read -r source_file; do + add_file_info "${source_file}" "${files_info}" + done < <(find "${REPO_ROOT}/memory-testing" -maxdepth 1 -name '*.ts' | sort) + else + add_file_info "${REPO_ROOT}/${suite}" "${files_info}" + fi + "${ARK_ES2ABC}" --merge-abc --extension=ts --module --output "${abc}" "@${files_info}" + + : > "${log_file}" + ( + cd "${workspace}" + export LD_LIBRARY_PATH="${workspace}:${workspace}/module:${ARK_HOST_TOOLS_DIR}:${LD_LIBRARY_PATH:-}" + "${ARK_JS_NAPI_CLI}" --entry-point "${entry_point}" "${abc}" + ) >"${log_file}" 2>&1 & + + local pid=$! + local deadline=$((SECONDS + TEST_TIMEOUT_SEC)) + local result_deadline=0 + local reaped=0 + local exit_status=0 + while kill -0 "${pid}" 2>/dev/null; do + if [[ "${WAIT_FOR_EXIT}" != "1" ]] && (( result_deadline == 0 )) && grep -q "^${result_prefix}" "${log_file}" 2>/dev/null; then + result_deadline=$((SECONDS + RESULT_GRACE_SEC)) + fi + if (( result_deadline != 0 && SECONDS >= result_deadline )); then + kill -TERM "${pid}" 2>/dev/null || true + wait "${pid}" >/dev/null 2>&1 || true + reaped=1 + break + fi + if (( SECONDS >= deadline )); then + kill -TERM "${pid}" 2>/dev/null || true + sleep 1 + kill -KILL "${pid}" 2>/dev/null || true + wait "${pid}" >/dev/null 2>&1 || true + reaped=1 + echo "ArkVM suite timed out after ${TEST_TIMEOUT_SEC}s" >&2 + cat "${log_file}" >&2 + exit 124 + fi + sleep 0.2 + done + if (( reaped == 0 )); then + wait "${pid}" >/dev/null 2>&1 || exit_status=$? + fi + + cat "${log_file}" + if grep -Eq 'error\(DebugAllocator\)|Segmentation fault|SIGSEGV|panic:|Cannot execute panda file|load native module failed' "${log_file}"; then + echo "ArkVM suite emitted a fatal runtime or leak diagnostic" >&2 + exit 1 + fi + if [[ "${WAIT_FOR_EXIT}" == "1" && "${exit_status}" != "0" ]]; then + echo "ArkVM suite exited with status ${exit_status}" >&2 + exit "${exit_status}" + fi + grep -q "^${result_prefix} status=ok" "${log_file}" + if [[ -n "${EXPECT_LOG}" ]]; then + grep -Eq "${EXPECT_LOG}" "${log_file}" + fi +} + +rm -rf "${WORK_ROOT}" +mkdir -p "${WORK_ROOT}" + +if [[ -n "${ARKVM_TEST_SUITE:-}" ]]; then + run_case "${ARKVM_EXAMPLE_DIR:-examples/basic}" "${ARKVM_TEST_SUITE}" "${ARKVM_RESULT_PREFIX:-__ZIG_NAPI_TEST_RESULT__}" "${ARKVM_ENTRY_POINT:-${ARKVM_TEST_SUITE%.*}}" "${ARKVM_BUILD_ARGS:--Darkvm-test=true -Doptimize=ReleaseSafe}" "${ARKVM_ADDON_SUBDIR:-arkvm-host}" +else + run_case "examples/basic" "test/basic.ts" "__ZIG_NAPI_TEST_RESULT__" "test/basic" "-Darkvm-test=true -Doptimize=ReleaseSafe" "arkvm-host" + run_case "examples/init" "test/init.ts" "__ZIG_NAPI_INIT_TEST_RESULT__" "test/init" "-Darkvm-test=true -Doptimize=ReleaseSafe" "arkvm-host" +fi + +[[ "${KEEP_WORKDIR}" == "1" ]] || rm -rf "${WORK_ROOT}" diff --git a/src/build/napi-build.zig b/src/build/napi-build.zig index 7d9ab99..50abee6 100644 --- a/src/build/napi-build.zig +++ b/src/build/napi-build.zig @@ -108,15 +108,76 @@ pub const NativeAddonBuildOptionsWithModule = struct { win32_manifest: ?std.Build.LazyPath = null, }; +var cached_arkvm_test_build: ?*std.Build = null; +var cached_arkvm_test_value: bool = false; + +fn isArkvmTestBuild(build: *std.Build) bool { + if (cached_arkvm_test_build == build) return cached_arkvm_test_value; + + cached_arkvm_test_value = build.option(bool, "arkvm-test", "Build host ArkVM test addon without device-only libraries") orelse false; + cached_arkvm_test_build = build; + return cached_arkvm_test_value; +} + +fn createAddonBuildOptions(build: *std.Build) *std.Build.Step.Options { + const options = build.addOptions(); + options.addOption(bool, "napi_tsgen", false); + return options; +} + +fn arkvmHostAddonBuild(build: *std.Build, option: NativeAddonBuildOptionsWithModule) *std.Build.Step.Compile { + const target = build.resolveTargetQuery(.{ + .cpu_arch = .x86_64, + .os_tag = .linux, + .abi = .gnu, + }); + + var hostOption = cloneLibraryOptions(build, option, target); + hostOption.use_llvm = true; + + const compile = build.addLibrary(hostOption); + compile.linker_allow_shlib_undefined = true; + compile.root_module.link_libc = true; + compile.root_module.addOptions("build_options", createAddonBuildOptions(build)); + + const installStep = build.addInstallArtifact(compile, .{ + .dest_dir = .{ + .override = .{ + .custom = "arkvm-host", + }, + }, + }); + build.getInstallStep().dependOn(&installStep.step); + + return compile; +} + pub const TypeDefinitionBuildOptions = struct { root_source_file: std.Build.LazyPath, output: std.Build.LazyPath, napi_module: *std.Build.Module, // Optional text injected after the generated banner comments. header: ?[]const u8 = null, + options: ?*std.Build.Step.Options = null, }; pub fn generateTypeDefinition(build: *std.Build, option: TypeDefinitionBuildOptions) !*std.Build.Step.Run { + _ = isArkvmTestBuild(build); + + const tsgen_build_options = build.addOptions(); + tsgen_build_options.addOption(bool, "napi_tsgen", true); + + const tsgen_napi_sys = build.addModule("zig-napi-tsgen-napi-sys", .{ + .root_source_file = option.napi_module.owner.path("src/sys/api.zig"), + }); + const tsgen_napi = build.addModule("zig-napi-tsgen-napi", .{ + .root_source_file = option.napi_module.owner.path("src/napi.zig"), + }); + tsgen_napi.addImport("napi-sys", tsgen_napi_sys); + tsgen_napi.addOptions("build_options", tsgen_build_options); + tsgen_napi.addIncludePath(option.napi_module.owner.path("src/sys/header")); + tsgen_napi_sys.addIncludePath(option.napi_module.owner.path("src/sys/header")); + const generator_root = build.createModule(.{ .root_source_file = option.napi_module.owner.path("src/build/napi-tsgen.zig"), .target = build.graph.host, @@ -133,10 +194,11 @@ pub fn generateTypeDefinition(build: *std.Build, option: TypeDefinitionBuildOpti .imports = &.{ .{ .name = "napi", - .module = option.napi_module, + .module = tsgen_napi, }, }, }); + addon_root.addOptions("build_options", option.options orelse createAddonBuildOptions(build)); const ndk_root = try resolveNdkPath(build); if (ndk_root.len > 0) { @@ -154,7 +216,7 @@ pub fn generateTypeDefinition(build: *std.Build, option: TypeDefinitionBuildOpti } generator.root_module.addImport("addon_root", addon_root); - generator.root_module.addImport("napi", option.napi_module); + generator.root_module.addImport("napi", tsgen_napi); const run = build.addRunArtifact(generator); run.addFileArg(option.output); @@ -164,6 +226,14 @@ pub fn generateTypeDefinition(build: *std.Build, option: TypeDefinitionBuildOpti } pub fn nativeAddonBuild(build: *std.Build, option: NativeAddonBuildOptionsWithModule) !NativeAddonBuildResult { + const arkvm_test = isArkvmTestBuild(build); + if (arkvm_test) { + const host = arkvmHostAddonBuild(build, option); + return .{ .arm64 = null, .arm = null, .x64 = host }; + } + + const addon_build_options = createAddonBuildOptions(build); + const currentTarget = if (option.root_module_options.target) |target| target.result else build.graph.host.result; // Respect the target platform for command line. @@ -187,6 +257,7 @@ pub fn nativeAddonBuild(build: *std.Build, option: NativeAddonBuildOptionsWithMo const arm64Option = cloneLibraryOptions(build, option, target); arm64 = build.addLibrary(arm64Option); + arm64.?.root_module.addOptions("build_options", addon_build_options); try linkNapi(build, arm64.?, target.query); const arm64DistDir: []const u8 = build.dupePath("arm64-v8a"); @@ -203,6 +274,7 @@ pub fn nativeAddonBuild(build: *std.Build, option: NativeAddonBuildOptionsWithMo const target = build.resolveTargetQuery(targets[1]); const armOption = cloneLibraryOptions(build, option, target); arm = build.addLibrary(armOption); + arm.?.root_module.addOptions("build_options", addon_build_options); try linkNapi(build, arm.?, target.query); const armDistDir: []const u8 = build.dupePath("armeabi-v7a"); @@ -221,6 +293,7 @@ pub fn nativeAddonBuild(build: *std.Build, option: NativeAddonBuildOptionsWithMo // TODO: https://github.com/ziglang/zig/issues/25335 x64Option.use_llvm = true; x64 = build.addLibrary(x64Option); + x64.?.root_module.addOptions("build_options", addon_build_options); try linkNapi(build, x64.?, target.query); const x64DistDir: []const u8 = build.dupePath("x86_64"); diff --git a/src/build/napi-tsgen.zig b/src/build/napi-tsgen.zig index 2d3087a..3bd02cc 100644 --- a/src/build/napi-tsgen.zig +++ b/src/build/napi-tsgen.zig @@ -977,6 +977,9 @@ fn emitType(state: *State, comptime T: type) ![]const u8 { switch (T) { void => return "void", bool => return "boolean", + napi.Bool => return "boolean", + napi.Number => return "number", + napi.String => return "string", napi.Null => return "null", napi.Undefined => return "undefined", else => {}, @@ -1631,6 +1634,11 @@ fn emitSourceTypeExpr(state: *State, file_path: []const u8, type_expr: []const u if (std.mem.eql(u8, trimmed, "void")) return "void"; if (std.mem.eql(u8, trimmed, "bool")) return "boolean"; + if (std.mem.eql(u8, trimmed, "napi.Bool")) return "boolean"; + if (std.mem.eql(u8, trimmed, "napi.String")) return "string"; + if (std.mem.eql(u8, trimmed, "napi.Number")) return "number"; + if (std.mem.eql(u8, trimmed, "napi.Null")) return "null"; + if (std.mem.eql(u8, trimmed, "napi.Undefined")) return "undefined"; if (isSourceNumericType(trimmed)) return "number"; if (isSourceStringType(trimmed)) return "string"; if (std.mem.eql(u8, trimmed, "AbortSignal") or diff --git a/src/napi.zig b/src/napi.zig index 52dc1c1..dae9262 100644 --- a/src/napi.zig +++ b/src/napi.zig @@ -1,3 +1,4 @@ +const std = @import("std"); const env = @import("./napi/env.zig"); const value = @import("./napi/value.zig"); const function = @import("./napi/value/function.zig"); @@ -14,6 +15,7 @@ const arraybuffer = @import("./napi/wrapper/arraybuffer.zig"); const typedarray = @import("./napi/wrapper/typedarray.zig"); const dataview = @import("./napi/wrapper/dataview.zig"); const reference = @import("./napi/wrapper/reference.zig"); +const global_allocator = @import("./napi/util/allocator.zig"); pub const napi_sys = @import("napi-sys"); pub const Env = env.Env; @@ -65,6 +67,41 @@ pub fn FunctionRef(comptime Args: type, comptime Return: type) type { return reference.Reference(function.Function(Args, Return)); } pub const ObjectRef = reference.Reference(value.Object); + +/// Set the allocator used by napi wrappers, including JS-runtime-owned state. +pub fn setAllocator(new_allocator: std.mem.Allocator) void { + global_allocator.global_manager.set(new_allocator); + global_allocator.runtime_manager.set(new_allocator); +} + +/// Reset the allocator used by napi wrappers to the default page allocator. +pub fn resetAllocator() void { + global_allocator.global_manager.set(std.heap.page_allocator); + global_allocator.runtime_manager.set(std.heap.page_allocator); +} + +pub fn setGlobalAllocator(new_allocator: std.mem.Allocator) void { + setAllocator(new_allocator); +} + +pub fn resetGlobalAllocator() void { + resetAllocator(); +} + +pub fn globalAllocator() std.mem.Allocator { + return global_allocator.globalAllocator(); +} + +/// Override only short-lived conversion/operation allocations. +/// This is mainly useful for scoped allocator tests; applications should use setAllocator. +pub fn setOperationAllocator(new_allocator: std.mem.Allocator) void { + global_allocator.global_manager.set(new_allocator); +} + +pub fn resetOperationAllocator() void { + global_allocator.global_manager.set(std.heap.page_allocator); +} + pub fn AsyncContext(comptime Event: type) type { return async.AsyncContext(Event); } diff --git a/src/napi/abort_signal.zig b/src/napi/abort_signal.zig index 57331cf..4195420 100644 --- a/src/napi/abort_signal.zig +++ b/src/napi/abort_signal.zig @@ -9,11 +9,16 @@ const GlobalAllocator = @import("./util/allocator.zig"); pub const AbortCallback = *const fn (?*anyopaque) void; const AbortRegistrationStack = struct { + env: napi.napi_env, + signal: napi.napi_value, allocator: std.mem.Allocator, registrations: std.array_list.Managed(*AbortRegistration), + wrapped: bool = false, - fn init(allocator: std.mem.Allocator) AbortRegistrationStack { + fn init(env: napi.napi_env, signal: napi.napi_value, allocator: std.mem.Allocator) AbortRegistrationStack { return .{ + .env = env, + .signal = signal, .allocator = allocator, .registrations = std.array_list.Managed(*AbortRegistration).init(allocator), }; @@ -119,7 +124,7 @@ pub const AbortSignal = struct { }; fn ensureStack(env: napi.napi_env, signal: napi.napi_value) !*AbortRegistrationStack { - const allocator = GlobalAllocator.globalAllocator(); + const allocator = GlobalAllocator.runtimeAllocator(); var stack_ptr: ?*anyopaque = null; const remove_status = napi.napi_remove_wrap(env, signal, &stack_ptr); @@ -127,7 +132,7 @@ fn ensureStack(env: napi.napi_env, signal: napi.napi_value) !*AbortRegistrationS @ptrCast(@alignCast(stack_ptr.?)) else blk: { const new_stack = try allocator.create(AbortRegistrationStack); - new_stack.* = AbortRegistrationStack.init(allocator); + new_stack.* = AbortRegistrationStack.init(env, signal, allocator); break :blk new_stack; }; @@ -141,6 +146,7 @@ fn ensureStack(env: napi.napi_env, signal: napi.napi_value) !*AbortRegistrationS if (wrap_status != napi.napi_ok) { return NapiError.Error.fromStatus(NapiError.Status.New(wrap_status)); } + stack.wrapped = true; if (ref != null) { var ref_count: u32 = 0; _ = napi.napi_reference_unref(env, ref, &ref_count); @@ -191,8 +197,9 @@ fn onAbort(env: napi.napi_env, info: napi.napi_callback_info) callconv(.c) napi. fn finalizeStack(_: napi.napi_env, finalize_data: ?*anyopaque, _: ?*anyopaque) callconv(.c) void { const data = finalize_data orelse return; const stack: *AbortRegistrationStack = @ptrCast(@alignCast(data)); + const allocator = stack.allocator; stack.deinit(); - GlobalAllocator.globalAllocator().destroy(stack); + allocator.destroy(stack); } pub fn abortErrorValue(env: Env) napi.napi_value { diff --git a/src/napi/async.zig b/src/napi/async.zig index 383d3cf..ba6995f 100644 --- a/src/napi/async.zig +++ b/src/napi/async.zig @@ -12,6 +12,7 @@ const AbortRegistration = @import("./abort_signal.zig").AbortRegistration; var threaded_runtime_mutex: std.atomic.Mutex = .unlocked; var threaded_runtime_initialized = false; +var threaded_runtime_active_operations: usize = 0; var threaded_runtime: std.Io.Threaded = undefined; pub const RuntimeModel = enum { @@ -68,7 +69,7 @@ fn singleIo() std.Io { return std.Io.Threaded.global_single_threaded.io(); } -fn threadedIo() std.Io { +fn acquireThreadedRuntime() std.Io { while (!threaded_runtime_mutex.tryLock()) { std.Thread.yield() catch {}; } @@ -79,13 +80,43 @@ fn threadedIo() std.Io { threaded_runtime_initialized = true; } + threaded_runtime_active_operations += 1; return threaded_runtime.io(); } +fn activeThreadedIo() std.Io { + while (!threaded_runtime_mutex.tryLock()) { + std.Thread.yield() catch {}; + } + defer threaded_runtime_mutex.unlock(); + + std.debug.assert(threaded_runtime_initialized); + std.debug.assert(threaded_runtime_active_operations > 0); + return threaded_runtime.io(); +} + +fn releaseThreadedRuntime() void { + while (!threaded_runtime_mutex.tryLock()) { + std.Thread.yield() catch {}; + } + defer threaded_runtime_mutex.unlock(); + + if (!threaded_runtime_initialized or threaded_runtime_active_operations == 0) { + return; + } + + threaded_runtime_active_operations -= 1; + if (threaded_runtime_active_operations == 0) { + threaded_runtime.deinit(); + threaded_runtime_initialized = false; + threaded_runtime = undefined; + } +} + fn ioForRuntime(effective_runtime: EffectiveRuntime) std.Io { return switch (effective_runtime) { .single => singleIo(), - .thread => threadedIo(), + .thread => activeThreadedIo(), }; } @@ -290,18 +321,25 @@ fn AsyncTaskDescriptorImpl( return struct { base: AsyncTaskDescriptorBase, input: Input, + input_moved: bool = false, const Self = @This(); fn schedule(base: *AsyncTaskDescriptorBase, env_raw: napi.napi_env, listener: ?napi.napi_value, signal: ?AbortSignal) !Promise { const self: *Self = @alignCast(@fieldParentPtr("base", base)); + errdefer base.destroy_fn(base); const operation = try AsyncTaskOperation(Input, Result, Event, runtime, run_fn).create(Env.from_raw(env_raw), self.input, listener, signal); - defer base.destroy_fn(base); - return try operation.submit(); + self.input_moved = true; + const promise = try operation.submit(); + base.destroy_fn(base); + return promise; } fn destroy(base: *AsyncTaskDescriptorBase) void { const self: *Self = @alignCast(@fieldParentPtr("base", base)); + if (!self.input_moved) { + Napi.deinit_napi_value(Input, self.input); + } self.base.allocator.destroy(self); } }; @@ -333,6 +371,8 @@ fn AsyncTaskOperation( cancel_requested: bool = false, cancel_dispatched: bool = false, closed: bool = false, + result_ready: bool = false, + uses_threaded_runtime: bool = false, const Self = @This(); const Context = AsyncContext(Event); @@ -379,8 +419,10 @@ fn AsyncTaskOperation( switch (effectiveRuntime(runtime)) { .single => self.runSingle(), .thread => { + const io = acquireThreadedRuntime(); + self.uses_threaded_runtime = true; try self.initThreadDispatcher(); - self.future = std.Io.concurrent(threadedIo(), runTask, .{self}) catch |err| { + self.future = std.Io.concurrent(io, runTask, .{self}) catch |err| { self.err = mapAnyError(err); self.dispatchCompletion(self.env); return promise; @@ -392,7 +434,7 @@ fn AsyncTaskOperation( } fn controllerThreadMain(self: *Self) void { - const io = threadedIo(); + const io = self.operationIo(); const should_cancel = self.waitForTaskDoneOrAbort(); if (self.future) |*future| { if (should_cancel) { @@ -460,7 +502,7 @@ fn AsyncTaskOperation( } fn requestAbort(self: *Self) void { - const io = threadedIo(); + const io = self.operationIo(); self.cancel_token.cancel(); self.state_mutex.lockUncancelable(io); defer self.state_mutex.unlock(io); @@ -470,7 +512,7 @@ fn AsyncTaskOperation( } fn markTaskDone(self: *Self) void { - const io = threadedIo(); + const io = self.operationIo(); self.state_mutex.lockUncancelable(io); defer self.state_mutex.unlock(io); self.task_done = true; @@ -478,7 +520,7 @@ fn AsyncTaskOperation( } fn waitForTaskDoneOrAbort(self: *Self) bool { - const io = threadedIo(); + const io = self.operationIo(); self.state_mutex.lockUncancelable(io); defer self.state_mutex.unlock(io); @@ -488,6 +530,10 @@ fn AsyncTaskOperation( return self.cancel_requested and !self.task_done; } + fn operationIo(self: *const Self) std.Io { + return if (self.uses_threaded_runtime) activeThreadedIo() else singleIo(); + } + fn execute(self: *Self, context: Context) !void { if (run_info.params.len == 1) { if (@typeInfo(run_info.return_type.?) == .error_union) { @@ -495,12 +541,14 @@ fn AsyncTaskOperation( try run_fn(self.input); } else { self.result = try run_fn(self.input); + self.result_ready = true; } } else { if (Result == void) { _ = run_fn(self.input); } else { self.result = run_fn(self.input); + self.result_ready = true; } } } else { @@ -509,12 +557,14 @@ fn AsyncTaskOperation( try run_fn(context, self.input); } else { self.result = try run_fn(context, self.input); + self.result_ready = true; } } else { if (Result == void) { _ = run_fn(context, self.input); } else { self.result = run_fn(context, self.input); + self.result_ready = true; } } } @@ -649,6 +699,8 @@ fn AsyncTaskOperation( fn destroy(self: *Self, env_raw: napi.napi_env) void { if (self.closed) return; self.closed = true; + const should_release_threaded_runtime = self.uses_threaded_runtime; + self.uses_threaded_runtime = false; releaseCallbackRef(env_raw, &self.listener_ref); if (self.abort_registration) |registration| { @@ -659,7 +711,17 @@ fn AsyncTaskOperation( _ = napi.napi_release_threadsafe_function(self.tsfn_raw, napi.napi_tsfn_release); self.tsfn_raw = null; } + var deinit_state = Napi.DeinitState{}; + Napi.deinit_napi_value_with_state(Input, self.input, &deinit_state); + if (comptime Result != void) { + if (self.result_ready) { + Napi.deinit_napi_value_with_state(Result, self.result, &deinit_state); + } + } self.allocator.destroy(self); + if (should_release_threaded_runtime) { + releaseThreadedRuntime(); + } } }; } diff --git a/src/napi/util/allocator.zig b/src/napi/util/allocator.zig index 397ae50..c88ed61 100644 --- a/src/napi/util/allocator.zig +++ b/src/napi/util/allocator.zig @@ -20,9 +20,15 @@ pub const AllocatorManager = struct { } }; -pub const global_manager = AllocatorManager.init(); +pub var global_manager = AllocatorManager.init(); +pub var runtime_manager = AllocatorManager.init(); /// Get the global allocator pub fn globalAllocator() std.mem.Allocator { return global_manager.get(); } + +/// Get the allocator used for values whose lifetime is owned by the JS runtime. +pub fn runtimeAllocator() std.mem.Allocator { + return runtime_manager.get(); +} diff --git a/src/napi/util/napi.zig b/src/napi/util/napi.zig index aefcb70..191a03b 100644 --- a/src/napi/util/napi.zig +++ b/src/napi/util/napi.zig @@ -11,6 +11,7 @@ const Buffer = @import("../wrapper/buffer.zig").Buffer; const ArrayBuffer = @import("../wrapper/arraybuffer.zig").ArrayBuffer; const DataView = @import("../wrapper/dataview.zig").DataView; const AbortSignal = @import("../abort_signal.zig").AbortSignal; +const GlobalAllocator = @import("./allocator.zig"); fn napiTypeOf(env: napi.napi_env, raw: napi.napi_value) napi.napi_valuetype { var value_type: napi.napi_valuetype = undefined; @@ -178,6 +179,117 @@ fn valueMatchesType(env: napi.napi_env, raw: napi.napi_value, comptime T: type) } pub const Napi = struct { + pub const DeinitState = struct { + const Entry = struct { + addr: usize, + byte_len: usize, + }; + + entries: [128]Entry = undefined, + len: usize = 0, + + fn shouldFree(self: *DeinitState, addr: usize, byte_len: usize) bool { + for (self.entries[0..self.len]) |entry| { + if (entry.addr == addr and entry.byte_len == byte_len) { + return false; + } + } + if (self.len < self.entries.len) { + self.entries[self.len] = .{ .addr = addr, .byte_len = byte_len }; + self.len += 1; + } + return true; + } + }; + + pub fn deinit_napi_value(comptime T: type, value: T) void { + var state = DeinitState{}; + Napi.deinit_napi_value_with_state(T, value, &state); + } + + pub fn deinit_napi_value_with_state(comptime T: type, value: T, state: *DeinitState) void { + const allocator = GlobalAllocator.globalAllocator(); + const infos = @typeInfo(T); + + const string_mode = comptime helper.stringLike(T); + if (string_mode != .Unknown) { + if (infos == .pointer and infos.pointer.size == .slice) { + const bytes = std.mem.sliceAsBytes(value); + if (state.shouldFree(@intFromPtr(bytes.ptr), bytes.len)) { + allocator.free(value); + } + } + return; + } + + switch (infos) { + .array => { + for (value) |item| { + Napi.deinit_napi_value_with_state(infos.array.child, item, state); + } + }, + .pointer => |ptr| { + if (ptr.size == .slice) { + for (value) |item| { + Napi.deinit_napi_value_with_state(ptr.child, item, state); + } + const bytes = std.mem.sliceAsBytes(value); + if (state.shouldFree(@intFromPtr(bytes.ptr), bytes.len)) { + allocator.free(value); + } + } + }, + .optional => |optional| { + if (value) |payload| { + Napi.deinit_napi_value_with_state(optional.child, payload, state); + } + }, + .@"struct" => { + if (comptime helper.isNapiFunction(T) or + helper.isTypedArray(T) or + helper.isDataView(T) or + helper.isReference(T) or + helper.isAbortSignal(T) or + T == NapiValue.BigInt or + T == NapiValue.Bool or + T == NapiValue.Number or + T == NapiValue.String or + T == NapiValue.Object or + T == NapiValue.Promise or + T == NapiValue.Array or + T == NapiValue.Undefined or + T == NapiValue.Null or + T == Buffer or + T == ArrayBuffer or + T == DataView) + { + return; + } + + if (comptime helper.isArrayList(T)) { + const child = comptime helper.getArrayListElementType(T); + for (value.items) |item| { + Napi.deinit_napi_value_with_state(child, item, state); + } + var mutable = value; + mutable.deinit(allocator); + return; + } + + inline for (infos.@"struct".fields) |field| { + Napi.deinit_napi_value_with_state(field.type, @field(value, field.name), state); + } + }, + .@"union" => |union_info| { + if (union_info.tag_type == null) return; + switch (value) { + inline else => |payload| Napi.deinit_napi_value_with_state(@TypeOf(payload), payload, state), + } + }, + else => {}, + } + } + pub fn from_napi_value(env: napi.napi_env, raw: napi.napi_value, comptime T: type) T { const infos = @typeInfo(T); switch (T) { diff --git a/src/napi/value/bigint.zig b/src/napi/value/bigint.zig index c7b54df..1bdcc3a 100644 --- a/src/napi/value/bigint.zig +++ b/src/napi/value/bigint.zig @@ -12,18 +12,18 @@ pub const BigInt = struct { } pub fn from_napi_value(env: napi.napi_env, raw: napi.napi_value, comptime T: type) T { - const value_type = @TypeOf(T); - const infos = @typeInfo(value_type); - - switch (infos) { - .i64 => { + const value_type = T; + switch (value_type) { + i64 => { var result: T = undefined; - _ = napi.napi_get_value_bigint_int64(env, raw, @ptrCast(&result), false); + var lossless = false; + _ = napi.napi_get_value_bigint_int64(env, raw, @ptrCast(&result), &lossless); return result; }, - .u64 => { + u64 => { var result: T = undefined; - _ = napi.napi_get_value_bigint_uint64(env, raw, @ptrCast(&result), false); + var lossless = false; + _ = napi.napi_get_value_bigint_uint64(env, raw, @ptrCast(&result), &lossless); return result; }, else => { @@ -53,7 +53,7 @@ pub const BigInt = struct { const word_count: usize = if (words[1] != 0) 2 else 1; - _ = napi.napi_create_bigint_words(env.raw, 0, word_count, @ptrCast(words.ptr), &result); + _ = napi.napi_create_bigint_words(env.raw, 0, word_count, @ptrCast(&words), &result); return BigInt.from_raw(env.raw, result); }, i128 => { @@ -68,7 +68,7 @@ pub const BigInt = struct { const word_count: usize = if (words[1] != 0) 2 else 1; - _ = napi.napi_create_bigint_words(env.raw, if (is_negative) @as(c_int, 1) else @as(c_int, 0), word_count, @ptrCast(words.ptr), &result); + _ = napi.napi_create_bigint_words(env.raw, if (is_negative) @as(c_int, 1) else @as(c_int, 0), word_count, @ptrCast(&words), &result); return BigInt.from_raw(env.raw, result); }, else => {}, diff --git a/src/napi/value/function.zig b/src/napi/value/function.zig index 0e88706..6164da9 100644 --- a/src/napi/value/function.zig +++ b/src/napi/value/function.zig @@ -37,6 +37,20 @@ pub fn Function(comptime Args: type, comptime Return: type) type { } const FnImpl = struct { + const has_env = params.len > 0 and params[0].type.? == Env; + const env_index = if (has_env) 1 else 0; + + fn cleanupArgs(args: *std.meta.ArgsTuple(value_type), initialized: usize) void { + inline for (params, 0..) |param, i| { + if (comptime has_env and i == 0) { + continue; + } + if (i < initialized) { + Napi.deinit_napi_value(param.type.?, args[i]); + } + } + } + fn inner_fn(inner_env: napi.napi_env, info: napi.napi_callback_info) callconv(.c) napi.napi_value { const undefined_value = Undefined.New(Env.from_raw(inner_env)); const return_info = infos.@"fn".return_type.?; @@ -46,8 +60,6 @@ pub fn Function(comptime Args: type, comptime Return: type) type { }; const async_returns_descriptor = comptime helper.isAsyncDescriptor(return_payload); const has_async_events = comptime async_returns_descriptor and return_payload.async_has_events; - const has_env = comptime params.len > 0 and params[0].type.? == Env; - const env_index = if (has_env) 1 else 0; const expected_argc = params.len - env_index + if (has_async_events) 1 else 0; var init_argc: usize = expected_argc; @@ -62,8 +74,13 @@ pub fn Function(comptime Args: type, comptime Return: type) type { } var napi_params: std.meta.ArgsTuple(value_type) = undefined; + var initialized_params: usize = 0; + var cleanup_params = true; + defer if (cleanup_params) cleanupArgs(&napi_params, initialized_params); + if (comptime has_env) { napi_params[0] = Env.from_raw(inner_env); + initialized_params = 1; } var abort_signal: ?AbortSignal = null; @@ -72,6 +89,7 @@ pub fn Function(comptime Args: type, comptime Return: type) type { NapiError.clearLastError(); } napi_params[i] = Napi.from_napi_value(inner_env, args_raw[i - env_index], param_index.type.?); + initialized_params = i + 1; if (comptime helper.isAbortSignal(param_index.type.?)) { abort_signal = napi_params[i]; } @@ -96,6 +114,7 @@ pub fn Function(comptime Args: type, comptime Return: type) type { return undefined_value.raw; }; if (comptime async_returns_descriptor) { + cleanup_params = false; var task = ret; const promise = task.scheduleWithListenerAndSignal(Env.from_raw(inner_env), event_listener, abort_signal) catch { if (NapiError.last_error) |last_err| { @@ -115,6 +134,7 @@ pub fn Function(comptime Args: type, comptime Return: type) type { } else { const ret = @call(.auto, value, napi_params); if (comptime async_returns_descriptor) { + cleanup_params = false; var task = ret; const promise = task.scheduleWithListenerAndSignal(Env.from_raw(inner_env), event_listener, abort_signal) catch { if (NapiError.last_error) |last_err| { diff --git a/src/napi/value/object.zig b/src/napi/value/object.zig index 473e360..8c9262a 100644 --- a/src/napi/value/object.zig +++ b/src/napi/value/object.zig @@ -79,48 +79,21 @@ pub const Object = struct { } pub fn Set(self: Object, comptime key: []const u8, value: anytype) !void { - const value_type = @TypeOf(value); - const infos = @typeInfo(value_type); - - switch (infos) { - .@"fn" => { - const args_type = comptime helper.collectFunctionArgs(value_type); - const return_type = infos.@"fn".return_type.?; - const fn_impl = try Function(args_type, return_type).New(Env.from_raw(self.env), key, value); - const napi_desc = [_]napi.napi_property_descriptor{ - .{ - .utf8name = @ptrCast(key.ptr), - .method = fn_impl.inner_fn, - .getter = null, - .setter = null, - .value = null, - .attributes = napi.napi_default, - .data = null, - }, - }; - const status = napi.napi_define_properties(self.env, self.raw, 1, &napi_desc); - if (status != napi.napi_ok) { - return NapiError.Error.fromStatus(NapiError.Status.New(status)); - } - }, - else => { - const n_value = try Napi.to_napi_value(self.env, value, null); - const napi_desc = [_]napi.napi_property_descriptor{ - .{ - .utf8name = @ptrCast(key.ptr), - .method = null, - .getter = null, - .setter = null, - .value = n_value, - .attributes = napi.napi_default, - .data = null, - }, - }; - const status = napi.napi_define_properties(self.env, self.raw, 1, &napi_desc); - if (status != napi.napi_ok) { - return NapiError.Error.fromStatus(NapiError.Status.New(status)); - } + const n_value = try Napi.to_napi_value(self.env, value, key); + const napi_desc = [_]napi.napi_property_descriptor{ + .{ + .utf8name = @ptrCast(key.ptr), + .method = null, + .getter = null, + .setter = null, + .value = n_value, + .attributes = napi.napi_default, + .data = null, }, + }; + const status = napi.napi_define_properties(self.env, self.raw, 1, &napi_desc); + if (status != napi.napi_ok) { + return NapiError.Error.fromStatus(NapiError.Status.New(status)); } } diff --git a/src/napi/value/string.zig b/src/napi/value/string.zig index d2ab265..08732bb 100644 --- a/src/napi/value/string.zig +++ b/src/napi/value/string.zig @@ -22,18 +22,24 @@ pub const String = struct { var len: usize = 0; _ = napi.napi_get_value_string_utf8(env, raw, null, 0, &len); - const buf = allocator.alloc(u8, len + 1) catch @panic("OOM"); + const tmp = allocator.alloc(u8, len + 1) catch @panic("OOM"); + defer allocator.free(tmp); - _ = napi.napi_get_value_string_utf8(env, raw, buf.ptr, len + 1, null); + _ = napi.napi_get_value_string_utf8(env, raw, tmp.ptr, len + 1, null); + const buf = allocator.alloc(u8, len) catch @panic("OOM"); + @memcpy(buf, tmp[0..len]); return @as(T, buf[0..len]); }, .Utf16 => { var len: usize = 0; _ = napi.napi_get_value_string_utf16(env, raw, null, 0, &len); - const buf = allocator.alloc(u16, len + 1) catch @panic("OOM"); + const tmp = allocator.alloc(u16, len + 1) catch @panic("OOM"); + defer allocator.free(tmp); - _ = napi.napi_get_value_string_utf16(env, raw, buf.ptr, len + 1, null); + _ = napi.napi_get_value_string_utf16(env, raw, tmp.ptr, len + 1, null); + const buf = allocator.alloc(u16, len) catch @panic("OOM"); + @memcpy(buf, tmp[0..len]); return @as(T, buf[0..len]); }, else => { diff --git a/src/napi/wrapper/arraybuffer.zig b/src/napi/wrapper/arraybuffer.zig index 52d7b87..690c730 100644 --- a/src/napi/wrapper/arraybuffer.zig +++ b/src/napi/wrapper/arraybuffer.zig @@ -99,10 +99,14 @@ pub const ArrayBuffer = struct { /// // Don't free owned_data, it's now managed by JS /// ``` pub fn from(env: Env, data: []u8) !ArrayBuffer { + return try ArrayBuffer.fromWithFinalizer(env, data, null); + } + + pub fn fromWithFinalizer(env: Env, data: []u8, on_finalize: ?*const fn () void) !ArrayBuffer { var result: napi.napi_value = undefined; // Store the slice info for the finalizer - const hint = ArrayBufferHint.create(data) catch { + const hint = ArrayBufferHint.create(data, on_finalize) catch { return NapiError.Error.fromStatus(NapiError.Status.GenericFailure); }; @@ -211,23 +215,30 @@ pub const ArrayBuffer = struct { /// Helper struct to store ArrayBuffer info for the finalizer const ArrayBufferHint = struct { + allocator: std.mem.Allocator, ptr: [*]u8, len: usize, + on_finalize: ?*const fn () void, - fn create(data: []u8) !*ArrayBufferHint { + fn create(data: []u8, on_finalize: ?*const fn () void) !*ArrayBufferHint { const allocator = GlobalAllocator.globalAllocator(); const hint = try allocator.create(ArrayBufferHint); hint.* = .{ + .allocator = allocator, .ptr = data.ptr, .len = data.len, + .on_finalize = on_finalize, }; return hint; } fn destroy(self: *ArrayBufferHint) void { - const allocator = GlobalAllocator.globalAllocator(); + const allocator = self.allocator; // Free the original buffer data allocator.free(self.ptr[0..self.len]); + if (self.on_finalize) |on_finalize| { + on_finalize(); + } // Free the hint struct itself allocator.destroy(self); } @@ -238,7 +249,7 @@ fn externalArrayBufferFinalizer( _: napi.napi_env, _: ?*anyopaque, hint: ?*anyopaque, -) callconv(.C) void { +) callconv(.c) void { if (hint) |h| { const arraybuffer_hint: *ArrayBufferHint = @ptrCast(@alignCast(h)); arraybuffer_hint.destroy(); diff --git a/src/napi/wrapper/buffer.zig b/src/napi/wrapper/buffer.zig index 1ad375e..570b49b 100644 --- a/src/napi/wrapper/buffer.zig +++ b/src/napi/wrapper/buffer.zig @@ -99,10 +99,14 @@ pub const Buffer = struct { /// // Don't free owned_data, it's now managed by JS /// ``` pub fn from(env: Env, data: []u8) !Buffer { + return try Buffer.fromWithFinalizer(env, data, null); + } + + pub fn fromWithFinalizer(env: Env, data: []u8, on_finalize: ?*const fn () void) !Buffer { var result: napi.napi_value = undefined; // Store the slice info for the finalizer - const hint = BufferHint.create(data) catch { + const hint = BufferHint.create(data, on_finalize) catch { return NapiError.Error.fromStatus(NapiError.Status.GenericFailure); }; @@ -208,23 +212,30 @@ pub const Buffer = struct { /// Helper struct to store buffer info for the finalizer const BufferHint = struct { + allocator: std.mem.Allocator, ptr: [*]u8, len: usize, + on_finalize: ?*const fn () void, - fn create(data: []u8) !*BufferHint { + fn create(data: []u8, on_finalize: ?*const fn () void) !*BufferHint { const allocator = GlobalAllocator.globalAllocator(); const hint = try allocator.create(BufferHint); hint.* = .{ + .allocator = allocator, .ptr = data.ptr, .len = data.len, + .on_finalize = on_finalize, }; return hint; } fn destroy(self: *BufferHint) void { - const allocator = GlobalAllocator.globalAllocator(); + const allocator = self.allocator; // Free the original buffer data allocator.free(self.ptr[0..self.len]); + if (self.on_finalize) |on_finalize| { + on_finalize(); + } // Free the hint struct itself allocator.destroy(self); } @@ -235,7 +246,7 @@ fn externalBufferFinalizer( _: napi.napi_env, _: ?*anyopaque, hint: ?*anyopaque, -) callconv(.C) void { +) callconv(.c) void { if (hint) |h| { const buffer_hint: *BufferHint = @ptrCast(@alignCast(h)); buffer_hint.destroy(); diff --git a/src/napi/wrapper/class.zig b/src/napi/wrapper/class.zig index d93689d..0ad4f94 100644 --- a/src/napi/wrapper/class.zig +++ b/src/napi/wrapper/class.zig @@ -7,8 +7,6 @@ const helper = @import("../util/helper.zig"); const NapiError = @import("./error.zig"); const GlobalAllocator = @import("../util/allocator.zig"); -var class_constructors: std.StringHashMap(napi.napi_value) = std.StringHashMap(napi.napi_value).init(GlobalAllocator.globalAllocator()); - pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { const type_info = @typeInfo(T); @@ -31,13 +29,47 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { env: napi.napi_env, raw: napi.napi_value, const Self = @This(); + var cached_constructor_ref: ?napi.napi_ref = null; + const InstanceData = struct { + allocator: std.mem.Allocator, + value: T, + + fn create() !*InstanceData { + const allocator = GlobalAllocator.globalAllocator(); + const instance = try allocator.create(InstanceData); + instance.* = .{ + .allocator = allocator, + .value = undefined, + }; + return instance; + } + + fn destroyUninitialized(self: *InstanceData) void { + self.allocator.destroy(self); + } + + fn destroy(self: *InstanceData) void { + const previous_allocator = GlobalAllocator.globalAllocator(); + GlobalAllocator.global_manager.set(self.allocator); + defer GlobalAllocator.global_manager.set(previous_allocator); + + if (@hasDecl(T, "deinit")) { + self.value.deinit(); + } else { + Napi.deinit_napi_value(T, self.value); + } + self.allocator.destroy(self); + } + }; fn constructor_callback(env: napi.napi_env, callback_info: napi.napi_callback_info) callconv(.c) napi.napi_value { const infos = CallbackInfo.from_raw(env, callback_info); + defer infos.deinit(); - const data = GlobalAllocator.globalAllocator().create(T) catch return null; + const instance = InstanceData.create() catch return null; + const data = &instance.value; - if (@hasDecl(T, "init")) { + if (comptime HasInit and @hasDecl(T, "init")) { const init_fn = T.init; const init_fn_type = @TypeOf(init_fn); const init_fn_info = @typeInfo(init_fn_type); @@ -51,7 +83,7 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { if (comptime @typeInfo(arg.type.?) == .@"union") { if (NapiError.last_error) |last_err| { last_err.throwInto(napi_env.Env.from_raw(env)); - GlobalAllocator.globalAllocator().destroy(data); + instance.destroyUninitialized(); return null; } } @@ -62,7 +94,7 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { if (NapiError.last_error) |last_err| { last_err.throwInto(napi_env.Env.from_raw(env)); } - GlobalAllocator.globalAllocator().destroy(data); + instance.destroyUninitialized(); return null; }; } else { @@ -79,7 +111,7 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { if (comptime @typeInfo(field.type) == .@"union") { if (NapiError.last_error) |last_err| { last_err.throwInto(napi_env.Env.from_raw(env)); - GlobalAllocator.globalAllocator().destroy(data); + instance.destroyUninitialized(); return null; } } @@ -89,16 +121,12 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { const this_obj = infos.This(); - var ref: napi.napi_ref = undefined; - const status = napi.napi_wrap(env, this_obj, data, finalize_callback, null, &ref); + const status = napi.napi_wrap(env, this_obj, instance, finalize_callback, null, null); if (status != napi.napi_ok) { - GlobalAllocator.globalAllocator().destroy(data); + instance.destroy(); return null; } - var ref_count: u32 = undefined; - _ = napi.napi_reference_unref(env, ref, &ref_count); - return this_obj; } @@ -106,6 +134,7 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { return struct { fn call(env: napi.napi_env, callback_info: napi.napi_callback_info) callconv(.c) napi.napi_value { const infos = CallbackInfo.from_raw(env, callback_info); + defer infos.deinit(); const factory_fn = @field(T, factory_name); const factory_fn_type = @TypeOf(factory_fn); @@ -139,22 +168,27 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { } } - const constructor = class_constructors.get(class_name) orelse return null; - var js_instance: napi.napi_value = undefined; - _ = napi.napi_new_instance(env, constructor, infos.args_count, @ptrCast(infos.args_raw.ptr), &js_instance); - - const heap_data = GlobalAllocator.globalAllocator().create(T) catch return null; - heap_data.* = instance_data; + const class_constructor_ref = Self.cached_constructor_ref orelse return null; + defer Napi.deinit_napi_value(T, instance_data); - var ref: napi.napi_ref = undefined; - const status = napi.napi_wrap(env, js_instance, heap_data, finalize_callback, null, &ref); - if (status != napi.napi_ok) { - GlobalAllocator.globalAllocator().destroy(heap_data); - return null; + var constructor_args: [fields.len]napi.napi_value = undefined; + inline for (fields, 0..) |field, i| { + constructor_args[i] = Napi.to_napi_value(env, @field(instance_data, field.name), field.name) catch return null; } - var ref_count: u32 = undefined; - _ = napi.napi_reference_unref(env, ref, &ref_count); + var constructor: napi.napi_value = undefined; + const get_ref_status = napi.napi_get_reference_value(env, class_constructor_ref, &constructor); + if (get_ref_status != napi.napi_ok) return null; + + var js_instance: napi.napi_value = undefined; + const status = napi.napi_new_instance( + env, + constructor, + fields.len, + if (fields.len == 0) null else @ptrCast(&constructor_args), + &js_instance, + ); + if (status != napi.napi_ok) return null; return js_instance; } @@ -166,11 +200,8 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { _ = hint; if (data) |ptr| { - const typed_data: *T = @ptrCast(@alignCast(ptr)); - if (@hasDecl(T, "deinit")) { - typed_data.deinit(); - } - GlobalAllocator.globalAllocator().destroy(typed_data); + const instance: *InstanceData = @ptrCast(@alignCast(ptr)); + instance.destroy(); } } @@ -223,22 +254,24 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { const FieldAccessor = struct { fn getter(getter_env: napi.napi_env, info: napi.napi_callback_info) callconv(.c) napi.napi_value { const cb_info = CallbackInfo.from_raw(getter_env, info); + defer cb_info.deinit(); var data: ?*anyopaque = null; _ = napi.napi_unwrap(getter_env, cb_info.This(), &data); if (data == null) return null; - const instance: *T = @ptrCast(@alignCast(data.?)); - const field_value = @field(instance.*, field.name); + const instance: *InstanceData = @ptrCast(@alignCast(data.?)); + const field_value = @field(instance.value, field.name); return Napi.to_napi_value(getter_env, field_value, field.name) catch null; } fn setter(setter_env: napi.napi_env, info: napi.napi_callback_info) callconv(.c) napi.napi_value { const cb_info = CallbackInfo.from_raw(setter_env, info); + defer cb_info.deinit(); var data: ?*anyopaque = null; _ = napi.napi_unwrap(setter_env, cb_info.This(), &data); if (data == null) return null; - const instance: *T = @ptrCast(@alignCast(data.?)); + const instance: *InstanceData = @ptrCast(@alignCast(data.?)); const args = cb_info.args; if (args.len > 0) { if (comptime @typeInfo(field.type) == .@"union") { @@ -251,7 +284,7 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { return null; } } - @field(instance.*, field.name) = new_value; + @field(instance.value, field.name) = new_value; } return null; } @@ -321,6 +354,7 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { const params = method_info.@"fn".params; const is_instance_method = params.len > 0 and (params[0].type.? == *T or params[0].type.? == T); + const method_args_offset = if (is_instance_method) 1 else 0; const return_type = method_info.@"fn".return_type.?; const is_factory_method = blk: { @@ -345,30 +379,42 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { }; } else { const MethodWrapper = struct { + fn cleanupArgs(args: *std.meta.ArgsTuple(@TypeOf(method)), initialized: usize) void { + inline for (method_info.@"fn".params[method_args_offset..], method_args_offset..) |param, i| { + if (i < initialized) { + Napi.deinit_napi_value(param.type.?, args[i]); + } + } + } + fn call(method_env: napi.napi_env, info: napi.napi_callback_info) callconv(.c) napi.napi_value { const cb_info = CallbackInfo.from_raw(method_env, info); + defer cb_info.deinit(); var data: ?*anyopaque = null; _ = napi.napi_unwrap(method_env, cb_info.This(), &data); if (data == null) return null; var tuple_args: std.meta.ArgsTuple(@TypeOf(method)) = undefined; + var initialized_args: usize = 0; + defer cleanupArgs(&tuple_args, initialized_args); // inject instance if (is_instance_method) { if (method_info.@"fn".params[0].type.? != *T) { @compileError("Method " ++ fn_name ++ " must have a self parameter, which is a pointer to the class"); } - const instance: *T = @ptrCast(@alignCast(data.?)); - tuple_args[0] = instance; + const instance: *InstanceData = @ptrCast(@alignCast(data.?)); + tuple_args[0] = &instance.value; + initialized_args = 1; } - const args_offset = if (is_instance_method) 1 else 0; // inject args - inline for (method_info.@"fn".params[args_offset..], args_offset..) |param, i| { + inline for (method_info.@"fn".params[method_args_offset..], method_args_offset..) |param, i| { if (comptime @typeInfo(param.type.?) == .@"union") { NapiError.clearLastError(); } - tuple_args[i] = Napi.from_napi_value(method_env, cb_info.args[i - args_offset].raw, param.type.?); + tuple_args[i] = Napi.from_napi_value(method_env, cb_info.args[i - method_args_offset].raw, param.type.?); + initialized_args = i + 1; if (comptime @typeInfo(param.type.?) == .@"union") { if (NapiError.last_error) |last_err| { last_err.throwInto(napi_env.Env.from_raw(method_env)); @@ -398,9 +444,18 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { } var constructor: napi.napi_value = undefined; - _ = napi.napi_define_class(env, class_name.ptr, class_name.len, constructor_callback, null, prop_idx, &properties, &constructor); + const define_status = napi.napi_define_class(env, class_name.ptr, class_name.len, constructor_callback, null, prop_idx, &properties, &constructor); + if (define_status != napi.napi_ok) { + return NapiError.Error.fromStatus(NapiError.Status.New(define_status)); + } + + var new_constructor_ref: napi.napi_ref = undefined; + const ref_status = napi.napi_create_reference(env, constructor, 1, &new_constructor_ref); + if (ref_status != napi.napi_ok) { + return NapiError.Error.fromStatus(NapiError.Status.New(ref_status)); + } - try class_constructors.put(class_name, constructor); + Self.cached_constructor_ref = new_constructor_ref; return constructor; } diff --git a/src/napi/wrapper/typedarray.zig b/src/napi/wrapper/typedarray.zig index 5fe02b3..f3e347a 100644 --- a/src/napi/wrapper/typedarray.zig +++ b/src/napi/wrapper/typedarray.zig @@ -67,14 +67,23 @@ pub fn TypedArray(comptime T: type) type { &byte_offset, ); + const arraybuffer = ArrayBuffer.from_raw(env, arraybuffer_raw); + const remaining_byte_len = arraybuffer.length() -| byte_offset; + const element_len = if (len * @sizeOf(T) <= remaining_byte_len) + len + else if (len <= remaining_byte_len and len % @sizeOf(T) == 0) + len / @sizeOf(T) + else + 0; + return Self{ .env = env, .raw = raw, - .data = if (len == 0 or data == null) &[_]T{} else @ptrCast(@alignCast(data)), - .len = len, + .data = if (element_len == 0 or data == null) &[_]T{} else @ptrCast(@alignCast(data)), + .len = element_len, .typedarray_type = typedarray_type, .byte_offset = byte_offset, - .arraybuffer = ArrayBuffer.from_raw(env, arraybuffer_raw), + .arraybuffer = arraybuffer, }; } diff --git a/src/prelude/module.zig b/src/prelude/module.zig index 2dc71b5..7c39f7a 100644 --- a/src/prelude/module.zig +++ b/src/prelude/module.zig @@ -1,4 +1,5 @@ const builtin = @import("builtin"); +const build_options = @import("build_options"); const napi = @import("napi-sys").napi_sys; const Env = @import("../napi/env.zig").Env; const Object = @import("../napi/value.zig").Object; @@ -11,6 +12,10 @@ pub fn NODE_API_MODULE_WITH_INIT( comptime root: type, init: ?fn (env: Env, exports: Object) anyerror!?Object, ) void { + if (@hasDecl(build_options, "napi_tsgen") and build_options.napi_tsgen) { + return; + } + const root_infos = @typeInfo(root); if (root_infos != .@"struct") { @@ -96,7 +101,7 @@ pub fn NODE_API_MODULE_WITH_INIT( }; comptime { - if (builtin.target.abi.isOpenHarmony()) { + if (builtin.object_format == .elf) { const init_array = [1]*const fn () callconv(.c) void{&ModuleImpl.module_init}; @export(&init_array, .{ .linkage = .strong, .name = "init_array", .section = ".init_array" }); } diff --git a/test/assert.ts b/test/assert.ts new file mode 100644 index 0000000..67ca7fa --- /dev/null +++ b/test/assert.ts @@ -0,0 +1,64 @@ +export function fail(message: string): never { + throw new Error(message); +} + +export function assert(condition: boolean, message: string) { + if (!condition) { + fail(message); + } +} + +export function assertEqual(actual: ESObject, expected: ESObject, message: string) { + if (actual !== expected) { + fail(`${message}: expected=${String(expected)} actual=${String(actual)}`); + } +} + +export function assertNullish(actual: ESObject, message: string) { + if (actual !== null && actual !== undefined) { + fail(`${message}: expected nullish actual=${String(actual)}`); + } +} + +export function assertApproxEqual(actual: number, expected: number, message: string, epsilon: number = 0.00001) { + if (Math.abs(actual - expected) > epsilon) { + fail(`${message}: expected=${String(expected)} actual=${String(actual)}`); + } +} + +export function assertArrayEqual(actual: Array, expected: Array, message: string) { + assertEqual(actual.length, expected.length, `${message}.length`); + for (let i = 0; i < expected.length; i++) { + assertEqual(actual[i], expected[i], `${message}[${i}]`); + } +} + +export function assertIncludes(actual: string, expected: string, message: string) { + if (actual.indexOf(expected) < 0) { + fail(`${message}: expected to include=${expected} actual=${actual}`); + } +} + +export function assertThrows(fn: () => void, expectedMessage: string, message: string) { + let threw = false; + try { + fn(); + } catch (err) { + threw = true; + const actual = String(err && (err.message || err)); + assertIncludes(actual, expectedMessage, message); + } + assert(threw, `${message}: expected throw`); +} + +export async function assertRejects(promise: Promise, expectedMessage: string, message: string) { + let rejected = false; + try { + await promise; + } catch (err) { + rejected = true; + const actual = String(err && (err.message || err)); + assertIncludes(actual, expectedMessage, message); + } + assert(rejected, `${message}: expected rejection`); +} diff --git a/test/async.spec.ts b/test/async.spec.ts new file mode 100644 index 0000000..4f0c50e --- /dev/null +++ b/test/async.spec.ts @@ -0,0 +1,65 @@ +import { assertArrayEqual, assertEqual, assertRejects } from "./assert"; + +type NativeAddon = ESObject; + +function abortSignal(aborted: boolean): ESObject { + return { + aborted, + reason: "test abort", + onabort: null, + addEventListener(_: string, __: ESObject) {}, + removeEventListener(_: string, __: ESObject) {}, + throwIfAborted() { + if (aborted) { + throw new Error("test abort"); + } + }, + }; +} + +export async function testAsync(native: NativeAddon) { + assertEqual(await native.fib_async(10), 55, "fib_async"); + + const progressEvents: Array = []; + assertEqual(await native.fib_async_progress(8, (event: ESObject) => progressEvents.push(event)), 21, "fib_async_progress"); + assertEqual(progressEvents.length, 2, "fib_async_progress events"); + assertEqual(progressEvents[0].current, 0, "fib_async_progress first event"); + assertEqual(progressEvents[1].current, 8, "fib_async_progress last event"); + + const firstText = "alpha\n"; + const secondText = "bravo\n"; + assertEqual(await native.read_file_async("fixtures/first.txt"), firstText, "read_file_async"); + + const summary = await native.read_file_summary_async("fixtures/first.txt"); + assertEqual(summary.path, "fixtures/first.txt", "read_file_summary path"); + assertEqual(summary.bytes, firstText.length, "read_file_summary bytes"); + assertEqual(summary.text, firstText, "read_file_summary text"); + + const parallel = await native.parallel_read_files_async({ + first_path: "fixtures/first.txt", + second_path: "fixtures/second.txt", + preview_bytes: 3, + }); + assertEqual(parallel.first_bytes, firstText.length, "parallel first bytes"); + assertEqual(parallel.second_bytes, secondText.length, "parallel second bytes"); + assertEqual(parallel.total_bytes, firstText.length + secondText.length, "parallel total bytes"); + assertEqual(parallel.preview, "alp\n---\nbra", "parallel preview"); + + const math = await native.async_math_single({ left: 3, right: 4, scale: 2 }); + assertEqual(math.sum, 7, "async math sum"); + assertEqual(math.product, 12, "async math product"); + assertEqual(math.scaled_sum, 14, "async math scaled_sum"); + + await native.async_void_thread(); + await assertRejects(native.async_fail_thread("async boom"), "async boom", "async_fail_thread"); + + const countEvents: Array = []; + assertEqual(await native.count_async_progress_thread(3, (event: ESObject) => countEvents.push(event)), 3, "count_async_progress_thread result"); + assertArrayEqual(countEvents.map((event: ESObject) => event.current), [0, 1, 2, 3], "count_async_progress_thread current events"); + + const eventModeEvents: Array = []; + assertEqual(await native.event_mode_progress_async(2, (event: ESObject) => eventModeEvents.push(event)), 2, "event_mode_progress_async result"); + assertArrayEqual(eventModeEvents.map((event: ESObject) => event.current), [0, 1, 2], "event_mode_progress_async current events"); + + await assertRejects(native.abortable_count_async(4096, abortSignal(true)), "AbortError", "abortable_count_async pre-aborted"); +} diff --git a/test/basic.ts b/test/basic.ts new file mode 100644 index 0000000..bb5ec09 --- /dev/null +++ b/test/basic.ts @@ -0,0 +1,18 @@ +import { testAsync } from "./async.spec"; +import { testBinary } from "./binary.spec"; +import { testErrorsAndThreadSafeFunction } from "./errors-tsfn.spec"; +import { testFunctionsAndClasses } from "./functions-classes.spec"; +import { testObjectsAndArrays } from "./objects-arrays.spec"; +import { testPrimitives } from "./primitives.spec"; +import { testUnionsAndEnums } from "./unions-enums.spec"; +import { runSuite } from "./native"; + +runSuite("__ZIG_NAPI_TEST_RESULT__", async (native) => { + testPrimitives(native); + testObjectsAndArrays(native); + testBinary(native); + testFunctionsAndClasses(native); + await testAsync(native); + testUnionsAndEnums(native); + await testErrorsAndThreadSafeFunction(native); +}); diff --git a/test/binary.spec.ts b/test/binary.spec.ts new file mode 100644 index 0000000..120f333 --- /dev/null +++ b/test/binary.spec.ts @@ -0,0 +1,23 @@ +import { assertArrayEqual, assertEqual } from "./assert"; + +type NativeAddon = ESObject; + +export function testBinary(native: NativeAddon) { + const bufferValue = native.create_buffer(); + assertEqual(native.get_buffer(bufferValue), 1024, "buffer length"); + assertEqual(native.get_buffer_as_string(bufferValue).length, 1024, "buffer string length"); + + const arrayBufferValue = native.create_arraybuffer(); + assertEqual(native.get_arraybuffer(arrayBufferValue), 1024, "arraybuffer length"); + assertEqual(native.get_arraybuffer_as_string(arrayBufferValue).length, 1024, "arraybuffer string length"); + + const typedArrayValue = native.create_uint8_typedarray(); + assertEqual(native.get_uint8_typedarray_length(typedArrayValue), 4, "typedarray length"); + assertArrayEqual(Array.from(typedArrayValue), [1, 2, 3, 4], "typedarray content"); + assertEqual(native.sum_float32_typedarray(new Float32Array([1.5, 2.5, -1])), 3, "float32 typedarray sum"); + + const dataViewValue = native.create_dataview(); + assertEqual(native.get_dataview_length(dataViewValue), 4, "dataview length"); + assertEqual(native.get_dataview_first_byte(dataViewValue), 0x78, "dataview first byte"); + assertEqual(native.get_dataview_uint32_le(dataViewValue), 0x12345678, "dataview uint32 le"); +} diff --git a/test/errors-tsfn.spec.ts b/test/errors-tsfn.spec.ts new file mode 100644 index 0000000..a74d7aa --- /dev/null +++ b/test/errors-tsfn.spec.ts @@ -0,0 +1,42 @@ +import { assert, assertEqual, assertIncludes, assertThrows } from "./assert"; + +type NativeAddon = ESObject; + +async function waitForThreadSafeFunction(native: NativeAddon) { + await new Promise((resolve, reject) => { + let sawOk = false; + let sawErr = false; + + function maybeResolve() { + if (sawOk && sawErr) { + resolve(); + } + } + + try { + native.call_thread_safe_function((err: ESObject, left: number, right: number) => { + try { + if (err) { + assert(!sawErr, "thread safe function error callback duplicated"); + assertIncludes(String(err && (err.message || err)), "TSFN Error", "thread safe function error callback"); + sawErr = true; + } else { + assert(!sawOk, "thread safe function success callback duplicated"); + assertEqual(left + right, 3, "thread safe function success callback"); + sawOk = true; + } + maybeResolve(); + } catch (callbackErr) { + reject(callbackErr); + } + }); + } catch (err) { + reject(err); + } + }); +} + +export async function testErrorsAndThreadSafeFunction(native: NativeAddon) { + assertThrows(() => native.throw_error(), "test", "throw_error repeat"); + await waitForThreadSafeFunction(native); +} diff --git a/test/functions-classes.spec.ts b/test/functions-classes.spec.ts new file mode 100644 index 0000000..4a47b04 --- /dev/null +++ b/test/functions-classes.spec.ts @@ -0,0 +1,33 @@ +import { assertEqual } from "./assert"; + +type NativeAddon = ESObject; + +export function testFunctionsAndClasses(native: NativeAddon) { + assertEqual(native.basic_function(19, 23), 42, "basic_function"); + + const createdFunction = native.create_function(); + assertEqual(createdFunction(19, 23), 42, "create_function"); + assertEqual(native.call_function((left: number, right: number) => left + right + 1), 4, "call_function"); + assertEqual(native.call_function_with_reference((left: number, right: number) => left * right), 2, "call_function_with_reference"); + + const classValue = new native.TestClass("Lin", 9); + assertEqual(classValue.name, "Lin", "class.name"); + assertEqual(classValue.age, 9, "class.age"); + + assertEqual(native.TestWithInitClass.hello, "Hello", "class static value"); + const initClassValue = new native.TestWithInitClass(11, "Init"); + assertEqual(initClassValue.name, "Init", "class init.name"); + assertEqual(initClassValue.age, 11, "class init.age"); + + assertEqual(native.TestWithoutInitClass.hello, "Hello", "class without init static value"); + + const factoryClassValue = native.TestFactoryClass.initWithFactory(13, "Factory"); + assertEqual(factoryClassValue.name, "Factory", "class factory.name"); + assertEqual(factoryClassValue.age, 13, "class factory.age"); + assertEqual(factoryClassValue.format(), "TestFactory { name = Factory, age = 13 }", "class factory format"); + + const constructedFactory = new native.TestFactoryClass("Ctor", 14); + assertEqual(constructedFactory.name, "Ctor", "class factory constructor.name"); + assertEqual(constructedFactory.age, 14, "class factory constructor.age"); + assertEqual(constructedFactory.format(), "TestFactory { name = Ctor, age = 14 }", "class factory constructor format"); +} diff --git a/test/init.ts b/test/init.ts new file mode 100644 index 0000000..009010e --- /dev/null +++ b/test/init.ts @@ -0,0 +1,20 @@ +import { assertArrayEqual, assertEqual, assertThrows } from "./assert"; +import { runSuite } from "./native"; + +runSuite("__ZIG_NAPI_INIT_TEST_RESULT__", async (native) => { + assertEqual(native.add(19, 23), 42, "init add"); + assertEqual(native.test_i32(1, 2), 3, "init test_i32"); + assertEqual(native.test_f32(1.25, 2.5), 3.75, "init test_f32"); + assertEqual(native.test_u32(4, 5), 9, "init test_u32"); + assertEqual(native.hello("ArkTS"), "Hello, ArkTS!", "init hello"); + assertEqual(native.text, "Hello", "init text"); + + native.fib(1); + assertEqual(await native.fib_async(10), 55, "init fib_async"); + + assertArrayEqual(native.get_and_return_array([1, 2, 3]), [1, 2, 3], "init array roundtrip"); + assertArrayEqual(native.get_arraylist([3, 2, 1]), [3, 2, 1], "init arraylist roundtrip"); + assertArrayEqual(native.get_named_array([7, true, "tuple"]), [7, true, "tuple"], "init tuple roundtrip"); + + assertThrows(() => native.throw_error(), "test", "init throw_error"); +}); diff --git a/test/native.ts b/test/native.ts new file mode 100644 index 0000000..ce7e5f0 --- /dev/null +++ b/test/native.ts @@ -0,0 +1,74 @@ +declare function requireNapiPreview(name: string, isApp: boolean): ESObject; +declare function print(message: string): void; +declare function setInterval(callback: () => void, delay: number): number; +declare function clearInterval(id: number): void; + +export type NativeAddon = ESObject; + +const SUITE_TIMEOUT_MS = 60000; +const KEEP_ALIVE_INTERVAL_MS = 10; + +// ark_js_napi_cli drains uv handles after entry execution. The official interop +// runtime installs timer globals, then a timer handle can keep Promise/TSFN work alive. +function installTimerGlobals() { + const etsInterop = requireNapiPreview("ets_interop_js_napi", true) as ESObject; + const created = etsInterop.createRuntime({ + "panda-files": "./hello.abc", + "boot-panda-files": "./etsstdlib.abc:./hello.abc", + "xgc-trigger-type": "never", + }); + if (!created) { + throw new Error("failed to initialize ArkVM timer runtime"); + } +} + +export function loadNative(): NativeAddon { + return requireNapiPreview("hello", true) as NativeAddon; +} + +function fail(resultPrefix: string, err: ESObject): never { + const message = String(err && (err.message || err)); + print(`${resultPrefix} status=fail message=${message}`); + throw err; +} + +export function runSuite(resultPrefix: string, run: (native: NativeAddon) => Promise | void) { + installTimerGlobals(); + + let finished = false; + let elapsed = 0; + + const keepAlive = setInterval(() => { + if (finished) { + clearInterval(keepAlive); + return; + } + elapsed += KEEP_ALIVE_INTERVAL_MS; + if (elapsed >= SUITE_TIMEOUT_MS) { + finished = true; + clearInterval(keepAlive); + fail(resultPrefix, new Error(`suite timed out after ${SUITE_TIMEOUT_MS}ms`)); + } + }, KEEP_ALIVE_INTERVAL_MS); + + Promise.resolve() + .then(() => run(loadNative())) + .then( + () => { + if (finished) { + return; + } + finished = true; + print(`${resultPrefix} status=ok`); + clearInterval(keepAlive); + }, + (err) => { + if (finished) { + return; + } + finished = true; + clearInterval(keepAlive); + fail(resultPrefix, err); + }, + ); +} diff --git a/test/objects-arrays.spec.ts b/test/objects-arrays.spec.ts new file mode 100644 index 0000000..d88d964 --- /dev/null +++ b/test/objects-arrays.spec.ts @@ -0,0 +1,36 @@ +import { assertArrayEqual, assertEqual, assertNullish } from "./assert"; + +type NativeAddon = ESObject; + +export function testObjectsAndArrays(native: NativeAddon) { + assertArrayEqual(native.get_and_return_array([1, 2, 3]), [1, 2, 3], "array roundtrip"); + assertArrayEqual(native.get_arraylist([3, 2, 1]), [3, 2, 1], "arraylist roundtrip"); + assertArrayEqual(native.get_named_array([7, true, "tuple"]), [7, true, "tuple"], "tuple roundtrip"); + + const objectResult = native.get_object({ + name: "Ada", + age: 36, + is_student: false, + }); + assertEqual(objectResult.name, "Ada", "object.name"); + assertEqual(objectResult.age, 36, "object.age"); + assertEqual(objectResult.is_student, false, "object.is_student"); + + const optionalDefaults = native.get_object_optional({ name: "Defaulted" }); + assertEqual(optionalDefaults.name, "Defaulted", "object optional.name"); + assertEqual(optionalDefaults.age, 18, "object optional default age"); + assertEqual(optionalDefaults.is_student, true, "object optional default is_student"); + + const optionalRoundtrip = native.get_optional_object_and_return_optional({ + name: "Present", + age: 20, + is_student: false, + }); + assertEqual(optionalRoundtrip.name, "Present", "optional roundtrip.name"); + assertEqual(optionalRoundtrip.age, 20, "optional roundtrip.age"); + assertEqual(optionalRoundtrip.is_student, false, "optional roundtrip.is_student"); + + assertEqual(native.get_nullable_object({ name: "Nullable" }).name, "Nullable", "nullable object value"); + assertNullish(native.get_nullable_object({ name: null }).name, "nullable object null"); + assertNullish(native.return_nullable().name, "return_nullable"); +} diff --git a/test/primitives.spec.ts b/test/primitives.spec.ts new file mode 100644 index 0000000..41bbc55 --- /dev/null +++ b/test/primitives.spec.ts @@ -0,0 +1,18 @@ +import { assertApproxEqual, assertEqual, assertThrows } from "./assert"; + +type NativeAddon = ESObject; + +export function testPrimitives(native: NativeAddon) { + assertEqual(native.test_i32(1, 2), 3, "test_i32 positive"); + assertEqual(native.test_i32(-7, 2), -5, "test_i32 negative"); + assertEqual(native.test_u32(4, 5), 9, "test_u32"); + assertApproxEqual(native.test_f32(1.25, 2.5), 3.75, "test_f32"); + + assertEqual(native.hello("ArkTS"), "Hello, ArkTS!", "hello"); + assertEqual(native.hello(""), "Hello, !", "hello empty"); + assertEqual(native.text, "Hello World", "const text"); + + assertThrows(() => native.throw_error(), "test", "throw_error"); + native.test_hilog(); + native.fib(1); +} diff --git a/test/unions-enums.spec.ts b/test/unions-enums.spec.ts new file mode 100644 index 0000000..28b8945 --- /dev/null +++ b/test/unions-enums.spec.ts @@ -0,0 +1,69 @@ +import { assertArrayEqual, assertEqual, assertNullish } from "./assert"; + +type NativeAddon = ESObject; + +function assertPayload(value: ESObject, title: string, count: number, message: string) { + assertEqual(value.title, title, `${message}.title`); + assertEqual(value.count, count, `${message}.count`); +} + +export function testUnionsAndEnums(native: NativeAddon) { + assertEqual(native.union_identity(42), 42, "union_identity number"); + assertEqual(native.union_identity("forty-two"), "forty-two", "union_identity text"); + assertEqual(native.make_union(true), 42, "make_union number"); + assertEqual(native.make_union(false), "hello", "make_union text"); + assertEqual(native.union_kind(42), "number", "union_kind number"); + assertEqual(native.union_kind("forty-two"), "text", "union_kind text"); + + assertPayload(native.object_or_text_identity({ title: "payload", count: 5 }), "payload", 5, "object_or_text payload"); + assertEqual(native.object_or_text_identity("plain"), "plain", "object_or_text text"); + assertPayload(native.make_object_or_text(true), "hello", 2, "make_object_or_text payload"); + assertEqual(native.make_object_or_text(false), "plain", "make_object_or_text text"); + + assertPayload(native.object_or_array_identity({ title: "list-payload", count: 6 }), "list-payload", 6, "object_or_array payload"); + assertArrayEqual(native.object_or_array_identity([1, 2, 3]), [1, 2, 3], "object_or_array list"); + assertArrayEqual(native.tuple_or_text_identity([1, true, "tuple"]), [1, true, "tuple"], "tuple_or_text tuple"); + assertEqual(native.tuple_or_text_identity("tuple-text"), "tuple-text", "tuple_or_text text"); + + assertEqual(native.flip_flag_or_increment(true), false, "flip_flag_or_increment bool"); + assertEqual(native.flip_flag_or_increment(9), 10, "flip_flag_or_increment number"); + + assertEqual(native.enum_identity(1), 1, "enum_identity"); + assertEqual(native.favorite_color(), 2, "favorite_color"); + assertEqual(native.is_primary(4), true, "is_primary"); + assertEqual(native.string_enum_identity("Red"), "Red", "string_enum_identity"); + assertEqual(native.favorite_string_color(), "Blue", "favorite_string_color"); + + assertEqual(native.color_or_text_identity(1), 1, "color_or_text color"); + assertEqual(native.color_or_text_identity("fallback"), "fallback", "color_or_text text"); + assertEqual(native.favorite_color_or_text(true), 4, "favorite_color_or_text color"); + assertEqual(native.favorite_color_or_text(false), "fallback", "favorite_color_or_text text"); + + assertEqual(native.maybe_text_or_count_identity("maybe"), "maybe", "maybe_text string"); + assertNullish(native.maybe_text_or_count_identity(null), "maybe_text null"); + assertEqual(native.maybe_text_or_count_identity(8), 8, "maybe_text count"); + assertNullish(native.make_maybe_text_or_count(true), "make_maybe_text null"); + assertEqual(native.make_maybe_text_or_count(false), 7, "make_maybe_text count"); + + const madeBuffer = native.make_buffer_or_text(true); + assertEqual(native.get_buffer(madeBuffer), 16, "make_buffer_or_text buffer"); + assertEqual(native.get_buffer(native.buffer_or_text_identity(madeBuffer)), 16, "buffer_or_text buffer"); + assertEqual(native.buffer_or_text_identity("buffer-text"), "buffer-text", "buffer_or_text text"); + assertEqual(native.make_buffer_or_text(false), "buffer-fallback", "make_buffer_or_text text"); + + const madeArrayBuffer = native.make_arraybuffer_or_array(true); + assertEqual(native.get_arraybuffer(madeArrayBuffer), 16, "make_arraybuffer_or_array arraybuffer"); + assertEqual(native.get_arraybuffer(native.arraybuffer_or_array_identity(madeArrayBuffer)), 16, "arraybuffer_or_array arraybuffer"); + assertArrayEqual(native.arraybuffer_or_array_identity([4, 5]), [4, 5], "arraybuffer_or_array list"); + assertArrayEqual(native.make_arraybuffer_or_array(false), [1, 2, 3], "make_arraybuffer_or_array list"); + + assertPayload(native.payload_or_color_identity({ title: "mixed", count: 11 }), "mixed", 11, "payload_or_color payload"); + assertEqual(native.payload_or_color_identity(1), 1, "payload_or_color color"); + assertPayload(native.make_payload_or_color(true), "mixed", 9, "make_payload_or_color payload"); + assertEqual(native.make_payload_or_color(false), 1, "make_payload_or_color color"); + + assertPayload(native.payload_or_string_color_identity({ title: "string-color", count: 12 }), "string-color", 12, "payload_or_string_color payload"); + assertEqual(native.payload_or_string_color_identity("Red"), "Red", "payload_or_string_color color"); + assertPayload(native.make_payload_or_string_color(true), "string-enum", 3, "make_payload_or_string_color payload"); + assertEqual(native.make_payload_or_string_color(false), "Green", "make_payload_or_string_color color"); +}