From aa0f94ef71ec923b4d8191e607a1019ff954703b Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Thu, 9 Jul 2026 22:10:47 +0800 Subject: [PATCH 01/17] chore: update zig and translate-c --- .github/workflows/test-windows.yml | 60 +++++++++++-------- .gitignore | 5 ++ bench/zig_ext/build.zig.zon | 2 +- build.zig.zon | 6 +- build/Phpz.zig | 12 +--- build/phpz.h | 27 --------- examples/my_php_extension/build.zig.zon | 5 +- examples/pjs/build.zig.zon | 9 ++- .../pjs/deps/zig-quickjs-ng/build.zig.zon | 2 +- examples/skeleton/build.zig.zon | 5 +- src/Ctx.zig | 2 +- src/class.zig | 10 ++-- src/function.zig | 18 +++--- src/module.zig | 16 ++--- src/zend/callable.zig | 2 +- src/zend/function.zig | 2 +- 16 files changed, 86 insertions(+), 97 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index bbb0349..917a7b4 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -1,4 +1,4 @@ -name: Test (Windows) +name: Windows Test on: # push: @@ -19,15 +19,23 @@ on: jobs: test: - runs-on: windows-latest + runs-on: windows-2025 strategy: fail-fast: false matrix: - php: ["8.2", "8.3", "8.4", "8.5"] + # php: ["8.2", "8.3", "8.4", "8.5"] + php: ["8.5"] steps: - name: Checkout uses: actions/checkout@v4 - uses: mlugg/setup-zig@v2 + - name: Install pkg-config + shell: pwsh + run: | + # Strawberry Perl's pkg-config.BAT cannot be executed directly by + # Zig's ChildProcess. Install a native .exe to shadow it. + choco install pkgconfiglite --no-progress -y + where pkg-config - name: Setup PHP SDK with dev headers id: setup-php-sdk uses: php/setup-php-sdk@v0.12 @@ -61,26 +69,26 @@ jobs: -Dtarget=native-native-msvc \ -Dphp-include-dir="${{ steps.php-paths.outputs.include }}" \ -Dphp-lib-dir="${{ steps.php-paths.outputs.lib }}" - - name: Run example tests - shell: bash - run: | - zig build test-examples \ - -Dtarget=native-native-msvc \ - -Dphp-include-dir="${{ steps.php-paths.outputs.include }}" \ - -Dphp-lib-dir="${{ steps.php-paths.outputs.lib }}" - - name: Run example tests (ReleaseSafe) - shell: bash - run: | - zig build test-examples \ - -Dtarget=native-native-msvc \ - -Dphp-include-dir="${{ steps.php-paths.outputs.include }}" \ - -Dphp-lib-dir="${{ steps.php-paths.outputs.lib }}" \ - -Doptimize=ReleaseSafe - - name: Run example tests (ReleaseFast) - shell: bash - run: | - zig build test-examples \ - -Dtarget=native-native-msvc \ - -Dphp-include-dir="${{ steps.php-paths.outputs.include }}" \ - -Dphp-lib-dir="${{ steps.php-paths.outputs.lib }}" \ - -Doptimize=ReleaseFast + # - name: Run example tests + # shell: bash + # run: | + # zig build test-examples \ + # -Dtarget=native-native-msvc \ + # -Dphp-include-dir="${{ steps.php-paths.outputs.include }}" \ + # -Dphp-lib-dir="${{ steps.php-paths.outputs.lib }}" + # - name: Run example tests (ReleaseSafe) + # shell: bash + # run: | + # zig build test-examples \ + # -Dtarget=native-native-msvc \ + # -Dphp-include-dir="${{ steps.php-paths.outputs.include }}" \ + # -Dphp-lib-dir="${{ steps.php-paths.outputs.lib }}" \ + # -Doptimize=ReleaseSafe + # - name: Run example tests (ReleaseFast) + # shell: bash + # run: | + # zig build test-examples \ + # -Dtarget=native-native-msvc \ + # -Dphp-include-dir="${{ steps.php-paths.outputs.include }}" \ + # -Dphp-lib-dir="${{ steps.php-paths.outputs.lib }}" \ + # -Doptimize=ReleaseFast diff --git a/.gitignore b/.gitignore index ee8dfc3..cc99937 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,8 @@ zig-out .zig-cache zig-pkg + +# For msvc cross-compilation test +.xwin +xwin +libc.txt diff --git a/bench/zig_ext/build.zig.zon b/bench/zig_ext/build.zig.zon index aaa2b13..a6a46a1 100644 --- a/bench/zig_ext/build.zig.zon +++ b/bench/zig_ext/build.zig.zon @@ -2,7 +2,7 @@ .name = .bench_zig, .version = "0.1.0", .fingerprint = 0x5dddb3f44fe50dfa, - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0-dev.1282+c0f9b51d8", .dependencies = .{ .phpz = .{ .path = "../..", diff --git a/build.zig.zon b/build.zig.zon index 9c375c5..5584239 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,11 +2,11 @@ .name = .phpz, .version = "0.0.0", .fingerprint = 0x935004cc3861700f, - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0-dev.1282+c0f9b51d8", .dependencies = .{ .translate_c = .{ - .url = "git+https://codeberg.org/ziglang/translate-c#7a1a9fdc4ab00835748a6657ecbb835e3d5d45f7", - .hash = "translate_c-0.0.0-Q_BUWvP1BgCjAk6PWv5286tOlvzD9-X-NkuTzh0KxY0Q", + .url = "git+https://codeberg.org/happystraw/translate-c?ref=vectorcall#6d456ba6a90d3a63b56b85a6ad7c295d5707def3", + .hash = "translate_c-0.0.0-Q_BUWndCBwCKJ9V9WhZPG6IzFL9_HF_oCZvvYl5-aBBK", }, }, .paths = .{ diff --git a/build/Phpz.zig b/build/Phpz.zig index f8a0fad..d05e07d 100644 --- a/build/Phpz.zig +++ b/build/Phpz.zig @@ -70,20 +70,9 @@ fn createPhpCTranslator(b: *Build, options: Options) Translator { translator_options.strict_flex_arrays = .@"1"; const c: Translator = .init(translate_c_dep, translator_options); - if (translator_options.target.query.isNative() and translator_options.target.result.os.tag == .linux) { - // FIXME: remove in zig 0.17.0 - // Add Zig's C include path (for stdint.h, stddef.h, etc.) - if (b.graph.zig_lib_directory.path) |path| { - c.addIncludePath(.{ .cwd_relative = b.fmt("{s}/include", .{path}) }); - } - } // phpz.h c.addIncludePath(b.path("build")); - // FIXME: remove in zig 0.17.0 - // See: https://codeberg.org/ziglang/translate-c/issues/362 - c.defineCMacro("__thread", ""); - // Configure PHP include paths for the C preprocessor if (options.php_include_dir) |root| { c.addIncludePath(root); @@ -95,6 +84,7 @@ fn createPhpCTranslator(b: *Build, options: Options) Translator { c.defineCMacro("ZEND_WIN32", "1"); c.defineCMacro("PHP_WIN32", "1"); c.defineCMacro("WINDOWS", "1"); + c.defineCMacro("ZEND_DEBUG", "0"); } } diff --git a/build/phpz.h b/build/phpz.h index 48149fa..f74034d 100644 --- a/build/phpz.h +++ b/build/phpz.h @@ -1,33 +1,6 @@ #ifndef PHPZ_H #define PHPZ_H -/* - * Workaround: skip on aarch64. - * - * Why it's needed: - * On aarch64, `php.h` -> `Zend/zend_types.h` unconditionally includes - * , which pulls in Clang's . Those - * headers use the `__mfp8` builtin type and thousands of - * `__builtin_neon_*` intrinsics that Zig's translate-c (aro) cannot - * parse, so binding generation fails before any of our code is reached. - * - * Why it's safe: - * PHP only uses NEON inside macro bodies (e.g. HT_HASH_RESET in - * zend_types.h) and static inline functions. translate-c only parses - * top-level declarations (typedefs / structs / function prototypes), - * none of which reference NEON types. The macros are expanded later by - * the real C compiler when the extension is built, where - * is included normally and handled by native Clang. - * - * How it works: - * Predefining the headers' own include guards makes their bodies expand - * to nothing during translate-c, while the C build still sees them - * unchanged. - */ -#if defined(__aarch64__) || defined(_M_ARM64) -#define __ARM_NEON_H -#endif - #include "php.h" #include "Zend/zend_API.h" #include "Zend/zend_exceptions.h" diff --git a/examples/my_php_extension/build.zig.zon b/examples/my_php_extension/build.zig.zon index 2d22f7c..61447e8 100644 --- a/examples/my_php_extension/build.zig.zon +++ b/examples/my_php_extension/build.zig.zon @@ -2,7 +2,7 @@ .name = .my_php_extension, .version = "0.0.0", .fingerprint = 0x8628f6becbe8e7c3, - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0-dev.1282+c0f9b51d8", .dependencies = .{ .phpz = .{ .path = "../..", @@ -11,7 +11,10 @@ .paths = .{ "build.zig", "build.zig.zon", + "README.md", + "run-tests.php", "src", + "tests", "config.m4", "Makefile.frag", "my_php_extension.c", diff --git a/examples/pjs/build.zig.zon b/examples/pjs/build.zig.zon index a5f70de..73a7daf 100644 --- a/examples/pjs/build.zig.zon +++ b/examples/pjs/build.zig.zon @@ -2,7 +2,7 @@ .name = .pjs, .version = "0.0.0", .fingerprint = 0xfdae12e932c96e8d, - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0-dev.947+36069a2a7", .dependencies = .{ .phpz = .{ .path = "../../", @@ -14,6 +14,13 @@ .paths = .{ "build.zig", "build.zig.zon", + "README.md", + "deps", + "pjs.h", + "pjs.stub.php", + "pjs_arginfo.h", + "run-tests.php", "src", + "tests", }, } diff --git a/examples/pjs/deps/zig-quickjs-ng/build.zig.zon b/examples/pjs/deps/zig-quickjs-ng/build.zig.zon index e661a65..c8d344b 100644 --- a/examples/pjs/deps/zig-quickjs-ng/build.zig.zon +++ b/examples/pjs/deps/zig-quickjs-ng/build.zig.zon @@ -2,7 +2,7 @@ .name = .quickjs_ng, .version = "0.0.0", .fingerprint = 0xb2bca9fa0367c6d1, - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0-dev.813+2153f8143", .paths = .{ "build.zig", "build.zig.zon", diff --git a/examples/skeleton/build.zig.zon b/examples/skeleton/build.zig.zon index 74ba181..e5cc786 100644 --- a/examples/skeleton/build.zig.zon +++ b/examples/skeleton/build.zig.zon @@ -2,7 +2,7 @@ .name = .skeleton, .version = "0.0.0", .fingerprint = 0xddd8a92a8b2a4054, - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0-dev.1282+c0f9b51d8", .dependencies = .{ .phpz = .{ .path = "../..", @@ -11,7 +11,10 @@ .paths = .{ "build.zig", "build.zig.zon", + "README.md", + "run-tests.php", "src", + "tests", "skeleton.h", "skeleton.stub.php", "skeleton_arginfo.h", diff --git a/src/Ctx.zig b/src/Ctx.zig index 85a67d6..44040b1 100644 --- a/src/Ctx.zig +++ b/src/Ctx.zig @@ -311,7 +311,7 @@ pub const Call = opaque { const TagInt = @typeInfo(ExpectArgKind).@"enum".tag_type; var field_names: [u.len][]const u8 = undefined; var field_types: [u.len]type = undefined; - var field_attrs: [u.len]std.builtin.Type.UnionField.Attributes = undefined; + var field_attrs: [u.len]std.lang.Type.Union.FieldAttributes = undefined; var field_values: [u.len]TagInt = undefined; inline for (u, 0..) |kind, i| { if (kind == .mixed) @compileError("invalid .mixed specification: unions cannot contain .mixed"); diff --git a/src/class.zig b/src/class.zig index 7a6b3eb..65a9e7d 100644 --- a/src/class.zig +++ b/src/class.zig @@ -180,11 +180,11 @@ pub fn Class(comptime class_name: [:0]const u8, comptime T: type) type { c.zend_object_std_init(&intern.std, ce); c.object_properties_init(&intern.std, ce); if (@hasDecl(T, "init")) { - if (comptime @typeInfo(@TypeOf(T.init)).@"fn".params.len == 1) { + if (comptime @typeInfo(@TypeOf(T.init)).@"fn".param_types.len == 1) { intern.impl.init(); } else { comptime { - if (@typeInfo(@TypeOf(T.init)).@"fn".params.len != 2) @compileError("T.init must take 1 or 2 parameters"); + if (@typeInfo(@TypeOf(T.init)).@"fn".param_types.len != 2) @compileError("T.init must take 1 or 2 parameters"); } intern.impl.init(entry); } @@ -263,9 +263,9 @@ pub fn Class(comptime class_name: [:0]const u8, comptime T: type) type { /// pub const Class = phpz.Class("User", User); /// ``` pub fn setObjectHandlers(comptime hs: ObjectHandlers) void { - inline for (@typeInfo(ObjectHandlers).@"struct".fields) |field| { - const fv = @field(hs, field.name); - if (fv != null) @field(handlers, field.name) = fv; + inline for (@typeInfo(ObjectHandlers).@"struct".field_names) |field_name| { + const fv = @field(hs, field_name); + if (fv != null) @field(handlers, field_name) = fv; } } diff --git a/src/function.zig b/src/function.zig index 8700d3d..b9ad699 100644 --- a/src/function.zig +++ b/src/function.zig @@ -91,7 +91,7 @@ pub fn methodWithClass(comptime Class: anytype, comptime class_name: [:0]const u fn wrapFn(comptime func_desc: [:0]const u8, comptime func: anytype) Fn { const Args = std.meta.ArgsTuple(@TypeOf(func)); return struct { - fn @"fn"(execute_data: ?*c.zend_execute_data, return_value: ?*c.zval) callconv(.c) void { + fn handle(execute_data: ?*c.zend_execute_data, return_value: ?*c.zval) callconv(.c) void { var ctx: Ctx = .{ .call = .from(execute_data.?), .ret = .from(return_value.?) }; const args = switch (Args) { @Tuple(&.{Ctx}) => .{ctx}, @@ -107,28 +107,28 @@ fn wrapFn(comptime func_desc: [:0]const u8, comptime func: anytype) Fn { } }; } - }.@"fn"; + }.handle; } fn wrapMethod(comptime Class: type, comptime func_desc: [:0]const u8, comptime func: anytype) Fn { const Args = std.meta.ArgsTuple(@TypeOf(func)); const args_type_info = @typeInfo(Args).@"struct"; const impl_type = @FieldType(Class, "impl"); - const kind: enum { static, object } = comptime if (args_type_info.fields.len > 0) blk: { - const first = args_type_info.fields[0].type; + const kind: enum { static, object } = comptime if (args_type_info.field_types.len > 0) blk: { + const first = args_type_info.field_types[0]; break :blk if (first == impl_type or first == *impl_type or first == *const impl_type) .object else .static; } else .static; const impl_offset = comptime @intFromBool(kind == .object); return struct { - fn @"fn"(execute_data: ?*c.zend_execute_data, return_value: ?*c.zval) callconv(.c) void { + fn handle(execute_data: ?*c.zend_execute_data, return_value: ?*c.zval) callconv(.c) void { var ctx: Ctx = .{ .call = .from(execute_data.?), .ret = .from(return_value.?) }; const args: Args = (if (kind == .object) blk: { const obj: *Class = .from(.std, ctx.call.thisObject().?.ptr()); - break :blk if (impl_type == args_type_info.fields[0].type) .{obj.impl} else .{&obj.impl}; + break :blk if (impl_type == args_type_info.field_types[0]) .{obj.impl} else .{&obj.impl}; } else .{}) ++ rest: { - const rest_count = args_type_info.fields.len - impl_offset; + const rest_count = args_type_info.field_types.len - impl_offset; break :rest if (rest_count == 1) - if (comptime args_type_info.fields[impl_offset].type == Ctx) + if (comptime args_type_info.field_types[impl_offset] == Ctx) .{ctx} else @compileError(std.fmt.comptimePrint("unsupported method signature for {s}: expected Ctx", .{func_desc})) @@ -145,7 +145,7 @@ fn wrapMethod(comptime Class: type, comptime func_desc: [:0]const u8, comptime f } }; } - }.@"fn"; + }.handle; } fn exportFn(comptime kind: enum { function, method }, comptime func_name: [:0]const u8, comptime func: Fn) void { diff --git a/src/module.zig b/src/module.zig index 1e41e65..35c3ad1 100644 --- a/src/module.zig +++ b/src/module.zig @@ -85,14 +85,14 @@ pub fn module(comptime cfg: Config) void { fn makePhpHookFn(comptime hook_fn: PhpHookFn) *const fn (c_int, c_int) callconv(.c) c.zend_result { return struct { - fn @"fn"(_: c_int, _: c_int) callconv(.c) c.zend_result { + fn handle(_: c_int, _: c_int) callconv(.c) c.zend_result { hook_fn() catch |err| { errors.err(.err, "Module hook function failed: %s", .{@errorName(err).ptr}); return c.FAILURE; }; return c.SUCCESS; } - }.@"fn"; + }.handle; } fn makePhpModuleStartupFn( @@ -104,7 +104,7 @@ fn makePhpModuleStartupFn( const has_symbols = comptime @hasDecl(c, symbols_fn_name); if (!has_symbols and hook_fn == null and ini_defs == null) return null; return struct { - fn @"fn"(_: c_int, module_number: c_int) callconv(.c) c.zend_result { + fn handle(_: c_int, module_number: c_int) callconv(.c) c.zend_result { if (comptime has_symbols) { // Constants: register_{name}_symbols is generated by gen_stub.php @field(c, symbols_fn_name)(module_number); @@ -122,13 +122,13 @@ fn makePhpModuleStartupFn( } return c.SUCCESS; } - }.@"fn"; + }.handle; } fn makePhpModuleShutdownFn(comptime hook_fn: ?PhpHookFn, comptime ini_defs: ?[]const c.zend_ini_entry_def) ?*const fn (c_int, c_int) callconv(.c) c.zend_result { if (hook_fn == null and ini_defs == null) return null; return struct { - fn @"fn"(_: c_int, module_number: c_int) callconv(.c) c.zend_result { + fn handle(_: c_int, module_number: c_int) callconv(.c) c.zend_result { if (comptime ini_defs != null) { c.zend_unregister_ini_entries(module_number); } @@ -140,15 +140,15 @@ fn makePhpModuleShutdownFn(comptime hook_fn: ?PhpHookFn, comptime ini_defs: ?[]c } return c.SUCCESS; } - }.@"fn"; + }.handle; } fn makePhpInfoFn(comptime info_fn: PhpInfoFn) *const fn ([*c]ModuleEntry) callconv(.c) void { return struct { - fn @"fn"(entry: [*c]ModuleEntry) callconv(.c) void { + fn handle(entry: [*c]ModuleEntry) callconv(.c) void { info_fn(@ptrCast(entry)); } - }.@"fn"; + }.handle; } inline fn createModuleEntry(comptime cfg: Config) ModuleEntry { diff --git a/src/zend/callable.zig b/src/zend/callable.zig index 9d9e998..fc24212 100644 --- a/src/zend/callable.zig +++ b/src/zend/callable.zig @@ -95,7 +95,7 @@ pub const Callable = extern struct { if (owns_retval) self.fci.retval = &discard; defer if (owns_retval) native.dtor(&discard); - const n = info.@"struct".fields.len; + const n = info.@"struct".field_types.len; switch (n) { 0 => { self.fci.param_count = 0; diff --git a/src/zend/function.zig b/src/zend/function.zig index aec519b..65d9399 100644 --- a/src/zend/function.zig +++ b/src/zend/function.zig @@ -110,7 +110,7 @@ pub const Function = opaque { if (!(info == .@"struct" and info.@"struct".is_tuple)) @compileError("params must be a tuple, e.g. .{} or .{a, b}"); - const n = info.@"struct".fields.len; + const n = info.@"struct".field_types.len; switch (n) { 0 => c.zend_call_known_function(self.ptr(), obj, scope, retval, 0, null, null), else => { From 6733c58d3bad8d3ccee8f29c909f9160edcb16b9 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Thu, 9 Jul 2026 22:10:50 +0800 Subject: [PATCH 02/17] feat(ini): add generic typed directives and setter APIs - add parse and format hooks for custom typed values - add configurable update stages and forced writes for runtime setters - validate wrapper types and raw Zend entry definitions in collect() - replace direct directive value access with get() --- src/ini.zig | 356 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 305 insertions(+), 51 deletions(-) diff --git a/src/ini.zig b/src/ini.zig index 99944f0..8e847a7 100644 --- a/src/ini.zig +++ b/src/ini.zig @@ -15,43 +15,229 @@ pub const Access = enum(c_int) { all = c.ZEND_INI_ALL, }; -// Define — typed INI declarations +/// INI update stage passed to Zend's alter-ini API. +pub const Stage = enum(c_int) { + startup = c.ZEND_INI_STAGE_STARTUP, + shutdown = c.ZEND_INI_STAGE_SHUTDOWN, + activate = c.ZEND_INI_STAGE_ACTIVATE, + deactivate = c.ZEND_INI_STAGE_DEACTIVATE, + runtime = c.ZEND_INI_STAGE_RUNTIME, + htaccess = c.ZEND_INI_STAGE_HTACCESS, +}; + +pub const SetOptions = struct { + modify: Access = .user, + stage: Stage = .runtime, + force: bool = false, +}; -/// Typed INI definition factory. Use via `ini.string`, `ini.int`, etc. -fn define(comptime T: type) type { +pub const SetError = error{ AlterIniFailed, FormatIniValueFailed }; + +// Typed INI declarations + +/// Typed custom INI definition factory. +/// +/// `parse` converts the Zend INI string into `T` whenever PHP changes the +/// directive. The input is length-bounded and also has a 0 sentinel at `len`. +/// `format` is optional; provide it to enable typed `set(T)`. +/// +/// ```zig +/// const Mode = enum { safe, fast }; +/// +/// const mode = phpz.ini.typed(Mode).new(.{ +/// .name = "ext.mode", +/// .default = .safe, +/// .default_text = "safe", +/// .access = .all, +/// .parse = parseMode, +/// .format = formatMode, +/// }); +/// ``` +pub fn typed(comptime T: type) type { return struct { - pub fn new( - comptime name: [:0]const u8, - comptime default_value: T, - comptime access: Access, - ) type { + pub const ParseFn = fn ([:0]const u8) anyerror!T; + pub const ParseWithEntryFn = fn (*c.zend_ini_entry, [:0]const u8) anyerror!T; + pub const FormatFn = fn (T, []u8) anyerror![]const u8; + + pub const Config = struct { + name: [:0]const u8, + default: T, + default_text: [:0]const u8, + access: Access, + parse: ?*const ParseFn = null, + parse_with_entry: ?*const ParseWithEntryFn = null, + format: ?*const FormatFn = null, + }; + + pub fn new(comptime cfg: Config) type { return struct { - pub var value: T = default_value; + var value: T = cfg.default; + + pub inline fn get() T { + return value; + } + + pub fn set(new_value: T) SetError!void { + return setWith(new_value, .{}); + } + + pub fn setWith(new_value: T, opts: SetOptions) SetError!void { + const format = cfg.format orelse @compileError( + "ini.typed(" ++ @typeName(T) ++ ").new(...) requires .format to use set()", + ); + var buffer: [256]u8 = undefined; + const text = format(new_value, &buffer) catch return error.FormatIniValueFailed; + return alter(cfg.name, text, opts); + } + + pub fn setText(new_value: []const u8) SetError!void { + return setTextWith(new_value, .{}); + } + + pub fn setTextWith(new_value: []const u8, opts: SetOptions) SetError!void { + return alter(cfg.name, new_value, opts); + } fn iniDef() c.zend_ini_entry_def { - const val = formatDefault(T, default_value); return .{ - .name = name, - .on_modify = @ptrCast(onModifyFor(T, &value)), + .name = cfg.name, + .on_modify = @ptrCast(onModify(&value)), .mh_arg1 = null, .mh_arg2 = null, .mh_arg3 = null, - .value = val, + .value = cfg.default_text, .displayer = null, - .value_length = @intCast(val.len), - .name_length = @intCast(name.len), - .modifiable = @intFromEnum(access), + .value_length = @intCast(cfg.default_text.len), + .name_length = @intCast(cfg.name.len), + .modifiable = @intFromEnum(cfg.access), }; } + + fn onModify(comptime global: *T) OnModifyFn { + return struct { + fn handle(entry: *c.zend_ini_entry, new_value: ?*c.zend_string, _: ?*anyopaque, _: ?*anyopaque, _: ?*anyopaque, _: c_int) callconv(.c) c_int { + if (new_value) |nv| { + const text = nv.*.val()[0..nv.*.len :0]; + global.* = parse(entry, text) catch return c.FAILURE; + } + return c.SUCCESS; + } + }.handle; + } + + fn parse(entry: *c.zend_ini_entry, text: [:0]const u8) anyerror!T { + if (comptime cfg.parse_with_entry) |f| return f(entry, text); + if (comptime cfg.parse) |f| return f(text); + @compileError( + "ini.typed(" ++ @typeName(T) ++ ").new(...) requires .parse or .parse_with_entry", + ); + } }; } }; } -pub const string = define([:0]const u8); -pub const int = define(i64); -pub const float = define(f64); -pub const boolean = define(bool); +pub const string = struct { + pub fn new( + comptime name: [:0]const u8, + comptime default_value: [:0]const u8, + comptime access: Access, + ) type { + return typed([:0]const u8).new(.{ + .name = name, + .default = default_value, + .default_text = default_value, + .access = access, + .parse = parse, + .format = format, + }); + } + + fn parse(text: [:0]const u8) ![:0]const u8 { + return text; + } + + fn format(value: [:0]const u8, _: []u8) ![]const u8 { + return value; + } +}; + +pub const int = struct { + pub fn new( + comptime name: [:0]const u8, + comptime default_value: i64, + comptime access: Access, + ) type { + return typed(i64).new(.{ + .name = name, + .default = default_value, + .default_text = std.fmt.comptimePrint("{d}", .{default_value}), + .access = access, + .parse_with_entry = parse, + .format = format, + }); + } + + fn parse(entry: *c.zend_ini_entry, text: [:0]const u8) !i64 { + const zstr = c.zend_string_init(text.ptr, text.len, false); + defer c.zend_string_release(zstr); + return c.zend_ini_parse_quantity_warn(zstr, entry.*.name); + } + + fn format(value: i64, buffer: []u8) ![]const u8 { + return std.fmt.bufPrint(buffer, "{d}", .{value}); + } +}; + +pub const float = struct { + pub fn new( + comptime name: [:0]const u8, + comptime default_value: f64, + comptime access: Access, + ) type { + return typed(f64).new(.{ + .name = name, + .default = default_value, + .default_text = std.fmt.comptimePrint("{d}", .{default_value}), + .access = access, + .parse = parse, + .format = format, + }); + } + + fn parse(text: [:0]const u8) !f64 { + return std.fmt.parseFloat(f64, std.mem.trim(u8, text, " \t\r\n")); + } + + fn format(value: f64, buffer: []u8) ![]const u8 { + return std.fmt.bufPrint(buffer, "{d}", .{value}); + } +}; + +pub const boolean = struct { + pub fn new( + comptime name: [:0]const u8, + comptime default_value: bool, + comptime access: Access, + ) type { + return typed(bool).new(.{ + .name = name, + .default = default_value, + .default_text = if (default_value) "1" else "0", + .access = access, + .parse = parse, + .format = format, + }); + } + + fn parse(text: [:0]const u8) !bool { + return parseBool(std.mem.trim(u8, text, " \t\r\n")); + } + + fn format(value: bool, _: []u8) ![]const u8 { + return if (value) "1" else "0"; + } +}; // Custom — free-form INI entry @@ -89,29 +275,31 @@ pub fn custom( // Collect -/// Collects typed INI declarations into a null-terminated `zend_ini_entry_def` -/// array ready for `module()`. +/// Collects INI declarations into a null-terminated `zend_ini_entry_def` array. +/// Usually `module()` calls this internally for `.ini = &.{ ... }`; use this +/// directly only when integrating with lower-level Zend APIs. /// /// ```zig /// const greeting = ini.string.new("ext.greeting", "Hello", .all); /// const max = ini.int.new("ext.max", 100, .system); /// const debug = ini.boolean.new("ext.debug", false, .user); /// -/// pub const defs = ini.collect(.{ greeting, max, debug }); -/// /// comptime { /// phpz.module(.{ /// .name = "ext", -/// .ini_defs = &defs, +/// .ini = &.{ greeting, max, debug }, /// }); /// } /// ``` -pub fn collect(comptime defs: anytype) [defs.len + 1]c.zend_ini_entry_def { - var result: [defs.len + 1]c.zend_ini_entry_def = undefined; - inline for (defs, 0..) |d, i| { - result[i] = d.iniDef(); +/// +/// `entries` may also contain raw `c.zend_ini_entry_def` values for low-level +/// integrations. +pub fn collect(comptime entries: anytype) [entries.len + 1]c.zend_ini_entry_def { + var result: [entries.len + 1]c.zend_ini_entry_def = undefined; + inline for (entries, 0..) |entry, i| { + result[i] = entryDef(entry); } - result[defs.len] = std.mem.zeroes(c.zend_ini_entry_def); + result[entries.len] = std.mem.zeroes(c.zend_ini_entry_def); return result; } @@ -201,6 +389,51 @@ pub fn onUpdateBool(comptime global: *bool) OnModifyFn { // Internal +fn entryDef(comptime entry: anytype) c.zend_ini_entry_def { + const Entry = @TypeOf(entry); + if (Entry == type) { + checkEntry(entry); + return entry.iniDef(); + } + if (Entry == c.zend_ini_entry_def) return entry; + if (Entry == *const c.zend_ini_entry_def) return entry.*; + + @compileError( + "ini.collect entry '" ++ @typeName(Entry) ++ "' must be an INI wrapper type " ++ + "or c.zend_ini_entry_def", + ); +} + +fn checkEntry(comptime Entry: type) void { + if (!@hasDecl(Entry, "iniDef")) { + @compileError( + "module .ini entry '" ++ @typeName(Entry) ++ "' has no iniDef() function; " ++ + "pass a value returned by phpz.ini.string/int/float/boolean/typed.new(...) or phpz.ini.custom(...)", + ); + } + + const ini_def_info = @typeInfo(@TypeOf(Entry.iniDef)); + if (ini_def_info != .@"fn") { + @compileError("module .ini entry '" ++ @typeName(Entry) ++ "' iniDef declaration is not a function"); + } + + const fn_info = ini_def_info.@"fn"; + if (fn_info.param_types.len != 0 or fn_info.return_type.? != c.zend_ini_entry_def) { + @compileError("module .ini entry '" ++ @typeName(Entry) ++ "' iniDef must be fn() zend_ini_entry_def"); + } +} + +fn alter(comptime name: [:0]const u8, value: []const u8, opts: SetOptions) SetError!void { + const zname = c.zend_string_init(name.ptr, name.len, false); + defer c.zend_string_release(zname); + + const result = if (opts.force) + c.zend_alter_ini_entry_chars_ex(zname, value.ptr, value.len, @intFromEnum(opts.modify), @intFromEnum(opts.stage), 1) + else + c.zend_alter_ini_entry_chars(zname, value.ptr, value.len, @intFromEnum(opts.modify), @intFromEnum(opts.stage)); + if (result != c.SUCCESS) return error.AlterIniFailed; +} + fn unpackMhArgs(comptime args: anytype) [3]?*anyopaque { if (args.len > 3) @compileError("custom: at most 3 args (mh_arg1/mh_arg2/mh_arg3)"); var result: [3]?*anyopaque = .{ null, null, null }; @@ -208,28 +441,49 @@ fn unpackMhArgs(comptime args: anytype) [3]?*anyopaque { return result; } -fn formatDefault(comptime T: type, comptime val: T) [:0]const u8 { - return switch (T) { - [:0]const u8 => val, - i64, f64 => std.fmt.comptimePrint("{d}", .{val}), - bool => if (val) "1" else "0", - else => @compileError("Unsupported INI value type: " ++ @typeName(T)), - }; -} - -fn onModifyFor(comptime T: type, comptime ptr: *T) OnModifyFn { - return switch (T) { - [:0]const u8 => onUpdateString(ptr), - i64 => onUpdateInt(ptr), - f64 => onUpdateFloat(ptr), - bool => onUpdateBool(ptr), - else => @compileError("Unsupported INI value type: " ++ @typeName(T)), - }; -} - test { std.testing.refAllDecls(@This()); + const Mode = enum { safe, fast }; + const ModeIni = struct { + fn parse(text: [:0]const u8) !Mode { + if (std.mem.eql(u8, text, "safe")) return .safe; + if (std.mem.eql(u8, text, "fast")) return .fast; + return error.InvalidMode; + } + + fn format(value: Mode, _: []u8) ![]const u8 { + return switch (value) { + .safe => "safe", + .fast => "fast", + }; + } + }; + const greeting = string.new("test.greeting", "Hello", .all); + const max = int.new("test.max", 100, .system); + const debug = boolean.new("test.debug", false, .user); + const mode = typed(Mode).new(.{ + .name = "test.mode", + .default = .safe, + .default_text = "safe", + .access = .all, + .parse = ModeIni.parse, + .format = ModeIni.format, + }); + _ = collect(&.{ greeting, max, debug, mode }); + const raw = c.zend_ini_entry_def{ + .name = "test.raw", + .on_modify = null, + .mh_arg1 = null, + .mh_arg2 = null, + .mh_arg3 = null, + .value = "raw", + .displayer = null, + .value_length = 3, + .name_length = 8, + .modifiable = @intFromEnum(Access.all), + }; + _ = collect(.{ raw, &raw }); _ = &unpackMhArgs; - _ = &formatDefault; - _ = &onModifyFor; + _ = &entryDef; + _ = &alter; } From 52a7a1145dfe20444897a7581478593837681bcc Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Thu, 9 Jul 2026 22:10:52 +0800 Subject: [PATCH 03/17] refactor(Ctx): rename expectArg runtime extension API Rename With/withs terminology to Runtime/runtime and clarify callable resolution and object type payload names. --- src/Ctx.zig | 70 ++++++++++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/Ctx.zig b/src/Ctx.zig index 44040b1..d800bf7 100644 --- a/src/Ctx.zig +++ b/src/Ctx.zig @@ -110,12 +110,12 @@ pub const Call = opaque { } } - fn ExpectArgWiths(comptime specs: []const ExpectArgKind.Spec) type { + fn ExpectArgsRuntime(comptime specs: []const ExpectArgKind.Spec) type { comptime { var types: [specs.len]type = undefined; var is_all_void = true; for (specs, 0..) |spec, i| { - types[i] = ExpectArgKind.With(spec); + types[i] = ExpectArgKind.Runtime(spec); if (is_all_void and types[i] != void) is_all_void = false; } return if (is_all_void) void else @Tuple(&types); @@ -125,13 +125,13 @@ pub const Call = opaque { /// Extract all arguments with compile-time validation. /// Optionals must come after required args. min/max derived automatically. /// - /// Each entry is an `ExpectArgKind.Spec` tagged union literal. The `withs` - /// parameter is `ExpectArgWiths(specs)` — `{}` when all specs are scalar, - /// or a tuple of per-position `With` structs for callable/object bindings. + /// Each entry is an `ExpectArgKind.Spec` tagged union literal. The `runtime` + /// parameter is `ExpectArgsRuntime(specs)` — `{}` when all specs need no + /// runtime extension, or a tuple of per-position `Runtime` structs. /// /// Example: /// ```zig - /// // Scalar only — withs is void, pass {}: + /// // Scalar only — runtime is void, pass {}: /// // (string $name, int $age = 0, ?int $count = null) /// const args = try self.expectArgs(&.{ /// .{ .string = .{} }, @@ -142,15 +142,15 @@ pub const Call = opaque { /// const age: ?i64 = args[1]; /// const count: ?Nullable(i64) = args[2]; /// - /// // With callable/object binding: + /// // With callable/object runtime extensions: /// // (callable $cb, \User $user) /// var cb: zend.Callable = .nil; /// const args2 = try self.expectArgs(&.{ - /// .{ .callable = .{ .target = true } }, + /// .{ .callable = .{ .resolve = true } }, /// .{ .object = .{ .instanceof = true } }, /// }, .{ - /// .{ .cb = &cb }, - /// .{ .class = UserClass.entry }, + /// .{ .target = &cb }, + /// .{ .type = UserClass.entry }, /// }); /// /// // .mixed with union types: @@ -166,7 +166,7 @@ pub const Call = opaque { pub inline fn expectArgs( self: *Call, comptime specs: []const ExpectArgKind.Spec, - withs: ExpectArgWiths(specs), + runtime: ExpectArgsRuntime(specs), ) ExpectArgsError!ExpectArgResults(specs) { comptime { var seen_optional = false; @@ -195,14 +195,14 @@ pub const Call = opaque { const Results = ExpectArgResults(specs); var result: Results = undefined; inline for (specs, 0..) |spec, i| { - const with = if (comptime @TypeOf(withs) != void) - @field(withs, std.fmt.comptimePrint("{d}", .{i})) + const arg_runtime = if (comptime @TypeOf(runtime) != void) + @field(runtime, std.fmt.comptimePrint("{d}", .{i})) else {}; const n: u32 = @intCast(i + 1); result[i] = if (comptime spec.isOptional()) - if (n > self.argCount()) null else try self.extractArg(n, spec, with) + if (n > self.argCount()) null else try self.extractArg(n, spec, arg_runtime) else - try self.extractArg(n, spec, with); + try self.extractArg(n, spec, arg_runtime); } return result; } @@ -249,7 +249,7 @@ pub const Call = opaque { .null => unreachable, .mixed => struct { optional: bool = false, unions: ?[]const ExpectArgKind = null }, .object => struct { optional: bool = false, nullable: bool = false, zval: bool = false, instanceof: bool = false }, - .callable => struct { optional: bool = false, nullable: bool = false, target: bool = false }, + .callable => struct { optional: bool = false, nullable: bool = false, resolve: bool = false }, .reference => struct { optional: bool = false, zval: bool = false }, else => struct { optional: bool = false, nullable: bool = false, zval: bool = false }, }; @@ -276,16 +276,16 @@ pub const Call = opaque { } }; - /// Runtime value for a single `Spec`. - /// - `.callable` with `.target = true` → `struct { cb: *zend.Callable }` - /// - `.callable` without target → `void` - /// - `.object` with `.instanceof = true` → `struct { class: *zend.ClassEntry }` + /// Runtime extension data for a single `Spec`. + /// - `.callable` with `.resolve = true` → `struct { target: *zend.Callable }` + /// - `.callable` without resolve → `void` + /// - `.object` with `.instanceof = true` → `struct { type: *zend.ClassEntry }` (class/interface) /// - `.object` without instanceof → `void` /// - scalar types → `void` (pass `{}`) - pub fn With(comptime spec: Spec) type { + pub fn Runtime(comptime spec: Spec) type { return switch (spec) { - .callable => |s| if (s.target) struct { cb: *zend.Callable } else void, - .object => |s| if (s.instanceof) struct { class: *zend.ClassEntry } else void, + .callable => |s| if (s.resolve) struct { target: *zend.Callable } else void, + .object => |s| if (s.instanceof) struct { type: *zend.ClassEntry } else void, else => void, }; } @@ -375,10 +375,10 @@ pub const Call = opaque { /// Extract and type-check argument N (1-indexed). Must call expectArgCount first. /// /// Accepts an `ExpectArgKind.Spec` tagged union specifying the expected type - /// and options (optional, nullable, zval). Pass runtime bindings via `with`: - /// - `.callable` with `.target = true` → `.{ .cb = &cb }` - /// - `.callable` without target → `{}` - /// - `.object` with `.instanceof = true` → `.{ .class = entry }` + /// and options (optional, nullable, zval). Pass runtime extension data via `runtime`: + /// - `.callable` with `.resolve = true` → `.{ .target = &cb }` + /// - `.callable` without resolve → `{}` + /// - `.object` with `.instanceof = true` → `.{ .type = entry }` (class/interface) /// - `.object` without instanceof → `{}` /// - scalar types → `{}` /// @@ -387,7 +387,7 @@ pub const Call = opaque { self: *Call, n: u32, comptime spec: ExpectArgKind.Spec, - with: ExpectArgKind.With(spec), + runtime: ExpectArgKind.Runtime(spec), ) ExpectArgError!ExpectArgResult(spec) { if (n > self.argCount()) { if (comptime spec.isOptional()) @@ -396,14 +396,14 @@ pub const Call = opaque { return errors.argumentValueError(n, "must be provided", .{}); } } - return self.extractArg(n, spec, with); + return self.extractArg(n, spec, runtime); } inline fn extractArg( self: *Call, n: u32, comptime spec: ExpectArgKind.Spec, - with: ExpectArgKind.With(spec), + runtime: ExpectArgKind.Runtime(spec), ) ExpectArgError!ExpectArgResult(spec) { const zv = self.arg(n); switch (comptime spec) { @@ -429,9 +429,9 @@ pub const Call = opaque { .callable => |s| { const or_null = comptime if (s.nullable) " or null" else ""; if (comptime s.nullable) if (native.is(zv, .null)) return .null; - if (comptime s.target) { + if (comptime s.resolve) { var err: ?[*:0]u8 = null; - with.cb.parse(zv, false, &err) catch { + runtime.target.parse(zv, false, &err) catch { if (err) |e| { defer if (comptime c.ZEND_DEBUG == 1) c._efree(@as(*anyopaque, @ptrCast(e)), @src().file.ptr, @intCast(@src().line), null, 0) else c.efree(@as(*anyopaque, @ptrCast(e))); return errors.argumentTypeError(n, "must be a valid callback" ++ or_null ++ ", %s", .{e}); @@ -453,9 +453,9 @@ pub const Call = opaque { const or_null = comptime if (s.nullable) " or null" else ""; if (comptime s.nullable) if (native.is(zv, .null)) return .null; if (comptime s.instanceof) { - const ce = with.class; - const obj: *zend.Object = native.as(zv, .object) catch return errors.argumentTypeError(n, "must be instance of %s" ++ or_null ++ ", %s given", .{ ce.name().ptr, native.kind(zv).cstr() }); - if (!obj.instanceof(ce)) return errors.argumentTypeError(n, "must be instance of %s" ++ or_null ++ ", %s given", .{ ce.name().ptr, obj.class().name().ptr }); + const expected_type = runtime.type; + const obj: *zend.Object = native.as(zv, .object) catch return errors.argumentTypeError(n, "must be instance of %s" ++ or_null ++ ", %s given", .{ expected_type.name().ptr, native.kind(zv).cstr() }); + if (!obj.instanceof(expected_type)) return errors.argumentTypeError(n, "must be instance of %s" ++ or_null ++ ", %s given", .{ expected_type.name().ptr, obj.class().name().ptr }); const raw = if (comptime s.zval) zv else obj; return if (comptime s.nullable) .{ .value = raw } else raw; } else if (!native.is(zv, .object)) { From e8d52589b717c1df18edb298efff69ad541e1f53 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Thu, 9 Jul 2026 22:10:55 +0800 Subject: [PATCH 04/17] feat: add bailout-safe Zend runtime APIs - bridge zend_try and zend_first_try through C wrappers - add try variants for callable, function, object, and class operations - map allocator bailouts to error.OutOfMemory and resume Zend bailouts at PHP boundaries - reshape class creation around create(), new(), and tryNew() --- build/Phpz.zig | 1 + build/phpz.h | 78 +++---------------- build/phpz_translate_c_compat.h | 41 ++++++++++ build/phpz_wrapper.c | 19 +++++ build/phpz_wrapper.h | 86 +++++++++++++++++++++ src/class.zig | 31 ++++++-- src/function.zig | 2 + src/heap.zig | 70 ++++++++++++----- src/root.zig | 1 + src/zend.zig | 18 +++++ src/zend/callable.zig | 59 +++++++++++++++ src/zend/function.zig | 79 ++++++++++++++++++- src/zend/object.zig | 68 ++++++++++++++++- src/zend/try_catch.zig | 129 ++++++++++++++++++++++++++++++++ 14 files changed, 589 insertions(+), 93 deletions(-) create mode 100644 build/phpz_translate_c_compat.h create mode 100644 build/phpz_wrapper.c create mode 100644 build/phpz_wrapper.h create mode 100644 src/zend/try_catch.zig diff --git a/build/Phpz.zig b/build/Phpz.zig index d05e07d..ca93266 100644 --- a/build/Phpz.zig +++ b/build/Phpz.zig @@ -72,6 +72,7 @@ fn createPhpCTranslator(b: *Build, options: Options) Translator { const c: Translator = .init(translate_c_dep, translator_options); // phpz.h c.addIncludePath(b.path("build")); + c.defineCMacro("PHPZ_TRANSLATE_C", "1"); // Configure PHP include paths for the C preprocessor if (options.php_include_dir) |root| { diff --git a/build/phpz.h b/build/phpz.h index f74034d..fb7c16b 100644 --- a/build/phpz.h +++ b/build/phpz.h @@ -1,6 +1,12 @@ #ifndef PHPZ_H #define PHPZ_H +// compatibility +#ifdef PHPZ_TRANSLATE_C +#include "phpz_translate_c_compat.h" +#endif + +// core #include "php.h" #include "Zend/zend_API.h" #include "Zend/zend_exceptions.h" @@ -8,73 +14,7 @@ #include "ext/standard/info.h" #include "main/SAPI.h" -static zend_always_inline zend_executor_globals *phpz_executor_globals(void) { -#ifdef ZTS -#ifdef ZEND_ENABLE_STATIC_TSRMLS_CACHE - return (zend_executor_globals *) (((char *) TSRMLS_CACHE) + executor_globals_offset); -#else - return (zend_executor_globals *) (((char *) tsrm_get_ls_cache()) + executor_globals_offset); -#endif -#else - return &executor_globals; -#endif -} - -static zend_always_inline zend_compiler_globals *phpz_compiler_globals(void) { -#ifdef ZTS -#ifdef ZEND_ENABLE_STATIC_TSRMLS_CACHE - return (zend_compiler_globals *) (((char *) TSRMLS_CACHE) + compiler_globals_offset); -#else - return (zend_compiler_globals *) (((char *) tsrm_get_ls_cache()) + compiler_globals_offset); -#endif -#else - return &compiler_globals; -#endif -} - -static zend_always_inline php_core_globals *phpz_core_globals(void) { -#ifdef ZTS -#ifdef ZEND_ENABLE_STATIC_TSRMLS_CACHE - return (php_core_globals *) (((char *) TSRMLS_CACHE) + core_globals_offset); -#else - return (php_core_globals *) (((char *) tsrm_get_ls_cache()) + core_globals_offset); -#endif -#else - return &core_globals; -#endif -} +// wrapper +#include "phpz_wrapper.h" -static zend_always_inline sapi_globals_struct *phpz_sapi_globals(void) { -#ifdef ZTS -#ifdef ZEND_ENABLE_STATIC_TSRMLS_CACHE - return (sapi_globals_struct *) (((char *) TSRMLS_CACHE) + sapi_globals_offset); -#else - return (sapi_globals_struct *) (((char *) tsrm_get_ls_cache()) + sapi_globals_offset); -#endif -#else - return &sapi_globals; -#endif -} - -static zend_always_inline void phpz_zval_zval(zval *z, zval *src, bool copy, bool dtor_src) { - ZVAL_ZVAL(z, src, copy, dtor_src); -} - -/* Bridge helpers for zend_class_entry anonymous unions. - * translate-c numbers unnamed unions (unnamed_0, unnamed_1, ...), - * which break when PHP headers change union layout. These inline - * wrappers give translate-c stable symbol names to bind against. */ - -static zend_always_inline void phpz_class_entry_set_create_object(zend_class_entry *ce, zend_object* (*handler)(zend_class_entry *)) { - ce->create_object = handler; -} - -static zend_always_inline zend_class_entry *phpz_class_entry_get_parent(zend_class_entry *ce) { - return ce->parent; -} - -static zend_always_inline zend_class_entry *phpz_class_entry_get_interface(zend_class_entry *ce, uint32_t index) { - return ce->interfaces[index]; -} - -#endif +#endif // PHPZ_H diff --git a/build/phpz_translate_c_compat.h b/build/phpz_translate_c_compat.h new file mode 100644 index 0000000..746ebcb --- /dev/null +++ b/build/phpz_translate_c_compat.h @@ -0,0 +1,41 @@ +#ifndef PHPZ_TRANSLATE_C_COMPAT_H +#define PHPZ_TRANSLATE_C_COMPAT_H + +// Workaround for windows.h: https://codeberg.org/ziglang/translate-c/issues/307 +#if defined(PHP_WIN32) || defined(_WIN32) || defined(WIN32) +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif + +# ifndef NOMINMAX +# define NOMINMAX +# endif + +# ifndef DECLSPEC_NOINITALL +# define DECLSPEC_NOINITALL +# endif + +# ifndef ENABLE_INTSAFE_SIGNED_FUNCTIONS +# define ENABLE_INTSAFE_SIGNED_FUNCTIONS +# endif + +# ifndef __assume +# define __assume(expr) ((void)0) +# endif + +# include +# ifdef _CLASS_ARG +# undef _CLASS_ARG +# define _CLASS_ARG(_Val) (sizeof((_Val) + (float)0) == sizeof(float) ? 'f' : sizeof((_Val) + (double)0) == sizeof(double) ? 'd' : 'l') +# endif +#endif + + +// FIXME: regression in translate_c +#include "Zend/zend_portability.h" +#ifdef ZEND_ATTRIBUTE_COLD_LABEL +# undef ZEND_ATTRIBUTE_COLD_LABEL +# define ZEND_ATTRIBUTE_COLD_LABEL +#endif + +#endif // PHPZ_TRANSLATE_C_COMPAT_H diff --git a/build/phpz_wrapper.c b/build/phpz_wrapper.c new file mode 100644 index 0000000..4831f7e --- /dev/null +++ b/build/phpz_wrapper.c @@ -0,0 +1,19 @@ +#include "phpz.h" + +bool phpz_zend_try_catch(phpz_zend_try_callback callback, void *ctx) { + zend_try { + callback(ctx); + } zend_catch { + return true; + } zend_end_try(); + return false; +} + +bool phpz_zend_first_try_catch(phpz_zend_try_callback callback, void *ctx) { + zend_first_try { + callback(ctx); + } zend_catch { + return true; + } zend_end_try(); + return false; +} diff --git a/build/phpz_wrapper.h b/build/phpz_wrapper.h new file mode 100644 index 0000000..741d7a3 --- /dev/null +++ b/build/phpz_wrapper.h @@ -0,0 +1,86 @@ +#ifndef PHPZ_WRAPPER_H +#define PHPZ_WRAPPER_H + +static zend_always_inline const char *phpz_module_build_id(void) { + return ZEND_MODULE_BUILD_ID; +} + +static zend_always_inline zend_executor_globals *phpz_executor_globals(void) { +#ifdef ZTS +#ifdef ZEND_ENABLE_STATIC_TSRMLS_CACHE + return (zend_executor_globals *) (((char *) TSRMLS_CACHE) + executor_globals_offset); +#else + return (zend_executor_globals *) (((char *) tsrm_get_ls_cache()) + executor_globals_offset); +#endif +#else + return &executor_globals; +#endif +} + +static zend_always_inline zend_compiler_globals *phpz_compiler_globals(void) { +#ifdef ZTS +#ifdef ZEND_ENABLE_STATIC_TSRMLS_CACHE + return (zend_compiler_globals *) (((char *) TSRMLS_CACHE) + compiler_globals_offset); +#else + return (zend_compiler_globals *) (((char *) tsrm_get_ls_cache()) + compiler_globals_offset); +#endif +#else + return &compiler_globals; +#endif +} + +static zend_always_inline php_core_globals *phpz_core_globals(void) { +#ifdef ZTS +#ifdef ZEND_ENABLE_STATIC_TSRMLS_CACHE + return (php_core_globals *) (((char *) TSRMLS_CACHE) + core_globals_offset); +#else + return (php_core_globals *) (((char *) tsrm_get_ls_cache()) + core_globals_offset); +#endif +#else + return &core_globals; +#endif +} + +static zend_always_inline sapi_globals_struct *phpz_sapi_globals(void) { +#ifdef ZTS +#ifdef ZEND_ENABLE_STATIC_TSRMLS_CACHE + return (sapi_globals_struct *) (((char *) TSRMLS_CACHE) + sapi_globals_offset); +#else + return (sapi_globals_struct *) (((char *) tsrm_get_ls_cache()) + sapi_globals_offset); +#endif +#else + return &sapi_globals; +#endif +} + +typedef void (*phpz_zend_try_callback)(void *); + +bool phpz_zend_try_catch(phpz_zend_try_callback callback, void *ctx); +bool phpz_zend_first_try_catch(phpz_zend_try_callback callback, void *ctx); + +static ZEND_COLD ZEND_NORETURN zend_always_inline void phpz_zend_bailout() { + zend_bailout(); +} + +static zend_always_inline void phpz_zval_zval(zval *z, zval *src, bool copy, bool dtor_src) { + ZVAL_ZVAL(z, src, copy, dtor_src); +} + +/* Bridge helpers for zend_class_entry anonymous unions. + * translate-c numbers unnamed unions (unnamed_0, unnamed_1, ...), + * which break when PHP headers change union layout. These inline + * wrappers give translate-c stable symbol names to bind against. */ + +static zend_always_inline void phpz_class_entry_set_create_object(zend_class_entry *ce, zend_object* (*handler)(zend_class_entry *)) { + ce->create_object = handler; +} + +static zend_always_inline zend_class_entry *phpz_class_entry_get_parent(zend_class_entry *ce) { + return ce->parent; +} + +static zend_always_inline zend_class_entry *phpz_class_entry_get_interface(zend_class_entry *ce, uint32_t index) { + return ce->interfaces[index]; +} + +#endif // PHPZ_WRAPPER_H diff --git a/src/class.zig b/src/class.zig index 65a9e7d..c3c2568 100644 --- a/src/class.zig +++ b/src/class.zig @@ -359,13 +359,13 @@ pub fn Class(comptime class_name: [:0]const u8, comptime T: type) type { return .from(.std, zv.object().ptr()); } - /// Creates a new instance of the class. + /// Creates an instance of the class without calling its constructor. /// /// Ownership: caller owns the returned object reference; call /// `release()` unless the object is transferred into a zval/PHP return. /// /// Must be called after `register()`, otherwise `entry` is undefined. - pub fn new() *Self { + pub fn create() *Self { // emalloc never returns null (it aborts on OOM), so `orelse` is unreachable. return .from(.std, init(entry.ptr()) orelse unreachable); } @@ -380,7 +380,14 @@ pub fn Class(comptime class_name: [:0]const u8, comptime T: type) type { try obj.call(method_name, retval, params); } + /// Calls a method on the object and converts a Zend bailout into `error.ZendBailout`. + pub fn tryCall(self: *Self, method_name: []const u8, retval: ?*c.zval, params: anytype) zend.Object.TryCallError!void { + const obj: *zend.Object = .from(&self.std); + try obj.tryCall(method_name, retval, params); + } + pub const ConstructError = zend.Object.ConstructorError || zend.Function.Error; + pub const TryConstructError = zend.Object.ConstructorError || zend.Function.TryCallError; /// Calls the constructor (`__construct`) with the given parameters. /// Does nothing if the class has no constructor defined. @@ -392,9 +399,15 @@ pub fn Class(comptime class_name: [:0]const u8, comptime T: type) type { if (try obj.constructor()) |ctor| try ctor.callMethod(obj, null, params); } + /// Calls the constructor and converts a Zend bailout into `error.ZendBailout`. + pub fn tryConstruct(self: *Self, params: anytype) TryConstructError!void { + const obj: *zend.Object = .from(&self.std); + if (try obj.constructor()) |ctor| try ctor.tryCallMethod(obj, null, params); + } + /// Create a new instance and call its constructor in one step. /// - /// Combines `new()` and `construct()`. If the class has no constructor, + /// Combines `create()` and `construct()`. If the class has no constructor, /// the params are silently ignored. /// /// Must be called after `register()`, otherwise `entry` is undefined. @@ -404,13 +417,21 @@ pub fn Class(comptime class_name: [:0]const u8, comptime T: type) type { /// /// Returns `error.AccessDenied` if the constructor is inaccessible. /// Returns `error.PhpException` if the constructor throws a PHP exception. - pub fn newWith(params: anytype) ConstructError!*Self { - const instance = new(); + pub fn new(params: anytype) ConstructError!*Self { + const instance = create(); errdefer instance.release(); try instance.construct(params); return instance; } + /// Create a new instance, call its constructor, and convert a Zend bailout into `error.ZendBailout`. + pub fn tryNew(params: anytype) TryConstructError!*Self { + const instance = create(); + errdefer instance.release(); + try instance.tryConstruct(params); + return instance; + } + /// Update object property value. /// /// Returns `error.PhpException` if a magic `__set` handler throws. diff --git a/src/function.zig b/src/function.zig index b9ad699..ae0e3f8 100644 --- a/src/function.zig +++ b/src/function.zig @@ -102,6 +102,7 @@ fn wrapFn(comptime func_desc: [:0]const u8, comptime func: anytype) Fn { else => @compileError(std.fmt.comptimePrint("unsupported function signature for {s}: expected fn(Ctx) or fn()", .{func_desc})), }; _ = @as(anyerror!void, @call(.auto, func, args)) catch |err| { + if (err == error.ZendBailout or err == error.OutOfMemory) zend.bailout(); if (!errors.hasException()) { errors.throwError(null, "%s at %s", .{ @errorName(err).ptr, func_desc.ptr }); } @@ -140,6 +141,7 @@ fn wrapMethod(comptime Class: type, comptime func_desc: [:0]const u8, comptime f }; }; _ = @as(anyerror!void, @call(.auto, func, args)) catch |err| { + if (err == error.ZendBailout or err == error.OutOfMemory) zend.bailout(); if (!errors.hasException()) { errors.throwError(null, "%s at %s", .{ @errorName(err).ptr, func_desc.ptr }); } diff --git a/src/heap.zig b/src/heap.zig index 25b8770..f01b3bd 100644 --- a/src/heap.zig +++ b/src/heap.zig @@ -3,6 +3,7 @@ const Allocator = std.mem.Allocator; const Alignment = std.mem.Alignment; const c = @import("root.zig").c; +const zend = @import("zend.zig"); /// A wrapper around the PHP Memory Manager API which supports the full `Allocator` interface. /// @@ -14,11 +15,10 @@ const c = @import("root.zig").c; /// multi-frame source location trace using DWARF debug info, so PHP's leak reports point to /// the actual allocation site rather than this file. /// -/// **Warning:** `emalloc` never returns `null` on failure. Instead it calls `zend_mm_safe_error()` -/// which invokes `zend_error_noreturn(E_ERROR, ...)` — a `ZEND_NORETURN` function that terminates -/// script execution without returning to the C caller. This means Zig's `defer`/`errdefer` and any -/// pending cleanup will be silently skipped. Zig code using this allocator must not hold resources -/// that require deterministic cleanup across allocation boundaries. +/// `emalloc` / `erealloc` never return `null` on failure. Instead, PHP raises a +/// fatal error and performs a Zend bailout. This allocator catches that bailout +/// and reports allocation failure through Zig's allocator `null` path, which is +/// surfaced as `error.OutOfMemory` by `std.mem.Allocator`. pub const php_allocator: Allocator = .{ .ptr = undefined, .vtable = &php_allocator_impl.vtable, @@ -42,12 +42,29 @@ const php_allocator_impl = struct { _ = context; // same as raw c allocator alignment std.debug.assert(alignment.compare(.lte, .of(std.c.max_align_t))); - if (comptime c.ZEND_DEBUG == 1) { - const src = DebugSourceLocation.resolve(return_address); - return @ptrCast(c._emalloc(len, src.file.ptr, @intCast(src.line), null, 0)); - } else { - return @ptrCast(c.emalloc(len)); - } + + const Context = struct { + len: usize, + return_address: usize, + result: ?*anyopaque = null, + + fn call(raw: ?*anyopaque) callconv(.c) void { + const self: *@This() = @ptrCast(@alignCast(raw.?)); + if (comptime c.ZEND_DEBUG == 1) { + const src = DebugSourceLocation.resolve(self.return_address); + self.result = c._emalloc(self.len, src.file.ptr, @intCast(src.line), null, 0); + } else { + self.result = c.emalloc(self.len); + } + } + }; + + var ctx: Context = .{ + .len = len, + .return_address = return_address, + }; + zend.tryCatchRaw(Context.call, &ctx) catch return null; + return @ptrCast(ctx.result); } fn resize(context: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, return_address: usize) bool { @@ -62,12 +79,31 @@ const php_allocator_impl = struct { fn remap(context: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, return_address: usize) ?[*]u8 { _ = context; _ = alignment; - if (comptime c.ZEND_DEBUG == 1) { - const src = DebugSourceLocation.resolve(return_address); - return @ptrCast(c._erealloc(memory.ptr, new_len, src.file.ptr, @intCast(src.line), null, 0)); - } else { - return @ptrCast(c.erealloc(memory.ptr, new_len)); - } + + const Context = struct { + memory: []u8, + new_len: usize, + return_address: usize, + result: ?*anyopaque = null, + + fn call(raw: ?*anyopaque) callconv(.c) void { + const self: *@This() = @ptrCast(@alignCast(raw.?)); + if (comptime c.ZEND_DEBUG == 1) { + const src = DebugSourceLocation.resolve(self.return_address); + self.result = c._erealloc(self.memory.ptr, self.new_len, src.file.ptr, @intCast(src.line), null, 0); + } else { + self.result = c.erealloc(self.memory.ptr, self.new_len); + } + } + }; + + var ctx: Context = .{ + .memory = memory, + .new_len = new_len, + .return_address = return_address, + }; + zend.tryCatchRaw(Context.call, &ctx) catch return null; + return @ptrCast(ctx.result); } fn free(context: *anyopaque, memory: []u8, alignment: Alignment, return_address: usize) void { diff --git a/src/root.zig b/src/root.zig index 1df49d7..9b1057e 100644 --- a/src/root.zig +++ b/src/root.zig @@ -48,6 +48,7 @@ test { _ = @import("zend/object.zig"); _ = @import("zend/property_info.zig"); _ = @import("zend/string.zig"); + _ = @import("zend/try_catch.zig"); _ = @import("zval/array.zig"); _ = @import("zval/object.zig"); } diff --git a/src/zend.zig b/src/zend.zig index 2ceca2b..d50d087 100644 --- a/src/zend.zig +++ b/src/zend.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const c = @import("c.zig").c; pub const Array = @import("zend/array.zig").Array; pub const Callable = @import("zend/callable.zig").Callable; @@ -9,6 +10,23 @@ pub const PropertyInfo = @import("zend/property_info.zig").PropertyInfo; pub const Reference = @import("zend/reference.zig").Reference; pub const Resource = @import("zend/resource.zig").Resource; pub const String = @import("zend/string.zig").String; +pub const try_catch = @import("zend/try_catch.zig"); + +pub inline fn bailout() noreturn { + c.phpz_zend_bailout(); + unreachable; +} + +pub const TryCatchError = try_catch.TryCatchError; +pub const TryCatchRawCallback = try_catch.TryCatchRawCallback; +pub const tryCatchRaw = try_catch.tryCatchRaw; +pub const firstTryCatchRaw = try_catch.firstTryCatchRaw; +pub const tryCatch = try_catch.tryCatch; +pub const firstTryCatch = try_catch.firstTryCatch; +pub const tryCatchTyped = try_catch.tryCatchTyped; +pub const firstTryCatchTyped = try_catch.firstTryCatchTyped; +pub const tryCatchSimple = try_catch.tryCatchSimple; +pub const firstTryCatchSimple = try_catch.firstTryCatchSimple; test { std.testing.refAllDecls(@This()); diff --git a/src/zend/callable.zig b/src/zend/callable.zig index fc24212..c4c0876 100644 --- a/src/zend/callable.zig +++ b/src/zend/callable.zig @@ -4,6 +4,7 @@ const errors = @import("../errors.zig"); const c = @import("../root.zig").c; const native = @import("../zval.zig").Zval.native; const Object = @import("object.zig").Object; +const try_catch = @import("try_catch.zig"); /// Parsed callable ready for invocation. /// @@ -80,6 +81,7 @@ pub const Callable = extern struct { } pub const CallError = error{ CallFailed, PhpException }; + pub const TryCallError = CallError || try_catch.TryCatchError; /// Invoke the callable with positional arguments (comptime tuple of `c.zval`). /// @@ -119,6 +121,63 @@ pub const Callable = extern struct { if (errors.hasException()) return error.PhpException; } + /// Invoke the callable and convert a Zend bailout into `error.ZendBailout`. + /// + /// This variant restores `fci` temporary call fields and destroys the + /// internally-owned discard return value before returning `error.ZendBailout`. + pub fn tryCall(self: *Callable, args: anytype) TryCallError!void { + const info = @typeInfo(@TypeOf(args)); + if (!(info == .@"struct" and info.@"struct".is_tuple)) + @compileError("tryCall: args must be a tuple, e.g. .{} or .{a, b}"); + + const saved_retval = self.fci.retval; + const saved_param_count = self.fci.param_count; + const saved_params = self.fci.params; + const saved_named_params = self.fci.named_params; + + var discard: c.zval = native.undef; + const owns_retval = saved_retval == null; + if (owns_retval) self.fci.retval = &discard; + defer { + self.fci.retval = saved_retval; + self.fci.param_count = saved_param_count; + self.fci.params = saved_params; + self.fci.named_params = saved_named_params; + if (owns_retval) native.tryDtor(&discard); + } + + const n = info.@"struct".field_types.len; + const CallResult = @typeInfo(@TypeOf(c.zend_call_function)).@"fn".return_type.?; + const CallFrame = struct { + callable: *Callable, + + fn call(frame: *@This()) CallResult { + return c.zend_call_function(&frame.callable.fci, &frame.callable.fcc); + } + }; + + var frame: CallFrame = .{ .callable = self }; + const result = switch (n) { + 0 => blk: { + self.fci.param_count = 0; + self.fci.params = null; + self.fci.named_params = null; + break :blk try try_catch.tryCatchTyped(CallResult, CallFrame, &frame, CallFrame.call); + }, + else => blk: { + var arr: [n]c.zval = undefined; + inline for (0..n) |i| arr[i] = args[i]; + self.fci.param_count = @intCast(n); + self.fci.params = @ptrCast(&arr); + self.fci.named_params = null; + break :blk try try_catch.tryCatchTyped(CallResult, CallFrame, &frame, CallFrame.call); + }, + }; + + if (result == c.FAILURE) return error.CallFailed; + if (errors.hasException()) return error.PhpException; + } + /// Increment refcounts on `function_name` and `fcc.object`. /// Prevents premature destruction when the callable is retained. pub inline fn addref(self: *Callable) void { diff --git a/src/zend/function.zig b/src/zend/function.zig index 65d9399..b59c2d5 100644 --- a/src/zend/function.zig +++ b/src/zend/function.zig @@ -1,12 +1,15 @@ const errors = @import("../errors.zig"); -const c = @import("../root.zig").c; +const phpz = @import("../root.zig"); +const c = phpz.c; const ClassEntry = @import("class_entry.zig").ClassEntry; const Object = @import("object.zig").Object; +const try_catch = @import("try_catch.zig"); pub const Function = opaque { pub const Error = error{ PhpException, }; + pub const TryCallError = Error || try_catch.TryCatchError; /// Function type classification. pub const Kind = enum(u8) { @@ -83,6 +86,11 @@ pub const Function = opaque { try self.invoke(null, null, retval, params); } + /// Call as a global function and convert a Zend bailout into `error.ZendBailout`. + pub fn tryCall(self: *Function, retval: ?*c.zval, params: anytype) TryCallError!void { + try self.tryInvoke(null, null, retval, params); + } + /// Call as a static method (with class scope, no object). /// Pass params as a tuple: `.{}`, `.{a}`, `.{a, b}`. /// @@ -91,6 +99,11 @@ pub const Function = opaque { try self.invoke(null, ce.ptr(), retval, params); } + /// Call as a static method and convert a Zend bailout into `error.ZendBailout`. + pub fn tryCallStatic(self: *Function, ce: *ClassEntry, retval: ?*c.zval, params: anytype) TryCallError!void { + try self.tryInvoke(null, ce.ptr(), retval, params); + } + /// Call as an instance method on an object. /// Pass params as a tuple: `.{}`, `.{a}`, `.{a, b}`. /// @@ -99,6 +112,11 @@ pub const Function = opaque { try self.invoke(obj.ptr(), obj.class().ptr(), retval, params); } + /// Call as an instance method and convert a Zend bailout into `error.ZendBailout`. + pub fn tryCallMethod(self: *Function, obj: *Object, retval: ?*c.zval, params: anytype) TryCallError!void { + try self.tryInvoke(obj.ptr(), obj.class().ptr(), retval, params); + } + inline fn invoke( self: *Function, obj: ?*c.zend_object, @@ -121,6 +139,65 @@ pub const Function = opaque { } if (errors.hasException()) return error.PhpException; } + + inline fn tryInvoke( + self: *Function, + obj: ?*c.zend_object, + scope: ?*c.zend_class_entry, + retval: ?*c.zval, + params: anytype, + ) TryCallError!void { + const info = @typeInfo(@TypeOf(params)); + if (!(info == .@"struct" and info.@"struct".is_tuple)) + @compileError("params must be a tuple, e.g. .{} or .{a, b}"); + + const n = info.@"struct".field_types.len; + switch (n) { + 0 => { + const CallFrame = struct { + function: *Function, + obj: ?*c.zend_object, + scope: ?*c.zend_class_entry, + retval: ?*c.zval, + + fn call(frame: *@This()) void { + c.zend_call_known_function(frame.function.ptr(), frame.obj, frame.scope, frame.retval, 0, null, null); + } + }; + var frame: CallFrame = .{ + .function = self, + .obj = obj, + .scope = scope, + .retval = retval, + }; + try try_catch.tryCatchTyped(void, CallFrame, &frame, CallFrame.call); + }, + else => { + var arr: [n]c.zval = undefined; + inline for (0..n) |i| arr[i] = params[i]; + const CallFrame = struct { + function: *Function, + obj: ?*c.zend_object, + scope: ?*c.zend_class_entry, + retval: ?*c.zval, + params: *[n]c.zval, + + fn call(frame: *@This()) void { + c.zend_call_known_function(frame.function.ptr(), frame.obj, frame.scope, frame.retval, @intCast(n), @ptrCast(frame.params), null); + } + }; + var frame: CallFrame = .{ + .function = self, + .obj = obj, + .scope = scope, + .retval = retval, + .params = &arr, + }; + try try_catch.tryCatchTyped(void, CallFrame, &frame, CallFrame.call); + }, + } + if (errors.hasException()) return error.PhpException; + } }; test { diff --git a/src/zend/object.zig b/src/zend/object.zig index e55e9f9..fd42f7e 100644 --- a/src/zend/object.zig +++ b/src/zend/object.zig @@ -1,10 +1,12 @@ const errors = @import("../errors.zig"); -const c = @import("../root.zig").c; +const phpz = @import("../root.zig"); +const c = phpz.c; const native = @import("../zval.zig").Zval.native; const Array = @import("array.zig").Array; const ClassEntry = @import("class_entry.zig").ClassEntry; const Function = @import("function.zig").Function; const String = @import("string.zig").String; +const try_catch = @import("try_catch.zig"); pub const Object = opaque { pub const InitError = error{InitFailed}; @@ -248,6 +250,7 @@ pub const Object = opaque { } pub const CallError = error{MethodNotFound} || Function.Error; + pub const TryCallError = error{MethodNotFound} || Function.TryCallError; /// Call a known method by name. /// @@ -269,6 +272,17 @@ pub const Object = opaque { try method.callMethod(self, retval, params); } + /// Call a known method by name and convert a Zend bailout into `error.ZendBailout`. + pub fn tryCall( + self: *Object, + method_name: []const u8, + retval: ?*c.zval, + params: anytype, + ) TryCallError!void { + const method = self.findMethod(method_name) orelse return error.MethodNotFound; + try method.tryCallMethod(self, retval, params); + } + /// Call a known static method by name. /// /// Resolves the method from the class's function table, @@ -290,7 +304,20 @@ pub const Object = opaque { try method.callStatic(ce, retval, params); } + /// Call a known static method by name and convert a Zend bailout into `error.ZendBailout`. + pub fn tryCallStatic( + self: *Object, + method_name: []const u8, + ce: *ClassEntry, + retval: ?*c.zval, + params: anytype, + ) TryCallError!void { + const method = self.findMethod(method_name) orelse return error.MethodNotFound; + try method.tryCallStatic(ce, retval, params); + } + pub const CallIfExistsError = error{MethodCallFailed} || Function.Error; + pub const TryCallIfExistsError = error{MethodCallFailed} || Function.TryCallError; /// Call a method by name, succeeding only if the method exists. pub fn callIfExists( @@ -313,6 +340,45 @@ pub const Object = opaque { if (errors.hasException()) return error.PhpException; } + /// Call a method by name if it exists and convert a Zend bailout into `error.ZendBailout`. + pub fn tryCallIfExists( + self: *Object, + method_name: []const u8, + retval: ?*c.zval, + params: []c.zval, + ) TryCallIfExistsError!void { + const zstr = String.init(method_name, false); + defer zstr.release(); + + const CallResult = @typeInfo(@TypeOf(c.zend_call_method_if_exists)).@"fn".return_type.?; + const CallFrame = struct { + object: *Object, + method: *c.zend_string, + retval: ?*c.zval, + params: []c.zval, + + fn call(frame: *@This()) CallResult { + return c.zend_call_method_if_exists( + frame.object.ptr(), + frame.method, + frame.retval, + @intCast(frame.params.len), + if (frame.params.len > 0) @ptrCast(frame.params.ptr) else null, + ); + } + }; + + var frame: CallFrame = .{ + .object = self, + .method = zstr.ptr(), + .retval = retval, + .params = params, + }; + const result = try try_catch.tryCatchTyped(CallResult, CallFrame, &frame, CallFrame.call); + if (result == c.FAILURE) return error.MethodCallFailed; + if (errors.hasException()) return error.PhpException; + } + pub const CloneError = error{CloneFailed}; /// Clone the object. diff --git a/src/zend/try_catch.zig b/src/zend/try_catch.zig new file mode 100644 index 0000000..8879df2 --- /dev/null +++ b/src/zend/try_catch.zig @@ -0,0 +1,129 @@ +const std = @import("std"); +const c = @import("../c.zig").c; + +pub const TryCatchError = error{ZendBailout}; +pub const TryCatchRawCallback = *const fn (?*anyopaque) callconv(.c) void; + +pub inline fn tryCatchRaw(callback: TryCatchRawCallback, context: ?*anyopaque) TryCatchError!void { + if (c.phpz_zend_try_catch(callback, context)) return error.ZendBailout; +} + +pub inline fn firstTryCatchRaw(callback: TryCatchRawCallback, context: ?*anyopaque) TryCatchError!void { + if (c.phpz_zend_first_try_catch(callback, context)) return error.ZendBailout; +} + +pub inline fn tryCatch(comptime Context: type, context: *Context, comptime callback: fn (*Context) void) TryCatchError!void { + return tryCatchTyped(void, Context, context, callback); +} + +pub inline fn firstTryCatch(comptime Context: type, context: *Context, comptime callback: fn (*Context) void) TryCatchError!void { + return firstTryCatchTyped(void, Context, context, callback); +} + +pub inline fn tryCatchTyped(comptime Return: type, comptime Context: type, context: *Context, comptime callback: fn (*Context) Return) TryCatchError!Return { + return tryCatchTypedImpl(false, Return, Context, context, callback); +} + +pub inline fn firstTryCatchTyped(comptime Return: type, comptime Context: type, context: *Context, comptime callback: fn (*Context) Return) TryCatchError!Return { + return tryCatchTypedImpl(true, Return, Context, context, callback); +} + +pub inline fn tryCatchSimple(comptime callback: fn () void) TryCatchError!void { + return tryCatchSimpleTyped(false, callback); +} + +pub inline fn firstTryCatchSimple(comptime callback: fn () void) TryCatchError!void { + return tryCatchSimpleTyped(true, callback); +} + +inline fn tryCatchTypedImpl( + comptime first: bool, + comptime Return: type, + comptime Context: type, + context: *Context, + comptime callback: fn (*Context) Return, +) TryCatchError!Return { + const Frame = if (Return == void) struct { + context: *Context, + } else struct { + context: *Context, + result: Return = undefined, + }; + + const Wrapper = struct { + fn call(raw: ?*anyopaque) callconv(.c) void { + const frame: *Frame = @ptrCast(@alignCast(raw.?)); + if (comptime Return == void) { + callback(frame.context); + } else { + frame.result = callback(frame.context); + } + } + }; + + var frame: Frame = .{ .context = context }; + if (comptime first) + try firstTryCatchRaw(Wrapper.call, &frame) + else + try tryCatchRaw(Wrapper.call, &frame); + + if (comptime Return == void) return; + return frame.result; +} + +inline fn tryCatchSimpleTyped( + comptime first: bool, + comptime callback: fn () void, +) TryCatchError!void { + const Wrapper = struct { + fn call(_: ?*anyopaque) callconv(.c) void { + callback(); + } + }; + + if (comptime first) + try firstTryCatchRaw(Wrapper.call, null) + else + try tryCatchRaw(Wrapper.call, null); +} + +test { + std.testing.refAllDecls(@This()); +} + +test "tryCatch typed API" { + const Context = struct { + value: u32, + + fn callback(self: *@This()) void { + self.value += 1; + } + + fn callbackValue(self: *@This()) u32 { + return self.value; + } + }; + + try std.testing.expect(@TypeOf(tryCatch(Context, undefined, Context.callback)) == TryCatchError!void); + try std.testing.expect(@TypeOf(firstTryCatch(Context, undefined, Context.callback)) == TryCatchError!void); + try std.testing.expect(@TypeOf(tryCatchTyped(u32, Context, undefined, Context.callbackValue)) == TryCatchError!u32); + try std.testing.expect(@TypeOf(firstTryCatchTyped(u32, Context, undefined, Context.callbackValue)) == TryCatchError!u32); +} + +test "tryCatch raw API" { + const Callback = struct { + fn callback(_: ?*anyopaque) callconv(.c) void {} + }; + + try std.testing.expect(@TypeOf(tryCatchRaw(Callback.callback, null)) == TryCatchError!void); + try std.testing.expect(@TypeOf(firstTryCatchRaw(Callback.callback, null)) == TryCatchError!void); +} + +test "tryCatch simple API" { + const Callback = struct { + fn callback() void {} + }; + + try std.testing.expect(@TypeOf(tryCatchSimple(Callback.callback)) == TryCatchError!void); + try std.testing.expect(@TypeOf(firstTryCatchSimple(Callback.callback)) == TryCatchError!void); +} From 73c7b65246d76dc75f841922bfe78553ed604390 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Thu, 9 Jul 2026 22:10:57 +0800 Subject: [PATCH 05/17] feat(module): auto-register INI entries and classes Collect INI declarations and register class wrappers before the user startup hook, with compile-time validation for registration APIs. --- src/module.zig | 61 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/src/module.zig b/src/module.zig index 35c3ad1..aa3e4dc 100644 --- a/src/module.zig +++ b/src/module.zig @@ -5,11 +5,15 @@ const phpz_options = @import("phpz_options"); const c = @import("root.zig").c; pub const ModuleEntry = c.zend_module_entry; const errors = @import("errors.zig"); -const function_helper = @import("function.zig"); +const ini_helper = @import("ini.zig"); /// Configuration for creating a PHP extension module. /// /// This structure defines the metadata and lifecycle hooks for a PHP extension. /// All lifecycle hooks are optional and will be called by PHP at appropriate times. +/// INI declarations listed in `ini` are collected into Zend definitions and +/// registered during module startup. +/// Class wrappers listed in `classes` are registered during module startup, +/// before `module_startup_fn`. pub const Config = struct { /// Extension name (must be null-terminated). /// This should match the filename of your extension (.so file). @@ -21,6 +25,13 @@ pub const Config = struct { /// Default: "0.0.0" version: [:0]const u8 = "0.0.0", + /// Class wrappers to register during module startup, in the given order. + /// Parent classes and interfaces should appear before children. + classes: ?[]const type = null, + + /// INI declarations to register during module startup. + ini: []const type = &.{}, + /// Module startup hook, called when PHP starts (e.g., when Apache starts). /// Use this to initialize resources that persist across requests (e.g., register classes). /// Called once per PHP process/thread pool initialization. @@ -48,9 +59,6 @@ pub const Config = struct { /// Use this to display custom information about your extension. /// Example: configuration settings, compile-time options, credits. info_fn: ?PhpInfoFn = null, - - /// INI definitions from `ini.collect(.{...})`. Registered during module startup. - ini_defs: ?[]const c.zend_ini_entry_def = null, }; pub const PhpInfoFn = fn (*ModuleEntry) void; @@ -63,7 +71,7 @@ pub const PhpHookFn = fn () anyerror!void; /// additionally exports the `get_module` function for the PHP loader. /// /// Parameters: -/// - cfg: Module configuration including name, version, and lifecycle hook functions +/// - cfg: Module configuration including name, version, lifecycle hooks, INI declarations, and optional classes pub fn module(comptime cfg: Config) void { comptime { if (!@hasDecl(c, "zend_module_entry")) { @@ -95,14 +103,40 @@ fn makePhpHookFn(comptime hook_fn: PhpHookFn) *const fn (c_int, c_int) callconv( }.handle; } +fn checkModuleClass(comptime Class: type) void { + if (!@hasDecl(Class, "register")) { + @compileError( + "module .classes entry '" ++ @typeName(Class) ++ "' has no register() function; " ++ + "pass the wrapper type returned by phpz.Class()/phpz.SimpleClass(), e.g. .classes = &.{ classes.user.Class }", + ); + } + + const register_info = @typeInfo(@TypeOf(Class.register)); + if (register_info != .@"fn") { + @compileError( + "module .classes entry '" ++ @typeName(Class) ++ "' has a register declaration, but it is not a function; " ++ + "expected pub fn register() void", + ); + } + + const fn_info = register_info.@"fn"; + if (fn_info.param_types.len != 0 or fn_info.return_type.? != void) { + @compileError( + "module .classes entry '" ++ @typeName(Class) ++ "' register declaration must be pub fn register() void; " ++ + "pass the phpz class wrapper type, not the implementation type", + ); + } +} + fn makePhpModuleStartupFn( comptime module_name: [:0]const u8, comptime hook_fn: ?PhpHookFn, comptime ini_defs: ?[]const c.zend_ini_entry_def, + comptime classes: ?[]const type, ) ?*const fn (c_int, c_int) callconv(.c) c.zend_result { const symbols_fn_name = "register_" ++ module_name ++ "_symbols"; const has_symbols = comptime @hasDecl(c, symbols_fn_name); - if (!has_symbols and hook_fn == null and ini_defs == null) return null; + if (!has_symbols and hook_fn == null and ini_defs == null and classes == null) return null; return struct { fn handle(_: c_int, module_number: c_int) callconv(.c) c.zend_result { if (comptime has_symbols) { @@ -114,6 +148,12 @@ fn makePhpModuleStartupFn( return c.FAILURE; } } + if (comptime classes) |items| { + inline for (items) |Class| { + checkModuleClass(Class); + Class.register(); + } + } if (comptime hook_fn) |f| { f() catch |err| { errors.err(.err, "Module startup function failed: %s", .{@errorName(err).ptr}); @@ -152,6 +192,9 @@ fn makePhpInfoFn(comptime info_fn: PhpInfoFn) *const fn ([*c]ModuleEntry) callco } inline fn createModuleEntry(comptime cfg: Config) ModuleEntry { + const ini_defs = ini_helper.collect(cfg.ini); + const registered_ini_defs: ?[]const c.zend_ini_entry_def = if (cfg.ini.len == 0) null else &ini_defs; + var entry: ModuleEntry = undefined; // STANDARD_MODULE_HEADER @@ -165,8 +208,8 @@ inline fn createModuleEntry(comptime cfg: Config) ModuleEntry { // EXTENSION SETUP entry.name = cfg.name; entry.functions = if (@hasDecl(c, "ext_functions")) &c.ext_functions else null; - entry.module_startup_func = makePhpModuleStartupFn(cfg.name, cfg.module_startup_fn, cfg.ini_defs); - entry.module_shutdown_func = makePhpModuleShutdownFn(cfg.module_shutdown_fn, cfg.ini_defs); + entry.module_startup_func = makePhpModuleStartupFn(cfg.name, cfg.module_startup_fn, registered_ini_defs, cfg.classes); + entry.module_shutdown_func = makePhpModuleShutdownFn(cfg.module_shutdown_fn, registered_ini_defs); entry.request_startup_func = if (cfg.request_startup_fn) |f| makePhpHookFn(f) else null; entry.request_shutdown_func = if (cfg.request_shutdown_fn) |f| makePhpHookFn(f) else null; entry.info_func = if (cfg.info_fn) |f| makePhpInfoFn(f) else null; @@ -181,7 +224,7 @@ inline fn createModuleEntry(comptime cfg: Config) ModuleEntry { entry.type = 0; entry.handle = null; entry.module_number = 0; - entry.build_id = "API" ++ std.fmt.comptimePrint("{d}", .{c.ZEND_MODULE_API_NO}) ++ c.ZEND_BUILD_TS ++ c.ZEND_BUILD_DEBUG; + entry.build_id = c.phpz_module_build_id(); if (@hasField(ModuleEntry, "globals_id_ptr")) { // ZTS From a6e4f0f2d8d3bb32e6648c2337ac81620e33bee3 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Thu, 9 Jul 2026 22:10:59 +0800 Subject: [PATCH 06/17] refactor(build): replace apply with addExtension Create and configure extension libraries through one helper, and migrate the README and generated skeleton to the new build API. --- README.md | 15 ++++++++++----- build/Phpz.zig | 10 ++++++++-- tools/phpz_skel.php | 4 +--- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c7e819e..59f9f15 100644 --- a/README.md +++ b/README.md @@ -109,11 +109,16 @@ const phpz = Phpz.init(phpz_dep, .{ .php_include_dir = .{ .cwd_relative = "/usr/include/php" }, }); -// Import the phpz module into your extension library -lib.root_module.addImport("phpz", phpz.mod); - -// Apply OS-specific linker settings (macOS undefined symbols, Windows php8.lib) -phpz.apply(lib); +// Create the PHP extension library and import the phpz module. +const lib = phpz.addExtension(b, .{ + .name = "my_php_extension", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }), + .linkage = .dynamic, +}); ``` Then `zig build` ! diff --git a/build/Phpz.zig b/build/Phpz.zig index ca93266..d9d7986 100644 --- a/build/Phpz.zig +++ b/build/Phpz.zig @@ -92,8 +92,13 @@ fn createPhpCTranslator(b: *Build, options: Options) Translator { return c; } -/// Apply OS-specific linker settings to a PHP extension shared library. -pub fn apply(self: Phpz, lib: *Build.Step.Compile) void { +/// Create a PHP extension library and attach the phpz module. +/// +/// This also applies platform-specific linker settings required for PHP to +/// load the resulting shared library. +pub fn addExtension(self: Phpz, b: *Build, options: Build.LibraryOptions) *Build.Step.Compile { + options.root_module.addImport("phpz", self.mod); + const lib = b.addLibrary(options); switch (self.options.translator.target.result.os.tag) { // macOS: allows undefined symbols to be resolved at runtime by PHP .macos => lib.linker_allow_shlib_undefined = true, @@ -106,6 +111,7 @@ pub fn apply(self: Phpz, lib: *Build.Step.Compile) void { }, else => {}, } + return lib; } const std = @import("std"); diff --git a/tools/phpz_skel.php b/tools/phpz_skel.php index 99b8b62..349e3bb 100755 --- a/tools/phpz_skel.php +++ b/tools/phpz_skel.php @@ -883,7 +883,7 @@ public static function runGenStub( }); const ext_name = "{{EXT_NAME}}"; - const ext_lib = b.addLibrary(.{ + const ext_lib = phpz.addExtension(b, .{ .name = ext_name, .root_module = b.createModule(.{ .root_source_file = b.path("src/root.zig"), @@ -892,8 +892,6 @@ public static function runGenStub( }), .linkage = .dynamic, }); - ext_lib.root_module.addImport("phpz", phpz.mod); - phpz.apply(ext_lib); const ext_filename = ext_name ++ ".so"; From ce04850d3a74d1f499026f0fb8c57ed4a9492adc Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Thu, 9 Jul 2026 22:11:01 +0800 Subject: [PATCH 07/17] feat(windows): add MSVC PHP extension support - patch translated bindings for DLL-imported data symbols - add ABI-correct globals, callbacks, and PHP SDK linking - support ZTS and debug builds and run Windows example tests - migrate examples to the new build, module, INI, and runtime APIs - extend release-mode, cross-target, and allocator-bailout coverage --- .github/workflows/test-linux.yml | 7 + .github/workflows/test-macos.yml | 7 + .github/workflows/test-windows.yml | 46 +- .gitignore | 4 +- README.md | 2 +- bench/zig_ext/build.zig | 4 +- build.zig | 33 +- build/Phpz.zig | 92 +- examples/README.md | 1 - examples/my_php_extension/build.zig | 18 +- .../my_php_extension.stub.php | 8 + .../my_php_extension_arginfo.h | 10 +- .../src/classes/array_like.zig | 2 +- .../my_php_extension/src/classes/my_error.zig | 2 +- .../my_php_extension/src/classes/user.zig | 2 +- examples/my_php_extension/src/functions.zig | 66 +- examples/my_php_extension/src/ini.zig | 25 +- examples/my_php_extension/src/root.zig | 33 +- .../tests/heap_allocator_bailout.phpt | 23 + .../tests/ini_zig_getters.phpt | 6 + .../tests/new_with_constructor_failure.phpt | 2 +- .../tests/test_expect_arg_array_object.phpt | 14 +- examples/pjs/.gitignore | 9 - examples/pjs/README.md | 42 - examples/pjs/build.zig | 75 - examples/pjs/build.zig.zon | 26 - examples/pjs/deps/zig-quickjs-ng/LICENSE | 21 - examples/pjs/deps/zig-quickjs-ng/README.md | 101 - examples/pjs/deps/zig-quickjs-ng/build.zig | 100 - .../pjs/deps/zig-quickjs-ng/build.zig.zon | 18 - examples/pjs/deps/zig-quickjs-ng/src/atom.zig | 378 -- .../pjs/deps/zig-quickjs-ng/src/cfunc.zig | 509 -- .../pjs/deps/zig-quickjs-ng/src/class.zig | 380 -- .../pjs/deps/zig-quickjs-ng/src/context.zig | 1256 ----- examples/pjs/deps/zig-quickjs-ng/src/main.zig | 48 - .../pjs/deps/zig-quickjs-ng/src/module.zig | 297 -- .../pjs/deps/zig-quickjs-ng/src/opaque.zig | 22 - .../pjs/deps/zig-quickjs-ng/src/runtime.zig | 1099 ----- .../deps/zig-quickjs-ng/src/typed_array.zig | 96 - .../pjs/deps/zig-quickjs-ng/src/value.zig | 3567 -------------- examples/pjs/pjs.h | 9 - examples/pjs/pjs.stub.php | 30 - examples/pjs/pjs_arginfo.h | 99 - examples/pjs/run-tests.php | 4081 ----------------- examples/pjs/src/allocator.zig | 15 - examples/pjs/src/classes.zig | 5 - examples/pjs/src/classes/context.zig | 75 - examples/pjs/src/classes/exception.zig | 13 - examples/pjs/src/classes/runtime.zig | 16 - examples/pjs/src/classes/value.zig | 136 - examples/pjs/src/root.zig | 24 - examples/pjs/tests/eval_date.phpt | 18 - examples/pjs/tests/eval_error.phpt | 23 - examples/pjs/tests/eval_math.phpt | 18 - examples/pjs/tests/eval_testjs.phpt | 36 - examples/pjs/tests/eval_testjs_full.phpt | 19 - examples/pjs/tests/eval_types.phpt | 44 - examples/pjs/tests/init_error.phpt | 21 - examples/pjs/tests/test.js | 54 - examples/skeleton/build.zig | 4 +- examples/skeleton/src/root.zig | 6 +- scripts/test-msvc.sh | 124 + src/class.zig | 10 +- src/errors.zig | 12 +- src/function.zig | 17 +- src/globals.zig | 100 + src/root.zig | 9 + src/zend/function.zig | 3 +- src/zend/object.zig | 20 +- src/zend/string.zig | 13 +- src/zval.zig | 2 +- tools/phpz_skel.php | 2 +- tools/windows_patcher.zig | 328 ++ 73 files changed, 922 insertions(+), 12915 deletions(-) create mode 100644 examples/my_php_extension/tests/heap_allocator_bailout.phpt delete mode 100644 examples/pjs/.gitignore delete mode 100644 examples/pjs/README.md delete mode 100644 examples/pjs/build.zig delete mode 100644 examples/pjs/build.zig.zon delete mode 100644 examples/pjs/deps/zig-quickjs-ng/LICENSE delete mode 100644 examples/pjs/deps/zig-quickjs-ng/README.md delete mode 100644 examples/pjs/deps/zig-quickjs-ng/build.zig delete mode 100644 examples/pjs/deps/zig-quickjs-ng/build.zig.zon delete mode 100644 examples/pjs/deps/zig-quickjs-ng/src/atom.zig delete mode 100644 examples/pjs/deps/zig-quickjs-ng/src/cfunc.zig delete mode 100644 examples/pjs/deps/zig-quickjs-ng/src/class.zig delete mode 100644 examples/pjs/deps/zig-quickjs-ng/src/context.zig delete mode 100644 examples/pjs/deps/zig-quickjs-ng/src/main.zig delete mode 100644 examples/pjs/deps/zig-quickjs-ng/src/module.zig delete mode 100644 examples/pjs/deps/zig-quickjs-ng/src/opaque.zig delete mode 100644 examples/pjs/deps/zig-quickjs-ng/src/runtime.zig delete mode 100644 examples/pjs/deps/zig-quickjs-ng/src/typed_array.zig delete mode 100644 examples/pjs/deps/zig-quickjs-ng/src/value.zig delete mode 100644 examples/pjs/pjs.h delete mode 100644 examples/pjs/pjs.stub.php delete mode 100644 examples/pjs/pjs_arginfo.h delete mode 100644 examples/pjs/run-tests.php delete mode 100644 examples/pjs/src/allocator.zig delete mode 100644 examples/pjs/src/classes.zig delete mode 100644 examples/pjs/src/classes/context.zig delete mode 100644 examples/pjs/src/classes/exception.zig delete mode 100644 examples/pjs/src/classes/runtime.zig delete mode 100644 examples/pjs/src/classes/value.zig delete mode 100644 examples/pjs/src/root.zig delete mode 100644 examples/pjs/tests/eval_date.phpt delete mode 100644 examples/pjs/tests/eval_error.phpt delete mode 100644 examples/pjs/tests/eval_math.phpt delete mode 100644 examples/pjs/tests/eval_testjs.phpt delete mode 100644 examples/pjs/tests/eval_testjs_full.phpt delete mode 100644 examples/pjs/tests/eval_types.phpt delete mode 100644 examples/pjs/tests/init_error.phpt delete mode 100644 examples/pjs/tests/test.js create mode 100755 scripts/test-msvc.sh create mode 100644 src/globals.zig create mode 100644 tools/windows_patcher.zig diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index 820222e..f1af5cd 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -61,10 +61,17 @@ jobs: PHP_INCLUDE_DIR=$(php-config --include-dir) zig build test-examples -Dphp-include-dir="$PHP_INCLUDE_DIR" - name: Run example tests (ReleaseSafe) + if: ${{ matrix.debug == false }} run: | PHP_INCLUDE_DIR=$(php-config --include-dir) zig build test-examples -Dphp-include-dir="$PHP_INCLUDE_DIR" -Doptimize=ReleaseSafe - name: Run example tests (ReleaseFast) + if: ${{ matrix.debug == false }} run: | PHP_INCLUDE_DIR=$(php-config --include-dir) zig build test-examples -Dphp-include-dir="$PHP_INCLUDE_DIR" -Doptimize=ReleaseFast + - name: Run example tests (ReleaseSmall) + if: ${{ matrix.debug == false }} + run: | + PHP_INCLUDE_DIR=$(php-config --include-dir) + zig build test-examples -Dphp-include-dir="$PHP_INCLUDE_DIR" -Doptimize=ReleaseSmall diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml index da3043d..a396e71 100644 --- a/.github/workflows/test-macos.yml +++ b/.github/workflows/test-macos.yml @@ -58,10 +58,17 @@ jobs: PHP_INCLUDE_DIR=$(php-config --include-dir) zig build test-examples -Dphp-include-dir="$PHP_INCLUDE_DIR" - name: Run example tests (ReleaseSafe) + if: ${{ matrix.debug == false }} run: | PHP_INCLUDE_DIR=$(php-config --include-dir) zig build test-examples -Dphp-include-dir="$PHP_INCLUDE_DIR" -Doptimize=ReleaseSafe - name: Run example tests (ReleaseFast) + if: ${{ matrix.debug == false }} run: | PHP_INCLUDE_DIR=$(php-config --include-dir) zig build test-examples -Dphp-include-dir="$PHP_INCLUDE_DIR" -Doptimize=ReleaseFast + - name: Run example tests (ReleaseSmall) + if: ${{ matrix.debug == false }} + run: | + PHP_INCLUDE_DIR=$(php-config --include-dir) + zig build test-examples -Dphp-include-dir="$PHP_INCLUDE_DIR" -Doptimize=ReleaseSmall diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 917a7b4..270982f 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -69,26 +69,26 @@ jobs: -Dtarget=native-native-msvc \ -Dphp-include-dir="${{ steps.php-paths.outputs.include }}" \ -Dphp-lib-dir="${{ steps.php-paths.outputs.lib }}" - # - name: Run example tests - # shell: bash - # run: | - # zig build test-examples \ - # -Dtarget=native-native-msvc \ - # -Dphp-include-dir="${{ steps.php-paths.outputs.include }}" \ - # -Dphp-lib-dir="${{ steps.php-paths.outputs.lib }}" - # - name: Run example tests (ReleaseSafe) - # shell: bash - # run: | - # zig build test-examples \ - # -Dtarget=native-native-msvc \ - # -Dphp-include-dir="${{ steps.php-paths.outputs.include }}" \ - # -Dphp-lib-dir="${{ steps.php-paths.outputs.lib }}" \ - # -Doptimize=ReleaseSafe - # - name: Run example tests (ReleaseFast) - # shell: bash - # run: | - # zig build test-examples \ - # -Dtarget=native-native-msvc \ - # -Dphp-include-dir="${{ steps.php-paths.outputs.include }}" \ - # -Dphp-lib-dir="${{ steps.php-paths.outputs.lib }}" \ - # -Doptimize=ReleaseFast + - name: Run example tests + shell: bash + run: | + zig build test-examples \ + -Dtarget=native-native-msvc \ + -Dphp-include-dir="${{ steps.php-paths.outputs.include }}" \ + -Dphp-lib-dir="${{ steps.php-paths.outputs.lib }}" + - name: Run example tests (ReleaseSafe) + shell: bash + run: | + zig build test-examples \ + -Dtarget=native-native-msvc \ + -Dphp-include-dir="${{ steps.php-paths.outputs.include }}" \ + -Dphp-lib-dir="${{ steps.php-paths.outputs.lib }}" \ + -Doptimize=ReleaseSafe + - name: Run example tests (ReleaseFast) + shell: bash + run: | + zig build test-examples \ + -Dtarget=native-native-msvc \ + -Dphp-include-dir="${{ steps.php-paths.outputs.include }}" \ + -Dphp-lib-dir="${{ steps.php-paths.outputs.lib }}" \ + -Doptimize=ReleaseFast diff --git a/.gitignore b/.gitignore index cc99937..a8814d3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,4 @@ zig-out zig-pkg # For msvc cross-compilation test -.xwin -xwin -libc.txt +/tmp diff --git a/README.md b/README.md index 59f9f15..60aae1e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A Zig framework for building PHP extensions with PHP C API bindings. ## Requirements -- zig: 0.16.0 +- zig: 0.17.0-dev.1282+c0f9b51d8 - php: 8.2-8.5 (tested on linux/macos) ## Features diff --git a/bench/zig_ext/build.zig b/bench/zig_ext/build.zig index bec76e3..c7839dc 100644 --- a/bench/zig_ext/build.zig +++ b/bench/zig_ext/build.zig @@ -22,7 +22,7 @@ pub fn build(b: *std.Build) void { }); const ext_name = "bench_zig"; - const ext_lib = b.addLibrary(.{ + const ext_lib = phpz.addExtension(b, .{ .name = ext_name, .root_module = b.createModule(.{ .root_source_file = b.path("src/root.zig"), @@ -31,8 +31,6 @@ pub fn build(b: *std.Build) void { }), .linkage = .dynamic, }); - ext_lib.root_module.addImport("phpz", phpz.mod); - phpz.apply(ext_lib); const ext_filename = if (target.result.os.tag == .windows) "php_" ++ ext_name ++ ".dll" diff --git a/build.zig b/build.zig index 686c7f5..f086fee 100644 --- a/build.zig +++ b/build.zig @@ -5,24 +5,33 @@ pub const Phpz = @import("./build/Phpz.zig"); const BuildOptions = struct { php_include_dir: []const u8, php_lib_dir: ?[]const u8, + libc_file: ?[]const u8, + windows_zts: bool, + windows_debug: bool, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, + + fn getLibCFilePath(self: BuildOptions) ?std.Build.LazyPath { + return if (self.libc_file) |libc_file| .{ .cwd_relative = libc_file } else null; + } }; pub fn build(b: *std.Build) void { const options = BuildOptions{ .php_include_dir = b.option([]const u8, "php-include-dir", "PHP include directory (main/, Zend/, TSRM/, ext/)") orelse "/usr/include/php", - .php_lib_dir = b.option([]const u8, "php-lib-dir", "PHP SDK library directory (Windows only, contains php8.lib)"), + .php_lib_dir = b.option([]const u8, "php-lib-dir", "PHP SDK library directory (Windows only, contains php8*.lib)"), + .libc_file = b.option([]const u8, "libc", "Libc paths file for local cross-compilation tests"), + .windows_zts = b.option(bool, "windows-zts", "Windows only: link against the thread-safe PHP library") orelse false, + .windows_debug = b.option(bool, "windows-debug", "Windows only: build against a debug PHP SDK") orelse false, .target = b.standardTargetOptions(.{}), .optimize = b.standardOptimizeOption(.{}), }; - const mod = createPhpzModule(b, options); - b.default_step = addCheckStep(b, mod); - addGenerateDocsStep(b, mod); + b.default_step = addCheckStep(b, mod, options); + addGenerateDocsStep(b, mod, options); addTestExamplesStep(b, options); - addTestStep(b, mod); + addTestStep(b, mod, options); } fn createPhpzModule(b: *std.Build, options: BuildOptions) *std.Build.Module { @@ -31,23 +40,27 @@ fn createPhpzModule(b: *std.Build, options: BuildOptions) *std.Build.Module { .c_source_file = b.path("build/phpz.h"), .target = options.target, .optimize = options.optimize, + .libc_file = options.getLibCFilePath(), }, .php_include_dir = .{ .cwd_relative = options.php_include_dir }, .php_lib_dir = if (options.php_lib_dir) |d| .{ .cwd_relative = d } else null, + .windows_zts = options.windows_zts, + .windows_debug = options.windows_debug, }).mod; } -fn addCheckStep(b: *std.Build, mod: *std.Build.Module) *std.Build.Step { +fn addCheckStep(b: *std.Build, mod: *std.Build.Module, options: BuildOptions) *std.Build.Step { const lib_check = b.addLibrary(.{ .name = "phpz", .root_module = mod, }); + lib_check.setLibCFile(options.getLibCFilePath()); const check = b.step("check", "Check that phpz builds correctly"); check.dependOn(&lib_check.step); return check; } -fn addGenerateDocsStep(b: *std.Build, mod: *std.Build.Module) void { +fn addGenerateDocsStep(b: *std.Build, mod: *std.Build.Module, options: BuildOptions) void { const doc_step = b.step("docs", "Generate documentation for phpz"); // Generate docs for main library (src/root.zig) @@ -55,6 +68,7 @@ fn addGenerateDocsStep(b: *std.Build, mod: *std.Build.Module) void { .name = "phpz", .root_module = mod, }); + doc_obj.setLibCFile(options.getLibCFilePath()); const install_docs = b.addInstallDirectory(.{ .source_dir = doc_obj.getEmittedDocs(), .install_dir = .prefix, @@ -71,6 +85,7 @@ fn addGenerateDocsStep(b: *std.Build, mod: *std.Build.Module) void { .optimize = mod.optimize, }), }); + build_doc_obj.setLibCFile(options.getLibCFilePath()); const install_build_docs = b.addInstallDirectory(.{ .source_dir = build_doc_obj.getEmittedDocs(), .install_dir = .prefix, @@ -79,10 +94,11 @@ fn addGenerateDocsStep(b: *std.Build, mod: *std.Build.Module) void { doc_step.dependOn(&install_build_docs.step); } -fn addTestStep(b: *std.Build, mod: *std.Build.Module) void { +fn addTestStep(b: *std.Build, mod: *std.Build.Module, options: BuildOptions) void { const test_lib = b.addTest(.{ .root_module = mod, }); + test_lib.setLibCFile(options.getLibCFilePath()); const step = b.step("test", "Run unit tests"); step.dependOn(&test_lib.step); } @@ -92,7 +108,6 @@ fn addTestExamplesStep(b: *std.Build, options: BuildOptions) void { const examples = [_][]const u8{ "skeleton", "my_php_extension", - "pjs", }; inline for (examples) |test_example| { const test_cmd = b.addSystemCommand(&[_][]const u8{ b.graph.zig_exe, "build", "test" }); diff --git a/build/Phpz.zig b/build/Phpz.zig index d9d7986..5fe6f14 100644 --- a/build/Phpz.zig +++ b/build/Phpz.zig @@ -24,7 +24,7 @@ pub const Options = struct { /// Windows: C:\php-sdk\php-8.5.6-devel-vs17-x64\include php_include_dir: ?Build.LazyPath = null, - /// Windows only: directory containing php8.lib (usually /lib). + /// Windows only: directory containing the matching php8*.lib import library. php_lib_dir: ?Build.LazyPath = null, /// Build as a shared library for PHP to load dynamically. @@ -36,6 +36,9 @@ pub const Options = struct { /// to `file:line:column` via DWARF debug info and included in PHP's /// leak reports. Has no effect when using a different allocator. debug_leak_trace_frames: usize = 3, + + windows_zts: bool = false, + windows_debug: bool = false, }; /// Initialize Phpz from a build dependency. @@ -53,8 +56,38 @@ pub fn initInner(b: *Build, options: Options) Phpz { // Import PHP C bindings as "php_c" .{ .name = "php_c", .module = c.mod }, }, + .link_libc = true, }); + // Add phpz_wrapper.c to the build and include path for phpz.h + mod.addIncludePath(b.path("build")); + if (options.php_include_dir) |root| { + mod.addIncludePath(root); + mod.addIncludePath(root.path(b, "main")); + mod.addIncludePath(root.path(b, "Zend")); + mod.addIncludePath(root.path(b, "TSRM")); + mod.addIncludePath(root.path(b, "ext")); + } + switch (options.translator.target.result.os.tag) { + .windows => { + if (options.php_lib_dir) |dir| mod.addLibraryPath(dir); + const php_lib_name = if (options.windows_debug) + if (options.windows_zts) "php8ts_debug" else "php8_debug" + else if (options.windows_zts) "php8ts" else "php8"; + mod.linkSystemLibrary(php_lib_name, .{}); + + if (options.windows_zts) mod.addCMacro("ZTS", "1"); + mod.addCMacro("ZEND_DEBUG", if (options.windows_debug) "1" else "0"); + mod.addCMacro("ZEND_WIN32", "1"); + mod.addCMacro("PHP_WIN32", "1"); + mod.addCMacro("WINDOWS", "1"); + mod.addCMacro("WIN32", "1"); + mod.addCMacro("ENABLE_INTSAFE_SIGNED_FUNCTIONS", "1"); + }, + else => {}, + } + mod.addCSourceFile(.{ .file = b.path("build/phpz_wrapper.c") }); + // Create build options for conditional compilation const mod_opts = b.addOptions(); mod_opts.addOption(bool, "shared", options.shared); @@ -69,7 +102,7 @@ fn createPhpCTranslator(b: *Build, options: Options) Translator { var translator_options = options.translator; translator_options.strict_flex_arrays = .@"1"; - const c: Translator = .init(translate_c_dep, translator_options); + var c: Translator = .init(translate_c_dep, translator_options); // phpz.h c.addIncludePath(b.path("build")); c.defineCMacro("PHPZ_TRANSLATE_C", "1"); @@ -81,14 +114,31 @@ fn createPhpCTranslator(b: *Build, options: Options) Translator { c.addIncludePath(root.path(b, "Zend")); c.addIncludePath(root.path(b, "TSRM")); c.addIncludePath(root.path(b, "ext")); - if (translator_options.target.result.os.tag == .windows) { - c.defineCMacro("ZEND_WIN32", "1"); - c.defineCMacro("PHP_WIN32", "1"); - c.defineCMacro("WINDOWS", "1"); - c.defineCMacro("ZEND_DEBUG", "0"); + + switch (translator_options.target.result.os.tag) { + .windows => { + if (options.windows_zts) c.defineCMacro("ZTS", "1"); + c.defineCMacro("ZEND_DEBUG", if (options.windows_debug) "1" else "0"); + c.defineCMacro("ZEND_WIN32", "1"); + c.defineCMacro("PHP_WIN32", "1"); + c.defineCMacro("WINDOWS", "1"); + c.defineCMacro("WIN32", "1"); + c.defineCMacro("_CRT_USE_BUILTIN_OFFSETOF", "1"); + }, + .macos => { + // FIXME: regression in translate_c + c.defineCMacro("_Nonnull", ""); + c.defineCMacro("_Nullable", ""); + c.defineCMacro("_Null_unspecified", ""); + }, + else => {}, } } + if (translator_options.target.result.os.tag == .windows and translator_options.target.result.abi == .msvc) { + patchWindowsBindings(b, &c, translator_options); + } + return c; } @@ -102,18 +152,32 @@ pub fn addExtension(self: Phpz, b: *Build, options: Build.LibraryOptions) *Build switch (self.options.translator.target.result.os.tag) { // macOS: allows undefined symbols to be resolved at runtime by PHP .macos => lib.linker_allow_shlib_undefined = true, - // Windows: links against php8.lib in the PHP SDK - .windows => { - if (self.options.php_lib_dir) |dir| { - lib.root_module.addLibraryPath(dir); - } - lib.root_module.linkSystemLibrary("php8", .{}); - }, else => {}, } return lib; } +fn patchWindowsBindings(b: *Build, translate_c: *Translator, options: Translator.Options) void { + const patcher = b.addExecutable(.{ + .name = "windows-patcher", + .root_module = b.createModule(.{ + .root_source_file = b.path("tools/windows_patcher.zig"), + .target = b.graph.host, + .optimize = .ReleaseSafe, + }), + }); + const run = b.addRunArtifact(patcher); + run.setName("patch Windows PHP bindings"); + + const raw_output_file = translate_c.output_file; + run.addFileArg(raw_output_file); + const name = std.fs.path.stem(b.fmt("{f}", .{options.c_source_file})); + const output_file = run.addOutputFileArg(b.fmt("{s}.patched.zig", .{name})); + + translate_c.mod.root_source_file = output_file; + translate_c.output_file = output_file; +} + const std = @import("std"); const Build = std.Build; diff --git a/examples/README.md b/examples/README.md index 1ddfaea..85dee14 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,7 +4,6 @@ |---------|-------------| | [skeleton](./skeleton/) | Minimal starter: hello, greet, Counter class | | [my_php_extension](./my_php_extension/) | Full-featured extension: functions, classes, methods | -| [pjs](./pjs/) | Embed QuickJS in PHP to run JavaScript | ## Running diff --git a/examples/my_php_extension/build.zig b/examples/my_php_extension/build.zig index 9ea5a75..c5bba8d 100644 --- a/examples/my_php_extension/build.zig +++ b/examples/my_php_extension/build.zig @@ -10,8 +10,11 @@ pub fn build(b: *std.Build) void { // Directory containing PHP header files (main/, Zend/, TSRM/, ext/) const php_include_dir = b.option([]const u8, "php-include-dir", "PHP include directory (main/, Zend/, TSRM/, ext/)") orelse "/usr/include/php"; - // Windows only: PHP SDK lib directory containing php8.lib - const php_lib_dir = b.option([]const u8, "php-lib-dir", "PHP SDK library directory (Windows only, contains php8.lib)"); + // Windows only: PHP SDK lib directory containing php8*.lib + const php_lib_dir = b.option([]const u8, "php-lib-dir", "PHP SDK library directory (Windows only, contains php8*.lib)"); + const libc_file = b.option([]const u8, "libc", "Libc paths file for cross-compilation"); + const libc_file_path: ?std.Build.LazyPath = if (libc_file) |path| .{ .cwd_relative = path } else null; + const windows_zts = b.option(bool, "windows-zts", "Windows only: link against the thread-safe PHP library") orelse false; // Fetch the phpz dependency declared in build.zig.zon const phpz_dep = b.dependency("phpz", .{}); @@ -21,14 +24,16 @@ pub fn build(b: *std.Build) void { .c_source_file = b.path("my_php_extension.h"), .target = target, .optimize = optimize, + .libc_file = libc_file_path, }, .php_include_dir = .{ .cwd_relative = php_include_dir }, .php_lib_dir = if (php_lib_dir) |d| .{ .cwd_relative = d } else null, + .windows_zts = windows_zts, }); // Create the PHP extension as a dynamic library (.so / .dll / .dylib) const php_ext_name = "my_php_extension"; - const php_ext_lib = b.addLibrary(.{ + const php_ext_lib = phpz.addExtension(b, .{ .name = php_ext_name, .root_module = b.createModule(.{ .root_source_file = b.path("src/root.zig"), @@ -37,12 +42,7 @@ pub fn build(b: *std.Build) void { }), .linkage = .dynamic, }); - - // Import the phpz module into your extension library - php_ext_lib.root_module.addImport("phpz", phpz.mod); - - // Apply OS-specific linker settings (macOS undefined symbols, Windows php8.lib) - phpz.apply(php_ext_lib); + php_ext_lib.setLibCFile(libc_file_path); // PHP expects extensions at a specific naming convention const php_ext_filename = if (target.result.os.tag == .windows) diff --git a/examples/my_php_extension/my_php_extension.stub.php b/examples/my_php_extension/my_php_extension.stub.php index 5b4857d..ea4b233 100644 --- a/examples/my_php_extension/my_php_extension.stub.php +++ b/examples/my_php_extension/my_php_extension.stub.php @@ -34,6 +34,10 @@ function iniGetDebug(): bool { } + function iniGetMode(): string + { + } + /** @var string */ const MY_EXT_VERSION = "1.0.0"; } @@ -84,6 +88,10 @@ function tryCreateInvalidUser(): void { } + function testHeapAllocatorBailout(): array + { + } + interface Identifiable { public function getId(): int; diff --git a/examples/my_php_extension/my_php_extension_arginfo.h b/examples/my_php_extension/my_php_extension_arginfo.h index e8e9f7f..744c31e 100644 --- a/examples/my_php_extension/my_php_extension_arginfo.h +++ b/examples/my_php_extension/my_php_extension_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: b69c5e57a4b8a6464f198f66a4b83e8b7d3ac83b */ + * Stub hash: 34215a54f14dd37a56a161859166f9ee56368b8d */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_hello, 0, 0, IS_VOID, 0) ZEND_END_ARG_INFO() @@ -17,6 +17,8 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_iniGetDebug, 0, 0, _IS_BOOL, 0) ZEND_END_ARG_INFO() +#define arginfo_iniGetMode arginfo_iniGetGreeting + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_MyPHPExt_increment, 0, 1, IS_VOID, 0) ZEND_ARG_TYPE_INFO(1, value, IS_LONG, 0) ZEND_END_ARG_INFO() @@ -63,6 +65,8 @@ ZEND_END_ARG_INFO() #define arginfo_MyPHPExt_tryCreateInvalidUser arginfo_hello +#define arginfo_MyPHPExt_testHeapAllocatorBailout arginfo_MyPHPExt_listStatuses + #define arginfo_class_MyPHPExt_Identifiable_getId arginfo_iniGetMaxUsers ZEND_BEGIN_ARG_INFO_EX(arginfo_class_MyPHPExt_ExampleAttribute___construct, 0, 0, 1) @@ -145,6 +149,7 @@ ZEND_FUNCTION(greet); ZEND_FUNCTION(iniGetGreeting); ZEND_FUNCTION(iniGetMaxUsers); ZEND_FUNCTION(iniGetDebug); +ZEND_FUNCTION(iniGetMode); ZEND_FUNCTION(MyPHPExt_increment); ZEND_FUNCTION(MyPHPExt_findById); ZEND_FUNCTION(MyPHPExt_getDefaultUser); @@ -155,6 +160,7 @@ ZEND_FUNCTION(MyPHPExt_testExpectArgArrayObject); ZEND_FUNCTION(MyPHPExt_testExpectArgMixed); ZEND_FUNCTION(MyPHPExt_inspectObjectProperty); ZEND_FUNCTION(MyPHPExt_tryCreateInvalidUser); +ZEND_FUNCTION(MyPHPExt_testHeapAllocatorBailout); ZEND_METHOD(MyPHPExt_ExampleAttribute, __construct); ZEND_METHOD(MyPHPExt_AbstractEntity, onLoad); ZEND_METHOD(MyPHPExt_User, __construct); @@ -186,6 +192,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(iniGetGreeting, arginfo_iniGetGreeting) ZEND_FE(iniGetMaxUsers, arginfo_iniGetMaxUsers) ZEND_FE(iniGetDebug, arginfo_iniGetDebug) + ZEND_FE(iniGetMode, arginfo_iniGetMode) ZEND_NS_FALIAS("MyPHPExt", increment, MyPHPExt_increment, arginfo_MyPHPExt_increment) ZEND_NS_FALIAS("MyPHPExt", findById, MyPHPExt_findById, arginfo_MyPHPExt_findById) ZEND_NS_FALIAS("MyPHPExt", getDefaultUser, MyPHPExt_getDefaultUser, arginfo_MyPHPExt_getDefaultUser) @@ -196,6 +203,7 @@ static const zend_function_entry ext_functions[] = { ZEND_NS_FALIAS("MyPHPExt", testExpectArgMixed, MyPHPExt_testExpectArgMixed, arginfo_MyPHPExt_testExpectArgMixed) ZEND_NS_FALIAS("MyPHPExt", inspectObjectProperty, MyPHPExt_inspectObjectProperty, arginfo_MyPHPExt_inspectObjectProperty) ZEND_NS_FALIAS("MyPHPExt", tryCreateInvalidUser, MyPHPExt_tryCreateInvalidUser, arginfo_MyPHPExt_tryCreateInvalidUser) + ZEND_NS_FALIAS("MyPHPExt", testHeapAllocatorBailout, MyPHPExt_testHeapAllocatorBailout, arginfo_MyPHPExt_testHeapAllocatorBailout) ZEND_FE_END }; diff --git a/examples/my_php_extension/src/classes/array_like.zig b/examples/my_php_extension/src/classes/array_like.zig index 08b536e..b459703 100644 --- a/examples/my_php_extension/src/classes/array_like.zig +++ b/examples/my_php_extension/src/classes/array_like.zig @@ -3,7 +3,7 @@ const ArrayLike = extern struct { iter: phpz.zend.Array.Iterator, pub fn register(impl: anytype) *phpz.ClassEntry { - return .from(impl(c.zend_ce_arrayaccess, c.zend_ce_countable, c.zend_ce_iterator)); + return .from(impl(phpz.globals.class.entry("ArrayAccess"), phpz.globals.class.entry("Countable"), phpz.globals.class.entry("Iterator"))); } pub fn init(self: *ArrayLike) void { diff --git a/examples/my_php_extension/src/classes/my_error.zig b/examples/my_php_extension/src/classes/my_error.zig index 8f1ed59..ad91de5 100644 --- a/examples/my_php_extension/src/classes/my_error.zig +++ b/examples/my_php_extension/src/classes/my_error.zig @@ -3,7 +3,7 @@ const c = phpz.c; const MyError = struct { pub fn register(impl: anytype) *phpz.ClassEntry { - return .from(impl(c.zend_ce_exception)); + return .from(impl(phpz.globals.class.entry("Exception"))); } }; diff --git a/examples/my_php_extension/src/classes/user.zig b/examples/my_php_extension/src/classes/user.zig index cf0ab47..0f36972 100644 --- a/examples/my_php_extension/src/classes/user.zig +++ b/examples/my_php_extension/src/classes/user.zig @@ -1,6 +1,6 @@ const User = extern struct { pub fn register(impl: anytype) *phpz.ClassEntry { - return .from(impl(AbstractEntity.entry.ptr(), c.zend_ce_stringable)); + return .from(impl(AbstractEntity.entry.ptr(), phpz.globals.class.entry("Stringable"))); } /// __construct(string $name, int|null $age = null) diff --git a/examples/my_php_extension/src/functions.zig b/examples/my_php_extension/src/functions.zig index 7f33f9b..5fd09df 100644 --- a/examples/my_php_extension/src/functions.zig +++ b/examples/my_php_extension/src/functions.zig @@ -55,7 +55,7 @@ fn getDefaultUser(ctx: phpz.Ctx) !void { defer Zval.native.dtor(&name_zv); const age_zv = Zval.native.init(.int, 25); - const user: *UserClass = try .newWith(.{ name_zv, age_zv }); + const user: *UserClass = try .new(.{ name_zv, age_zv }); ctx.ret.set(.object, .from(&user.std)); } @@ -130,8 +130,8 @@ fn testExpectArgArrayObject(ctx: phpz.Ctx) !void { .{ .object = .{ .instanceof = true, .optional = true, .nullable = true } }, }, .{ {}, - .{ .class = UserClass.entry }, - .{ .class = UserClass.entry }, + .{ .type = UserClass.entry }, + .{ .type = UserClass.entry }, }); const data: *phpz.zend.Array = args[0]; @@ -227,10 +227,51 @@ fn tryCreateInvalidUser(ctx: phpz.Ctx) !void { const bad_name = Zval.native.init(.int, 123); const age = Zval.native.init(.int, 1); - const user = try UserClass.newWith(.{ bad_name, age }); + const user = try UserClass.new(.{ bad_name, age }); user.release(); } +fn exhaustPhpAllocator(scope_defer: *bool, cleanup_defer: *bool, freed_blocks: *usize) !void { + const allocator = phpz.heap.php_allocator; + + defer scope_defer.* = true; + + var blocks: [64][]u8 = undefined; + var count: usize = 0; + defer { + for (blocks[0..count]) |block| allocator.free(block); + freed_blocks.* = count; + cleanup_defer.* = true; + } + + while (count < blocks.len) : (count += 1) { + const block = try allocator.alloc(u8, 1024 * 1024); + block[0] = @intCast(count); + block[block.len - 1] = @intCast(count); + blocks[count] = block; + } +} + +/// testHeapAllocatorBailout(): array +fn testHeapAllocatorBailout(ctx: phpz.Ctx) !void { + _ = try ctx.call.expectArgs(&.{}, {}); + + var scope_defer = false; + var cleanup_defer = false; + var freed_blocks: usize = 0; + var out_of_memory = false; + + exhaustPhpAllocator(&scope_defer, &cleanup_defer, &freed_blocks) catch |err| switch (err) { + error.OutOfMemory => out_of_memory = true, + }; + + var result = phpz.Zval.Array.empty(ctx.ret.ptr()); + result.set(.bool, "out_of_memory", out_of_memory); + result.set(.bool, "scope_defer", scope_defer); + result.set(.bool, "cleanup_defer", cleanup_defer); + result.set(.bool, "freed_blocks", freed_blocks > 0); +} + /// map(array $arr, callable $cb): array fn map(ctx: phpz.Ctx) !void { var cb: phpz.zend.Callable = .nil; @@ -238,11 +279,11 @@ fn map(ctx: phpz.Ctx) !void { const ht, _ = try ctx.call.expectArgs( &.{ .{ .array = .{} }, - .{ .callable = .{ .target = true } }, + .{ .callable = .{ .resolve = true } }, }, .{ {}, - .{ .cb = &cb }, + .{ .target = &cb }, }, ); // _ = cb.parse(cb_zv, false, null); // parse twice ! @@ -262,17 +303,22 @@ fn map(ctx: phpz.Ctx) !void { /// iniGetGreeting(): string fn iniGetGreeting(ctx: phpz.Ctx) !void { - ctx.ret.set(.string, ini_config.greeting.value); + ctx.ret.set(.string, ini_config.greeting.get()); } /// iniGetMaxUsers(): int fn iniGetMaxUsers(ctx: phpz.Ctx) !void { - ctx.ret.set(.int, ini_config.max_users.value); + ctx.ret.set(.int, ini_config.max_users.get()); } /// iniGetDebug(): bool fn iniGetDebug(ctx: phpz.Ctx) !void { - ctx.ret.set(.bool, ini_config.debug.value); + ctx.ret.set(.bool, ini_config.debug.get()); +} + +/// iniGetMode(): string +fn iniGetMode(ctx: phpz.Ctx) !void { + ctx.ret.set(.string, @tagName(ini_config.mode.get())); } comptime { @@ -288,7 +334,9 @@ comptime { phpz.function("MyPHPExt\\testExpectArgMixed", testExpectArgMixed); phpz.function("MyPHPExt\\inspectObjectProperty", inspectObjectProperty); phpz.function("MyPHPExt\\tryCreateInvalidUser", tryCreateInvalidUser); + phpz.function("MyPHPExt\\testHeapAllocatorBailout", testHeapAllocatorBailout); phpz.function("iniGetGreeting", iniGetGreeting); phpz.function("iniGetMaxUsers", iniGetMaxUsers); phpz.function("iniGetDebug", iniGetDebug); + phpz.function("iniGetMode", iniGetMode); } diff --git a/examples/my_php_extension/src/ini.zig b/examples/my_php_extension/src/ini.zig index 56cac08..697c0de 100644 --- a/examples/my_php_extension/src/ini.zig +++ b/examples/my_php_extension/src/ini.zig @@ -1,7 +1,30 @@ const phpz = @import("phpz"); +const std = @import("std"); pub const greeting = phpz.ini.string.new("my_php_extension.greeting", "Hello", .all); pub const max_users = phpz.ini.int.new("my_php_extension.max_users", 100, .system); pub const debug = phpz.ini.boolean.new("my_php_extension.debug", false, .user); -pub const defs = phpz.ini.collect(.{ greeting, max_users, debug }); +pub const Mode = enum { safe, fast }; + +pub const mode = phpz.ini.typed(Mode).new(.{ + .name = "my_php_extension.mode", + .default = .safe, + .default_text = "safe", + .access = .all, + .parse = parseMode, + .format = formatMode, +}); + +fn parseMode(text: [:0]const u8) !Mode { + if (std.mem.eql(u8, text, "safe")) return .safe; + if (std.mem.eql(u8, text, "fast")) return .fast; + return error.InvalidMode; +} + +fn formatMode(value: Mode, _: []u8) ![]const u8 { + return switch (value) { + .safe => "safe", + .fast => "fast", + }; +} diff --git a/examples/my_php_extension/src/root.zig b/examples/my_php_extension/src/root.zig index 1b3cff6..b8efe17 100644 --- a/examples/my_php_extension/src/root.zig +++ b/examples/my_php_extension/src/root.zig @@ -4,19 +4,6 @@ const allocator = @import("allocator.zig"); const classes = @import("classes.zig"); const ini_config = @import("ini.zig"); -fn startup() !void { - classes.identifiable.Interface.register(); - classes.status.Enum.register(); - classes.role.Enum.register(); - classes.my_error.Class.register(); - classes.example_attribute.Class.register(); - classes.abstract_entity.Class.register(); - classes.user.Class.register(); - classes.counter.Class.register(); - classes.array_like.Class.register(); - classes.dumper.Class.register(); -} - fn shutdown() !void { allocator.deinit(); } @@ -38,11 +25,27 @@ comptime { phpz.module(.{ .name = "my_php_extension", .version = "0.1.0", - .module_startup_fn = startup, + .classes = &.{ + classes.identifiable.Interface, + classes.status.Enum, + classes.role.Enum, + classes.my_error.Class, + classes.example_attribute.Class, + classes.abstract_entity.Class, + classes.user.Class, + classes.counter.Class, + classes.array_like.Class, + classes.dumper.Class, + }, .module_shutdown_fn = shutdown, // .request_startup_fn = active, // .request_shutdown_fn = deactivate, .info_fn = info, - .ini_defs = &ini_config.defs, + .ini = &.{ + ini_config.greeting, + ini_config.max_users, + ini_config.debug, + ini_config.mode, + }, }); } diff --git a/examples/my_php_extension/tests/heap_allocator_bailout.phpt b/examples/my_php_extension/tests/heap_allocator_bailout.phpt new file mode 100644 index 0000000..d499778 --- /dev/null +++ b/examples/my_php_extension/tests/heap_allocator_bailout.phpt @@ -0,0 +1,23 @@ +--TEST-- +php allocator converts memory_limit bailout to OutOfMemory and runs defers +--INI-- +memory_limit=8M +display_errors=0 +log_errors=0 +--FILE-- + +--EXPECT-- +array(4) { + ["out_of_memory"]=> + bool(true) + ["scope_defer"]=> + bool(true) + ["cleanup_defer"]=> + bool(true) + ["freed_blocks"]=> + bool(true) +} diff --git a/examples/my_php_extension/tests/ini_zig_getters.phpt b/examples/my_php_extension/tests/ini_zig_getters.phpt index 51ab887..c78f74c 100644 --- a/examples/my_php_extension/tests/ini_zig_getters.phpt +++ b/examples/my_php_extension/tests/ini_zig_getters.phpt @@ -4,6 +4,7 @@ INI values read via Zig typed getters, with ini_set my_php_extension.greeting=ZigRules my_php_extension.max_users=42 my_php_extension.debug=1 +my_php_extension.mode=fast --FILE-- --EXPECT-- @@ -27,9 +31,11 @@ echo "debug: ", var_export(iniGetDebug(), true), "\n"; greeting: ZigRules max_users: 42 debug: true +mode: fast --- after ini_set --- bool(false) greeting: Runtime max_users: 42 debug: false +mode: safe diff --git a/examples/my_php_extension/tests/new_with_constructor_failure.phpt b/examples/my_php_extension/tests/new_with_constructor_failure.phpt index 35325d3..b074e5b 100644 --- a/examples/my_php_extension/tests/new_with_constructor_failure.phpt +++ b/examples/my_php_extension/tests/new_with_constructor_failure.phpt @@ -1,5 +1,5 @@ --TEST-- -newWith releases the object when the constructor throws +new releases the object when the constructor throws --FILE-- 1, "y" => 2], $u1)); -// === Success: array + User + extra User (covers: nullable+class with User value) === +// === Success: array + User + extra User (covers: nullable+type with User value) === echo "=== array + User + nullable User ===", PHP_EOL; $u2a = new \MyPHPExt\User("First", 10); $u2b = new \MyPHPExt\User("Second", 20); var_dump(testExpectArgArrayObject([1, 2, 3], $u2a, $u2b)); -// === Success: array + User + null (covers: nullable+class with null -> .null) === +// === Success: array + User + null (covers: nullable+type with null -> .null) === echo "=== array + User + null ===", PHP_EOL; $u3 = new \MyPHPExt\User("Bob", 25); var_dump(testExpectArgArrayObject(["a"], $u3, null)); @@ -42,7 +42,7 @@ try { testExpectArgArrayObject([], $u, null, "extra"); } catch (ArgumentCountErr echo "=== wrong type: string for array ===", PHP_EOL; try { testExpectArgArrayObject("not_array", $u); } catch (TypeError $e) { echo $e->getMessage(), PHP_EOL; } -// === Error: not an object for user (covers: .object class check fails on non-object) === +// === Error: not an object for user (covers: .object type check fails on non-object) === echo "=== not object for user: string ===", PHP_EOL; try { testExpectArgArrayObject([], "not_user"); } catch (TypeError $e) { echo $e->getMessage(), PHP_EOL; } @@ -50,11 +50,11 @@ try { testExpectArgArrayObject([], "not_user"); } catch (TypeError $e) { echo $e echo "=== wrong class for user: stdClass ===", PHP_EOL; try { testExpectArgArrayObject([], new stdClass()); } catch (TypeError $e) { echo $e->getMessage(), PHP_EOL; } -// === Error: not object for nullable_user (covers: nullable+class fails on non-object) === +// === Error: not object for nullable_user (covers: nullable+type fails on non-object) === echo "=== not object for nullable_user: int ===", PHP_EOL; try { testExpectArgArrayObject([], $u, 123); } catch (TypeError $e) { echo $e->getMessage(), PHP_EOL; } -// === Error: wrong class for nullable_user (covers: nullable+class instanceof fails) === +// === Error: wrong class for nullable_user (covers: nullable+type instanceof fails) === echo "=== wrong class for nullable_user: stdClass ===", PHP_EOL; try { testExpectArgArrayObject([], $u, new stdClass()); } catch (TypeError $e) { echo $e->getMessage(), PHP_EOL; } diff --git a/examples/pjs/.gitignore b/examples/pjs/.gitignore deleted file mode 100644 index dcd67f4..0000000 --- a/examples/pjs/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -/.vscode -/.cursor -/.idea -/.claude -/*.lua -zig-out -.zig-cache -/modules -/zig-pkg \ No newline at end of file diff --git a/examples/pjs/README.md b/examples/pjs/README.md deleted file mode 100644 index 1da14dc..0000000 --- a/examples/pjs/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# pjs - -Embed [QuickJS-ng](https://github.com/mitchellh/zig-quickjs-ng) in PHP to run JavaScript. - -- `Pjs\Runtime` — JS runtime environment -- `Pjs\Context` — evaluate JS code (`$ctx->eval("1+1")`) -- `Pjs\Value` — wraps JS return values, implements `Stringable` -- `Pjs\Exception` — JS exceptions thrown as PHP exceptions - -## Run - -```bash -zig build test -``` - -Custom PHP include path: - -```bash -zig build test -Dphp-include-dir=/usr/include/php8.4 -``` - -## Tests - -PHPT tests are located in `tests/`. Run with: - -```bash -php run-tests.php -``` - -## Manually - -```bash -# Build -zig build - -# Run JS evaluation -php -dextension=./modules/pjs.so -r ' -$rt = new Pjs\Runtime(); -$ctx = new Pjs\Context($rt); -var_dump($ctx->eval("1+1")); -' -``` diff --git a/examples/pjs/build.zig b/examples/pjs/build.zig deleted file mode 100644 index 6a30b2d..0000000 --- a/examples/pjs/build.zig +++ /dev/null @@ -1,75 +0,0 @@ -const std = @import("std"); - -// Import the Phpz build system module -const Phpz = @import("phpz").Phpz; - -pub fn build(b: *std.Build) void { - // Standard Zig build options: target platform and optimization mode - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - // Directory containing PHP header files (main/, Zend/, TSRM/, ext/) - const php_include_dir = b.option([]const u8, "php-include-dir", "PHP include directory (main/, Zend/, TSRM/, ext/)") orelse "/usr/include/php"; - // Windows only: PHP SDK lib directory containing php8.lib - const php_lib_dir = b.option([]const u8, "php-lib-dir", "PHP SDK library directory (Windows only, contains php8.lib)"); - - // Fetch the phpz dependency declared in build.zig.zon - const phpz_dep = b.dependency("phpz", .{}); - // Initialize Phpz: translates PHP C headers into Zig bindings - const phpz = Phpz.init(phpz_dep, .{ - .translator = .{ - .c_source_file = b.path("pjs.h"), - .target = target, - .optimize = optimize, - }, - .php_include_dir = .{ .cwd_relative = php_include_dir }, - .php_lib_dir = if (php_lib_dir) |d| .{ .cwd_relative = d } else null, - }); - - // QuickJS dependency for JavaScript runtime - const quickjs_dep = b.dependency("quickjs", .{ .target = target, .optimize = optimize }); - - // Create the PHP extension as a dynamic library (.so / .dll / .dylib) - const php_ext_name = "pjs"; - const php_ext_lib = b.addLibrary(.{ - .name = php_ext_name, - .root_module = b.createModule(.{ - .root_source_file = b.path("src/root.zig"), - .target = target, - .optimize = optimize, - }), - .use_llvm = true, - .linkage = .dynamic, - }); - - // Import the phpz and quickjs modules into your extension library - php_ext_lib.root_module.addImport("phpz", phpz.mod); - php_ext_lib.root_module.addImport("quickjs", quickjs_dep.module("quickjs")); - - // Apply OS-specific linker settings (macOS undefined symbols, Windows php8.lib) - phpz.apply(php_ext_lib); - - // PHP expects extensions at a specific naming convention - const php_ext_filename = if (target.result.os.tag == .windows) - "php_" ++ php_ext_name ++ ".dll" - else - php_ext_name ++ ".so"; - - // Copy the built extension to /modules/ - const php_ext_file = std.Build.Step.UpdateSourceFiles.create(b); - php_ext_file.addCopyFileToSource(php_ext_lib.getEmittedBin(), b.fmt("modules/{s}", .{php_ext_filename})); - b.getInstallStep().dependOn(&php_ext_file.step); - - // Test step: runs PHPT tests - const test_step = b.step("test", "Run PHPT tests"); - const test_phpt_cmd = b.addSystemCommand(&[_][]const u8{ - "php", - "run-tests.php", - "-q", - "--show-diff", - "-d", - b.fmt("extension=modules/{s}", .{php_ext_filename}), - }); - test_phpt_cmd.step.dependOn(b.getInstallStep()); - test_step.dependOn(&test_phpt_cmd.step); -} diff --git a/examples/pjs/build.zig.zon b/examples/pjs/build.zig.zon deleted file mode 100644 index 73a7daf..0000000 --- a/examples/pjs/build.zig.zon +++ /dev/null @@ -1,26 +0,0 @@ -.{ - .name = .pjs, - .version = "0.0.0", - .fingerprint = 0xfdae12e932c96e8d, - .minimum_zig_version = "0.17.0-dev.947+36069a2a7", - .dependencies = .{ - .phpz = .{ - .path = "../../", - }, - .quickjs = .{ - .path = "./deps/zig-quickjs-ng", - }, - }, - .paths = .{ - "build.zig", - "build.zig.zon", - "README.md", - "deps", - "pjs.h", - "pjs.stub.php", - "pjs_arginfo.h", - "run-tests.php", - "src", - "tests", - }, -} diff --git a/examples/pjs/deps/zig-quickjs-ng/LICENSE b/examples/pjs/deps/zig-quickjs-ng/LICENSE deleted file mode 100644 index 015ba0d..0000000 --- a/examples/pjs/deps/zig-quickjs-ng/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Mitchell Hashimoto - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/examples/pjs/deps/zig-quickjs-ng/README.md b/examples/pjs/deps/zig-quickjs-ng/README.md deleted file mode 100644 index 02a935f..0000000 --- a/examples/pjs/deps/zig-quickjs-ng/README.md +++ /dev/null @@ -1,101 +0,0 @@ -# zig-quickjs-ng - -Zig build and bindings for [QuickJS-ng](https://github.com/quickjs-ng/quickjs). - -**API coverage:** The bindings cover around 95% of the full quickjs-ng -API, including unit tests from Zig to verify the bindings work correctly -for just about every type and function. - -**AI Disclaimer:** I use AI assistance to help write and review code, but -I've manually reviewed and tested everything, and I'm using these bindings -in an actual project. I'm an expert at Zig, have written dozens of Zig -bindings to C projects, and have contributed to Zig itself. I'm not -generating slop here. - -## Example - -```zig -const quickjs = @import("quickjs"); - -pub fn main() !void { - const rt: *quickjs.Runtime = try .init(); - defer rt.deinit(); - - const ctx: *quickjs.Context = try .init(rt); - defer ctx.deinit(); - - const result = ctx.eval( - \\(function() { return 40 + 2; })() - , "
", .{}); - defer result.deinit(ctx); - - if (result.isException()) { - // Handle error - return error.JavaScriptError; - } - - const value = try result.toInt32(ctx); - std.debug.assert(value == 42); -} -``` - -## Usage - -**Zig version: zig-quickjs-ng only works with the released version of Zig specified -in the `build.zig.zon` file.** We don't support nightly versions because the Zig -compiler is still changing too much. - -### Add Dependency - -Add this to your `build.zig.zon`: - -```zig -.{ - .name = "my-project", - .version = "0.0.0", - .dependencies = .{ - .quickjs = .{ - .url = "https://github.com/user/zig-quickjs-ng/archive/.tar.gz", - .hash = "...", - }, - }, -} -``` - -### Configure build.zig - -In your `build.zig`: - -```zig -const std = @import("std"); - -pub fn build(b: *std.Build) !void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - const exe = b.addExecutable(.{ - .name = "my-app", - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); - - // Get the quickjs dependency - const dep = b.dependency("quickjs", .{ - .target = target, - .optimize = optimize, - }); - - // Add the Zig module - exe.root_module.addImport("quickjs", dep.module("quickjs")); - - // Link the C library - exe.linkLibrary(dep.artifact("quickjs-ng")); - - b.installArtifact(exe); -} -``` - -## Documentation - -Read the source code and header files - they are well commented. diff --git a/examples/pjs/deps/zig-quickjs-ng/build.zig b/examples/pjs/deps/zig-quickjs-ng/build.zig deleted file mode 100644 index 2cd6c5b..0000000 --- a/examples/pjs/deps/zig-quickjs-ng/build.zig +++ /dev/null @@ -1,100 +0,0 @@ -const std = @import("std"); - -pub fn build(b: *std.Build) !void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - // C headers - const c = translateC(b, target, optimize); - const c_mod = c.addModule("quickjs_c"); - - // Library - const lib = try library(b, target, optimize); - b.installArtifact(lib); - - // Zig module - const mod = b.addModule("quickjs", .{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - .imports = &.{.{ - .name = "quickjs_c", - .module = c_mod, - }}, - }); - - // Tests - const tests = b.addTest(.{ - .root_module = mod, - // Compiler crash without this. - .use_llvm = true, - }); - tests.root_module.linkLibrary(lib); - const run_tests = b.addRunArtifact(tests); - const test_step = b.step("test", "Run unit tests"); - test_step.dependOn(&run_tests.step); -} - -pub fn translateC( - b: *std.Build, - target: std.Build.ResolvedTarget, - optimize: std.builtin.OptimizeMode, -) *std.Build.Step.TranslateC { - const upstream = b.dependency("quickjs", .{}); - - const translate = b.addTranslateC(.{ - .root_source_file = upstream.path("quickjs.h"), - .target = target, - .optimize = optimize, - }); - - translate.addIncludePath(upstream.path("")); - return translate; -} - -pub fn library( - b: *std.Build, - target: std.Build.ResolvedTarget, - optimize: std.builtin.OptimizeMode, -) !*std.Build.Step.Compile { - const upstream = b.dependency("quickjs", .{}); - - const lib = b.addLibrary(.{ - .name = "quickjs-ng", - .root_module = b.createModule(.{ - .target = target, - .optimize = optimize, - }), - .linkage = .static, - }); - lib.root_module.link_libc = true; - - lib.root_module.addIncludePath(upstream.path("")); - lib.installHeader( - upstream.path("quickjs.h"), - "quickjs.h", - ); - - var flags: std.ArrayList([]const u8) = .empty; - try flags.appendSlice(b.allocator, &.{ - "-D_GNU_SOURCE", - "-funsigned-char", - "-fno-omit-frame-pointer", - "-fno-sanitize=undefined", - "-fno-sanitize-trap=undefined", - "-fvisibility=hidden", - }); - lib.root_module.addCSourceFiles(.{ - .root = upstream.path(""), - .files = &.{ - "cutils.c", - "dtoa.c", - "libregexp.c", - "libunicode.c", - "quickjs.c", - }, - .flags = flags.items, - }); - - return lib; -} diff --git a/examples/pjs/deps/zig-quickjs-ng/build.zig.zon b/examples/pjs/deps/zig-quickjs-ng/build.zig.zon deleted file mode 100644 index c8d344b..0000000 --- a/examples/pjs/deps/zig-quickjs-ng/build.zig.zon +++ /dev/null @@ -1,18 +0,0 @@ -.{ - .name = .quickjs_ng, - .version = "0.0.0", - .fingerprint = 0xb2bca9fa0367c6d1, - .minimum_zig_version = "0.17.0-dev.813+2153f8143", - .paths = .{ - "build.zig", - "build.zig.zon", - "src", - }, - .dependencies = .{ - // quickjs-ng/quickjs - .quickjs = .{ - .url = "https://github.com/quickjs-ng/quickjs/archive/85640f81e04bc93940acc2756c792c66076dd768.tar.gz", - .hash = "N-V-__8AAIZ_PAA7y10jIaLigzkK4qd5-jfKEoTOOfHCsIGM", - }, - }, -} diff --git a/examples/pjs/deps/zig-quickjs-ng/src/atom.zig b/examples/pjs/deps/zig-quickjs-ng/src/atom.zig deleted file mode 100644 index 81f9dbf..0000000 --- a/examples/pjs/deps/zig-quickjs-ng/src/atom.zig +++ /dev/null @@ -1,378 +0,0 @@ -const std = @import("std"); -const testing = std.testing; -const c = @import("quickjs_c"); -const Context = @import("context.zig").Context; -const Runtime = @import("runtime.zig").Runtime; -const Value = @import("value.zig").Value; - -/// Wrapper for the QuickJS `JSAtom`. -/// -/// Atoms are interned strings used for property names and symbols. -/// They are reference-counted and must be freed with `deinit` when no -/// longer needed. -/// -/// C: `JSAtom` -pub const Atom = enum(u32) { - null = 0, - _, - - /// Creates an atom from a null-terminated string. - /// - /// C: `JS_NewAtom` - pub fn init(ctx: *Context, str: [:0]const u8) Atom { - return @enumFromInt(c.JS_NewAtom(ctx.cval(), str.ptr)); - } - - /// Creates an atom from a string slice. - /// - /// C: `JS_NewAtomLen` - pub fn initLen(ctx: *Context, str: []const u8) Atom { - return @enumFromInt(c.JS_NewAtomLen(ctx.cval(), str.ptr, str.len)); - } - - /// Creates an atom from an unsigned 32-bit integer. - /// - /// C: `JS_NewAtomUInt32` - pub fn initUint32(ctx: *Context, n: u32) Atom { - return @enumFromInt(c.JS_NewAtomUInt32(ctx.cval(), n)); - } - - /// Creates an atom from a JavaScript value. - /// - /// The value is converted to a string and interned as an atom. - /// - /// C: `JS_ValueToAtom` - pub fn fromValue(ctx: *Context, val: Value) Atom { - return @enumFromInt(c.JS_ValueToAtom(ctx.cval(), val.cval())); - } - - /// Duplicates the atom, incrementing its reference count. - /// - /// The returned atom must also be freed with `deinit`. - /// - /// C: `JS_DupAtom` - pub fn dup(self: Atom, ctx: *Context) Atom { - return @enumFromInt(c.JS_DupAtom(ctx.cval(), @intFromEnum(self))); - } - - /// Duplicates the atom using the runtime, incrementing its reference count. - /// - /// The returned atom must also be freed with `deinitRT`. - /// - /// C: `JS_DupAtomRT` - pub fn dupRT(self: Atom, rt: *Runtime) Atom { - return @enumFromInt(c.JS_DupAtomRT(rt.cval(), @intFromEnum(self))); - } - - /// Frees the atom, decrementing its reference count. - /// - /// C: `JS_FreeAtom` - pub fn deinit(self: Atom, ctx: *Context) void { - c.JS_FreeAtom(ctx.cval(), @intFromEnum(self)); - } - - /// Frees the atom using the runtime, decrementing its reference count. - /// - /// C: `JS_FreeAtomRT` - pub fn deinitRT(self: Atom, rt: *Runtime) void { - c.JS_FreeAtomRT(rt.cval(), @intFromEnum(self)); - } - - /// Converts the atom to a JavaScript value (symbol). - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_AtomToValue` - pub fn toValue(self: Atom, ctx: *Context) Value { - return Value.fromCVal(c.JS_AtomToValue(ctx.cval(), @intFromEnum(self))); - } - - /// Converts the atom to a JavaScript string value. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_AtomToString` - pub fn toString(self: Atom, ctx: *Context) Value { - return Value.fromCVal(c.JS_AtomToString(ctx.cval(), @intFromEnum(self))); - } - - /// Converts the atom to a C string. - /// - /// Returns null if the atom is invalid. The returned string must be - /// freed with `Context.freeCString`. - /// - /// C: `JS_AtomToCString` - pub fn toCString(self: Atom, ctx: *Context) ?[*:0]const u8 { - const ptr = c.JS_AtomToCString(ctx.cval(), @intFromEnum(self)); - return @ptrCast(ptr); - } - - /// Converts the atom to a C string with length. - /// - /// Returns null if the atom is invalid. The returned pointer must be - /// freed with `Context.freeCString`. - /// - /// C: `JS_AtomToCStringLen` - pub fn toCStringLen(self: Atom, ctx: *Context) ?struct { ptr: [*:0]const u8, len: usize } { - var len: usize = 0; - const ptr = c.JS_AtomToCStringLen(ctx.cval(), &len, @intFromEnum(self)); - if (ptr == null) return null; - return .{ .ptr = @ptrCast(ptr), .len = len }; - } - - /// Converts the atom to a Zig string slice. - /// - /// Returns null if the atom is invalid. The returned slice must be - /// freed by passing the pointer to `Context.freeCString`. - /// - /// C: `JS_AtomToCStringLen` - pub fn toZigSlice(self: Atom, ctx: *Context) ?[:0]const u8 { - const result = self.toCStringLen(ctx) orelse return null; - return result.ptr[0..result.len :0]; - } -}; - -// ============================================================================= -// Tests -// ============================================================================= - -test "Atom init and deinit" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const atom = Atom.init(ctx, "testProperty"); - defer atom.deinit(ctx); - - try testing.expect(atom != .null); -} - -test "Atom initLen" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const str = "helloWorld"; - const atom = Atom.initLen(ctx, str[0..5]); - defer atom.deinit(ctx); - - const cstr = atom.toCString(ctx).?; - defer ctx.freeCString(cstr); - - try testing.expectEqualStrings("hello", std.mem.span(cstr)); -} - -test "Atom initUint32" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const atom = Atom.initUint32(ctx, 42); - defer atom.deinit(ctx); - - const cstr = atom.toCString(ctx).?; - defer ctx.freeCString(cstr); - - try testing.expectEqualStrings("42", std.mem.span(cstr)); -} - -test "Atom fromValue" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const str_val = Value.initString(ctx, "myProperty"); - defer str_val.deinit(ctx); - - const atom = Atom.fromValue(ctx, str_val); - defer atom.deinit(ctx); - - const cstr = atom.toCString(ctx).?; - defer ctx.freeCString(cstr); - - try testing.expectEqualStrings("myProperty", std.mem.span(cstr)); -} - -test "Atom dup" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const atom1 = Atom.init(ctx, "shared"); - defer atom1.deinit(ctx); - - const atom2 = atom1.dup(ctx); - defer atom2.deinit(ctx); - - try testing.expectEqual(@intFromEnum(atom1), @intFromEnum(atom2)); -} - -test "Atom dupRT and deinitRT" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const atom1 = Atom.init(ctx, "runtimeAtom"); - defer atom1.deinit(ctx); - - const atom2 = atom1.dupRT(rt); - defer atom2.deinitRT(rt); - - try testing.expectEqual(@intFromEnum(atom1), @intFromEnum(atom2)); -} - -test "Atom toValue" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const atom = Atom.init(ctx, "symbolName"); - defer atom.deinit(ctx); - - const val = atom.toValue(ctx); - defer val.deinit(ctx); - - try testing.expect(!val.isException()); -} - -test "Atom toString" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const atom = Atom.init(ctx, "stringAtom"); - defer atom.deinit(ctx); - - const str_val = atom.toString(ctx); - defer str_val.deinit(ctx); - - try testing.expect(str_val.isString()); - - const cstr = str_val.toCString(ctx).?; - defer ctx.freeCString(cstr); - - try testing.expectEqualStrings("stringAtom", std.mem.span(cstr)); -} - -test "Atom toCString" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const atom = Atom.init(ctx, "cstringTest"); - defer atom.deinit(ctx); - - const cstr = atom.toCString(ctx).?; - defer ctx.freeCString(cstr); - - try testing.expectEqualStrings("cstringTest", std.mem.span(cstr)); -} - -test "Atom toCStringLen" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const atom = Atom.init(ctx, "lengthTest"); - defer atom.deinit(ctx); - - const result = atom.toCStringLen(ctx).?; - defer ctx.freeCString(result.ptr); - - try testing.expectEqualStrings("lengthTest", result.ptr[0..result.len]); - try testing.expectEqual(@as(usize, 10), result.len); -} - -test "Atom toZigSlice" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const atom = Atom.init(ctx, "sliceTest"); - defer atom.deinit(ctx); - - const slice = atom.toZigSlice(ctx).?; - defer ctx.freeCString(slice.ptr); - - try testing.expectEqualStrings("sliceTest", slice); - try testing.expectEqual(@as(usize, 9), slice.len); -} - -test "Atom used as property name in JavaScript" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const obj = Value.initObject(ctx); - defer obj.deinit(ctx); - - const atom = Atom.init(ctx, "myProp"); - defer atom.deinit(ctx); - - const val = Value.initInt32(42); - try obj.setProperty(ctx, atom, val); - - const retrieved = obj.getProperty(ctx, atom); - defer retrieved.deinit(ctx); - - try testing.expectEqual(@as(i32, 42), try retrieved.toInt32(ctx)); -} - -test "Atom integer property access" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const arr = ctx.eval("[10, 20, 30]", "", .{}); - defer arr.deinit(ctx); - - const atom = Atom.initUint32(ctx, 1); - defer atom.deinit(ctx); - - const val = arr.getProperty(ctx, atom); - defer val.deinit(ctx); - - try testing.expectEqual(@as(i32, 20), try val.toInt32(ctx)); -} - -test "Atom same strings share same atom" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const atom1 = Atom.init(ctx, "sharedString"); - defer atom1.deinit(ctx); - - const atom2 = Atom.init(ctx, "sharedString"); - defer atom2.deinit(ctx); - - try testing.expectEqual(@intFromEnum(atom1), @intFromEnum(atom2)); -} diff --git a/examples/pjs/deps/zig-quickjs-ng/src/cfunc.zig b/examples/pjs/deps/zig-quickjs-ng/src/cfunc.zig deleted file mode 100644 index 458faa7..0000000 --- a/examples/pjs/deps/zig-quickjs-ng/src/cfunc.zig +++ /dev/null @@ -1,509 +0,0 @@ -const std = @import("std"); -const testing = std.testing; -const c = @import("quickjs_c"); -const Context = @import("context.zig").Context; -const Value = @import("value.zig").Value; -const opaquepkg = @import("opaque.zig"); -const Opaque = opaquepkg.Opaque; - -/// C function prototype type. -/// -/// C: `JSCFunctionEnum` -pub const Proto = enum(c_uint) { - generic = c.JS_CFUNC_generic, - generic_magic = c.JS_CFUNC_generic_magic, - constructor = c.JS_CFUNC_constructor, - constructor_magic = c.JS_CFUNC_constructor_magic, - constructor_or_func = c.JS_CFUNC_constructor_or_func, - constructor_or_func_magic = c.JS_CFUNC_constructor_or_func_magic, - f_f = c.JS_CFUNC_f_f, - f_f_f = c.JS_CFUNC_f_f_f, - getter = c.JS_CFUNC_getter, - setter = c.JS_CFUNC_setter, - getter_magic = c.JS_CFUNC_getter_magic, - setter_magic = c.JS_CFUNC_setter_magic, - iterator_next = c.JS_CFUNC_iterator_next, -}; - -/// C function signature for simple functions. -/// -/// C: `JSCFunction` -pub const Func = *const fn ( - ?*Context, - Value, - []const c.JSValue, -) Value; - -/// Wraps a Zig-friendly Func into a C-callable function. -pub fn wrapFunc(comptime func: Func) c.JSCFunction { - return struct { - fn callback( - ctx: ?*c.JSContext, - this_val: c.JSValue, - argc: c_int, - argv: [*c]c.JSValue, - ) callconv(.c) c.JSValue { - const args: []const c.JSValue = if (argc > 0) - argv[0..@intCast(argc)] - else - &.{}; - return @call(.always_inline, func, .{ - @as(?*Context, @ptrCast(ctx)), - @as(Value, @bitCast(this_val)), - args, - }).cval(); - } - }.callback; -} - -/// C function signature for functions with magic value. -/// -/// C: `JSCFunctionMagic` -pub const FuncMagic = *const fn ( - ?*Context, - Value, - []const c.JSValue, - c_int, -) Value; - -/// Wraps a Zig-friendly FuncMagic into a C-callable function. -pub fn wrapFuncMagic(comptime func: FuncMagic) c.JSCFunctionMagic { - return struct { - fn callback( - ctx: ?*c.JSContext, - this_val: c.JSValue, - argc: c_int, - argv: [*c]c.JSValue, - magic: c_int, - ) callconv(.c) c.JSValue { - const args: []const c.JSValue = if (argc > 0) - argv[0..@intCast(argc)] - else - &.{}; - return @call(.always_inline, func, .{ - @as(?*Context, @ptrCast(ctx)), - @as(Value, @bitCast(this_val)), - args, - magic, - }).cval(); - } - }.callback; -} - -/// C function signature for functions with closure data. -/// -/// The magic parameter is the magic value passed at construction. -/// The func_data pointer points to the array of data values passed at construction. -/// -/// C: `JSCFunctionData` -pub const FuncData = *const fn ( - ?*Context, - Value, - []const c.JSValue, - c_int, - [*c]c.JSValue, -) Value; - -/// Wraps a Zig-friendly FuncData into a C-callable function. -pub fn wrapFuncData(comptime func: FuncData) c.JSCFunctionData { - return struct { - fn callback( - ctx: ?*c.JSContext, - this_val: c.JSValue, - argc: c_int, - argv: [*c]c.JSValue, - magic: c_int, - data: [*c]c.JSValue, - ) callconv(.c) c.JSValue { - const args: []const c.JSValue = if (argc > 0) - argv[0..@intCast(argc)] - else - &.{}; - return @call(.always_inline, func, .{ - @as(?*Context, @ptrCast(ctx)), - @as(Value, @bitCast(this_val)), - args, - magic, - data, - }).cval(); - } - }.callback; -} - -/// C closure function signature. -/// -/// C: `JSCClosure` -pub fn Closure(comptime T: type) type { - return *const fn ( - ?*Context, - Value, - []const c.JSValue, - c_int, - Opaque(T), - ) Value; -} - -/// Wraps a Zig-friendly Closure into a C-callable function. -pub fn wrapClosure(comptime T: type, comptime func: Closure(T)) c.JSCClosure { - return struct { - fn callback( - ctx: ?*c.JSContext, - this_val: c.JSValue, - argc: c_int, - argv: [*c]c.JSValue, - magic: c_int, - opaque_ptr: ?*anyopaque, - ) callconv(.c) c.JSValue { - const args: []const c.JSValue = if (argc > 0) - argv[0..@intCast(argc)] - else - &.{}; - return @call(.always_inline, func, .{ - @as(?*Context, @ptrCast(ctx)), - @as(Value, @bitCast(this_val)), - args, - magic, - opaquepkg.fromC(T, opaque_ptr), - }).cval(); - } - }.callback; -} - -/// Finalizer function for C closures. -/// -/// C: `JSCClosureFinalizerFunc` -pub fn ClosureFinalizer(comptime T: type) type { - return *const fn (Opaque(T)) void; -} - -/// Wraps a Zig-friendly ClosureFinalizer into a C-callable function. -pub fn wrapClosureFinalizer(comptime T: type, comptime func: ClosureFinalizer(T)) c.JSCClosureFinalizerFunc { - return struct { - fn callback(opaque_ptr: ?*anyopaque) callconv(.c) void { - @call(.always_inline, func, .{opaquepkg.fromC(T, opaque_ptr)}); - } - }.callback; -} - -/// Getter function signature. -/// -/// C: getter in `JSCFunctionType` -pub const Getter = *const fn (?*Context, Value) Value; - -const CGetterFn = ?*const fn (?*c.JSContext, c.JSValue) callconv(.c) c.JSValue; - -/// Wraps a Zig-friendly Getter into a C-callable function. -pub fn wrapGetter(comptime func: Getter) CGetterFn { - return struct { - fn callback( - ctx: ?*c.JSContext, - this_val: c.JSValue, - ) callconv(.c) c.JSValue { - return @call(.always_inline, func, .{ - @as(?*Context, @ptrCast(ctx)), - @as(Value, @bitCast(this_val)), - }).cval(); - } - }.callback; -} - -/// Setter function signature. -/// -/// C: setter in `JSCFunctionType` -pub const Setter = *const fn (?*Context, Value, Value) Value; - -const CSetterFn = ?*const fn (?*c.JSContext, c.JSValue, c.JSValue) callconv(.c) c.JSValue; - -/// Wraps a Zig-friendly Setter into a C-callable function. -pub fn wrapSetter(comptime func: Setter) CSetterFn { - return struct { - fn callback( - ctx: ?*c.JSContext, - this_val: c.JSValue, - val: c.JSValue, - ) callconv(.c) c.JSValue { - return @call(.always_inline, func, .{ - @as(?*Context, @ptrCast(ctx)), - @as(Value, @bitCast(this_val)), - @as(Value, @bitCast(val)), - }).cval(); - } - }.callback; -} - -/// Getter function signature with magic value. -/// -/// C: getter_magic in `JSCFunctionType` -pub const GetterMagic = *const fn (?*Context, Value, c_int) Value; - -const CGetterMagicFn = ?*const fn (?*c.JSContext, c.JSValue, c_int) callconv(.c) c.JSValue; - -/// Wraps a Zig-friendly GetterMagic into a C-callable function. -pub fn wrapGetterMagic(comptime func: GetterMagic) CGetterMagicFn { - return struct { - fn callback( - ctx: ?*c.JSContext, - this_val: c.JSValue, - magic: c_int, - ) callconv(.c) c.JSValue { - return @call(.always_inline, func, .{ - @as(?*Context, @ptrCast(ctx)), - @as(Value, @bitCast(this_val)), - magic, - }).cval(); - } - }.callback; -} - -/// Setter function signature with magic value. -/// -/// C: setter_magic in `JSCFunctionType` -pub const SetterMagic = *const fn (?*Context, Value, Value, c_int) Value; - -const CSetterMagicFn = ?*const fn (?*c.JSContext, c.JSValue, c.JSValue, c_int) callconv(.c) c.JSValue; - -/// Wraps a Zig-friendly SetterMagic into a C-callable function. -pub fn wrapSetterMagic(comptime func: SetterMagic) CSetterMagicFn { - return struct { - fn callback( - ctx: ?*c.JSContext, - this_val: c.JSValue, - val: c.JSValue, - magic: c_int, - ) callconv(.c) c.JSValue { - return @call(.always_inline, func, .{ - @as(?*Context, @ptrCast(ctx)), - @as(Value, @bitCast(this_val)), - @as(Value, @bitCast(val)), - magic, - }).cval(); - } - }.callback; -} - -/// Definition type for function list entries. -pub const DefType = enum(u8) { - cfunc = c.JS_DEF_CFUNC, - cgetset = c.JS_DEF_CGETSET, - cgetset_magic = c.JS_DEF_CGETSET_MAGIC, - prop_string = c.JS_DEF_PROP_STRING, - prop_int32 = c.JS_DEF_PROP_INT32, - prop_int64 = c.JS_DEF_PROP_INT64, - prop_double = c.JS_DEF_PROP_DOUBLE, - prop_undefined = c.JS_DEF_PROP_UNDEFINED, - object = c.JS_DEF_OBJECT, - alias = c.JS_DEF_ALIAS, -}; - -/// Property flags for function list entries. -pub const PropFlags = packed struct(u8) { - configurable: bool = false, - writable: bool = false, - enumerable: bool = false, - _padding: u5 = 0, - - pub const default: PropFlags = .{ .writable = true, .configurable = true }; -}; - -/// Function list entry for bulk property definition. -/// -/// C: `JSCFunctionListEntry` -pub const FunctionListEntry = c.JSCFunctionListEntry; - -/// Helper functions for creating FunctionListEntry values. -pub const FunctionListEntryHelpers = struct { - /// Creates a function definition entry. - pub fn func( - name: [:0]const u8, - length: u8, - comptime cfunc_ptr: Func, - ) FunctionListEntry { - return funcWithFlags(name, length, wrapFunc(cfunc_ptr), PropFlags.default); - } - - /// Creates a function definition entry with custom flags. - pub fn funcWithFlags( - name: [:0]const u8, - length: u8, - cfunc_ptr: c.JSCFunction, - flags: PropFlags, - ) FunctionListEntry { - var entry: FunctionListEntry = std.mem.zeroes(FunctionListEntry); - entry.name = name.ptr; - entry.prop_flags = @bitCast(flags); - entry.def_type = @intFromEnum(DefType.cfunc); - entry.magic = 0; - entry.u.func.length = length; - entry.u.func.cproto = @intFromEnum(Proto.generic); - entry.u.func.cfunc.generic = cfunc_ptr; - return entry; - } - - /// Creates a function definition with magic value. - pub fn funcMagic( - name: [:0]const u8, - length: u8, - comptime cfunc_ptr: FuncMagic, - magic: i16, - ) FunctionListEntry { - var entry: FunctionListEntry = std.mem.zeroes(FunctionListEntry); - entry.name = name.ptr; - entry.prop_flags = @bitCast(PropFlags.default); - entry.def_type = @intFromEnum(DefType.cfunc); - entry.magic = magic; - entry.u.func.length = length; - entry.u.func.cproto = @intFromEnum(Proto.generic_magic); - entry.u.func.cfunc.generic_magic = wrapFuncMagic(cfunc_ptr); - return entry; - } - - /// Creates a getter/setter property definition. - pub fn getset( - name: [:0]const u8, - comptime getter_fn: ?Getter, - comptime setter_fn: ?Setter, - ) FunctionListEntry { - var entry: FunctionListEntry = std.mem.zeroes(FunctionListEntry); - entry.name = name.ptr; - entry.prop_flags = c.JS_PROP_CONFIGURABLE; - entry.def_type = @intFromEnum(DefType.cgetset); - entry.magic = 0; - entry.u.getset.get.getter = if (getter_fn) |g| wrapGetter(g) else null; - entry.u.getset.set.setter = if (setter_fn) |s| wrapSetter(s) else null; - return entry; - } - - /// Creates a getter/setter property definition with magic value. - pub fn getsetMagic( - name: [:0]const u8, - comptime getter_fn: ?GetterMagic, - comptime setter_fn: ?SetterMagic, - magic: i16, - ) FunctionListEntry { - var entry: FunctionListEntry = std.mem.zeroes(FunctionListEntry); - entry.name = name.ptr; - entry.prop_flags = c.JS_PROP_CONFIGURABLE; - entry.def_type = @intFromEnum(DefType.cgetset_magic); - entry.magic = magic; - entry.u.getset.get.getter_magic = if (getter_fn) |g| wrapGetterMagic(g) else null; - entry.u.getset.set.setter_magic = if (setter_fn) |s| wrapSetterMagic(s) else null; - return entry; - } - - /// Creates a string property definition. - pub fn propString( - name: [:0]const u8, - value: [:0]const u8, - flags: PropFlags, - ) FunctionListEntry { - var entry: FunctionListEntry = std.mem.zeroes(FunctionListEntry); - entry.name = name.ptr; - entry.prop_flags = @bitCast(flags); - entry.def_type = @intFromEnum(DefType.prop_string); - entry.magic = 0; - entry.u.str = value.ptr; - return entry; - } - - /// Creates an i32 property definition. - pub fn propInt32( - name: [:0]const u8, - value: i32, - flags: PropFlags, - ) FunctionListEntry { - var entry: FunctionListEntry = std.mem.zeroes(FunctionListEntry); - entry.name = name.ptr; - entry.prop_flags = @bitCast(flags); - entry.def_type = @intFromEnum(DefType.prop_int32); - entry.magic = 0; - entry.u.i32 = value; - return entry; - } - - /// Creates an i64 property definition. - pub fn propInt64( - name: [:0]const u8, - value: i64, - flags: PropFlags, - ) FunctionListEntry { - var entry: FunctionListEntry = std.mem.zeroes(FunctionListEntry); - entry.name = name.ptr; - entry.prop_flags = @bitCast(flags); - entry.def_type = @intFromEnum(DefType.prop_int64); - entry.magic = 0; - entry.u.i64 = value; - return entry; - } - - /// Creates a double property definition. - pub fn propDouble( - name: [:0]const u8, - value: f64, - flags: PropFlags, - ) FunctionListEntry { - var entry: FunctionListEntry = std.mem.zeroes(FunctionListEntry); - entry.name = name.ptr; - entry.prop_flags = @bitCast(flags); - entry.def_type = @intFromEnum(DefType.prop_double); - entry.magic = 0; - entry.u.f64 = value; - return entry; - } - - /// Creates an undefined property definition. - pub fn propUndefined( - name: [:0]const u8, - flags: PropFlags, - ) FunctionListEntry { - var entry: FunctionListEntry = std.mem.zeroes(FunctionListEntry); - entry.name = name.ptr; - entry.prop_flags = @bitCast(flags); - entry.def_type = @intFromEnum(DefType.prop_undefined); - entry.magic = 0; - entry.u.i32 = 0; - return entry; - } -}; - -test "Proto enum matches C constants" { - try testing.expectEqual(@as(c_uint, c.JS_CFUNC_generic), @intFromEnum(Proto.generic)); - try testing.expectEqual(@as(c_uint, c.JS_CFUNC_generic_magic), @intFromEnum(Proto.generic_magic)); - try testing.expectEqual(@as(c_uint, c.JS_CFUNC_constructor), @intFromEnum(Proto.constructor)); - try testing.expectEqual(@as(c_uint, c.JS_CFUNC_constructor_magic), @intFromEnum(Proto.constructor_magic)); - try testing.expectEqual(@as(c_uint, c.JS_CFUNC_constructor_or_func), @intFromEnum(Proto.constructor_or_func)); - try testing.expectEqual(@as(c_uint, c.JS_CFUNC_constructor_or_func_magic), @intFromEnum(Proto.constructor_or_func_magic)); - try testing.expectEqual(@as(c_uint, c.JS_CFUNC_f_f), @intFromEnum(Proto.f_f)); - try testing.expectEqual(@as(c_uint, c.JS_CFUNC_f_f_f), @intFromEnum(Proto.f_f_f)); - try testing.expectEqual(@as(c_uint, c.JS_CFUNC_getter), @intFromEnum(Proto.getter)); - try testing.expectEqual(@as(c_uint, c.JS_CFUNC_setter), @intFromEnum(Proto.setter)); - try testing.expectEqual(@as(c_uint, c.JS_CFUNC_getter_magic), @intFromEnum(Proto.getter_magic)); - try testing.expectEqual(@as(c_uint, c.JS_CFUNC_setter_magic), @intFromEnum(Proto.setter_magic)); - try testing.expectEqual(@as(c_uint, c.JS_CFUNC_iterator_next), @intFromEnum(Proto.iterator_next)); -} - -test "DefType enum matches C constants" { - try testing.expectEqual(@as(u8, c.JS_DEF_CFUNC), @intFromEnum(DefType.cfunc)); - try testing.expectEqual(@as(u8, c.JS_DEF_CGETSET), @intFromEnum(DefType.cgetset)); - try testing.expectEqual(@as(u8, c.JS_DEF_CGETSET_MAGIC), @intFromEnum(DefType.cgetset_magic)); - try testing.expectEqual(@as(u8, c.JS_DEF_PROP_STRING), @intFromEnum(DefType.prop_string)); - try testing.expectEqual(@as(u8, c.JS_DEF_PROP_INT32), @intFromEnum(DefType.prop_int32)); - try testing.expectEqual(@as(u8, c.JS_DEF_PROP_INT64), @intFromEnum(DefType.prop_int64)); - try testing.expectEqual(@as(u8, c.JS_DEF_PROP_DOUBLE), @intFromEnum(DefType.prop_double)); - try testing.expectEqual(@as(u8, c.JS_DEF_PROP_UNDEFINED), @intFromEnum(DefType.prop_undefined)); - try testing.expectEqual(@as(u8, c.JS_DEF_OBJECT), @intFromEnum(DefType.object)); - try testing.expectEqual(@as(u8, c.JS_DEF_ALIAS), @intFromEnum(DefType.alias)); -} - -test "PropFlags matches C constants" { - const configurable: PropFlags = .{ .configurable = true }; - try testing.expectEqual(@as(u8, c.JS_PROP_CONFIGURABLE), @as(u8, @bitCast(configurable))); - - const writable: PropFlags = .{ .writable = true }; - try testing.expectEqual(@as(u8, c.JS_PROP_WRITABLE), @as(u8, @bitCast(writable))); - - const enumerable: PropFlags = .{ .enumerable = true }; - try testing.expectEqual(@as(u8, c.JS_PROP_ENUMERABLE), @as(u8, @bitCast(enumerable))); - - const default_flags: PropFlags = PropFlags.default; - try testing.expectEqual(@as(u8, c.JS_PROP_WRITABLE | c.JS_PROP_CONFIGURABLE), @as(u8, @bitCast(default_flags))); -} diff --git a/examples/pjs/deps/zig-quickjs-ng/src/class.zig b/examples/pjs/deps/zig-quickjs-ng/src/class.zig deleted file mode 100644 index 5c4a713..0000000 --- a/examples/pjs/deps/zig-quickjs-ng/src/class.zig +++ /dev/null @@ -1,380 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; -const testing = std.testing; -const c = @import("quickjs_c"); -const Atom = @import("atom.zig").Atom; -const Context = @import("context.zig").Context; -const Runtime = @import("runtime.zig").Runtime; -const Value = @import("value.zig").Value; - -/// Class identifier for custom JavaScript classes. -/// -/// Class IDs are used to register custom native-backed classes with QuickJS. -/// Each class has a unique ID that is used to associate objects with their -/// class definition, prototype, and opaque data. -/// -/// C: `JSClassID` -pub const Id = enum(u32) { - /// Invalid class ID (returned when object has no class). - invalid = c.JS_INVALID_CLASS_ID, - _, - - /// Creates a new unique class ID. - /// - /// C: `JS_NewClassID` - pub fn new(rt: *Runtime) Id { - var raw: u32 = 0; - return @enumFromInt(c.JS_NewClassID(rt.cval(), &raw)); - } -}; - -/// Finalizer callback called when an object of this class is garbage collected. -/// -/// C: `JSClassFinalizer` -pub const Finalizer = *const fn (*Runtime, Value) callconv(.c) void; - -/// GC mark callback for marking references held by this class. -/// -/// C: `JSClassGCMark` -pub const GCMark = *const fn (*Runtime, Value, c.JS_MarkFunc) callconv(.c) void; - -/// Call callback for callable class objects. -/// -/// If `flags & JS_CALL_FLAG_CONSTRUCTOR` is set, the object is being -/// called as a constructor and `this_val` is `new.target`. -/// -/// C: `JSClassCall` -pub const Call = *const fn ( - *Context, - Value, - Value, - c_int, - [*]Value, - c_int, -) callconv(.c) Value; - -/// Exotic methods for customizing property access behavior. -/// -/// These methods allow implementing Proxy-like behavior for custom classes. -/// -/// C: `JSClassExoticMethods` -pub const ExoticMethods = extern struct { - get_own_property: ?*const fn ( - ?*c.JSContext, - [*c]c.JSPropertyDescriptor, - c.JSValue, - c.JSAtom, - ) callconv(.c) c_int = null, - get_own_property_names: ?*const fn ( - ?*c.JSContext, - [*c][*c]c.JSPropertyEnum, - [*c]u32, - c.JSValue, - ) callconv(.c) c_int = null, - delete_property: ?*const fn ( - ?*c.JSContext, - c.JSValue, - c.JSAtom, - ) callconv(.c) c_int = null, - define_own_property: ?*const fn ( - ?*c.JSContext, - c.JSValue, - c.JSAtom, - c.JSValue, - c.JSValue, - c.JSValue, - c_int, - ) callconv(.c) c_int = null, - has_property: ?*const fn ( - ?*c.JSContext, - c.JSValue, - c.JSAtom, - ) callconv(.c) c_int = null, - get_property: ?*const fn ( - ?*c.JSContext, - c.JSValue, - c.JSAtom, - c.JSValue, - ) callconv(.c) c.JSValue = null, - set_property: ?*const fn ( - ?*c.JSContext, - c.JSValue, - c.JSAtom, - c.JSValue, - c.JSValue, - c_int, - ) callconv(.c) c_int = null, -}; - -/// Definition for a custom JavaScript class. -/// -/// Used with `Runtime.newClass` to register a custom class. -/// -/// C: `JSClassDef` -pub const Def = extern struct { - /// Class name (pure ASCII only). - class_name: [*:0]const u8 = "", - /// Finalizer called when objects of this class are garbage collected. - finalizer: ?*const c.JSClassFinalizer = null, - /// GC mark function for marking references. - gc_mark: ?*const c.JSClassGCMark = null, - /// Call function (makes instances of this class callable). - call: ?*const c.JSClassCall = null, - /// Exotic methods for custom property access. - exotic: ?*ExoticMethods = null, -}; - -/// Flag indicating a constructor call. -pub const call_flag_constructor: c_int = 1 << 0; - -comptime { - assert(@intFromEnum(Id.invalid) == c.JS_INVALID_CLASS_ID); - assert(@sizeOf(Def) == @sizeOf(c.JSClassDef)); - assert(@alignOf(Def) == @alignOf(c.JSClassDef)); - assert(@sizeOf(ExoticMethods) == @sizeOf(c.JSClassExoticMethods)); - assert(@alignOf(ExoticMethods) == @alignOf(c.JSClassExoticMethods)); -} - -test "Id.new allocates unique IDs" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const id1: Id = .new(rt); - const id2: Id = .new(rt); - - try testing.expect(id1 != .invalid); - try testing.expect(id2 != .invalid); - try testing.expect(id1 != id2); -} - -test "Runtime.newClass registers a class" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const class_id: Id = .new(rt); - - try testing.expect(!rt.isRegisteredClass(class_id)); - - const def: Def = .{ .class_name = "TestClass" }; - try rt.newClass(class_id, &def); - - try testing.expect(rt.isRegisteredClass(class_id)); -} - -test "Runtime.getClassName returns class name" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const class_id: Id = .new(rt); - - try testing.expect(!rt.isRegisteredClass(class_id)); - - const def: Def = .{ .class_name = "MyCustomClass" }; - try rt.newClass(class_id, &def); - - try testing.expect(rt.isRegisteredClass(class_id)); - - const name_atom = rt.getClassName(class_id); - defer name_atom.deinit(ctx); - - try testing.expect(name_atom != .null); -} - -test "Value.initObjectClass creates class instance" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const class_id: Id = .new(rt); - - const def: Def = .{ .class_name = "InstanceTest" }; - try rt.newClass(class_id, &def); - - const proto = Value.initObject(ctx); - defer proto.deinit(ctx); - ctx.setClassProto(class_id, proto.dup(ctx)); - - const obj = Value.initObjectClass(ctx, class_id); - defer obj.deinit(ctx); - - try testing.expect(obj.isObject()); - try testing.expectEqual(class_id, obj.getClassId()); -} - -test "Value opaque data round-trip" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const class_id: Id = .new(rt); - - const def: Def = .{ .class_name = "OpaqueTest" }; - try rt.newClass(class_id, &def); - - const proto = Value.initObject(ctx); - defer proto.deinit(ctx); - ctx.setClassProto(class_id, proto.dup(ctx)); - - const obj = Value.initObjectClass(ctx, class_id); - defer obj.deinit(ctx); - - const TestData = struct { value: i32 }; - var data: TestData = .{ .value = 42 }; - - try testing.expect(obj.setOpaque(&data)); - - const retrieved = obj.getOpaque(TestData, class_id); - try testing.expect(retrieved != null); - try testing.expectEqual(@as(i32, 42), retrieved.?.value); - - const retrieved2 = obj.getOpaque2(ctx, TestData, class_id); - try testing.expect(retrieved2 != null); - try testing.expectEqual(@as(i32, 42), retrieved2.?.value); -} - -test "Value.getAnyOpaque retrieves opaque with class ID" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const class_id: Id = .new(rt); - - const def: Def = .{ .class_name = "AnyOpaqueTest" }; - try rt.newClass(class_id, &def); - - const proto = Value.initObject(ctx); - defer proto.deinit(ctx); - ctx.setClassProto(class_id, proto.dup(ctx)); - - const obj = Value.initObjectClass(ctx, class_id); - defer obj.deinit(ctx); - - const TestData = struct { value: i32 }; - var data: TestData = .{ .value = 99 }; - try testing.expect(obj.setOpaque(&data)); - - const result = obj.getAnyOpaque(TestData); - try testing.expect(result.ptr != null); - try testing.expectEqual(class_id, result.class_id); - try testing.expectEqual(@as(i32, 99), result.ptr.?.value); -} - -test "Value.getClassId returns class ID for class objects" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const class_id: Id = .new(rt); - - const def: Def = .{ .class_name = "ClassIdTest" }; - try rt.newClass(class_id, &def); - - const proto = Value.initObject(ctx); - defer proto.deinit(ctx); - ctx.setClassProto(class_id, proto.dup(ctx)); - - const obj = Value.initObjectClass(ctx, class_id); - defer obj.deinit(ctx); - - try testing.expectEqual(class_id, obj.getClassId()); -} - -test "Value.getClassId returns invalid for non-objects" { - const num = Value.initInt32(42); - try testing.expectEqual(Id.invalid, num.getClassId()); -} - -test "Context.setClassProto and getClassProto" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const class_id: Id = .new(rt); - - const def: Def = .{ .class_name = "ProtoTest" }; - try rt.newClass(class_id, &def); - - const proto = Value.initObject(ctx); - try proto.setPropertyStr(ctx, "testProp", Value.initInt32(123)); - ctx.setClassProto(class_id, proto); - - const retrieved = ctx.getClassProto(class_id); - defer retrieved.deinit(ctx); - - try testing.expect(retrieved.isObject()); - - const prop = retrieved.getPropertyStr(ctx, "testProp"); - defer prop.deinit(ctx); - try testing.expectEqual(@as(i32, 123), try prop.toInt32(ctx)); -} - -test "Value.setConstructorBit" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const func = ctx.eval("(function MyClass() {})", "", .{}); - defer func.deinit(ctx); - - try testing.expect(func.isFunction(ctx)); - - _ = func.setConstructorBit(ctx, false); - - const can_construct_before = ctx.eval("try { new MyClass(); true } catch(e) { false }", "", .{}); - defer can_construct_before.deinit(ctx); - - _ = func.setConstructorBit(ctx, true); - - try testing.expect(!func.isException()); -} - -test "Class with finalizer" { - const State = struct { - var finalized: bool = false; - }; - - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const class_id: Id = .new(rt); - - const def = Def{ - .class_name = "FinalizerTest", - .finalizer = &struct { - fn finalize(_: ?*c.JSRuntime, _: c.JSValue) callconv(.c) void { - State.finalized = true; - } - }.finalize, - }; - try rt.newClass(class_id, &def); - - { - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const proto = Value.initObject(ctx); - ctx.setClassProto(class_id, proto); - - const obj = Value.initObjectClass(ctx, class_id); - obj.deinit(ctx); - } - - rt.runGC(); - - try testing.expect(State.finalized); -} diff --git a/examples/pjs/deps/zig-quickjs-ng/src/context.zig b/examples/pjs/deps/zig-quickjs-ng/src/context.zig deleted file mode 100644 index 12a6897..0000000 --- a/examples/pjs/deps/zig-quickjs-ng/src/context.zig +++ /dev/null @@ -1,1256 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; -const c = @import("quickjs_c"); -const Atom = @import("atom.zig").Atom; -const ModuleDef = @import("module.zig").ModuleDef; -const Runtime = @import("runtime.zig").Runtime; -const Value = @import("value.zig").Value; -const class = @import("class.zig"); - -/// Wrapper for the QuickJS `JSContext`. -/// -/// A context represents an isolated JavaScript execution environment -/// with its own global object and set of built-in objects. Multiple -/// contexts can be created from the same runtime and share atoms -/// and certain resources, but have separate global states. -pub const Context = opaque { - /// Creates a new JavaScript context within the given runtime. - /// - /// The context must be freed with `deinit` when no longer needed. - /// Contexts must be freed before freeing the runtime they belong to. - /// - /// C: `JS_NewContext` - pub fn init(rt: *Runtime) Allocator.Error!*Context { - const ctx = c.JS_NewContext(rt.cval()); - if (ctx == null) return error.OutOfMemory; - return @ptrCast(ctx); - } - - /// Creates a raw JavaScript context without any intrinsic objects. - /// - /// Use this to create a minimal context, then add only the intrinsics - /// you need with the `addIntrinsic*` methods. - /// - /// C: `JS_NewContextRaw` - pub fn initRaw(rt: *Runtime) Allocator.Error!*Context { - const ctx = c.JS_NewContextRaw(rt.cval()); - if (ctx == null) return error.OutOfMemory; - return @ptrCast(ctx); - } - - /// Frees the JavaScript context and all associated resources. - /// - /// All values created in this context should be freed before - /// calling this function. - /// - /// C: `JS_FreeContext` - pub fn deinit(self: *Context) void { - c.JS_FreeContext(self.cval()); - } - - pub inline fn cval(self: *Context) *c.JSContext { - return @ptrCast(self); - } - - /// Gets the runtime associated with this context. - /// - /// C: `JS_GetRuntime` - pub fn getRuntime(self: *Context) *Runtime { - return @ptrCast(c.JS_GetRuntime(self.cval())); - } - - /// Gets the global object for this context. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_GetGlobalObject` - pub fn getGlobalObject(self: *Context) Value { - return Value.fromCVal(c.JS_GetGlobalObject(self.cval())); - } - - /// Gets the current exception, if any. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_GetException` - pub fn getException(self: *Context) Value { - return Value.fromCVal(c.JS_GetException(self.cval())); - } - - /// Checks if there is a pending exception. - /// - /// C: `JS_HasException` - pub fn hasException(self: *Context) bool { - return c.JS_HasException(self.cval()); - } - - /// Resets the uncatchable error flag on the current exception. - /// - /// C: `JS_ResetUncatchableError` - pub fn resetUncatchableError(self: *Context) void { - c.JS_ResetUncatchableError(self.cval()); - } - - /// Frees a C string allocated by QuickJS. - /// - /// C: `JS_FreeCString` - pub fn freeCString(self: *Context, ptr: [*:0]const u8) void { - c.JS_FreeCString(self.cval(), ptr); - } - - /// Throws an out of memory exception. - /// - /// C: `JS_ThrowOutOfMemory` - pub fn throwOutOfMemory(self: *Context) Value { - return Value.fromCVal(c.JS_ThrowOutOfMemory(self.cval())); - } - - /// Evaluates JavaScript code and returns the result. - /// - /// The input is evaluated as a script (not a module) by default. - /// Use `flags` to customize evaluation behavior. - /// - /// The returned value must be freed with `Value.deinit` when no - /// longer needed. Check `Value.isException` to detect errors. - /// - /// C: `JS_Eval` - pub fn eval(self: *Context, input: []const u8, filename: [:0]const u8, flags: EvalFlags) Value { - return Value.fromCVal(c.JS_Eval( - self.cval(), - input.ptr, - input.len, - filename.ptr, - @bitCast(flags), - )); - } - - /// Evaluates JavaScript code with extended options. - /// - /// This is an extended version of `eval` that accepts an `EvalOptions` - /// struct for more control over evaluation, including line number offset. - /// - /// The returned value must be freed with `Value.deinit` when no - /// longer needed. Check `Value.isException` to detect errors. - /// - /// C: `JS_Eval2` - pub fn eval2(self: *Context, input: []const u8, options: *EvalOptions) Value { - return Value.fromCVal(c.JS_Eval2( - self.cval(), - input.ptr, - input.len, - @ptrCast(options), - )); - } - - /// Evaluates JavaScript code with a custom `this` object. - /// - /// The returned value must be freed with `Value.deinit` when no - /// longer needed. Check `Value.isException` to detect errors. - /// - /// C: `JS_EvalThis` - pub fn evalThis(self: *Context, this_obj: Value, input: []const u8, filename: [:0]const u8, flags: EvalFlags) Value { - return Value.fromCVal(c.JS_EvalThis( - self.cval(), - this_obj.cval(), - input.ptr, - input.len, - filename.ptr, - @bitCast(flags), - )); - } - - /// Evaluates JavaScript code with a custom `this` object and extended options. - /// - /// This combines the functionality of `evalThis` and `eval2`. - /// - /// The returned value must be freed with `Value.deinit` when no - /// longer needed. Check `Value.isException` to detect errors. - /// - /// C: `JS_EvalThis2` - pub fn evalThis2(self: *Context, this_obj: Value, input: []const u8, options: *EvalOptions) Value { - return Value.fromCVal(c.JS_EvalThis2( - self.cval(), - this_obj.cval(), - input.ptr, - input.len, - @ptrCast(options), - )); - } - - /// Executes a compiled bytecode function. - /// - /// The function object must be a bytecode function created with - /// `EvalFlags.compile_only = true`. Takes ownership of the function - /// object. - /// - /// The returned value must be freed with `Value.deinit` when no - /// longer needed. Check `Value.isException` to detect errors. - /// - /// C: `JS_EvalFunction` - pub fn evalFunction(self: *Context, func_obj: Value) Value { - return Value.fromCVal(c.JS_EvalFunction(self.cval(), func_obj.cval())); - } - - // ========================================================================= - // Module Loading - // ========================================================================= - - /// Loads a module by filename. - /// - /// Uses the module loader function set on the runtime. The basename - /// is used for resolving relative imports within the module. - /// - /// Returns the module value or an exception on error. - /// - /// C: `JS_LoadModule` - pub fn loadModule(self: *Context, basename: [*:0]const u8, filename: [*:0]const u8) Value { - return Value.fromCVal(c.JS_LoadModule(self.cval(), basename, filename)); - } - - /// Gets the name of the current script or module. - /// - /// The `n_stack_levels` parameter specifies how many stack levels to - /// go up (0 = current function). - /// - /// Returns the script/module name as an atom, or `Atom.null` if not available. - /// The returned atom must be freed with `Atom.deinit`. - /// - /// C: `JS_GetScriptOrModuleName` - pub fn getScriptOrModuleName(self: *Context, n_stack_levels: i32) Atom { - return @enumFromInt(c.JS_GetScriptOrModuleName(self.cval(), n_stack_levels)); - } - - // ========================================================================= - // Job Queue - // ========================================================================= - - /// Enqueues a job to be executed later. - /// - /// The job function will be called with the provided arguments when - /// `Runtime.executePendingJob` is called. The arguments are duplicated - /// (reference count incremented) when enqueued and freed after the job runs. - /// - /// Returns an error if the job could not be enqueued. - /// - /// C: `JS_EnqueueJob` - pub fn enqueueJob( - self: *Context, - comptime job_func: *const fn (*Context, []const Value) Value, - args: []const Value, - ) Allocator.Error!void { - const Wrapper = struct { - fn callback( - ctx: ?*c.JSContext, - argc: c_int, - argv: [*c]c.JSValue, - ) callconv(.c) c.JSValue { - const zig_ctx: *Context = @ptrCast(ctx); - const zig_args: []const Value = if (argc > 0) - @ptrCast(argv[0..@intCast(argc)]) - else - &.{}; - return @call(.always_inline, job_func, .{ zig_ctx, zig_args }).cval(); - } - }; - const result = c.JS_EnqueueJob( - self.cval(), - &Wrapper.callback, - @intCast(args.len), - @ptrCast(@constCast(args.ptr)), - ); - if (result < 0) return error.OutOfMemory; - } - - // ========================================================================= - // Context Duplication - // ========================================================================= - - /// Duplicates the context, incrementing its reference count. - /// - /// The returned context must also be freed with `deinit`. - /// - /// C: `JS_DupContext` - pub fn dup(self: *Context) *Context { - return @ptrCast(c.JS_DupContext(self.cval())); - } - - // ========================================================================= - // Opaque Data - // ========================================================================= - - /// Gets the opaque pointer associated with this context. - /// - /// C: `JS_GetContextOpaque` - pub fn getOpaque(self: *Context, comptime T: type) ?*T { - return @ptrCast(@alignCast(c.JS_GetContextOpaque(self.cval()))); - } - - /// Sets the opaque pointer for this context. - /// - /// C: `JS_SetContextOpaque` - pub fn setOpaque(self: *Context, comptime T: type, ptr: ?*T) void { - c.JS_SetContextOpaque(self.cval(), ptr); - } - - // ========================================================================= - // Intrinsics - // ========================================================================= - - /// Adds base objects (Object, Function, Array, etc.). - /// - /// C: `JS_AddIntrinsicBaseObjects` - pub fn addIntrinsicBaseObjects(self: *Context) void { - c.JS_AddIntrinsicBaseObjects(self.cval()); - } - - /// Adds the Date constructor and prototype. - /// - /// C: `JS_AddIntrinsicDate` - pub fn addIntrinsicDate(self: *Context) void { - c.JS_AddIntrinsicDate(self.cval()); - } - - /// Adds eval() function. - /// - /// C: `JS_AddIntrinsicEval` - pub fn addIntrinsicEval(self: *Context) void { - c.JS_AddIntrinsicEval(self.cval()); - } - - /// Adds RegExp compiler (needed for literal regexp support). - /// - /// C: `JS_AddIntrinsicRegExpCompiler` - pub fn addIntrinsicRegExpCompiler(self: *Context) void { - c.JS_AddIntrinsicRegExpCompiler(self.cval()); - } - - /// Adds the RegExp constructor and prototype. - /// - /// C: `JS_AddIntrinsicRegExp` - pub fn addIntrinsicRegExp(self: *Context) void { - c.JS_AddIntrinsicRegExp(self.cval()); - } - - /// Adds JSON object with parse() and stringify(). - /// - /// C: `JS_AddIntrinsicJSON` - pub fn addIntrinsicJSON(self: *Context) void { - c.JS_AddIntrinsicJSON(self.cval()); - } - - /// Adds the Proxy constructor and Reflect object. - /// - /// C: `JS_AddIntrinsicProxy` - pub fn addIntrinsicProxy(self: *Context) void { - c.JS_AddIntrinsicProxy(self.cval()); - } - - /// Adds Map and Set constructors and prototypes. - /// - /// C: `JS_AddIntrinsicMapSet` - pub fn addIntrinsicMapSet(self: *Context) void { - c.JS_AddIntrinsicMapSet(self.cval()); - } - - /// Adds TypedArray and ArrayBuffer constructors. - /// - /// C: `JS_AddIntrinsicTypedArrays` - pub fn addIntrinsicTypedArrays(self: *Context) void { - c.JS_AddIntrinsicTypedArrays(self.cval()); - } - - /// Adds Promise constructor and related functions. - /// - /// C: `JS_AddIntrinsicPromise` - pub fn addIntrinsicPromise(self: *Context) void { - c.JS_AddIntrinsicPromise(self.cval()); - } - - /// Adds BigInt constructor and prototype. - /// - /// C: `JS_AddIntrinsicBigInt` - pub fn addIntrinsicBigInt(self: *Context) void { - c.JS_AddIntrinsicBigInt(self.cval()); - } - - /// Adds WeakRef and FinalizationRegistry. - /// - /// C: `JS_AddIntrinsicWeakRef` - pub fn addIntrinsicWeakRef(self: *Context) void { - c.JS_AddIntrinsicWeakRef(self.cval()); - } - - /// Adds performance object with now(). - /// - /// C: `JS_AddPerformance` - pub fn addPerformance(self: *Context) void { - c.JS_AddPerformance(self.cval()); - } - - /// Adds DOMException constructor. - /// - /// C: `JS_AddIntrinsicDOMException` - pub fn addIntrinsicDOMException(self: *Context) void { - c.JS_AddIntrinsicDOMException(self.cval()); - } - - // ========================================================================= - // Class Prototypes - // ========================================================================= - - /// Gets the prototype for a class ID. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_GetClassProto` - pub fn getClassProto(self: *Context, class_id: class.Id) Value { - return Value.fromCVal(c.JS_GetClassProto(self.cval(), @intFromEnum(class_id))); - } - - /// Sets the prototype for a class ID. - /// - /// Takes ownership of the prototype value. - /// - /// C: `JS_SetClassProto` - pub fn setClassProto(self: *Context, class_id: class.Id, proto: Value) void { - c.JS_SetClassProto(self.cval(), @intFromEnum(class_id), proto.cval()); - } - - /// Gets the Function prototype object. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_GetFunctionProto` - pub fn getFunctionProto(self: *Context) Value { - return Value.fromCVal(c.JS_GetFunctionProto(self.cval())); - } - - // ========================================================================= - // Error Factories - // ========================================================================= - - /// Creates a new TypeError object (does not throw). - /// - /// C: `JS_NewTypeError` - pub fn newTypeError(self: *Context, msg: [:0]const u8) Value { - return Value.fromCVal(c.JS_NewTypeError(self.cval(), "%s", msg.ptr)); - } - - /// Creates a new SyntaxError object (does not throw). - /// - /// C: `JS_NewSyntaxError` - pub fn newSyntaxError(self: *Context, msg: [:0]const u8) Value { - return Value.fromCVal(c.JS_NewSyntaxError(self.cval(), "%s", msg.ptr)); - } - - /// Creates a new ReferenceError object (does not throw). - /// - /// C: `JS_NewReferenceError` - pub fn newReferenceError(self: *Context, msg: [:0]const u8) Value { - return Value.fromCVal(c.JS_NewReferenceError(self.cval(), "%s", msg.ptr)); - } - - /// Creates a new RangeError object (does not throw). - /// - /// C: `JS_NewRangeError` - pub fn newRangeError(self: *Context, msg: [:0]const u8) Value { - return Value.fromCVal(c.JS_NewRangeError(self.cval(), "%s", msg.ptr)); - } - - /// Creates a new InternalError object (does not throw). - /// - /// C: `JS_NewInternalError` - pub fn newInternalError(self: *Context, msg: [:0]const u8) Value { - return Value.fromCVal(c.JS_NewInternalError(self.cval(), "%s", msg.ptr)); - } - - /// Throws a TypeError and returns the exception value. - /// - /// C: `JS_ThrowTypeError` - pub fn throwTypeError(self: *Context, msg: [:0]const u8) Value { - return Value.fromCVal(c.JS_ThrowTypeError(self.cval(), "%s", msg.ptr)); - } - - /// Throws a SyntaxError and returns the exception value. - /// - /// C: `JS_ThrowSyntaxError` - pub fn throwSyntaxError(self: *Context, msg: [:0]const u8) Value { - return Value.fromCVal(c.JS_ThrowSyntaxError(self.cval(), "%s", msg.ptr)); - } - - /// Throws a ReferenceError and returns the exception value. - /// - /// C: `JS_ThrowReferenceError` - pub fn throwReferenceError(self: *Context, msg: [:0]const u8) Value { - return Value.fromCVal(c.JS_ThrowReferenceError(self.cval(), "%s", msg.ptr)); - } - - /// Throws a RangeError and returns the exception value. - /// - /// C: `JS_ThrowRangeError` - pub fn throwRangeError(self: *Context, msg: [:0]const u8) Value { - return Value.fromCVal(c.JS_ThrowRangeError(self.cval(), "%s", msg.ptr)); - } - - /// Throws an InternalError and returns the exception value. - /// - /// C: `JS_ThrowInternalError` - pub fn throwInternalError(self: *Context, msg: [:0]const u8) Value { - return Value.fromCVal(c.JS_ThrowInternalError(self.cval(), "%s", msg.ptr)); - } -}; - -/// Evaluation flags for `Context.eval`. -/// -/// C: `JS_EVAL_TYPE_*` and `JS_EVAL_FLAG_*` -pub const EvalFlags = packed struct(c_int) { - /// Evaluation type (2 bits). - /// - global (0): evaluate in global scope - /// - module (1): evaluate as ES module - /// - direct (2): direct call to eval - /// - indirect (3): indirect call to eval - type: Type = .global, - _reserved: u1 = 0, - /// Force strict mode. - strict: bool = false, - _unused: u1 = 0, - /// Compile but don't run (returns bytecode). - compile_only: bool = false, - /// Don't include stack frames in Error objects. - backtrace_barrier: bool = false, - /// Evaluate as async module. - async_module: bool = false, - _padding: u24 = 0, - - pub const Type = enum(u2) { - global = 0, - module = 1, - direct = 2, - indirect = 3, - }; -}; - -/// Extended evaluation options for `eval2` and `evalThis2`. -/// -/// C: `JSEvalOptions` -pub const EvalOptions = extern struct { - /// Version field for ABI compatibility. - version: c_int = c.JS_EVAL_OPTIONS_VERSION, - /// Evaluation flags (type and behavior modifiers). - flags: EvalFlags = .{}, - /// Source filename for error messages. - filename: [*:0]const u8 = "", - /// Starting line number for error messages (1-based). - line_num: c_int = 1, -}; - -comptime { - std.debug.assert(c.JS_EVAL_OPTIONS_VERSION == 1); - std.debug.assert(@sizeOf(EvalOptions) == @sizeOf(c.JSEvalOptions)); - std.debug.assert(@alignOf(EvalOptions) == @alignOf(c.JSEvalOptions)); -} - -test "Context init and deinit" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); -} - -test "EvalFlags bit layout matches C header" { - const testing = std.testing; - - // Type values (bits 0-1) - try testing.expectEqual(c.JS_EVAL_TYPE_GLOBAL, @as(c_int, @bitCast(EvalFlags{ .type = .global }))); - try testing.expectEqual(c.JS_EVAL_TYPE_MODULE, @as(c_int, @bitCast(EvalFlags{ .type = .module }))); - try testing.expectEqual(c.JS_EVAL_TYPE_DIRECT, @as(c_int, @bitCast(EvalFlags{ .type = .direct }))); - try testing.expectEqual(c.JS_EVAL_TYPE_INDIRECT, @as(c_int, @bitCast(EvalFlags{ .type = .indirect }))); - - // Flag values (bits 3, 5-7) - try testing.expectEqual(c.JS_EVAL_FLAG_STRICT, @as(c_int, @bitCast(EvalFlags{ .strict = true }))); - try testing.expectEqual(c.JS_EVAL_FLAG_COMPILE_ONLY, @as(c_int, @bitCast(EvalFlags{ .compile_only = true }))); - try testing.expectEqual(c.JS_EVAL_FLAG_BACKTRACE_BARRIER, @as(c_int, @bitCast(EvalFlags{ .backtrace_barrier = true }))); - try testing.expectEqual(c.JS_EVAL_FLAG_ASYNC, @as(c_int, @bitCast(EvalFlags{ .async_module = true }))); - - // Combined flags - try testing.expectEqual( - c.JS_EVAL_TYPE_MODULE | c.JS_EVAL_FLAG_STRICT, - @as(c_int, @bitCast(EvalFlags{ .type = .module, .strict = true })), - ); -} - -test "Context dup" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const ctx2 = ctx.dup(); - defer ctx2.deinit(); - - // Both contexts should work and share the same runtime - try std.testing.expectEqual(rt, ctx2.getRuntime()); - - // Both should be able to evaluate - const result = ctx2.eval("1 + 1", "", .{}); - defer result.deinit(ctx2); - try std.testing.expectEqual(@as(i32, 2), try result.toInt32(ctx2)); -} - -test "Context opaque data" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const TestData = struct { - value: i32, - }; - - var data: TestData = .{ .value = 42 }; - - // Initially null - try std.testing.expectEqual(@as(?*TestData, null), ctx.getOpaque(TestData)); - - // Set opaque - ctx.setOpaque(TestData, &data); - - // Get opaque - const retrieved = ctx.getOpaque(TestData); - try std.testing.expect(retrieved != null); - try std.testing.expectEqual(@as(i32, 42), retrieved.?.value); - - // Modify through pointer - retrieved.?.value = 100; - try std.testing.expectEqual(@as(i32, 100), data.value); - - // Clear - ctx.setOpaque(TestData, null); - try std.testing.expectEqual(@as(?*TestData, null), ctx.getOpaque(TestData)); -} - -test "Context initRaw" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .initRaw(rt); - defer ctx.deinit(); - - // Just test that raw context can be created - try std.testing.expectEqual(rt, ctx.getRuntime()); -} - -test "Context addIntrinsicBaseObjects" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .initRaw(rt); - defer ctx.deinit(); - - // Just test that the method doesn't crash - ctx.addIntrinsicBaseObjects(); -} - -test "Context addIntrinsicDate" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // Date should already be available in normal context - const result = ctx.eval("new Date(0).getTime()", "", .{}); - defer result.deinit(ctx); - try std.testing.expectEqual(@as(i64, 0), try result.toInt64(ctx)); -} - -test "Context addIntrinsicEval" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .initRaw(rt); - defer ctx.deinit(); - - ctx.addIntrinsicBaseObjects(); - ctx.addIntrinsicEval(); - - // eval() should work now - const result = ctx.eval("eval('2 + 3')", "", .{}); - defer result.deinit(ctx); - try std.testing.expectEqual(@as(i32, 5), try result.toInt32(ctx)); -} - -test "Context addIntrinsicJSON" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // JSON should work - const result = ctx.eval("JSON.parse('{\"a\": 42}').a", "", .{}); - defer result.deinit(ctx); - try std.testing.expectEqual(@as(i32, 42), try result.toInt32(ctx)); -} - -test "Context addIntrinsicMapSet" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // Map and Set should work - const result = ctx.eval( - \\const m = new Map(); - \\m.set('key', 123); - \\m.get('key') - , "", .{}); - defer result.deinit(ctx); - try std.testing.expectEqual(@as(i32, 123), try result.toInt32(ctx)); -} - -test "Context addIntrinsicPromise" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // Promise should work - const result = ctx.eval("new Promise((r) => r(42))", "", .{}); - defer result.deinit(ctx); - try std.testing.expect(result.isPromise()); -} - -test "Context addIntrinsicBigInt" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // BigInt should work - const result = ctx.eval("BigInt(9007199254740991)", "", .{}); - defer result.deinit(ctx); - try std.testing.expect(result.isBigInt()); -} - -test "Context addIntrinsicRegExp" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // RegExp should work - const result = ctx.eval("/test/.test('test')", "", .{}); - defer result.deinit(ctx); - try std.testing.expect(try result.toBool(ctx)); -} - -test "Context addIntrinsicProxy" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // Proxy should work - const result = ctx.eval( - \\const target = { a: 1 }; - \\const p = new Proxy(target, {}); - \\p.a - , "", .{}); - defer result.deinit(ctx); - try std.testing.expectEqual(@as(i32, 1), try result.toInt32(ctx)); -} - -test "Context addIntrinsicTypedArrays" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // TypedArray should work - const result = ctx.eval( - \\const arr = new Uint8Array([1, 2, 3]); - \\arr[1] - , "", .{}); - defer result.deinit(ctx); - try std.testing.expectEqual(@as(i32, 2), try result.toInt32(ctx)); -} - -test "Context addPerformance" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - // Use a full context since performance depends on other intrinsics - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // performance object should exist (added by default) - const result = ctx.eval("typeof performance", "", .{}); - defer result.deinit(ctx); - const str = result.toCString(ctx).?; - defer ctx.freeCString(str); - try std.testing.expectEqualStrings("object", std.mem.span(str)); - - // performance.now() should return a number - const result2 = ctx.eval("typeof performance.now()", "", .{}); - defer result2.deinit(ctx); - const str2 = result2.toCString(ctx).?; - defer ctx.freeCString(str2); - try std.testing.expectEqualStrings("number", std.mem.span(str2)); -} - -test "Context getFunctionProto" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const func_proto = ctx.getFunctionProto(); - defer func_proto.deinit(ctx); - - try std.testing.expect(func_proto.isObject()); - - // Verify it's the actual Function.prototype - const global = ctx.getGlobalObject(); - defer global.deinit(ctx); - const func_ctor = global.getPropertyStr(ctx, "Function"); - defer func_ctor.deinit(ctx); - const expected_proto = func_ctor.getPropertyStr(ctx, "prototype"); - defer expected_proto.deinit(ctx); - - try std.testing.expect(func_proto.isStrictEqual(ctx, expected_proto)); -} - -test "Context newTypeError" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const err = ctx.newTypeError("test type error"); - defer err.deinit(ctx); - - try std.testing.expect(err.isError()); - - // Verify it's a TypeError by checking constructor name - const ctor = err.getPropertyStr(ctx, "constructor"); - defer ctor.deinit(ctx); - const name = ctor.getPropertyStr(ctx, "name"); - defer name.deinit(ctx); - const str = name.toCString(ctx).?; - defer ctx.freeCString(str); - try std.testing.expectEqualStrings("TypeError", std.mem.span(str)); -} - -test "Context newSyntaxError" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const err = ctx.newSyntaxError("test syntax error"); - defer err.deinit(ctx); - - try std.testing.expect(err.isError()); - - const ctor = err.getPropertyStr(ctx, "constructor"); - defer ctor.deinit(ctx); - const name = ctor.getPropertyStr(ctx, "name"); - defer name.deinit(ctx); - const str = name.toCString(ctx).?; - defer ctx.freeCString(str); - try std.testing.expectEqualStrings("SyntaxError", std.mem.span(str)); -} - -test "Context newReferenceError" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const err = ctx.newReferenceError("test reference error"); - defer err.deinit(ctx); - - try std.testing.expect(err.isError()); - - const ctor = err.getPropertyStr(ctx, "constructor"); - defer ctor.deinit(ctx); - const name = ctor.getPropertyStr(ctx, "name"); - defer name.deinit(ctx); - const str = name.toCString(ctx).?; - defer ctx.freeCString(str); - try std.testing.expectEqualStrings("ReferenceError", std.mem.span(str)); -} - -test "Context newRangeError" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const err = ctx.newRangeError("test range error"); - defer err.deinit(ctx); - - try std.testing.expect(err.isError()); - - const ctor = err.getPropertyStr(ctx, "constructor"); - defer ctor.deinit(ctx); - const name = ctor.getPropertyStr(ctx, "name"); - defer name.deinit(ctx); - const str = name.toCString(ctx).?; - defer ctx.freeCString(str); - try std.testing.expectEqualStrings("RangeError", std.mem.span(str)); -} - -test "Context newInternalError" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const err = ctx.newInternalError("test internal error"); - defer err.deinit(ctx); - - try std.testing.expect(err.isError()); - - const ctor = err.getPropertyStr(ctx, "constructor"); - defer ctor.deinit(ctx); - const name = ctor.getPropertyStr(ctx, "name"); - defer name.deinit(ctx); - const str = name.toCString(ctx).?; - defer ctx.freeCString(str); - try std.testing.expectEqualStrings("InternalError", std.mem.span(str)); -} - -test "Context throwTypeError" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const result = ctx.throwTypeError("expected a number"); - defer result.deinit(ctx); - - try std.testing.expect(result.isException()); - try std.testing.expect(ctx.hasException()); - - const exc = ctx.getException(); - defer exc.deinit(ctx); - try std.testing.expect(exc.isError()); - - const msg = exc.getPropertyStr(ctx, "message"); - defer msg.deinit(ctx); - const str = msg.toCString(ctx).?; - defer ctx.freeCString(str); - try std.testing.expectEqualStrings("expected a number", std.mem.span(str)); -} - -test "Context throwSyntaxError" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const result = ctx.throwSyntaxError("unexpected token"); - defer result.deinit(ctx); - - try std.testing.expect(result.isException()); - try std.testing.expect(ctx.hasException()); - - const exc = ctx.getException(); - defer exc.deinit(ctx); - try std.testing.expect(exc.isError()); -} - -test "Context throwReferenceError" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const result = ctx.throwReferenceError("x is not defined"); - defer result.deinit(ctx); - - try std.testing.expect(result.isException()); - try std.testing.expect(ctx.hasException()); - - const exc = ctx.getException(); - defer exc.deinit(ctx); - try std.testing.expect(exc.isError()); -} - -test "Context throwRangeError" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const result = ctx.throwRangeError("value out of range"); - defer result.deinit(ctx); - - try std.testing.expect(result.isException()); - try std.testing.expect(ctx.hasException()); - - const exc = ctx.getException(); - defer exc.deinit(ctx); - try std.testing.expect(exc.isError()); -} - -test "Context throwInternalError" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const result = ctx.throwInternalError("internal failure"); - defer result.deinit(ctx); - - try std.testing.expect(result.isException()); - try std.testing.expect(ctx.hasException()); - - const exc = ctx.getException(); - defer exc.deinit(ctx); - try std.testing.expect(exc.isError()); -} - -test "Context error message preserved in JavaScript" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // Throw a TypeError - _ = ctx.throwTypeError("custom error message"); - - // Get the exception and verify message - const exc = ctx.getException(); - defer exc.deinit(ctx); - - const msg = exc.getPropertyStr(ctx, "message"); - defer msg.deinit(ctx); - const str = msg.toCString(ctx).?; - defer ctx.freeCString(str); - try std.testing.expectEqualStrings("custom error message", std.mem.span(str)); - - // Verify stack trace exists - const stack = exc.getPropertyStr(ctx, "stack"); - defer stack.deinit(ctx); - try std.testing.expect(stack.isString()); -} - -test "Context raw with selective intrinsics" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - // Create a minimal context with only what we need - const ctx: *Context = try .initRaw(rt); - defer ctx.deinit(); - - // Test that we can add multiple intrinsics without crashing - ctx.addIntrinsicBaseObjects(); - ctx.addIntrinsicJSON(); - ctx.addIntrinsicDate(); - ctx.addIntrinsicPromise(); - - // The context should still be valid - try std.testing.expectEqual(rt, ctx.getRuntime()); -} - -test "Context getScriptOrModuleName in module" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const loader = struct { - fn load( - _: void, - load_ctx: *Context, - name: [:0]const u8, - ) ?*ModuleDef { - if (!std.mem.eql(u8, name, "name_test")) return null; - - const m = ModuleDef.init(load_ctx, name, initFn) orelse return null; - _ = m.addExport(load_ctx, "dummy"); - return m; - } - - fn initFn(init_ctx: *Context, m: *ModuleDef) bool { - if (!m.setExport(init_ctx, "dummy", Value.initInt32(1))) return false; - return true; - } - }; - - rt.setModuleLoaderFunc(void, {}, null, loader.load); - - const result = ctx.eval( - \\import { dummy } from "name_test"; - \\dummy - , "test_script.js", .{ .type = .module }); - defer result.deinit(ctx); - - try std.testing.expect(!result.isException()); -} - -test "Context loadModule with loader" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const loader = struct { - fn load( - _: void, - load_ctx: *Context, - name: [:0]const u8, - ) ?*ModuleDef { - if (!std.mem.eql(u8, name, "loaded_module")) return null; - - const m = ModuleDef.init(load_ctx, name, initFn) orelse return null; - _ = m.addExport(load_ctx, "value"); - return m; - } - - fn initFn(init_ctx: *Context, m: *ModuleDef) bool { - if (!m.setExport(init_ctx, "value", Value.initInt32(999))) return false; - return true; - } - }; - - rt.setModuleLoaderFunc(void, {}, null, loader.load); - - const result = ctx.loadModule(".", "loaded_module"); - defer result.deinit(ctx); - - try std.testing.expect(!result.isException()); -} - -test "Context eval2" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - var options: EvalOptions = .{ - .filename = "test.js", - .line_num = 10, - }; - const result = ctx.eval2("1 + 2", &options); - defer result.deinit(ctx); - - try std.testing.expect(!result.isException()); - try std.testing.expectEqual(@as(i32, 3), try result.toInt32(ctx)); -} - -test "Context evalThis" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const this_obj = Value.initObject(ctx); - defer this_obj.deinit(ctx); - try this_obj.setPropertyStr(ctx, "x", Value.initInt32(42)); - - const result = ctx.evalThis(this_obj, "this.x", "", .{}); - defer result.deinit(ctx); - - try std.testing.expect(!result.isException()); - try std.testing.expectEqual(@as(i32, 42), try result.toInt32(ctx)); -} - -test "Context evalThis2" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const this_obj = Value.initObject(ctx); - defer this_obj.deinit(ctx); - try this_obj.setPropertyStr(ctx, "value", Value.initInt32(100)); - - var options: EvalOptions = .{ - .filename = "custom.js", - .line_num = 5, - }; - const result = ctx.evalThis2(this_obj, "this.value * 2", &options); - defer result.deinit(ctx); - - try std.testing.expect(!result.isException()); - try std.testing.expectEqual(@as(i32, 200), try result.toInt32(ctx)); -} - -test "Context evalFunction" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const bytecode = ctx.eval("1 + 2", "", .{ .compile_only = true }); - try std.testing.expect(!bytecode.isException()); - - const result = ctx.evalFunction(bytecode); - defer result.deinit(ctx); - - try std.testing.expect(!result.isException()); - try std.testing.expectEqual(@as(i32, 3), try result.toInt32(ctx)); -} - -test "Context enqueueJob" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const global = ctx.getGlobalObject(); - defer global.deinit(ctx); - - try global.setPropertyStr(ctx, "jobRan", Value.initBool(false)); - - const job = struct { - fn run(job_ctx: *Context, _: []const Value) Value { - const g = job_ctx.getGlobalObject(); - defer g.deinit(job_ctx); - g.setPropertyStr(job_ctx, "jobRan", Value.initBool(true)) catch {}; - return Value.undefined; - } - }; - - try ctx.enqueueJob(job.run, &.{}); - - try std.testing.expect(rt.isJobPending()); - - _ = try rt.executePendingJob(); - - const ran = global.getPropertyStr(ctx, "jobRan"); - defer ran.deinit(ctx); - try std.testing.expect(try ran.toBool(ctx)); -} diff --git a/examples/pjs/deps/zig-quickjs-ng/src/main.zig b/examples/pjs/deps/zig-quickjs-ng/src/main.zig deleted file mode 100644 index 7f85c63..0000000 --- a/examples/pjs/deps/zig-quickjs-ng/src/main.zig +++ /dev/null @@ -1,48 +0,0 @@ -const atom = @import("atom.zig"); -const class = @import("class.zig"); -const context = @import("context.zig"); -const module = @import("module.zig"); -const runtime = @import("runtime.zig"); -const value = @import("value.zig"); - -pub const c = @import("quickjs_c"); -pub const cfunc = @import("cfunc.zig"); -pub const typed_array = @import("typed_array.zig"); - -pub const Atom = atom.Atom; -pub const Context = context.Context; -pub const EvalFlags = context.EvalFlags; -pub const ModuleDef = module.ModuleDef; -pub const Runtime = runtime.Runtime; -pub const DumpFlags = runtime.DumpFlags; -pub const Value = value.Value; -pub const Promise = value.Value.Promise; -pub const PromiseState = value.PromiseState; -pub const ClassId = class.Id; -pub const ClassDef = class.Def; -pub const ClassExoticMethods = class.ExoticMethods; - -pub fn version() [*:0]const u8 { - return c.JS_GetVersion(); -} - -/// Detects if input looks like an ES module. -/// -/// Returns true if the input contains `import` or `export` statements -/// at the top level. -/// -/// C: `JS_DetectModule` -pub fn detectModule(input: []const u8) bool { - return c.JS_DetectModule(input.ptr, input.len); -} - -test { - @import("std").testing.refAllDecls(@This()); -} - -test "detectModule" { - const testing = @import("std").testing; - try testing.expect(detectModule("import { foo } from 'bar';")); - try testing.expect(detectModule("export const x = 1;")); - try testing.expect(detectModule("export default function() {}")); -} diff --git a/examples/pjs/deps/zig-quickjs-ng/src/module.zig b/examples/pjs/deps/zig-quickjs-ng/src/module.zig deleted file mode 100644 index 53c66b7..0000000 --- a/examples/pjs/deps/zig-quickjs-ng/src/module.zig +++ /dev/null @@ -1,297 +0,0 @@ -const std = @import("std"); -const testing = std.testing; -const c = @import("quickjs_c"); -const Atom = @import("atom.zig").Atom; -const Context = @import("context.zig").Context; -const Runtime = @import("runtime.zig").Runtime; -const Value = @import("value.zig").Value; - -/// Wrapper for the QuickJS `JSModuleDef`. -/// -/// A module definition represents a JavaScript module created from native code. -/// Use `init` to create a new C module, then add exports before the module is -/// instantiated. -/// -/// C: `JSModuleDef` -pub const ModuleDef = opaque { - /// Module initialization function type. - /// - /// Called when the module is instantiated. Use this to set module exports. - /// Return true on success, false on error. - /// - /// C: `JSModuleInitFunc` - pub const InitFunc = *const fn (*Context, *ModuleDef) bool; - - /// Creates a new C module with the given name. - /// - /// The `func` is called when the module is instantiated and should set - /// the module exports using `setExport`. - /// - /// Returns null on allocation failure. - /// - /// C: `JS_NewCModule` - pub fn init(ctx: *Context, name: [:0]const u8, comptime func: InitFunc) ?*ModuleDef { - const Wrapper = struct { - fn callback(inner_ctx: *Context, m: *ModuleDef) callconv(.c) c_int { - return @intFromBool(!@call(.always_inline, func, .{ inner_ctx, m })); - } - }; - return @ptrCast(c.JS_NewCModule(ctx.cval(), name.ptr, @ptrCast(&Wrapper.callback))); - } - - /// Declares an export for this module. - /// - /// Must be called before the module is instantiated. The actual value - /// is set later with `setExport` in the init function. - /// - /// Returns true on success. - /// - /// C: `JS_AddModuleExport` - pub fn addExport(self: *ModuleDef, ctx: *Context, name: [:0]const u8) bool { - return c.JS_AddModuleExport(ctx.cval(), @ptrCast(self), name.ptr) == 0; - } - - /// Sets the value of a module export. - /// - /// Must be called after the module is instantiated, typically in the - /// init function. The value's ownership is transferred to the module. - /// - /// Returns true on success. - /// - /// C: `JS_SetModuleExport` - pub fn setExport(self: *ModuleDef, ctx: *Context, name: [:0]const u8, val: Value) bool { - return c.JS_SetModuleExport(ctx.cval(), @ptrCast(self), name.ptr, val.cval()) == 0; - } - - /// Gets the import.meta object for this module. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_GetImportMeta` - pub fn getImportMeta(self: *ModuleDef, ctx: *Context) Value { - return Value.fromCVal(c.JS_GetImportMeta(ctx.cval(), @ptrCast(self))); - } - - /// Gets the module name as an atom. - /// - /// The returned atom must be freed with `Atom.deinit`. - /// - /// C: `JS_GetModuleName` - pub fn getName(self: *ModuleDef, ctx: *Context) Atom { - return @enumFromInt(c.JS_GetModuleName(ctx.cval(), @ptrCast(self))); - } - - /// Gets the module namespace object. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_GetModuleNamespace` - pub fn getNamespace(self: *ModuleDef, ctx: *Context) Value { - return Value.fromCVal(c.JS_GetModuleNamespace(ctx.cval(), @ptrCast(self))); - } - - pub inline fn cval(self: *ModuleDef) *c.JSModuleDef { - return @ptrCast(self); - } -}; - -// ============================================================================= -// Tests -// ============================================================================= - -fn testModuleInit(ctx: *Context, m: *ModuleDef) bool { - if (!m.setExport(ctx, "value", Value.initInt32(42))) return false; - if (!m.setExport(ctx, "greeting", Value.initString(ctx, "hello"))) return false; - return true; -} - -test "ModuleDef init and addExport" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const m = ModuleDef.init(ctx, "test_module", testModuleInit); - try testing.expect(m != null); - - try testing.expect(m.?.addExport(ctx, "value")); - try testing.expect(m.?.addExport(ctx, "greeting")); -} - -test "ModuleDef getName" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const m = ModuleDef.init(ctx, "my_module", testModuleInit).?; - _ = m.addExport(ctx, "value"); - _ = m.addExport(ctx, "greeting"); - - const name_atom = m.getName(ctx); - defer name_atom.deinit(ctx); - - const name_str = name_atom.toCString(ctx).?; - defer ctx.freeCString(name_str); - - try testing.expectEqualStrings("my_module", std.mem.span(name_str)); -} - -test "ModuleDef full module import via eval" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const loader = struct { - fn load(_: void, load_ctx: *Context, name: [:0]const u8) ?*ModuleDef { - if (!std.mem.eql(u8, name, "test_module")) return null; - - const m = ModuleDef.init(load_ctx, name, initFn) orelse return null; - _ = m.addExport(load_ctx, "value"); - return m; - } - - fn initFn(init_ctx: *Context, m: *ModuleDef) bool { - if (!m.setExport(init_ctx, "value", Value.initInt32(123))) return false; - return true; - } - }; - - rt.setModuleLoaderFunc(void, {}, null, loader.load); - - const result = ctx.eval( - \\import { value } from "test_module"; - \\globalThis.testResult = value; - , "", .{ .type = .module }); - defer result.deinit(ctx); - - if (result.isException()) { - const exc = ctx.getException(); - defer exc.deinit(ctx); - if (exc.getPropertyStr(ctx, "message").toCString(ctx)) |msg| { - defer ctx.freeCString(msg); - std.debug.print("Exception: {s}\n", .{msg}); - } - try testing.expect(false); - } - - const global = ctx.getGlobalObject(); - defer global.deinit(ctx); - const test_result = global.getPropertyStr(ctx, "testResult"); - defer test_result.deinit(ctx); - - try testing.expectEqual(@as(i32, 123), try test_result.toInt32(ctx)); -} - -test "ModuleDef getImportMeta" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const State = struct { captured_module: ?*ModuleDef = null }; - - const loader = struct { - fn load( - state: ?*State, - load_ctx: *Context, - name: [:0]const u8, - ) ?*ModuleDef { - if (!std.mem.eql(u8, name, "meta_test")) return null; - - const m = ModuleDef.init(load_ctx, name, initFn) orelse return null; - _ = m.addExport(load_ctx, "dummy"); - - state.?.captured_module = m; - - return m; - } - - fn initFn(init_ctx: *Context, m: *ModuleDef) bool { - if (!m.setExport(init_ctx, "dummy", Value.initInt32(1))) return false; - return true; - } - }; - - var state: State = .{}; - rt.setModuleLoaderFunc(State, &state, null, loader.load); - - const result = ctx.eval( - \\import { dummy } from "meta_test"; - \\dummy - , "", .{ .type = .module }); - defer result.deinit(ctx); - - try testing.expect(!result.isException()); - try testing.expect(state.captured_module != null); - - const meta = state.captured_module.?.getImportMeta(ctx); - defer meta.deinit(ctx); - - try testing.expect(meta.isObject()); -} - -test "ModuleDef getNamespace" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const State = struct { captured_module: ?*ModuleDef = null }; - - const loader = struct { - fn load( - state: ?*State, - load_ctx: *Context, - name: [:0]const u8, - ) ?*ModuleDef { - if (!std.mem.eql(u8, name, "ns_test")) return null; - - const m = ModuleDef.init(load_ctx, name, initFn) orelse return null; - _ = m.addExport(load_ctx, "foo"); - _ = m.addExport(load_ctx, "bar"); - - state.?.captured_module = m; - - return m; - } - - fn initFn(init_ctx: *Context, m: *ModuleDef) bool { - if (!m.setExport(init_ctx, "foo", Value.initInt32(10))) return false; - if (!m.setExport(init_ctx, "bar", Value.initInt32(20))) return false; - return true; - } - }; - - var state: State = .{}; - rt.setModuleLoaderFunc(State, &state, null, loader.load); - - const result = ctx.eval( - \\import * as ns from "ns_test"; - \\globalThis.nsResult = ns.foo + ns.bar; - , "", .{ .type = .module }); - defer result.deinit(ctx); - - try testing.expect(!result.isException()); - - const global = ctx.getGlobalObject(); - defer global.deinit(ctx); - const ns_result = global.getPropertyStr(ctx, "nsResult"); - defer ns_result.deinit(ctx); - try testing.expectEqual(@as(i32, 30), try ns_result.toInt32(ctx)); - - const ns = state.captured_module.?.getNamespace(ctx); - defer ns.deinit(ctx); - - try testing.expect(ns.isObject()); - - const foo = ns.getPropertyStr(ctx, "foo"); - defer foo.deinit(ctx); - try testing.expectEqual(@as(i32, 10), try foo.toInt32(ctx)); -} diff --git a/examples/pjs/deps/zig-quickjs-ng/src/opaque.zig b/examples/pjs/deps/zig-quickjs-ng/src/opaque.zig deleted file mode 100644 index 1b3fe87..0000000 --- a/examples/pjs/deps/zig-quickjs-ng/src/opaque.zig +++ /dev/null @@ -1,22 +0,0 @@ -// Helpers for userdata handling. - -/// Given an opaque type T, returns the expected type T that the user -/// should pass in. This is necessary because if the user provides `void`, -/// then we don't want a `void` pointer, we just want void. -pub fn Opaque(comptime T: type) type { - // Void as-is so the function signature is cleaner. - if (T == void) return void; - - // Optional pointer to the type. - return ?*T; -} - -pub fn toC(comptime T: type, value: Opaque(T)) ?*anyopaque { - if (T == void) return null; - return @ptrCast(value); -} - -pub fn fromC(comptime T: type, ud: ?*anyopaque) Opaque(T) { - if (T == void) return; - return @ptrCast(@alignCast(ud)); -} diff --git a/examples/pjs/deps/zig-quickjs-ng/src/runtime.zig b/examples/pjs/deps/zig-quickjs-ng/src/runtime.zig deleted file mode 100644 index 5cf5c27..0000000 --- a/examples/pjs/deps/zig-quickjs-ng/src/runtime.zig +++ /dev/null @@ -1,1099 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; -const c = @import("quickjs_c"); -const Context = @import("context.zig").Context; -const ModuleDef = @import("module.zig").ModuleDef; -const typed_array = @import("typed_array.zig"); -const Value = @import("value.zig").Value; -const class = @import("class.zig"); -const Atom = @import("atom.zig").Atom; -const opaquepkg = @import("opaque.zig"); -const Opaque = opaquepkg.Opaque; - -/// Custom malloc functions for runtime memory allocation. -/// -/// Note: A Zig `std.mem.Allocator` cannot be directly wrapped because -/// Zig allocators require the original allocation size for `free` and -/// `realloc`, but this C-style interface does not provide it. Use -/// allocators that internally track sizes (e.g., libc malloc via -/// `std.c.malloc`/`std.c.free`). -/// -/// C: `JSMallocFunctions` -pub const MallocFunctions = extern struct { - calloc: *const fn (?*anyopaque, usize, usize) callconv(.c) ?*anyopaque, - malloc: *const fn (?*anyopaque, usize) callconv(.c) ?*anyopaque, - free: *const fn (?*anyopaque, ?*anyopaque) callconv(.c) void, - realloc: *const fn (?*anyopaque, ?*anyopaque, usize) callconv(.c) ?*anyopaque, - malloc_usable_size: ?*const fn (?*const anyopaque) callconv(.c) usize = null, - - comptime { - if (@sizeOf(MallocFunctions) != @sizeOf(c.JSMallocFunctions)) - @compileError("MallocFunctions size mismatch"); - if (@alignOf(MallocFunctions) != @alignOf(c.JSMallocFunctions)) - @compileError("MallocFunctions alignment mismatch"); - } -}; - -/// Wrapper for the QuickJS `JSRuntime`. -/// -/// The runtime represents a JavaScript execution environment. It manages -/// memory allocation, garbage collection, and atom (interned string) tables -/// shared by all contexts created from it. -pub const Runtime = opaque { - /// Creates a new JavaScript runtime. - /// - /// The runtime must be freed with `deinit` when no longer needed. - /// All contexts created from this runtime must be freed before - /// freeing the runtime itself. - /// - /// C: `JS_NewRuntime` - pub fn init() Allocator.Error!*Runtime { - const rt = c.JS_NewRuntime(); - if (rt == null) return error.OutOfMemory; - return @ptrCast(rt); - } - - /// Creates a new JavaScript runtime with custom malloc functions. - /// - /// This allows using a custom allocator for all runtime memory operations. - /// The opaque pointer is passed to all malloc function callbacks. - /// - /// C: `JS_NewRuntime2` - pub fn initWithMallocFunctions(mf: *const MallocFunctions, opaque_ptr: ?*anyopaque) Allocator.Error!*Runtime { - const rt = c.JS_NewRuntime2(@ptrCast(mf), opaque_ptr); - if (rt == null) return error.OutOfMemory; - return @ptrCast(rt); - } - - /// Frees the JavaScript runtime and all associated resources. - /// - /// All contexts created from this runtime must be freed before - /// calling this function. If `JS_DUMP_LEAKS` or similar dump flags - /// were set, this will output diagnostic information about leaked - /// objects, strings, atoms, or memory. - /// - /// C: `JS_FreeRuntime` - pub fn deinit(self: *Runtime) void { - c.JS_FreeRuntime(self.cval()); - } - - /// Creates a new JavaScript context within this runtime. - /// - /// The context must be freed with `Context.deinit` when no longer needed. - /// Contexts must be freed before freeing the runtime they belong to. - /// - /// C: `JS_NewContext` - pub fn newContext(self: *Runtime) Allocator.Error!*Context { - return Context.init(self); - } - - /// Creates a raw JavaScript context without any intrinsic objects. - /// - /// Use this to create a minimal context, then add only the intrinsics - /// you need with `addIntrinsic*` methods. - /// - /// C: `JS_NewContextRaw` - pub fn newContextRaw(self: *Runtime) Allocator.Error!*Context { - return Context.initRaw(self); - } - - // ========================================================================= - // Runtime Configuration - // ========================================================================= - - /// Sets runtime description for debugging purposes. - /// - /// The info string lifetime must exceed that of the runtime. - /// - /// C: `JS_SetRuntimeInfo` - pub fn setInfo(self: *Runtime, info: [:0]const u8) void { - c.JS_SetRuntimeInfo(self.cval(), info.ptr); - } - - /// Sets the memory limit for the runtime. - /// - /// Use 0 to disable the memory limit. - /// - /// C: `JS_SetMemoryLimit` - pub fn setMemoryLimit(self: *Runtime, limit: usize) void { - c.JS_SetMemoryLimit(self.cval(), limit); - } - - /// Sets the maximum stack size for the runtime. - /// - /// Use 0 to disable the stack size check. - /// - /// C: `JS_SetMaxStackSize` - pub fn setMaxStackSize(self: *Runtime, size: usize) void { - c.JS_SetMaxStackSize(self.cval(), size); - } - - /// Updates the stack top value used to check stack overflow. - /// - /// Should be called when changing threads. - /// - /// C: `JS_UpdateStackTop` - pub fn updateStackTop(self: *Runtime) void { - c.JS_UpdateStackTop(self.cval()); - } - - // ========================================================================= - // Garbage Collection - // ========================================================================= - - /// Sets the GC threshold. - /// - /// The threshold determines when automatic garbage collection is triggered. - /// - /// C: `JS_SetGCThreshold` - pub fn setGCThreshold(self: *Runtime, threshold: usize) void { - c.JS_SetGCThreshold(self.cval(), threshold); - } - - /// Gets the current GC threshold. - /// - /// C: `JS_GetGCThreshold` - pub fn getGCThreshold(self: *Runtime) usize { - return c.JS_GetGCThreshold(self.cval()); - } - - /// Runs the garbage collector. - /// - /// C: `JS_RunGC` - pub fn runGC(self: *Runtime) void { - c.JS_RunGC(self.cval()); - } - - /// Checks if a value is a live object in this runtime. - /// - /// C: `JS_IsLiveObject` - pub fn isLiveObject(self: *Runtime, obj: Value) bool { - return c.JS_IsLiveObject(self.cval(), obj.cval()); - } - - // ========================================================================= - // Opaque Data - // ========================================================================= - - /// Gets the opaque pointer associated with this runtime. - /// - /// C: `JS_GetRuntimeOpaque` - pub fn getOpaque(self: *Runtime, comptime T: type) ?*T { - return @ptrCast(@alignCast(c.JS_GetRuntimeOpaque(self.cval()))); - } - - /// Sets the opaque pointer for this runtime. - /// - /// C: `JS_SetRuntimeOpaque` - pub fn setOpaque(self: *Runtime, comptime T: type, ptr: ?*T) void { - c.JS_SetRuntimeOpaque(self.cval(), ptr); - } - - // ========================================================================= - // Dump Flags (Debugging) - // ========================================================================= - - /// Sets the dump flags for debugging output. - /// - /// C: `JS_SetDumpFlags` - pub fn setDumpFlags(self: *Runtime, flags: DumpFlags) void { - c.JS_SetDumpFlags(self.cval(), @bitCast(flags)); - } - - /// Gets the current dump flags. - /// - /// C: `JS_GetDumpFlags` - pub fn getDumpFlags(self: *Runtime) DumpFlags { - return @bitCast(c.JS_GetDumpFlags(self.cval())); - } - - // ========================================================================= - // Interrupt Handler - // ========================================================================= - - /// Interrupt handler callback type. - /// - /// Called periodically during JavaScript execution. - /// - userdata: The opaque pointer passed to setInterruptHandler - /// - runtime: The runtime - /// Return true to interrupt execution, false to continue. - pub fn InterruptHandler(comptime T: type) type { - return *const fn (Opaque(T), *Runtime) bool; - } - - /// Sets the interrupt handler for this runtime. - /// - /// The interrupt handler is called periodically during JavaScript execution - /// and can be used to implement timeouts or cancellation. - /// - /// C: `JS_SetInterruptHandler` - pub fn setInterruptHandler( - self: *Runtime, - comptime T: type, - userdata: Opaque(T), - comptime handler: ?InterruptHandler(T), - ) void { - const h = handler orelse { - c.JS_SetInterruptHandler(self.cval(), null, null); - return; - }; - - const Wrapper = struct { - fn callback( - runtime: *Runtime, - inner_userdata: ?*anyopaque, - ) callconv(.c) c_int { - const should_interrupt = @call(.always_inline, h, .{ - opaquepkg.fromC(T, inner_userdata), - runtime, - }); - return if (should_interrupt) 1 else 0; - } - }; - - c.JS_SetInterruptHandler( - self.cval(), - @ptrCast(&Wrapper.callback), - opaquepkg.toC(T, userdata), - ); - } - - // ========================================================================= - // Atomics Support - // ========================================================================= - - /// Sets whether Atomics.wait() can be used. - /// - /// If can_block is true, Atomics.wait() can be used. - /// - /// C: `JS_SetCanBlock` - pub fn setCanBlock(self: *Runtime, can_block: bool) void { - c.JS_SetCanBlock(self.cval(), can_block); - } - - /// Sets the SharedArrayBuffer functions for this runtime. - /// - /// These functions are used to allocate, free, and duplicate SharedArrayBuffer memory. - /// - /// C: `JS_SetSharedArrayBufferFunctions` - pub fn setSharedArrayBufferFunctions(self: *Runtime, sf: typed_array.SharedBufferFunctions) void { - var c_sf = sf.toCStruct(); - c.JS_SetSharedArrayBufferFunctions(self.cval(), &c_sf); - } - - // ========================================================================= - // Module Loader - // ========================================================================= - - /// Module name normalization function type. - /// - /// Called to normalize a module specifier. - /// - userdata: The opaque pointer passed to setModuleLoaderFunc - /// - ctx: The context - /// - module_base_name: The base name (requester) of the module - /// - module_name: The specifier being requested - /// - /// Returns the normalized module specifier (allocated with js_malloc) or null on exception. - pub fn ModuleNormalizeFunc(comptime T: type) type { - return *const fn (Opaque(T), *Context, [:0]const u8, [:0]const u8) ?[*:0]u8; - } - - /// Module loader function type. - /// - /// Called to load a module. - /// - userdata: The opaque pointer passed to setModuleLoaderFunc - /// - ctx: The context - /// - module_name: The normalized module specifier - /// - /// Returns the module definition or null on error. - pub fn ModuleLoaderFunc(comptime T: type) type { - return *const fn (Opaque(T), *Context, [:0]const u8) ?*ModuleDef; - } - - /// Sets the module loader functions. - /// - /// module_normalize can be null to use the default normalizer. - /// module_loader can be null to disable module loading. - /// - /// C: `JS_SetModuleLoaderFunc` - pub fn setModuleLoaderFunc( - self: *Runtime, - comptime T: type, - userdata: Opaque(T), - comptime module_normalize: ?ModuleNormalizeFunc(T), - comptime module_loader: ?ModuleLoaderFunc(T), - ) void { - const Wrapper = struct { - fn normCallback( - ctx: *Context, - module_base_name: [*:0]const u8, - module_name: [*:0]const u8, - inner_userdata: ?*anyopaque, - ) callconv(.c) ?[*:0]u8 { - const norm = module_normalize orelse return null; - return @call(.always_inline, norm, .{ - opaquepkg.fromC(T, inner_userdata), - ctx, - std.mem.span(module_base_name), - std.mem.span(module_name), - }); - } - - fn loadCallback( - ctx: *Context, - module_name: [*:0]const u8, - inner_userdata: ?*anyopaque, - ) callconv(.c) ?*ModuleDef { - const loader = module_loader orelse return null; - return @call(.always_inline, loader, .{ - opaquepkg.fromC(T, inner_userdata), - ctx, - std.mem.span(module_name), - }); - } - }; - - c.JS_SetModuleLoaderFunc( - self.cval(), - if (module_normalize != null) @ptrCast(&Wrapper.normCallback) else null, - if (module_loader != null) @ptrCast(&Wrapper.loadCallback) else null, - opaquepkg.toC(T, userdata), - ); - } - - // ========================================================================= - // Class Definition - // ========================================================================= - - /// Registers a new class with this runtime. - /// - /// The class ID must be obtained from `class.Id.new`. Once registered, - /// objects of this class can be created with `Value.initObjectClass`. - /// - /// C: `JS_NewClass` - pub fn newClass(self: *Runtime, class_id: class.Id, def: *const class.Def) !void { - const result = c.JS_NewClass(self.cval(), @intFromEnum(class_id), @ptrCast(def)); - if (result < 0) return error.ClassRegistrationFailed; - } - - /// Checks if a class ID is registered with this runtime. - /// - /// C: `JS_IsRegisteredClass` - pub fn isRegisteredClass(self: *Runtime, class_id: class.Id) bool { - return c.JS_IsRegisteredClass(self.cval(), @intFromEnum(class_id)); - } - - /// Gets the name of a registered class. - /// - /// Returns the class name as an atom, or `Atom.null` if the class is not registered. - /// The returned atom must be freed with `Atom.deinit`. - /// - /// C: `JS_GetClassName` - pub fn getClassName(self: *Runtime, class_id: class.Id) Atom { - return @enumFromInt(c.JS_GetClassName(self.cval(), @intFromEnum(class_id))); - } - - // ========================================================================= - // Job Queue (Microtasks) - // ========================================================================= - - /// Checks if there are pending jobs in the job queue. - /// - /// C: `JS_IsJobPending` - pub fn isJobPending(self: *Runtime) bool { - return c.JS_IsJobPending(self.cval()); - } - - /// Executes a pending job from the job queue. - /// - /// Returns the context in which the job was executed, or null if no job was pending. - /// Returns error.Exception if the job threw an exception. - /// - /// C: `JS_ExecutePendingJob` - pub fn executePendingJob(self: *Runtime) !?*Context { - var ctx: ?*c.JSContext = null; - const result = c.JS_ExecutePendingJob(self.cval(), &ctx); - if (result < 0) return error.Exception; - if (result == 0) return null; - return @ptrCast(ctx); - } - - // ========================================================================= - // Memory Usage - // ========================================================================= - - /// Computes memory usage statistics for this runtime. - /// - /// C: `JS_ComputeMemoryUsage` - pub fn computeMemoryUsage(self: *Runtime) c.JSMemoryUsage { - var usage: c.JSMemoryUsage = undefined; - c.JS_ComputeMemoryUsage(self.cval(), &usage); - return usage; - } - - // ========================================================================= - // Runtime Finalizers - // ========================================================================= - - /// Adds a finalizer to be called when the runtime is freed. - /// - /// Multiple finalizers can be added and will be called in reverse order - /// of registration when `deinit` is called. - /// - /// C: `JS_AddRuntimeFinalizer` - pub fn addFinalizer( - self: *Runtime, - comptime T: type, - userdata: Opaque(T), - comptime finalizer: *const fn ( - Opaque(T), - *Runtime, - ) void, - ) Allocator.Error!void { - const result = c.JS_AddRuntimeFinalizer( - self.cval(), - @ptrCast(&(struct { - fn callback( - runtime: *Runtime, - inner_userdata: ?*anyopaque, - ) callconv(.c) void { - @call(.always_inline, finalizer, .{ - opaquepkg.fromC(T, inner_userdata), - runtime, - }); - } - }).callback), - opaquepkg.toC(T, userdata), - ); - if (result < 0) return error.OutOfMemory; - } - - // ========================================================================= - // Promise Hooks - // ========================================================================= - - /// Promise hook type for tracking promise lifecycle. - pub const PromiseHookType = enum(c_uint) { - /// Promise was created - init = c.JS_PROMISE_HOOK_INIT, - /// About to execute promise reaction - before = c.JS_PROMISE_HOOK_BEFORE, - /// Finished executing promise reaction - after = c.JS_PROMISE_HOOK_AFTER, - /// Promise was resolved or rejected - resolve = c.JS_PROMISE_HOOK_RESOLVE, - }; - - /// Promise hook callback type. - /// - /// Called at various points in a promise's lifecycle. - /// - userdata: The opaque pointer passed to setPromiseHook - /// - ctx: The context - /// - hook_type: The type of hook event - /// - promise: The promise object - /// - parent_or_value: For init, the parent promise; for resolve, the resolution value - pub fn PromiseHook(comptime T: type) type { - return *const fn (Opaque(T), *Context, PromiseHookType, Value, Value) void; - } - - /// Sets the promise hook for debugging/tracing promise lifecycle. - /// - /// The hook is called when promises are created, before/after reactions, - /// and when resolved/rejected. - /// - /// C: `JS_SetPromiseHook` - pub fn setPromiseHook( - self: *Runtime, - comptime T: type, - userdata: Opaque(T), - comptime hook: ?PromiseHook(T), - ) void { - const h = hook orelse { - c.JS_SetPromiseHook(self.cval(), null, null); - return; - }; - - const Wrapper = struct { - fn callback( - ctx: ?*c.JSContext, - hook_type: c.JSPromiseHookType, - promise: c.JSValue, - parent_or_value: c.JSValue, - inner_userdata: ?*anyopaque, - ) callconv(.c) void { - @call(.always_inline, h, .{ - opaquepkg.fromC(T, inner_userdata), - @as(*Context, @ptrCast(ctx)), - @as(PromiseHookType, @enumFromInt(hook_type)), - @as(Value, @bitCast(promise)), - @as(Value, @bitCast(parent_or_value)), - }); - } - }; - c.JS_SetPromiseHook(self.cval(), &Wrapper.callback, opaquepkg.toC(T, userdata)); - } - - /// Promise rejection tracker callback type. - /// - /// Called when a promise is rejected without a handler, or when a handler - /// is added to a previously unhandled rejection. - /// - userdata: The opaque pointer passed to setHostPromiseRejectionTracker - /// - ctx: The context - /// - promise: The promise object - /// - reason: The rejection reason - /// - is_handled: true if the rejection now has a handler, false if unhandled - pub fn HostPromiseRejectionTracker(comptime T: type) type { - return *const fn (Opaque(T), *Context, Value, Value, bool) void; - } - - /// Sets the host promise rejection tracker. - /// - /// This is called when a promise rejection is unhandled, allowing the host - /// to log or report the unhandled rejection. - /// - /// C: `JS_SetHostPromiseRejectionTracker` - pub fn setHostPromiseRejectionTracker( - self: *Runtime, - comptime T: type, - userdata: Opaque(T), - comptime tracker: ?HostPromiseRejectionTracker(T), - ) void { - const t = tracker orelse { - c.JS_SetHostPromiseRejectionTracker(self.cval(), null, null); - return; - }; - - const Wrapper = struct { - fn callback( - ctx: ?*c.JSContext, - promise: c.JSValue, - reason: c.JSValue, - is_handled: bool, - inner_userdata: ?*anyopaque, - ) callconv(.c) void { - @call(.always_inline, t, .{ - opaquepkg.fromC(T, inner_userdata), - @as(*Context, @ptrCast(ctx)), - @as(Value, @bitCast(promise)), - @as(Value, @bitCast(reason)), - is_handled, - }); - } - }; - c.JS_SetHostPromiseRejectionTracker(self.cval(), &Wrapper.callback, opaquepkg.toC(T, userdata)); - } - - // ========================================================================= - // Internal - // ========================================================================= - - pub inline fn cval(self: *Runtime) *c.JSRuntime { - return @ptrCast(self); - } -}; - -/// Debug dump flags for runtime diagnostics. -/// -/// C: `JS_DUMP_*` -pub const DumpFlags = packed struct(u64) { - bytecode_final: bool = false, - bytecode_pass2: bool = false, - bytecode_pass1: bool = false, - _reserved1: bool = false, - bytecode_hex: bool = false, - bytecode_pc2line: bool = false, - bytecode_stack: bool = false, - bytecode_step: bool = false, - read_object: bool = false, - free: bool = false, - gc: bool = false, - gc_free: bool = false, - module_resolve: bool = false, - promise: bool = false, - leaks: bool = false, - atom_leaks: bool = false, - mem: bool = false, - objects: bool = false, - atoms: bool = false, - shapes: bool = false, - _padding: u44 = 0, -}; - -test "Runtime init and deinit" { - const rt: *Runtime = try .init(); - defer rt.deinit(); -} - -test "Runtime newContext" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx = try rt.newContext(); - defer ctx.deinit(); - - // Verify the context is properly linked to the runtime - try std.testing.expectEqual(rt, ctx.getRuntime()); -} - -test "Runtime setMemoryLimit" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - // Set a memory limit - rt.setMemoryLimit(1024 * 1024); - - const ctx = try rt.newContext(); - defer ctx.deinit(); - - // Allocate something very large that exceeds the limit - const result = ctx.eval( - \\let arr = []; - \\for (let i = 0; i < 1000000; i++) arr.push(new Array(1000)); - \\arr.length - , "", .{}); - defer result.deinit(ctx); - - // Should fail with out of memory - try std.testing.expect(result.isException()); - - // Disable limit - rt.setMemoryLimit(0); -} - -test "Runtime setMaxStackSize" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - // Set a small stack size - rt.setMaxStackSize(32 * 1024); - - const ctx = try rt.newContext(); - defer ctx.deinit(); - - // Deep recursion should hit stack limit - const result = ctx.eval( - \\function recurse(n) { return recurse(n + 1); } - \\recurse(0); - , "", .{}); - defer result.deinit(ctx); - - try std.testing.expect(result.isException()); -} - -test "Runtime GC threshold" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - // Get default threshold - const default_threshold = rt.getGCThreshold(); - try std.testing.expect(default_threshold > 0); - - // Set a new threshold - rt.setGCThreshold(1024); - try std.testing.expectEqual(@as(usize, 1024), rt.getGCThreshold()); - - // Restore - rt.setGCThreshold(default_threshold); - try std.testing.expectEqual(default_threshold, rt.getGCThreshold()); -} - -test "Runtime runGC" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx = try rt.newContext(); - defer ctx.deinit(); - - // Create some garbage - const result = ctx.eval( - \\for (let i = 0; i < 1000; i++) { let x = {a: i, b: [1,2,3]}; } - \\true - , "", .{}); - defer result.deinit(ctx); - - // Run GC - should not crash - rt.runGC(); - - try std.testing.expect(!result.isException()); -} - -test "Runtime opaque data" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const TestData = struct { - value: i32, - }; - - var data: TestData = .{ .value = 42 }; - - // Initially null - try std.testing.expectEqual(@as(?*TestData, null), rt.getOpaque(TestData)); - - // Set opaque - rt.setOpaque(TestData, &data); - - // Get opaque - const retrieved = rt.getOpaque(TestData); - try std.testing.expect(retrieved != null); - try std.testing.expectEqual(@as(i32, 42), retrieved.?.value); - - // Modify through pointer - retrieved.?.value = 100; - try std.testing.expectEqual(@as(i32, 100), data.value); - - // Clear - rt.setOpaque(TestData, null); - try std.testing.expectEqual(@as(?*TestData, null), rt.getOpaque(TestData)); -} - -test "Runtime dump flags" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - // Get default flags - const default_flags = rt.getDumpFlags(); - - // Set some flags - rt.setDumpFlags(.{ .leaks = true, .gc = true }); - const new_flags = rt.getDumpFlags(); - try std.testing.expect(new_flags.leaks); - try std.testing.expect(new_flags.gc); - try std.testing.expect(!new_flags.promise); - - // Restore defaults - rt.setDumpFlags(default_flags); -} - -test "Runtime setInterruptHandler" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx = try rt.newContext(); - defer ctx.deinit(); - - const State = struct { - call_count: i32 = 0, - - fn handler(self: ?*@This(), _: *Runtime) bool { - self.?.call_count += 1; - // Interrupt after 100 calls - return self.?.call_count > 100; - } - }; - - var state: State = .{}; - - rt.setInterruptHandler(State, &state, State.handler); - - // Run an infinite loop - should be interrupted - const result = ctx.eval("while(true) {}", "", .{}); - defer result.deinit(ctx); - - try std.testing.expect(result.isException()); - try std.testing.expect(state.call_count > 100); - - // Clear handler - rt.setInterruptHandler(State, null, null); -} - -test "Runtime isJobPending with promises" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx = try rt.newContext(); - defer ctx.deinit(); - - // No jobs initially - try std.testing.expect(!rt.isJobPending()); - - // Create a resolved promise - this schedules a job - const result = ctx.eval("Promise.resolve(42).then(x => x * 2)", "", .{}); - defer result.deinit(ctx); - try std.testing.expect(!result.isException()); - - // Now there should be a pending job - try std.testing.expect(rt.isJobPending()); -} - -test "Runtime executePendingJob" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx = try rt.newContext(); - defer ctx.deinit(); - - // Track promise resolution through a global - _ = ctx.eval("globalThis.result = 0", "", .{}); - - // Create a promise that modifies global state - const promise_result = ctx.eval( - \\Promise.resolve(42).then(x => { globalThis.result = x * 2; }); - , "", .{}); - defer promise_result.deinit(ctx); - - // Execute the pending job - while (rt.isJobPending()) { - const job_ctx = try rt.executePendingJob(); - try std.testing.expect(job_ctx != null); - } - - // Check that the promise callback ran - const check = ctx.eval("globalThis.result", "", .{}); - defer check.deinit(ctx); - - try std.testing.expectEqual(@as(i32, 84), try check.toInt32(ctx)); -} - -test "Runtime computeMemoryUsage" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx = try rt.newContext(); - defer ctx.deinit(); - - // Create some objects - const result = ctx.eval( - \\let obj = {a: 1, b: 2, c: [1,2,3]}; - \\let str = "hello world"; - \\true - , "", .{}); - defer result.deinit(ctx); - - const usage = rt.computeMemoryUsage(); - - // Basic sanity checks - try std.testing.expect(usage.malloc_size > 0); - try std.testing.expect(usage.memory_used_size > 0); - try std.testing.expect(usage.atom_count > 0); - try std.testing.expect(usage.obj_count > 0); -} - -test "Runtime isLiveObject" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx = try rt.newContext(); - defer ctx.deinit(); - - // Create an object - const obj = ctx.eval("({a: 1})", "", .{}); - defer obj.deinit(ctx); - - // Object should be live - try std.testing.expect(rt.isLiveObject(obj)); - - // Primitives are not "live objects" in QuickJS sense - const num = Value.initInt32(42); - try std.testing.expect(!rt.isLiveObject(num)); -} - -test "Runtime setCanBlock" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - // Just test that it doesn't crash - rt.setCanBlock(true); - rt.setCanBlock(false); -} - -test "Runtime updateStackTop" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - // Just test that it doesn't crash - rt.updateStackTop(); -} - -test "Runtime setInfo" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - // Just test that it doesn't crash - rt.setInfo("test runtime"); -} - -test "DumpFlags bit layout matches C header" { - const testing = std.testing; - - try testing.expectEqual(c.JS_DUMP_BYTECODE_FINAL, @as(u64, @bitCast(DumpFlags{ .bytecode_final = true }))); - try testing.expectEqual(c.JS_DUMP_BYTECODE_PASS2, @as(u64, @bitCast(DumpFlags{ .bytecode_pass2 = true }))); - try testing.expectEqual(c.JS_DUMP_BYTECODE_PASS1, @as(u64, @bitCast(DumpFlags{ .bytecode_pass1 = true }))); - try testing.expectEqual(c.JS_DUMP_BYTECODE_HEX, @as(u64, @bitCast(DumpFlags{ .bytecode_hex = true }))); - try testing.expectEqual(c.JS_DUMP_BYTECODE_PC2LINE, @as(u64, @bitCast(DumpFlags{ .bytecode_pc2line = true }))); - try testing.expectEqual(c.JS_DUMP_BYTECODE_STACK, @as(u64, @bitCast(DumpFlags{ .bytecode_stack = true }))); - try testing.expectEqual(c.JS_DUMP_BYTECODE_STEP, @as(u64, @bitCast(DumpFlags{ .bytecode_step = true }))); - try testing.expectEqual(c.JS_DUMP_READ_OBJECT, @as(u64, @bitCast(DumpFlags{ .read_object = true }))); - try testing.expectEqual(c.JS_DUMP_FREE, @as(u64, @bitCast(DumpFlags{ .free = true }))); - try testing.expectEqual(c.JS_DUMP_GC, @as(u64, @bitCast(DumpFlags{ .gc = true }))); - try testing.expectEqual(c.JS_DUMP_GC_FREE, @as(u64, @bitCast(DumpFlags{ .gc_free = true }))); - try testing.expectEqual(c.JS_DUMP_MODULE_RESOLVE, @as(u64, @bitCast(DumpFlags{ .module_resolve = true }))); - try testing.expectEqual(c.JS_DUMP_PROMISE, @as(u64, @bitCast(DumpFlags{ .promise = true }))); - try testing.expectEqual(c.JS_DUMP_LEAKS, @as(u64, @bitCast(DumpFlags{ .leaks = true }))); - try testing.expectEqual(c.JS_DUMP_ATOM_LEAKS, @as(u64, @bitCast(DumpFlags{ .atom_leaks = true }))); - try testing.expectEqual(c.JS_DUMP_MEM, @as(u64, @bitCast(DumpFlags{ .mem = true }))); - try testing.expectEqual(c.JS_DUMP_OBJECTS, @as(u64, @bitCast(DumpFlags{ .objects = true }))); - try testing.expectEqual(c.JS_DUMP_ATOMS, @as(u64, @bitCast(DumpFlags{ .atoms = true }))); - try testing.expectEqual(c.JS_DUMP_SHAPES, @as(u64, @bitCast(DumpFlags{ .shapes = true }))); - - // Combined flags - try testing.expectEqual( - c.JS_DUMP_LEAKS | c.JS_DUMP_GC, - @as(u64, @bitCast(DumpFlags{ .leaks = true, .gc = true })), - ); -} - -test "Runtime initWithMallocFunctions" { - const State = struct { - var malloc_count: usize = 0; - var free_count: usize = 0; - - fn jsMalloc(_: ?*anyopaque, size: usize) callconv(.c) ?*anyopaque { - malloc_count += 1; - return std.c.malloc(size); - } - - fn jsCalloc(_: ?*anyopaque, count: usize, size: usize) callconv(.c) ?*anyopaque { - malloc_count += 1; - return std.c.calloc(count, size); - } - - fn jsFree(_: ?*anyopaque, ptr: ?*anyopaque) callconv(.c) void { - if (ptr != null) free_count += 1; - std.c.free(ptr); - } - - fn jsRealloc(_: ?*anyopaque, ptr: ?*anyopaque, size: usize) callconv(.c) ?*anyopaque { - return std.c.realloc(ptr, size); - } - - fn jsMallocUsableSize(_: ?*const anyopaque) callconv(.c) usize { - return 0; - } - }; - - const mf = MallocFunctions{ - .malloc = State.jsMalloc, - .calloc = State.jsCalloc, - .free = State.jsFree, - .realloc = State.jsRealloc, - .malloc_usable_size = State.jsMallocUsableSize, - }; - - State.malloc_count = 0; - State.free_count = 0; - - const rt = try Runtime.initWithMallocFunctions(&mf, null); - defer rt.deinit(); - - const ctx = try rt.newContext(); - defer ctx.deinit(); - - // Verify custom allocator was used - try std.testing.expect(State.malloc_count > 0); -} - -test "Runtime addFinalizer" { - const State = struct { - called: bool = false, - - fn finalizer(self: ?*@This(), _: *Runtime) void { - self.?.called = true; - } - }; - - var state: State = .{}; - const rt: *Runtime = try .init(); - try rt.addFinalizer(State, &state, State.finalizer); - rt.deinit(); - - // Finalizer should have been called during deinit - try std.testing.expect(state.called); -} - -test "Runtime setPromiseHook" { - const State = struct { - hook_calls: usize = 0, - last_hook_type: ?Runtime.PromiseHookType = null, - - fn hook( - self: ?*@This(), - _: *Context, - hook_type: Runtime.PromiseHookType, - _: Value, - _: Value, - ) void { - self.?.hook_calls += 1; - self.?.last_hook_type = hook_type; - } - }; - - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx = try rt.newContext(); - defer ctx.deinit(); - - var state: State = .{}; - - rt.setPromiseHook(State, &state, State.hook); - - // Create a promise - should trigger the hook - const result = ctx.eval("Promise.resolve(42)", "", .{}); - defer result.deinit(ctx); - - try std.testing.expect(state.hook_calls > 0); - try std.testing.expect(state.last_hook_type != null); - - // Clear the hook - rt.setPromiseHook(State, null, null); -} - -test "Runtime setHostPromiseRejectionTracker" { - const State = struct { - tracker_calls: usize = 0, - was_handled: bool = false, - - fn tracker( - self: ?*@This(), - _: *Context, - _: Value, - _: Value, - is_handled: bool, - ) void { - self.?.tracker_calls += 1; - self.?.was_handled = is_handled; - } - }; - - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx = try rt.newContext(); - defer ctx.deinit(); - - var state: State = .{}; - - rt.setHostPromiseRejectionTracker(State, &state, State.tracker); - - // Create an unhandled rejection - const result = ctx.eval("Promise.reject(new Error('test'))", "", .{}); - defer result.deinit(ctx); - - // Execute pending jobs to trigger rejection tracking - while (rt.isJobPending()) { - _ = rt.executePendingJob() catch break; - } - - try std.testing.expect(state.tracker_calls > 0); - - // Clear the tracker - rt.setHostPromiseRejectionTracker(State, null, null); -} - -test "PromiseHookType matches C constants" { - try std.testing.expectEqual(@as(c_uint, c.JS_PROMISE_HOOK_INIT), @intFromEnum(Runtime.PromiseHookType.init)); - try std.testing.expectEqual(@as(c_uint, c.JS_PROMISE_HOOK_BEFORE), @intFromEnum(Runtime.PromiseHookType.before)); - try std.testing.expectEqual(@as(c_uint, c.JS_PROMISE_HOOK_AFTER), @intFromEnum(Runtime.PromiseHookType.after)); - try std.testing.expectEqual(@as(c_uint, c.JS_PROMISE_HOOK_RESOLVE), @intFromEnum(Runtime.PromiseHookType.resolve)); -} diff --git a/examples/pjs/deps/zig-quickjs-ng/src/typed_array.zig b/examples/pjs/deps/zig-quickjs-ng/src/typed_array.zig deleted file mode 100644 index ab88b0f..0000000 --- a/examples/pjs/deps/zig-quickjs-ng/src/typed_array.zig +++ /dev/null @@ -1,96 +0,0 @@ -const std = @import("std"); -const testing = std.testing; -const c = @import("quickjs_c"); -const Context = @import("context.zig").Context; -const Runtime = @import("runtime.zig").Runtime; -const Value = @import("value.zig").Value; -const opaquepkg = @import("opaque.zig"); -const Opaque = opaquepkg.Opaque; - -/// TypedArray element type. -/// -/// C: `JSTypedArrayEnum` -pub const Type = enum(c_uint) { - uint8_clamped = c.JS_TYPED_ARRAY_UINT8C, - int8 = c.JS_TYPED_ARRAY_INT8, - uint8 = c.JS_TYPED_ARRAY_UINT8, - int16 = c.JS_TYPED_ARRAY_INT16, - uint16 = c.JS_TYPED_ARRAY_UINT16, - int32 = c.JS_TYPED_ARRAY_INT32, - uint32 = c.JS_TYPED_ARRAY_UINT32, - big_int64 = c.JS_TYPED_ARRAY_BIG_INT64, - big_uint64 = c.JS_TYPED_ARRAY_BIG_UINT64, - float16 = c.JS_TYPED_ARRAY_FLOAT16, - float32 = c.JS_TYPED_ARRAY_FLOAT32, - float64 = c.JS_TYPED_ARRAY_FLOAT64, -}; - -/// Information about a typed array's underlying buffer. -pub const Buffer = struct { - value: Value, - byte_offset: usize, - byte_length: usize, - bytes_per_element: usize, -}; - -/// Callback for freeing ArrayBuffer data. -/// -/// C: `JSFreeArrayBufferDataFunc` -pub fn FreeBufferDataFunc(comptime T: type) type { - return *const fn ( - rt: *Runtime, - userdata: Opaque(T), - ptr: ?*anyopaque, - ) void; -} - -/// Wraps a Zig-friendly FreeBufferDataFunc into a C-callable function. -pub fn wrapFreeBufferDataFunc(comptime T: type, comptime func: FreeBufferDataFunc(T)) c.JSFreeArrayBufferDataFunc { - return struct { - fn callback( - rt: ?*c.JSRuntime, - opaque_ptr: ?*anyopaque, - ptr: ?*anyopaque, - ) callconv(.c) void { - @call(.always_inline, func, .{ - @as(*Runtime, @ptrCast(rt)), - opaquepkg.fromC(T, opaque_ptr), - ptr, - }); - } - }.callback; -} - -/// Functions for SharedArrayBuffer support. -/// -/// C: `JSSharedArrayBufferFunctions` -pub const SharedBufferFunctions = struct { - alloc: ?*const fn (userdata: ?*anyopaque, size: usize) callconv(.c) ?*anyopaque = null, - free: ?*const fn (userdata: ?*anyopaque, ptr: ?*anyopaque) callconv(.c) void = null, - dup: ?*const fn (userdata: ?*anyopaque, ptr: ?*anyopaque) callconv(.c) void = null, - userdata: ?*anyopaque = null, - - fn toCStruct(self: SharedBufferFunctions) c.JSSharedArrayBufferFunctions { - return .{ - .sab_alloc = self.alloc, - .sab_free = self.free, - .sab_dup = self.dup, - .sab_opaque = self.userdata, - }; - } -}; - -test "Type matches C constants" { - try testing.expectEqual(@as(c_uint, 0), @intFromEnum(Type.uint8_clamped)); - try testing.expectEqual(@as(c_uint, 1), @intFromEnum(Type.int8)); - try testing.expectEqual(@as(c_uint, 2), @intFromEnum(Type.uint8)); - try testing.expectEqual(@as(c_uint, 3), @intFromEnum(Type.int16)); - try testing.expectEqual(@as(c_uint, 4), @intFromEnum(Type.uint16)); - try testing.expectEqual(@as(c_uint, 5), @intFromEnum(Type.int32)); - try testing.expectEqual(@as(c_uint, 6), @intFromEnum(Type.uint32)); - try testing.expectEqual(@as(c_uint, 7), @intFromEnum(Type.big_int64)); - try testing.expectEqual(@as(c_uint, 8), @intFromEnum(Type.big_uint64)); - try testing.expectEqual(@as(c_uint, 9), @intFromEnum(Type.float16)); - try testing.expectEqual(@as(c_uint, 10), @intFromEnum(Type.float32)); - try testing.expectEqual(@as(c_uint, 11), @intFromEnum(Type.float64)); -} diff --git a/examples/pjs/deps/zig-quickjs-ng/src/value.zig b/examples/pjs/deps/zig-quickjs-ng/src/value.zig deleted file mode 100644 index f1b30ea..0000000 --- a/examples/pjs/deps/zig-quickjs-ng/src/value.zig +++ /dev/null @@ -1,3567 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const assert = std.debug.assert; -const testing = std.testing; -const c = @import("quickjs_c"); -const Atom = @import("atom.zig").Atom; -const cfunc = @import("cfunc.zig"); -const Context = @import("context.zig").Context; -const ModuleDef = @import("module.zig").ModuleDef; -const Runtime = @import("runtime.zig").Runtime; -const typed_array = @import("typed_array.zig"); -const class = @import("class.zig"); -const opaquepkg = @import("opaque.zig"); -const Opaque = opaquepkg.Opaque; - -/// Wrapper for the QuickJS `JSValue`. -/// -/// A value represents any JavaScript value (number, string, object, etc.). -/// Values are reference-counted and must be freed with `deinit` when no -/// longer needed. -/// -/// C: `JSValue` -pub const Value = extern struct { - // If this is true then we are in nan-boxing mode which changes - // our Value representation. - pub const is_nan_boxed = builtin.target.ptrBitWidth() < 64; - - // Non-nan-boxed layout - u: if (!is_nan_boxed) c.JSValueUnion else void = if (!is_nan_boxed) .{ .int32 = 0 } else {}, - tag: if (!is_nan_boxed) Tag else void = if (!is_nan_boxed) .undefined else {}, - - // NaN-boxed layout - val: if (is_nan_boxed) u64 else void = if (is_nan_boxed) mkval(.undefined, 0) else {}, - - /// JavaScript value type tag. - /// - /// C: anonymous enum in quickjs.h - pub const Tag = enum(i64) { - big_int = c.JS_TAG_BIG_INT, - symbol = c.JS_TAG_SYMBOL, - string = c.JS_TAG_STRING, - module = c.JS_TAG_MODULE, - function_bytecode = c.JS_TAG_FUNCTION_BYTECODE, - object = c.JS_TAG_OBJECT, - int = c.JS_TAG_INT, - bool = c.JS_TAG_BOOL, - null = c.JS_TAG_NULL, - undefined = c.JS_TAG_UNDEFINED, - uninitialized = c.JS_TAG_UNINITIALIZED, - catch_offset = c.JS_TAG_CATCH_OFFSET, - exception = c.JS_TAG_EXCEPTION, - short_big_int = c.JS_TAG_SHORT_BIG_INT, - float64 = c.JS_TAG_FLOAT64, - _, - }; - - pub const @"null": Value = if (is_nan_boxed) - .{ .val = mkval(.null, 0) } - else - .{ .u = .{ .int32 = 0 }, .tag = .null }; - - pub const @"undefined": Value = if (is_nan_boxed) - .{ .val = mkval(.undefined, 0) } - else - .{ .u = .{ .int32 = 0 }, .tag = .undefined }; - - pub const @"false": Value = if (is_nan_boxed) - .{ .val = mkval(.bool, 0) } - else - .{ .u = .{ .int32 = 0 }, .tag = .bool }; - - pub const @"true": Value = if (is_nan_boxed) - .{ .val = mkval(.bool, 1) } - else - .{ .u = .{ .int32 = 1 }, .tag = .bool }; - - pub const exception: Value = if (is_nan_boxed) - .{ .val = mkval(.exception, 0) } - else - .{ .u = .{ .int32 = 0 }, .tag = .exception }; - - pub const uninitialized: Value = if (is_nan_boxed) - .{ .val = mkval(.uninitialized, 0) } - else - .{ .u = .{ .int32 = 0 }, .tag = .uninitialized }; - - /// Initialize a Value from a Zig type. - /// - /// If the value is already a `Value`, it is returned directly. - /// Otherwise, this function attempts to convert the Zig value to a - /// JavaScript Value. If conversion fails, an exception is thrown and - /// the exception Value is returned. - /// - /// Supported types: - /// - `Value` (returned as-is) - /// - `bool` → JavaScript boolean - /// - `void` → JavaScript null - /// - `@TypeOf(null)` → JavaScript null - /// - Optional types → unwrapped value or null - /// - Integers (≤32 bits) → JavaScript int32/uint32 - /// - Integers (≤64 bits) → JavaScript int64 (may become float64) - /// - `comptime_int` → JavaScript int64 - /// - Floats (≤64 bits) → JavaScript float64 - /// - `comptime_float` → JavaScript float64 - /// - `[]const u8` slices → JavaScript string - /// - `*const [N]u8` → JavaScript string - pub fn init(ctx: *Context, val: anytype) Value { - return switch (@TypeOf(val)) { - Value => val, - bool => initBool(val), - void => @"null", - - else => |T| switch (@typeInfo(T)) { - .null => @"null", - .optional => if (val) |v| init(ctx, v) else @"null", - - .int => |info| if (info.bits <= 32) switch (info.signedness) { - .signed => initInt32(@intCast(val)), - .unsigned => initUint32(@intCast(val)), - } else if (info.bits <= 63 or - (val >= std.math.minInt(i63) and val <= std.math.maxInt(i63))) - initInt64(@intCast(val)) - else if (info.bits == 64) - initFloat64(@floatFromInt(val)) - else - initConversionError(ctx), - - .comptime_int => initInt64(val), - - .float => |info| if (info.bits <= 64) - initFloat64(@floatCast(val)) - else - initConversionError(ctx), - .comptime_float => initFloat64(val), - - .pointer => |ptr| switch (ptr.size) { - .slice => if (ptr.child == u8) - initStringLen(ctx, val) - else - initConversionError(ctx), - - .one => if (@typeInfo(ptr.child) == .array and - @typeInfo(ptr.child).array.child == u8) - initStringLen(ctx, val) - else - initConversionError(ctx), - - else => initConversionError(ctx), - }, - - else => initConversionError(ctx), - }, - }; - } - - fn initConversionError(ctx: *Context) Value { - return initString(ctx, "failed to convert Zig value to JS Value").throw(ctx); - } - - /// Creates a JavaScript boolean value. - /// - /// C: `JS_NewBool` - pub fn initBool(val: bool) Value { - return if (is_nan_boxed) - .{ .val = mkval(.bool, @intFromBool(val)) } - else - .{ .u = .{ .int32 = @intFromBool(val) }, .tag = .bool }; - } - - /// Creates a JavaScript 32-bit integer value. - /// - /// C: `JS_NewInt32` - pub fn initInt32(val: i32) Value { - return if (is_nan_boxed) - .{ .val = mkval(.int, val) } - else - .{ .u = .{ .int32 = val }, .tag = .int }; - } - - /// Creates a JavaScript 64-bit integer value. - /// - /// If the value fits in an i32, creates an int value. Otherwise creates a float64. - /// - /// C: `JS_NewInt64` - pub fn initInt64(val: i64) Value { - return if (val >= std.math.minInt(i32) and val <= std.math.maxInt(i32)) - initInt32(@intCast(val)) - else - initFloat64(@floatFromInt(val)); - } - - /// Creates a JavaScript unsigned 32-bit integer value. - /// - /// If the value fits in an i32, creates an int value. Otherwise creates a float64. - /// - /// C: `JS_NewUint32` - pub fn initUint32(val: u32) Value { - return if (val <= std.math.maxInt(i32)) - initInt32(@intCast(val)) - else - initFloat64(@floatFromInt(val)); - } - - /// Creates a JavaScript floating-point number value. - /// - /// C: `JS_NewFloat64` - pub fn initFloat64(val: f64) Value { - return if (is_nan_boxed) - .{ .val = mkfloat64(val) } - else - .{ .u = .{ .float64 = val }, .tag = .float64 }; - } - - /// Creates a JavaScript number value from a double. - /// - /// May return an integer if the value is a whole number that fits in i32. - /// - /// C: `JS_NewNumber` - pub fn initNumber(ctx: *Context, val: f64) Value { - return fromCVal(c.JS_NewNumber(ctx.cval(), val)); - } - - /// Creates a JavaScript BigInt value from a signed 64-bit integer. - /// - /// C: `JS_NewBigInt64` - pub fn initBigInt64(ctx: *Context, val: i64) Value { - return fromCVal(c.JS_NewBigInt64(ctx.cval(), val)); - } - - /// Creates a JavaScript BigInt value from an unsigned 64-bit integer. - /// - /// C: `JS_NewBigUint64` - pub fn initBigUint64(ctx: *Context, val: u64) Value { - return fromCVal(c.JS_NewBigUint64(ctx.cval(), val)); - } - - /// Creates a JavaScript string value from a null-terminated string. - /// - /// C: `JS_NewString` - pub fn initString(ctx: *Context, str: [*:0]const u8) Value { - return fromCVal(c.JS_NewString(ctx.cval(), str)); - } - - /// Creates a JavaScript string value from a byte slice. - /// - /// C: `JS_NewStringLen` - pub fn initStringLen(ctx: *Context, str: []const u8) Value { - return fromCVal(c.JS_NewStringLen(ctx.cval(), str.ptr, str.len)); - } - - /// Creates an empty JavaScript object. - /// - /// C: `JS_NewObject` - pub fn initObject(ctx: *Context) Value { - return fromCVal(c.JS_NewObject(ctx.cval())); - } - - /// Creates a JavaScript object with a specific prototype. - /// - /// C: `JS_NewObjectProto` - pub fn initObjectProto(ctx: *Context, proto: Value) Value { - return fromCVal(c.JS_NewObjectProto(ctx.cval(), proto.cval())); - } - - /// Creates a JavaScript object with a specific class ID. - /// - /// The object will have the prototype associated with the class (set via - /// Context.setClassProto) and can store opaque data via setOpaque. - /// - /// C: `JS_NewObjectClass` - pub fn initObjectClass(ctx: *Context, class_id: class.Id) Value { - return fromCVal(c.JS_NewObjectClass(ctx.cval(), @intFromEnum(class_id))); - } - - /// Creates an empty JavaScript array. - /// - /// C: `JS_NewArray` - pub fn initArray(ctx: *Context) Value { - return fromCVal(c.JS_NewArray(ctx.cval())); - } - - /// Creates a JavaScript array from a slice of values. - /// - /// Takes ownership of the values. - /// - /// C: `JS_NewArrayFrom` - pub fn initArrayFrom(ctx: *Context, values: []const Value) Value { - return fromCVal(c.JS_NewArrayFrom( - ctx.cval(), - @intCast(values.len), - @ptrCast(values.ptr), - )); - } - - /// Creates a JavaScript Date object from epoch milliseconds. - /// - /// C: `JS_NewDate` - pub fn initDate(ctx: *Context, epoch_ms: f64) Value { - return fromCVal(c.JS_NewDate(ctx.cval(), epoch_ms)); - } - - /// Creates a JavaScript Symbol. - /// - /// C: `JS_NewSymbol` - pub fn initSymbol(ctx: *Context, description: [*:0]const u8, is_global: bool) Value { - return fromCVal(c.JS_NewSymbol(ctx.cval(), description, is_global)); - } - - /// Creates a JavaScript Error object. - /// - /// C: `JS_NewError` - pub fn initError(ctx: *Context) Value { - return fromCVal(c.JS_NewError(ctx.cval())); - } - - // ----------------------------------------------------------------------- - // C Function Constructors - // ----------------------------------------------------------------------- - - /// Creates a JavaScript function from a C function. - /// - /// C: `JS_NewCFunction` - pub fn initCFunction( - ctx: *Context, - comptime func: cfunc.Func, - name: [:0]const u8, - length: i32, - ) Value { - return fromCVal(c.JS_NewCFunction( - ctx.cval(), - cfunc.wrapFunc(func), - name.ptr, - length, - )); - } - - /// Creates a JavaScript function from a C function with prototype and magic. - /// - /// C: `JS_NewCFunction2` - pub fn initCFunction2( - ctx: *Context, - comptime func: cfunc.Func, - name: [:0]const u8, - length: i32, - cproto: cfunc.Proto, - magic: i32, - ) Value { - return fromCVal(c.JS_NewCFunction2( - ctx.cval(), - cfunc.wrapFunc(func), - name.ptr, - length, - @intFromEnum(cproto), - magic, - )); - } - - /// Creates a JavaScript function from a C function with prototype, magic and custom prototype object. - /// - /// C: `JS_NewCFunction3` - pub fn initCFunction3( - ctx: *Context, - comptime func: cfunc.Func, - name: [:0]const u8, - length: i32, - cproto: cfunc.Proto, - magic: i32, - proto_val: Value, - ) Value { - return fromCVal(c.JS_NewCFunction3( - ctx.cval(), - cfunc.wrapFunc(func), - name.ptr, - length, - @intFromEnum(cproto), - magic, - proto_val.cval(), - )); - } - - /// Creates a JavaScript function from a C function with closure data. - /// - /// The data values are copied and can be accessed in the function via func_data parameter. - /// - /// C: `JS_NewCFunctionData` - pub fn initCFunctionData( - ctx: *Context, - comptime func: cfunc.FuncData, - length: i32, - magic: i32, - data: []const Value, - ) Value { - return fromCVal(c.JS_NewCFunctionData( - ctx.cval(), - cfunc.wrapFuncData(func), - length, - magic, - @intCast(data.len), - @ptrCast(@constCast(data.ptr)), - )); - } - - /// Creates a named JavaScript function from a C function with closure data. - /// - /// C: `JS_NewCFunctionData2` - pub fn initCFunctionData2( - ctx: *Context, - comptime func: cfunc.FuncData, - name: [:0]const u8, - length: i32, - magic: i32, - data: []const Value, - ) Value { - return fromCVal(c.JS_NewCFunctionData2( - ctx.cval(), - cfunc.wrapFuncData(func), - name.ptr, - length, - magic, - @intCast(data.len), - @ptrCast(@constCast(data.ptr)), - )); - } - - /// Creates a JavaScript function from a C closure with opaque data. - /// - /// The opaque pointer is passed directly to the function and finalizer. - /// - /// C: `JS_NewCClosure` - pub fn initCClosure( - ctx: *Context, - comptime T: type, - comptime func: cfunc.Closure(T), - name: [:0]const u8, - comptime finalizer: ?cfunc.ClosureFinalizer(T), - length: i32, - magic: i32, - userdata: Opaque(T), - ) Value { - return fromCVal(c.JS_NewCClosure( - ctx.cval(), - cfunc.wrapClosure(T, func), - name.ptr, - if (finalizer) |f| cfunc.wrapClosureFinalizer(T, f) else null, - length, - magic, - opaquepkg.toC(T, userdata), - )); - } - - // ----------------------------------------------------------------------- - // ArrayBuffer & TypedArray Constructors - // ----------------------------------------------------------------------- - - /// Creates an ArrayBuffer with the given data. - /// - /// The buffer takes ownership of the data. When the buffer is garbage - /// collected, free_func will be called (if provided) to free the data. - /// - /// C: `JS_NewArrayBuffer` - pub fn initArrayBuffer( - ctx: *Context, - comptime T: type, - buf: []u8, - comptime free_func: ?typed_array.FreeBufferDataFunc(T), - userdata: Opaque(T), - is_shared: bool, - ) Value { - return fromCVal(c.JS_NewArrayBuffer( - ctx.cval(), - buf.ptr, - buf.len, - if (free_func) |f| typed_array.wrapFreeBufferDataFunc(T, f) else null, - opaquepkg.toC(T, userdata), - is_shared, - )); - } - - /// Creates an ArrayBuffer by copying the given data. - /// - /// C: `JS_NewArrayBufferCopy` - pub fn initArrayBufferCopy(ctx: *Context, buf: []const u8) Value { - return fromCVal(c.JS_NewArrayBufferCopy(ctx.cval(), buf.ptr, buf.len)); - } - - /// Creates a typed array of the specified type. - /// - /// The args should contain constructor arguments (e.g., length or ArrayBuffer). - /// - /// C: `JS_NewTypedArray` - pub fn initTypedArray( - ctx: *Context, - args: []const Value, - array_type: typed_array.Type, - ) Value { - return fromCVal(c.JS_NewTypedArray( - ctx.cval(), - @intCast(args.len), - @ptrCast(@constCast(args.ptr)), - @intFromEnum(array_type), - )); - } - - /// Creates a Uint8Array with the given data. - /// - /// The buffer takes ownership of the data. When the array is garbage - /// collected, free_func will be called (if provided) to free the data. - /// - /// C: `JS_NewUint8Array` - pub fn initUint8Array( - ctx: *Context, - buf: []u8, - free_func: ?typed_array.FreeBufferDataFunc, - opaque_ptr: ?*anyopaque, - is_shared: bool, - ) Value { - return fromCVal(c.JS_NewUint8Array( - ctx.cval(), - buf.ptr, - buf.len, - @ptrCast(free_func), - opaque_ptr, - is_shared, - )); - } - - /// Creates a Uint8Array by copying the given data. - /// - /// C: `JS_NewUint8ArrayCopy` - pub fn initUint8ArrayCopy(ctx: *Context, buf: []const u8) Value { - return fromCVal(c.JS_NewUint8ArrayCopy(ctx.cval(), buf.ptr, buf.len)); - } - - // ----------------------------------------------------------------------- - // Reference Counting - // ----------------------------------------------------------------------- - - /// Duplicates the value, incrementing its reference count. - /// - /// The returned value must also be freed with `deinit`. - /// - /// C: `JS_DupValue` - pub fn dup(self: Value, ctx: *Context) Value { - return fromCVal(c.JS_DupValue(ctx.cval(), self.cval())); - } - - /// Duplicates the value using the runtime, incrementing its reference count. - /// - /// The returned value must also be freed with `deinitRT`. - /// - /// C: `JS_DupValueRT` - pub fn dupRT(self: Value, rt: *Runtime) Value { - return fromCVal(c.JS_DupValueRT(rt.cval(), self.cval())); - } - - /// Frees the JavaScript value. - /// - /// This decrements the reference count and frees the value if it - /// reaches zero. Must be called for values returned from `eval`, - /// property getters, and other functions that return owned values. - /// - /// C: `JS_FreeValue` - pub fn deinit(self: Value, ctx: *Context) void { - c.JS_FreeValue(ctx.cval(), self.cval()); - } - - /// Frees the JavaScript value using the runtime. - /// - /// C: `JS_FreeValueRT` - pub fn deinitRT(self: Value, rt: *Runtime) void { - c.JS_FreeValueRT(rt.cval(), self.cval()); - } - - // ----------------------------------------------------------------------- - // Type Predicates - Check JavaScript value types - // ----------------------------------------------------------------------- - - /// Checks if the value is `null`. - /// - /// C: `JS_IsNull` - pub fn isNull(self: Value) bool { - return c.JS_IsNull(self.cval()); - } - - /// Checks if the value is `undefined`. - /// - /// C: `JS_IsUndefined` - pub fn isUndefined(self: Value) bool { - return c.JS_IsUndefined(self.cval()); - } - - /// Checks if the value is a boolean. - /// - /// C: `JS_IsBool` - pub fn isBool(self: Value) bool { - return c.JS_IsBool(self.cval()); - } - - /// Checks if the value is a number (integer or float). - /// - /// C: `JS_IsNumber` - pub fn isNumber(self: Value) bool { - return c.JS_IsNumber(self.cval()); - } - - /// Checks if the value is a BigInt. - /// - /// C: `JS_IsBigInt` - pub fn isBigInt(self: Value) bool { - return c.JS_IsBigInt(self.cval()); - } - - /// Checks if the value is a string. - /// - /// C: `JS_IsString` - pub fn isString(self: Value) bool { - return c.JS_IsString(self.cval()); - } - - /// Checks if the value is a symbol. - /// - /// C: `JS_IsSymbol` - pub fn isSymbol(self: Value) bool { - return c.JS_IsSymbol(self.cval()); - } - - /// Checks if the value is an object. - /// - /// C: `JS_IsObject` - pub fn isObject(self: Value) bool { - return c.JS_IsObject(self.cval()); - } - - /// Checks if the value is an exception. - /// - /// C: `JS_IsException` - pub fn isException(self: Value) bool { - return c.JS_IsException(self.cval()); - } - - /// Checks if the value is uninitialized. - /// - /// C: `JS_IsUninitialized` - pub fn isUninitialized(self: Value) bool { - return c.JS_IsUninitialized(self.cval()); - } - - /// Checks if the value is a module. - /// - /// C: `JS_IsModule` - pub fn isModule(self: Value) bool { - return c.JS_IsModule(self.cval()); - } - - /// Resolves module dependencies. - /// - /// Call this after reading a module with `JS_ReadObject` to load its - /// dependencies before evaluation. - /// - /// Returns error if resolution fails. - /// - /// C: `JS_ResolveModule` - pub fn resolveModule(self: Value, ctx: *Context) !void { - if (c.JS_ResolveModule(ctx.cval(), self.cval()) < 0) { - return error.ModuleResolutionFailed; - } - } - - /// Checks if the value is an array. - /// - /// C: `JS_IsArray` - pub fn isArray(self: Value) bool { - return c.JS_IsArray(self.cval()); - } - - /// Checks if the value is a Proxy. - /// - /// C: `JS_IsProxy` - pub fn isProxy(self: Value) bool { - return c.JS_IsProxy(self.cval()); - } - - /// Checks if the value is a Date. - /// - /// C: `JS_IsDate` - pub fn isDate(self: Value) bool { - return c.JS_IsDate(self.cval()); - } - - /// Checks if the value is a Promise. - /// - /// C: `JS_IsPromise` - pub fn isPromise(self: Value) bool { - return c.JS_IsPromise(self.cval()); - } - - /// Checks if the value is an Error object. - /// - /// C: `JS_IsError` - pub fn isError(self: Value) bool { - return c.JS_IsError(self.cval()); - } - - /// Checks if the value is an uncatchable error. - /// - /// C: `JS_IsUncatchableError` - pub fn isUncatchableError(self: Value) bool { - return c.JS_IsUncatchableError(self.cval()); - } - - /// Checks if the value is an ArrayBuffer. - /// - /// C: `JS_IsArrayBuffer` - pub fn isArrayBuffer(self: Value) bool { - return c.JS_IsArrayBuffer(self.cval()); - } - - /// Checks if the value is a RegExp. - /// - /// C: `JS_IsRegExp` - pub fn isRegExp(self: Value) bool { - return c.JS_IsRegExp(self.cval()); - } - - /// Checks if the value is a Map. - /// - /// C: `JS_IsMap` - pub fn isMap(self: Value) bool { - return c.JS_IsMap(self.cval()); - } - - /// Checks if the value is a Set. - /// - /// C: `JS_IsSet` - pub fn isSet(self: Value) bool { - return c.JS_IsSet(self.cval()); - } - - /// Checks if the value is a WeakRef. - /// - /// C: `JS_IsWeakRef` - pub fn isWeakRef(self: Value) bool { - return c.JS_IsWeakRef(self.cval()); - } - - /// Checks if the value is a WeakSet. - /// - /// C: `JS_IsWeakSet` - pub fn isWeakSet(self: Value) bool { - return c.JS_IsWeakSet(self.cval()); - } - - /// Checks if the value is a WeakMap. - /// - /// C: `JS_IsWeakMap` - pub fn isWeakMap(self: Value) bool { - return c.JS_IsWeakMap(self.cval()); - } - - /// Checks if the value is a DataView. - /// - /// C: `JS_IsDataView` - pub fn isDataView(self: Value) bool { - return c.JS_IsDataView(self.cval()); - } - - /// Checks if the value is a function. - /// - /// Requires context because this checks internal object state. - /// - /// C: `JS_IsFunction` - pub fn isFunction(self: Value, ctx: *Context) bool { - return c.JS_IsFunction(ctx.cval(), self.cval()); - } - - /// Checks if the value is a constructor. - /// - /// Requires context because this checks internal object state. - /// - /// C: `JS_IsConstructor` - pub fn isConstructor(self: Value, ctx: *Context) bool { - return c.JS_IsConstructor(ctx.cval(), self.cval()); - } - - // ----------------------------------------------------------------------- - // ArrayBuffer & TypedArray Operations - // ----------------------------------------------------------------------- - - /// Detaches the ArrayBuffer, making it unusable. - /// - /// After detaching, the buffer's byteLength becomes 0 and any views - /// on it become unusable. - /// - /// C: `JS_DetachArrayBuffer` - pub fn detachArrayBuffer(self: Value, ctx: *Context) void { - c.JS_DetachArrayBuffer(ctx.cval(), self.cval()); - } - - /// Gets the underlying data of an ArrayBuffer. - /// - /// Returns null if the value is not an ArrayBuffer or is detached. - /// - /// C: `JS_GetArrayBuffer` - pub fn getArrayBuffer(self: Value, ctx: *Context) ?[]u8 { - var size: usize = 0; - const ptr = c.JS_GetArrayBuffer(ctx.cval(), &size, self.cval()); - if (ptr == null) return null; - return ptr[0..size]; - } - - /// Gets the underlying data of a Uint8Array. - /// - /// Returns null if the value is not a Uint8Array or its buffer is detached. - /// - /// C: `JS_GetUint8Array` - pub fn getUint8Array(self: Value, ctx: *Context) ?[]u8 { - var size: usize = 0; - const ptr = c.JS_GetUint8Array(ctx.cval(), &size, self.cval()); - if (ptr == null) return null; - return ptr[0..size]; - } - - /// Gets the underlying ArrayBuffer of a typed array. - /// - /// Returns the buffer value along with offset, length, and element size info. - /// The returned buffer value must be freed with deinit. - /// - /// C: `JS_GetTypedArrayBuffer` - pub fn getTypedArrayBuffer(self: Value, ctx: *Context) ?typed_array.Buffer { - var byte_offset: usize = 0; - var byte_length: usize = 0; - var bytes_per_element: usize = 0; - const buffer = fromCVal(c.JS_GetTypedArrayBuffer( - ctx.cval(), - self.cval(), - &byte_offset, - &byte_length, - &bytes_per_element, - )); - if (buffer.isException()) return null; - return .{ - .value = buffer, - .byte_offset = byte_offset, - .byte_length = byte_length, - .bytes_per_element = bytes_per_element, - }; - } - - /// Gets the type of a typed array. - /// - /// Returns null if the value is not a typed array. - /// - /// C: `JS_GetTypedArrayType` - pub fn getTypedArrayType(self: Value) ?typed_array.Type { - const result = c.JS_GetTypedArrayType(self.cval()); - if (result < 0) return null; - return @enumFromInt(@as(c_uint, @intCast(result))); - } - - // ----------------------------------------------------------------------- - // Type Conversions - Convert JavaScript values to native types - // ----------------------------------------------------------------------- - - /// Converts the value to a boolean. - /// - /// C: `JS_ToBool` - pub fn toBool(self: Value, ctx: *Context) error{JSError}!bool { - const result = c.JS_ToBool(ctx.cval(), self.cval()); - if (result < 0) return error.JSError; - return result != 0; - } - - /// Converts the value to a 32-bit integer. - /// - /// Returns an error if the value cannot be converted to an integer. - /// - /// C: `JS_ToInt32` - pub fn toInt32(self: Value, ctx: *Context) error{JSError}!i32 { - var result: i32 = undefined; - const ret = c.JS_ToInt32(ctx.cval(), &result, self.cval()); - if (ret != 0) return error.JSError; - return result; - } - - /// Converts the value to an unsigned 32-bit integer. - /// - /// Returns an error if the value cannot be converted. - /// - /// C: `JS_ToUint32` - pub fn toUint32(self: Value, ctx: *Context) error{JSError}!u32 { - var result: u32 = undefined; - const ret = c.JS_ToUint32(ctx.cval(), &result, self.cval()); - if (ret != 0) return error.JSError; - return result; - } - - /// Converts the value to a 64-bit integer. - /// - /// C: `JS_ToInt64` - pub fn toInt64(self: Value, ctx: *Context) error{JSError}!i64 { - var result: i64 = undefined; - const ret = c.JS_ToInt64(ctx.cval(), &result, self.cval()); - if (ret != 0) return error.JSError; - return result; - } - - /// Converts the value to an array/string index. - /// - /// C: `JS_ToIndex` - pub fn toIndex(self: Value, ctx: *Context) error{JSError}!u64 { - var result: u64 = undefined; - const ret = c.JS_ToIndex(ctx.cval(), &result, self.cval()); - if (ret != 0) return error.JSError; - return result; - } - - /// Converts the value to a 64-bit float. - /// - /// C: `JS_ToFloat64` - pub fn toFloat64(self: Value, ctx: *Context) error{JSError}!f64 { - var result: f64 = undefined; - const ret = c.JS_ToFloat64(ctx.cval(), &result, self.cval()); - if (ret != 0) return error.JSError; - return result; - } - - /// Converts the value to a BigInt as i64. - /// - /// Returns an error if the value is not a BigInt. - /// - /// C: `JS_ToBigInt64` - pub fn toBigInt64(self: Value, ctx: *Context) error{JSError}!i64 { - var result: i64 = undefined; - const ret = c.JS_ToBigInt64(ctx.cval(), &result, self.cval()); - if (ret != 0) return error.JSError; - return result; - } - - /// Converts the value to a BigInt as u64. - /// - /// Returns an error if the value is not a BigInt. - /// - /// C: `JS_ToBigUint64` - pub fn toBigUint64(self: Value, ctx: *Context) error{JSError}!u64 { - var result: u64 = undefined; - const ret = c.JS_ToBigUint64(ctx.cval(), &result, self.cval()); - if (ret != 0) return error.JSError; - return result; - } - - /// Converts the value to a JavaScript Number. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_ToNumber` - pub fn toNumber(self: Value, ctx: *Context) Value { - return fromCVal(c.JS_ToNumber(ctx.cval(), self.cval())); - } - - /// Converts the value to a JavaScript String. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_ToString` - pub fn toStringValue(self: Value, ctx: *Context) Value { - return fromCVal(c.JS_ToString(ctx.cval(), self.cval())); - } - - /// Converts the value to a JavaScript Object. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_ToObject` - pub fn toObject(self: Value, ctx: *Context) Value { - return fromCVal(c.JS_ToObject(ctx.cval(), self.cval())); - } - - /// Converts the value to a property key. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_ToPropertyKey` - pub fn toPropertyKey(self: Value, ctx: *Context) Value { - return fromCVal(c.JS_ToPropertyKey(ctx.cval(), self.cval())); - } - - /// Converts the value to a Zig string slice. - /// - /// Returns null if the conversion fails. The returned slice must be - /// freed by passing the pointer to `Context.freeCString`. - /// - /// C: `JS_ToCStringLen` - pub fn toZigSlice(self: Value, ctx: *Context) ?[:0]const u8 { - const result = self.toCStringLen(ctx) orelse return null; - return result.ptr[0..result.len :0]; - } - - /// Converts the value to a C string with length. - /// - /// Returns null if the conversion fails. The returned pointer must be - /// freed with `Context.freeCString`. - /// - /// C: `JS_ToCStringLen` - pub fn toCStringLen(self: Value, ctx: *Context) ?struct { ptr: [*:0]const u8, len: usize } { - var len: usize = 0; - const ptr = c.JS_ToCStringLen(ctx.cval(), &len, self.cval()); - if (ptr == null) return null; - return .{ .ptr = ptr, .len = len }; - } - - /// Converts the value to a null-terminated C string. - /// - /// Returns null if the conversion fails. The returned pointer must be - /// freed with `Context.freeCString`. - /// - /// C: `JS_ToCString` - pub fn toCString(self: Value, ctx: *Context) ?[*:0]const u8 { - return c.JS_ToCString(ctx.cval(), self.cval()); - } - - // ----------------------------------------------------------------------- - // Property Access - // ----------------------------------------------------------------------- - - /// Gets a property by atom. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_GetProperty` - pub fn getProperty(self: Value, ctx: *Context, prop: Atom) Value { - return fromCVal(c.JS_GetProperty(ctx.cval(), self.cval(), @intFromEnum(prop))); - } - - /// Gets a property by string name. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_GetPropertyStr` - pub fn getPropertyStr(self: Value, ctx: *Context, name: [*:0]const u8) Value { - return fromCVal(c.JS_GetPropertyStr(ctx.cval(), self.cval(), name)); - } - - /// Gets a property by integer index. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_GetPropertyUint32` - pub fn getPropertyUint32(self: Value, ctx: *Context, idx: u32) Value { - return fromCVal(c.JS_GetPropertyUint32(ctx.cval(), self.cval(), idx)); - } - - /// Gets a property by 64-bit integer index. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_GetPropertyInt64` - pub fn getPropertyInt64(self: Value, ctx: *Context, idx: i64) Value { - return fromCVal(c.JS_GetPropertyInt64(ctx.cval(), self.cval(), idx)); - } - - /// Sets a property by atom. - /// - /// Takes ownership of the value. - /// - /// C: `JS_SetProperty` - pub fn setProperty(self: Value, ctx: *Context, prop: Atom, val: Value) error{JSError}!void { - const ret = c.JS_SetProperty(ctx.cval(), self.cval(), @intFromEnum(prop), val.cval()); - if (ret < 0) return error.JSError; - } - - /// Sets a property by string name. - /// - /// Takes ownership of the value. - /// - /// C: `JS_SetPropertyStr` - pub fn setPropertyStr(self: Value, ctx: *Context, name: [*:0]const u8, val: Value) error{JSError}!void { - const ret = c.JS_SetPropertyStr(ctx.cval(), self.cval(), name, val.cval()); - if (ret < 0) return error.JSError; - } - - /// Sets a property by integer index. - /// - /// Takes ownership of the value. - /// - /// C: `JS_SetPropertyUint32` - pub fn setPropertyUint32(self: Value, ctx: *Context, idx: u32, val: Value) error{JSError}!void { - const ret = c.JS_SetPropertyUint32(ctx.cval(), self.cval(), idx, val.cval()); - if (ret < 0) return error.JSError; - } - - /// Sets a property by 64-bit integer index. - /// - /// Takes ownership of the value. - /// - /// C: `JS_SetPropertyInt64` - pub fn setPropertyInt64(self: Value, ctx: *Context, idx: i64, val: Value) error{JSError}!void { - const ret = c.JS_SetPropertyInt64(ctx.cval(), self.cval(), idx, val.cval()); - if (ret < 0) return error.JSError; - } - - /// Checks if the object has the specified property. - /// - /// C: `JS_HasProperty` (via string) - pub fn hasPropertyStr(self: Value, ctx: *Context, name: [*:0]const u8) error{JSError}!bool { - const atom = c.JS_NewAtom(ctx.cval(), name); - defer c.JS_FreeAtom(ctx.cval(), atom); - const ret = c.JS_HasProperty(ctx.cval(), self.cval(), atom); - if (ret < 0) return error.JSError; - return ret != 0; - } - - /// Deletes a property by string name. - /// - /// C: `JS_DeleteProperty` - pub fn deletePropertyStr(self: Value, ctx: *Context, name: [*:0]const u8) error{JSError}!bool { - const atom = c.JS_NewAtom(ctx.cval(), name); - defer c.JS_FreeAtom(ctx.cval(), atom); - const ret = c.JS_DeleteProperty(ctx.cval(), self.cval(), atom, 0); - if (ret < 0) return error.JSError; - return ret != 0; - } - - /// Gets the prototype of an object. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_GetPrototype` - pub fn getPrototype(self: Value, ctx: *Context) Value { - return fromCVal(c.JS_GetPrototype(ctx.cval(), self.cval())); - } - - /// Sets the prototype of an object. - /// - /// C: `JS_SetPrototype` - pub fn setPrototype(self: Value, ctx: *Context, proto: Value) error{JSError}!void { - const ret = c.JS_SetPrototype(ctx.cval(), self.cval(), proto.cval()); - if (ret < 0) return error.JSError; - } - - /// Sets the constructor for a function object. - /// - /// This links a constructor function with its prototype object. - /// After calling this, instances created with `new func()` will have - /// `proto` as their prototype, and `proto.constructor` will be set to `func`. - /// - /// C: `JS_SetConstructor` - pub fn setConstructor(self: Value, ctx: *Context, proto: Value) void { - c.JS_SetConstructor(ctx.cval(), self.cval(), proto.cval()); - } - - /// Sets the constructor bit on a function object. - /// - /// This controls whether the function can be used as a constructor with `new`. - /// - /// C: `JS_SetConstructorBit` - pub fn setConstructorBit(self: Value, ctx: *Context, val: bool) bool { - return c.JS_SetConstructorBit(ctx.cval(), self.cval(), val); - } - - /// Defines multiple properties on an object from a function list. - /// - /// This is efficient for defining many properties at once (functions, getters/setters, constants). - /// - /// C: `JS_SetPropertyFunctionList` - pub fn setPropertyFunctionList( - self: Value, - ctx: *Context, - list: []const cfunc.FunctionListEntry, - ) error{JSError}!void { - const ret = c.JS_SetPropertyFunctionList( - ctx.cval(), - self.cval(), - @ptrCast(list.ptr), - @intCast(list.len), - ); - if (ret < 0) return error.JSError; - } - - /// Gets the length property of an array or array-like object. - /// - /// C: `JS_GetLength` - pub fn getLength(self: Value, ctx: *Context) error{JSError}!i64 { - var result: i64 = undefined; - const ret = c.JS_GetLength(ctx.cval(), self.cval(), &result); - if (ret < 0) return error.JSError; - return result; - } - - /// Sets the length property of an array or array-like object. - /// - /// C: `JS_SetLength` - pub fn setLength(self: Value, ctx: *Context, len: i64) error{JSError}!void { - const ret = c.JS_SetLength(ctx.cval(), self.cval(), len); - if (ret < 0) return error.JSError; - } - - /// Checks if the object is extensible. - /// - /// C: `JS_IsExtensible` - pub fn isExtensible(self: Value, ctx: *Context) error{JSError}!bool { - const ret = c.JS_IsExtensible(ctx.cval(), self.cval()); - if (ret < 0) return error.JSError; - return ret != 0; - } - - /// Prevents any extensions to the object. - /// - /// C: `JS_PreventExtensions` - pub fn preventExtensions(self: Value, ctx: *Context) error{JSError}!void { - const ret = c.JS_PreventExtensions(ctx.cval(), self.cval()); - if (ret < 0) return error.JSError; - } - - /// Seals the object (prevents adding new properties and marks existing ones non-configurable). - /// - /// C: `JS_SealObject` - pub fn seal(self: Value, ctx: *Context) error{JSError}!void { - const ret = c.JS_SealObject(ctx.cval(), self.cval()); - if (ret < 0) return error.JSError; - } - - /// Freezes the object (seals it and makes all properties non-writable). - /// - /// C: `JS_FreezeObject` - pub fn freeze(self: Value, ctx: *Context) error{JSError}!void { - const ret = c.JS_FreezeObject(ctx.cval(), self.cval()); - if (ret < 0) return error.JSError; - } - - // ----------------------------------------------------------------------- - // Property Definition - // ----------------------------------------------------------------------- - - /// Defines a property with full control over attributes. - /// - /// This is the most general form of property definition. Use the simpler - /// definePropertyValue or definePropertyGetSet for common cases. - /// - /// C: `JS_DefineProperty` - pub fn defineProperty( - self: Value, - ctx: *Context, - prop: Atom, - val: Value, - getter: Value, - setter: Value, - flags: PropertyFlags, - ) error{JSError}!bool { - const ret = c.JS_DefineProperty( - ctx.cval(), - self.cval(), - @intFromEnum(prop), - val.cval(), - getter.cval(), - setter.cval(), - flags.toInt(), - ); - if (ret < 0) return error.JSError; - return ret != 0; - } - - /// Defines a data property with the given value and flags. - /// - /// Takes ownership of the value. - /// - /// C: `JS_DefinePropertyValue` - pub fn definePropertyValue( - self: Value, - ctx: *Context, - prop: Atom, - val: Value, - flags: PropertyFlags, - ) error{JSError}!bool { - const ret = c.JS_DefinePropertyValue( - ctx.cval(), - self.cval(), - @intFromEnum(prop), - val.cval(), - flags.toInt(), - ); - if (ret < 0) return error.JSError; - return ret != 0; - } - - /// Defines a data property at an integer index with the given value and flags. - /// - /// Takes ownership of the value. - /// - /// C: `JS_DefinePropertyValueUint32` - pub fn definePropertyValueUint32( - self: Value, - ctx: *Context, - idx: u32, - val: Value, - flags: PropertyFlags, - ) error{JSError}!bool { - const ret = c.JS_DefinePropertyValueUint32( - ctx.cval(), - self.cval(), - idx, - val.cval(), - flags.toInt(), - ); - if (ret < 0) return error.JSError; - return ret != 0; - } - - /// Defines a data property by string name with the given value and flags. - /// - /// Takes ownership of the value. - /// - /// C: `JS_DefinePropertyValueStr` - pub fn definePropertyValueStr( - self: Value, - ctx: *Context, - name: [*:0]const u8, - val: Value, - flags: PropertyFlags, - ) error{JSError}!bool { - const ret = c.JS_DefinePropertyValueStr( - ctx.cval(), - self.cval(), - name, - val.cval(), - flags.toInt(), - ); - if (ret < 0) return error.JSError; - return ret != 0; - } - - /// Defines an accessor property with getter and/or setter. - /// - /// Takes ownership of getter and setter values. - /// - /// C: `JS_DefinePropertyGetSet` - pub fn definePropertyGetSet( - self: Value, - ctx: *Context, - prop: Atom, - getter: Value, - setter: Value, - flags: PropertyFlags, - ) error{JSError}!bool { - const ret = c.JS_DefinePropertyGetSet( - ctx.cval(), - self.cval(), - @intFromEnum(prop), - getter.cval(), - setter.cval(), - flags.toInt(), - ); - if (ret < 0) return error.JSError; - return ret != 0; - } - - /// Gets the own property names of an object. - /// - /// Returns an iterator over the property names. The iterator must be - /// freed with `freePropertyEnum` when done. - /// - /// C: `JS_GetOwnPropertyNames` - pub fn getOwnPropertyNames( - self: Value, - ctx: *Context, - flags: GetPropertyNamesFlags, - ) error{JSError}![]const PropertyEnum { - var tab: [*]PropertyEnum = undefined; - var len: u32 = 0; - const ret = c.JS_GetOwnPropertyNames( - ctx.cval(), - @ptrCast(&tab), - &len, - self.cval(), - flags.toInt(), - ); - if (ret < 0) return error.JSError; - return tab[0..len]; - } - - /// Frees a property enum slice returned by `getOwnPropertyNames`. - /// - /// C: `JS_FreePropertyEnum` - pub fn freePropertyEnum(ctx: *Context, props: []const PropertyEnum) void { - c.JS_FreePropertyEnum(ctx.cval(), @ptrCast(@constCast(props.ptr)), @intCast(props.len)); - } - - /// Gets the own property descriptor for a property. - /// - /// Returns null if the property does not exist. - /// The returned descriptor's values must be freed with deinit. - /// - /// C: `JS_GetOwnProperty` - pub fn getOwnProperty( - self: Value, - ctx: *Context, - prop: Atom, - ) error{JSError}!?PropertyDescriptor { - var desc: PropertyDescriptor = undefined; - const ret = c.JS_GetOwnProperty(ctx.cval(), @ptrCast(&desc), self.cval(), @intFromEnum(prop)); - if (ret < 0) return error.JSError; - if (ret == 0) return null; - return desc; - } - - // ----------------------------------------------------------------------- - // Comparison - // ----------------------------------------------------------------------- - - /// Checks if two values are equal (using JavaScript `==`). - /// - /// C: `JS_IsEqual` - pub fn isEqual(self: Value, ctx: *Context, other: Value) error{JSError}!bool { - const ret = c.JS_IsEqual(ctx.cval(), self.cval(), other.cval()); - if (ret < 0) return error.JSError; - return ret != 0; - } - - /// Checks if two values are strictly equal (using JavaScript `===`). - /// - /// C: `JS_IsStrictEqual` - pub fn isStrictEqual(self: Value, ctx: *Context, other: Value) bool { - return c.JS_IsStrictEqual(ctx.cval(), self.cval(), other.cval()); - } - - /// Checks if two values are the same value (Object.is semantics). - /// - /// C: `JS_IsSameValue` - pub fn isSameValue(self: Value, ctx: *Context, other: Value) bool { - return c.JS_IsSameValue(ctx.cval(), self.cval(), other.cval()); - } - - /// Checks if two values are the same value, treating +0 and -0 as equal. - /// - /// C: `JS_IsSameValueZero` - pub fn isSameValueZero(self: Value, ctx: *Context, other: Value) bool { - return c.JS_IsSameValueZero(ctx.cval(), self.cval(), other.cval()); - } - - /// Checks if this value is an instance of a constructor. - /// - /// C: `JS_IsInstanceOf` - pub fn isInstanceOf(self: Value, ctx: *Context, obj: Value) error{JSError}!bool { - const ret = c.JS_IsInstanceOf(ctx.cval(), self.cval(), obj.cval()); - if (ret < 0) return error.JSError; - return ret != 0; - } - - // ----------------------------------------------------------------------- - // Function Calls - // ----------------------------------------------------------------------- - - /// Calls a function with the given this value and arguments. - /// - /// Returns a new Value that must be freed. Check `isException` for errors. - /// - /// C: `JS_Call` - pub fn call(self: Value, ctx: *Context, this: Value, args: []const Value) Value { - return fromCVal(c.JS_Call( - ctx.cval(), - self.cval(), - this.cval(), - @intCast(args.len), - @ptrCast(@constCast(args.ptr)), - )); - } - - /// Calls a constructor function with the given arguments. - /// - /// Returns a new Value that must be freed. Check `isException` for errors. - /// - /// C: `JS_CallConstructor` - pub fn callConstructor(self: Value, ctx: *Context, args: []const Value) Value { - return fromCVal(c.JS_CallConstructor( - ctx.cval(), - self.cval(), - @intCast(args.len), - @ptrCast(args.ptr), - )); - } - - /// Calls a constructor function with a custom `new.target`. - /// - /// This allows specifying a different `new.target` value than the - /// constructor function itself. - /// - /// Returns a new Value that must be freed. Check `isException` for errors. - /// - /// C: `JS_CallConstructor2` - pub fn callConstructor2(self: Value, ctx: *Context, new_target: Value, args: []const Value) Value { - return fromCVal(c.JS_CallConstructor2( - ctx.cval(), - self.cval(), - new_target.cval(), - @intCast(args.len), - @ptrCast(@constCast(args.ptr)), - )); - } - - /// Invokes a method on this value by atom. - /// - /// This is equivalent to `this[method_name](...args)` in JavaScript. - /// - /// Returns a new Value that must be freed. Check `isException` for errors. - /// - /// C: `JS_Invoke` - pub fn invoke(self: Value, ctx: *Context, method: Atom, args: []const Value) Value { - return fromCVal(c.JS_Invoke( - ctx.cval(), - self.cval(), - @intFromEnum(method), - @intCast(args.len), - @ptrCast(@constCast(args.ptr)), - )); - } - - // ----------------------------------------------------------------------- - // JSON - // ----------------------------------------------------------------------- - - /// Parses a JSON string into a JavaScript value. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_ParseJSON` - pub fn parseJSON(ctx: *Context, buf: []const u8, filename: [*:0]const u8) Value { - return fromCVal(c.JS_ParseJSON(ctx.cval(), buf.ptr, buf.len, filename)); - } - - /// Converts a JavaScript value to a JSON string. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_JSONStringify` - pub fn jsonStringify(self: Value, ctx: *Context, replacer: Value, space: Value) Value { - return fromCVal(c.JS_JSONStringify(ctx.cval(), self.cval(), replacer.cval(), space.cval())); - } - - // ----------------------------------------------------------------------- - // Proxy - // ----------------------------------------------------------------------- - - /// Creates a new Proxy object. - /// - /// C: `JS_NewProxy` - pub fn initProxy(ctx: *Context, target: Value, handler: Value) Value { - return fromCVal(c.JS_NewProxy(ctx.cval(), target.cval(), handler.cval())); - } - - /// Gets the target of a Proxy object. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_GetProxyTarget` - pub fn getProxyTarget(self: Value, ctx: *Context) Value { - return fromCVal(c.JS_GetProxyTarget(ctx.cval(), self.cval())); - } - - /// Gets the handler of a Proxy object. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_GetProxyHandler` - pub fn getProxyHandler(self: Value, ctx: *Context) Value { - return fromCVal(c.JS_GetProxyHandler(ctx.cval(), self.cval())); - } - - // ----------------------------------------------------------------------- - // Exceptions - // ----------------------------------------------------------------------- - - /// Throws this value as an exception. - /// - /// Takes ownership of the value. - /// - /// C: `JS_Throw` - pub fn throw(self: Value, ctx: *Context) Value { - return fromCVal(c.JS_Throw(ctx.cval(), self.cval())); - } - - /// Marks this error as uncatchable. - /// - /// C: `JS_SetUncatchableError` - pub fn setUncatchableError(self: Value, ctx: *Context) void { - c.JS_SetUncatchableError(ctx.cval(), self.cval()); - } - - /// Clears the uncatchable flag on this error. - /// - /// C: `JS_ClearUncatchableError` - pub fn clearUncatchableError(self: Value, ctx: *Context) void { - c.JS_ClearUncatchableError(ctx.cval(), self.cval()); - } - - // ----------------------------------------------------------------------- - // Promise - // ----------------------------------------------------------------------- - - /// Gets the state of a promise. - /// - /// C: `JS_PromiseState` - pub fn promiseState(self: Value, ctx: *Context) PromiseState { - return @enumFromInt(c.JS_PromiseState(ctx.cval(), self.cval())); - } - - /// Gets the result of a fulfilled or rejected promise. - /// - /// Returns a new Value that must be freed. - /// - /// C: `JS_PromiseResult` - pub fn promiseResult(self: Value, ctx: *Context) Value { - return fromCVal(c.JS_PromiseResult(ctx.cval(), self.cval())); - } - - /// A promise with its resolve/reject capability functions. - pub const Promise = struct { - value: Value, - resolve: Value, - reject: Value, - - /// Deinitializes all three values. - pub fn deinit(self: Promise, ctx: *Context) void { - self.value.deinit(ctx); - self.resolve.deinit(ctx); - self.reject.deinit(ctx); - } - }; - - /// Creates a new Promise with its resolve/reject functions. - /// - /// Returns a Promise struct containing the promise value and its - /// resolve/reject functions. All three values must be freed when - /// no longer needed (or call `Promise.deinit` to free all at once). - /// - /// C: `JS_NewPromiseCapability` - pub fn initPromiseCapability(ctx: *Context) Promise { - var resolving_funcs: [2]Value = undefined; - const promise = fromCVal(c.JS_NewPromiseCapability(ctx.cval(), @ptrCast(&resolving_funcs))); - return .{ - .value = promise, - .resolve = resolving_funcs[0], - .reject = resolving_funcs[1], - }; - } - - // ----------------------------------------------------------------------- - // Class / Opaque data - // ----------------------------------------------------------------------- - - /// Gets the class ID of an object. - /// - /// Returns `class.Id.invalid` if not an object. - /// - /// C: `JS_GetClassID` - pub fn getClassId(self: Value) class.Id { - return @enumFromInt(c.JS_GetClassID(self.cval())); - } - - /// Sets opaque data on an object. - /// - /// Only works for custom class objects. Returns true on success. - /// - /// C: `JS_SetOpaque` - pub fn setOpaque(self: Value, opaque_ptr: ?*anyopaque) bool { - return c.JS_SetOpaque(self.cval(), opaque_ptr) == 0; - } - - /// Gets opaque data from an object. - /// - /// Returns null if the object is not of the expected class or has no opaque data. - /// - /// C: `JS_GetOpaque` - pub fn getOpaque(self: Value, comptime T: type, class_id: class.Id) ?*T { - return @ptrCast(@alignCast(c.JS_GetOpaque(self.cval(), @intFromEnum(class_id)))); - } - - /// Gets opaque data from an object with context validation. - /// - /// Throws a JavaScript exception if the object is not of the expected class. - /// - /// C: `JS_GetOpaque2` - pub fn getOpaque2(self: Value, ctx: *Context, comptime T: type, class_id: class.Id) ?*T { - return @ptrCast(@alignCast(c.JS_GetOpaque2(ctx.cval(), self.cval(), @intFromEnum(class_id)))); - } - - /// Gets opaque data from an object without knowing the class ID. - /// - /// Returns both the opaque pointer and the class ID of the object. - /// - /// C: `JS_GetAnyOpaque` - pub fn getAnyOpaque(self: Value, comptime T: type) struct { ptr: ?*T, class_id: class.Id } { - var raw_class_id: u32 = 0; - const ptr = c.JS_GetAnyOpaque(self.cval(), &raw_class_id); - return .{ - .ptr = @ptrCast(@alignCast(ptr)), - .class_id = @enumFromInt(raw_class_id), - }; - } - - // ----------------------------------------------------------------------- - // C Interop - // ----------------------------------------------------------------------- - - /// Initialize a Value from a C JSValue. - pub inline fn fromCVal(val: c.JSValue) Value { - return @bitCast(val); - } - - /// Get the underlying C JSValue representation. - pub inline fn cval(self: Value) c.JSValue { - return @bitCast(self); - } - - // ----------------------------------------------------------------------- - // NaN-boxing helpers (implementation details) - // - // On 32-bit platforms, QuickJS uses NaN-boxing to pack values into a u64: - // - Upper 32 bits: tag - // - Lower 32 bits: int32/bool value or pointer - // - Floats use a special encoding with JS_FLOAT64_TAG_ADDEND - // - // See quickjs.h under `#if defined(JS_NAN_BOXING)` for the C implementation. - // ----------------------------------------------------------------------- - - /// Constructs a nan-boxed value from a tag and 32-bit payload. - /// - /// C: `JS_MKVAL(tag, val)` macro in quickjs.h - fn mkval(t: Tag, val: i32) u64 { - const tag: u64 = @bitCast(@as(i64, @intFromEnum(t))); - return (tag << 32) | @as(u32, @bitCast(val)); - } - - /// Addend used for encoding floats in nan-boxed representation. - /// Floats are stored with their bits adjusted by this value to avoid - /// colliding with the tag space. - /// - /// C: `JS_FLOAT64_TAG_ADDEND` macro in quickjs.h - const float64_tag_addend: u64 = 0x7ff80000 -% @as(u64, @bitCast(@as(i64, c.JS_TAG_FIRST))) +% 1; - - /// Constructs a nan-boxed float64 value, normalizing NaN to a canonical form. - /// - /// C: `__JS_NewFloat64(double d)` function in quickjs.h - fn mkfloat64(val: f64) u64 { - const u: u64 = @bitCast(val); - const nan_val = 0x7ff8000000000000 -% (float64_tag_addend << 32); - if ((u & 0x7fffffffffffffff) > 0x7ff0000000000000) { - return nan_val; - } - return u -% (float64_tag_addend << 32); - } -}; - -/// Promise state enumeration. -pub const PromiseState = enum(c_int) { - not_a_promise = -1, - pending = 0, - fulfilled = 1, - rejected = 2, -}; - -/// Property type, encoded in the `tmask` field of `PropertyFlags`. -/// -/// C: `JS_PROP_NORMAL`, `JS_PROP_GETSET`, `JS_PROP_VARREF`, `JS_PROP_AUTOINIT` -pub const PropertyType = enum(u2) { - /// Regular data property with a value. - normal = 0, - /// Accessor property with getter and/or setter functions. - getset = 1, - /// Internal: references a closure variable. - varref = 2, - /// Internal: lazy-initialized property. - autoinit = 3, -}; - -/// Property attribute flags for defining and querying object properties. -/// -/// These flags correspond to the `JS_PROP_*` C constants in QuickJS. -/// Used with defineProperty* methods to control property attributes. -/// -/// ## Attribute Flags vs. HAS Flags -/// -/// The `has_*` flags indicate **whether an attribute is being specified**, while the base -/// flags contain the **actual value**. This mirrors `Object.defineProperty` behavior where -/// omitting an attribute leaves the existing value intact. For example, without -/// `has_configurable`, the engine doesn't know if you want `configurable: false` or if you -/// simply didn't specify it. -pub const PropertyFlags = packed struct(c_int) { - /// Property can be deleted and its attributes can be changed. - configurable: bool = false, - /// Property value can be modified (data properties only). - writable: bool = false, - /// Property appears in `for...in` loops and `Object.keys()`. - enumerable: bool = false, - /// Internal flag for Array `length` property (special setter behavior). - length: bool = false, - /// The property type (normal, getset, varref, or autoinit). - property_type: PropertyType = .normal, - _reserved1: u2 = 0, - /// When set, the `configurable` field value should be applied. - has_configurable: bool = false, - /// When set, the `writable` field value should be applied. - has_writable: bool = false, - /// When set, the `enumerable` field value should be applied. - has_enumerable: bool = false, - /// When set, a getter function is being provided. - has_get: bool = false, - /// When set, a setter function is being provided. - has_set: bool = false, - /// When set, a value is being provided. - has_value: bool = false, - /// Throw an exception instead of returning false on failure. - throw_flag: bool = false, - /// Throw an exception on failure only in strict mode. - throw_strict: bool = false, - /// Internal: don't add the property if it doesn't exist. - no_add: bool = false, - /// Internal: skip exotic object behavior. - no_exotic: bool = false, - /// Internal: called from `Object.defineProperty`. - define_property: bool = false, - /// Internal: called from `Reflect.defineProperty`. - reflect_define_property: bool = false, - _reserved2: u12 = 0, - - /// Standard data property flags (configurable, writable, enumerable). - /// - /// C: `JS_PROP_C_W_E` - pub const default: PropertyFlags = .{ - .configurable = true, - .writable = true, - .enumerable = true, - }; - - pub fn toInt(self: PropertyFlags) c_int { - return @bitCast(self); - } -}; - -/// Flags for `getOwnPropertyNames` to control which property keys are returned. -/// -/// These flags correspond to the `JS_GPN_*` C constants in QuickJS. -/// -/// ## Common Patterns -/// -/// - `Object.keys()`: `.enum_strings` (enumerable string keys only) -/// - `Object.getOwnPropertyNames()`: `.strings` (all string keys) -/// - `Object.getOwnPropertySymbols()`: `.symbols` (all symbol keys) -/// - `Reflect.ownKeys()`: `.all` (all keys: strings + symbols) -pub const GetPropertyNamesFlags = packed struct(c_int) { - /// Include string-keyed properties (normal property names). - string_mask: bool = false, - /// Include symbol-keyed properties. - symbol_mask: bool = false, - /// Include private class fields (internal use). - private_mask: bool = false, - _reserved1: u1 = 0, - /// Only include enumerable properties. - enum_only: bool = false, - /// Populate the `is_enumerable` field in returned `PropertyEnum` structs. - set_enum: bool = false, - _reserved2: u26 = 0, - - /// All string property keys. Equivalent to `Object.getOwnPropertyNames()`. - pub const strings: GetPropertyNamesFlags = .{ .string_mask = true }; - - /// All symbol property keys. Equivalent to `Object.getOwnPropertySymbols()`. - pub const symbols: GetPropertyNamesFlags = .{ .symbol_mask = true }; - - /// All property keys (strings and symbols). Equivalent to `Reflect.ownKeys()`. - pub const all: GetPropertyNamesFlags = .{ .string_mask = true, .symbol_mask = true }; - - /// Enumerable string keys only. Equivalent to `Object.keys()`. - pub const enum_strings: GetPropertyNamesFlags = .{ .string_mask = true, .enum_only = true }; - - pub fn toInt(self: GetPropertyNamesFlags) c_int { - return @bitCast(self); - } -}; - -/// Property descriptor returned by getOwnProperty. -/// -/// C: `JSPropertyDescriptor` -pub const PropertyDescriptor = extern struct { - flags: PropertyFlags, - value: Value, - getter: Value, - setter: Value, - - /// Frees all values in the descriptor. - pub fn deinit(self: *PropertyDescriptor, ctx: *Context) void { - self.value.deinit(ctx); - self.getter.deinit(ctx); - self.setter.deinit(ctx); - } -}; - -/// Property name entry returned by getOwnPropertyNames. -/// -/// C: `JSPropertyEnum` -pub const PropertyEnum = extern struct { - is_enumerable: bool, - atom: Atom, -}; - -comptime { - assert(@sizeOf(Value) == @sizeOf(c.JSValue)); - assert(@alignOf(Value) == @alignOf(c.JSValue)); - assert(@sizeOf(PropertyDescriptor) == @sizeOf(c.JSPropertyDescriptor)); - assert(@alignOf(PropertyDescriptor) == @alignOf(c.JSPropertyDescriptor)); - assert(@sizeOf(PropertyEnum) == @sizeOf(c.JSPropertyEnum)); - assert(@alignOf(PropertyEnum) == @alignOf(c.JSPropertyEnum)); -} - -test "constants match JavaScript values" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // Test null - const js_null = ctx.eval("null", "", .{}); - defer js_null.deinit(ctx); - try testing.expect(js_null.isNull()); - try testing.expect(Value.null.isNull()); - try testing.expect(js_null.isStrictEqual(ctx, Value.null)); - - // Test undefined - const js_undefined = ctx.eval("undefined", "", .{}); - defer js_undefined.deinit(ctx); - try testing.expect(js_undefined.isUndefined()); - try testing.expect(Value.undefined.isUndefined()); - try testing.expect(js_undefined.isStrictEqual(ctx, Value.undefined)); - - // Test true - const js_true = ctx.eval("true", "", .{}); - defer js_true.deinit(ctx); - try testing.expect(js_true.isBool()); - try testing.expect(try js_true.toBool(ctx)); - try testing.expect(try Value.true.toBool(ctx)); - try testing.expect(js_true.isStrictEqual(ctx, Value.true)); - - // Test false - const js_false = ctx.eval("false", "", .{}); - defer js_false.deinit(ctx); - try testing.expect(js_false.isBool()); - try testing.expect(!try js_false.toBool(ctx)); - try testing.expect(!try Value.false.toBool(ctx)); - try testing.expect(js_false.isStrictEqual(ctx, Value.false)); -} - -test "type predicates with JavaScript values" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // isNumber - const num = ctx.eval("42", "", .{}); - defer num.deinit(ctx); - try testing.expect(num.isNumber()); - - const float = ctx.eval("3.14", "", .{}); - defer float.deinit(ctx); - try testing.expect(float.isNumber()); - - // isString - const str = ctx.eval("'hello'", "", .{}); - defer str.deinit(ctx); - try testing.expect(str.isString()); - - // isArray - const arr = ctx.eval("[1, 2, 3]", "", .{}); - defer arr.deinit(ctx); - try testing.expect(arr.isArray()); - - // isObject (but not array) - const obj = ctx.eval("({a: 1})", "", .{}); - defer obj.deinit(ctx); - try testing.expect(obj.isObject()); - try testing.expect(!obj.isArray()); - - // isFunction - const func = ctx.eval("(function() {})", "", .{}); - defer func.deinit(ctx); - try testing.expect(func.isFunction(ctx)); - - // isBool - const boolean = ctx.eval("true", "", .{}); - defer boolean.deinit(ctx); - try testing.expect(boolean.isBool()); - - // isNull - const null_val = ctx.eval("null", "", .{}); - defer null_val.deinit(ctx); - try testing.expect(null_val.isNull()); - - // isUndefined - const undef = ctx.eval("undefined", "", .{}); - defer undef.deinit(ctx); - try testing.expect(undef.isUndefined()); - - // isDate - const date = ctx.eval("new Date()", "", .{}); - defer date.deinit(ctx); - try testing.expect(date.isDate()); - - // isRegExp - const regex = ctx.eval("/test/g", "", .{}); - defer regex.deinit(ctx); - try testing.expect(regex.isRegExp()); - - // isMap - const map = ctx.eval("new Map()", "", .{}); - defer map.deinit(ctx); - try testing.expect(map.isMap()); - - // isSet - const set = ctx.eval("new Set()", "", .{}); - defer set.deinit(ctx); - try testing.expect(set.isSet()); - - // isPromise - const promise = ctx.eval("new Promise(() => {})", "", .{}); - defer promise.deinit(ctx); - try testing.expect(promise.isPromise()); - - // isSymbol - const symbol = ctx.eval("Symbol('test')", "", .{}); - defer symbol.deinit(ctx); - try testing.expect(symbol.isSymbol()); - - // isBigInt - const bigint = ctx.eval("BigInt(9007199254740991)", "", .{}); - defer bigint.deinit(ctx); - try testing.expect(bigint.isBigInt()); - - // isError - const err = ctx.eval("new Error('test')", "", .{}); - defer err.deinit(ctx); - try testing.expect(err.isError()); -} - -test "constructors create valid JavaScript values" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const global = ctx.getGlobalObject(); - defer global.deinit(ctx); - - // initBool - const bool_val = Value.initBool(true); - try testing.expect(bool_val.isBool()); - try testing.expect(try bool_val.toBool(ctx)); - - // initInt32 - const int32_val = Value.initInt32(42); - try testing.expect(int32_val.isNumber()); - try testing.expectEqual(@as(i32, 42), try int32_val.toInt32(ctx)); - - // initFloat64 - const float_val = Value.initFloat64(3.14); - try testing.expect(float_val.isNumber()); - try testing.expectApproxEqAbs(@as(f64, 3.14), try float_val.toFloat64(ctx), 0.001); - - // initInt64 - fits in i32 - const int64_small = Value.initInt64(100); - try testing.expect(int64_small.isNumber()); - try testing.expectEqual(@as(i32, 100), try int64_small.toInt32(ctx)); - - // initInt64 - too large for i32, becomes float - const int64_large = Value.initInt64(std.math.maxInt(i32) + 1); - try testing.expect(int64_large.isNumber()); - - // initUint32 - fits in i32 - const uint32_small = Value.initUint32(100); - try testing.expect(uint32_small.isNumber()); - try testing.expectEqual(@as(i32, 100), try uint32_small.toInt32(ctx)); - - // initUint32 - too large for i32, becomes float - const uint32_large = Value.initUint32(std.math.maxInt(i32) + 1); - try testing.expect(uint32_large.isNumber()); - - // initString - const str_val = Value.initString(ctx, "hello"); - defer str_val.deinit(ctx); - try testing.expect(str_val.isString()); - const cstr = str_val.toCString(ctx).?; - defer ctx.freeCString(cstr); - try testing.expectEqualStrings("hello", std.mem.span(cstr)); - - // initStringLen - const str_len_val = Value.initStringLen(ctx, "hello world"); - defer str_len_val.deinit(ctx); - try testing.expect(str_len_val.isString()); - - // initObject - const obj = Value.initObject(ctx); - defer obj.deinit(ctx); - try testing.expect(obj.isObject()); - try testing.expect(!obj.isArray()); - - // initArray - const arr = Value.initArray(ctx); - defer arr.deinit(ctx); - try testing.expect(arr.isArray()); - - // Verify values work in JavaScript by setting as global and reading back - try global.setPropertyStr(ctx, "testInt", Value.initInt32(123)); - const read_back = ctx.eval("testInt", "", .{}); - defer read_back.deinit(ctx); - try testing.expectEqual(@as(i32, 123), try read_back.toInt32(ctx)); -} - -test "conversions" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // toInt32 - const int_val = ctx.eval("42", "", .{}); - defer int_val.deinit(ctx); - try testing.expectEqual(@as(i32, 42), try int_val.toInt32(ctx)); - - // toInt32 from float truncates - const float_to_int = ctx.eval("3.7", "", .{}); - defer float_to_int.deinit(ctx); - try testing.expectEqual(@as(i32, 3), try float_to_int.toInt32(ctx)); - - // toInt64 - const int64_val = ctx.eval("9007199254740991", "", .{}); - defer int64_val.deinit(ctx); - try testing.expectEqual(@as(i64, 9007199254740991), try int64_val.toInt64(ctx)); - - // toFloat64 - const float_val = ctx.eval("3.14159", "", .{}); - defer float_val.deinit(ctx); - try testing.expectApproxEqAbs(@as(f64, 3.14159), try float_val.toFloat64(ctx), 0.00001); - - // toBool - const true_val = ctx.eval("true", "", .{}); - defer true_val.deinit(ctx); - try testing.expect(try true_val.toBool(ctx)); - - const false_val = ctx.eval("false", "", .{}); - defer false_val.deinit(ctx); - try testing.expect(!try false_val.toBool(ctx)); - - // toCString - const str_val = ctx.eval("'test string'", "", .{}); - defer str_val.deinit(ctx); - const cstr = str_val.toCString(ctx).?; - defer ctx.freeCString(cstr); - try testing.expectEqualStrings("test string", std.mem.span(cstr)); - - // toCStringLen - const str_with_null = Value.initStringLen(ctx, "hello\x00world"); - defer str_with_null.deinit(ctx); - const cstr_result = str_with_null.toCStringLen(ctx).?; - defer ctx.freeCString(cstr_result.ptr); - try testing.expectEqual(@as(usize, 11), cstr_result.len); - - // toZigSlice - const str_val2 = ctx.eval("'zig slice test'", "", .{}); - defer str_val2.deinit(ctx); - const slice = str_val2.toZigSlice(ctx).?; - defer ctx.freeCString(slice.ptr); - try testing.expectEqualStrings("zig slice test", slice); - - // toZigSlice with embedded null - const str_with_null2 = Value.initStringLen(ctx, "foo\x00bar"); - defer str_with_null2.deinit(ctx); - const slice2 = str_with_null2.toZigSlice(ctx).?; - defer ctx.freeCString(slice2.ptr); - try testing.expectEqual(@as(usize, 7), slice2.len); - try testing.expectEqualStrings("foo\x00bar", slice2); -} - -test "property access" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // Create an object and set properties - const obj = Value.initObject(ctx); - defer obj.deinit(ctx); - - // setPropertyStr and getPropertyStr - try obj.setPropertyStr(ctx, "foo", Value.initInt32(42)); - const foo = obj.getPropertyStr(ctx, "foo"); - defer foo.deinit(ctx); - try testing.expectEqual(@as(i32, 42), try foo.toInt32(ctx)); - - // hasPropertyStr - try testing.expect(try obj.hasPropertyStr(ctx, "foo")); - try testing.expect(!try obj.hasPropertyStr(ctx, "bar")); - - // deletePropertyStr - try testing.expect(try obj.deletePropertyStr(ctx, "foo")); - try testing.expect(!try obj.hasPropertyStr(ctx, "foo")); - - // getLength on arrays - const arr = ctx.eval("[1, 2, 3, 4, 5]", "", .{}); - defer arr.deinit(ctx); - try testing.expectEqual(@as(i64, 5), try arr.getLength(ctx)); - - // Property access via JavaScript - const global = ctx.getGlobalObject(); - defer global.deinit(ctx); - - const test_obj = Value.initObject(ctx); - try test_obj.setPropertyStr(ctx, "x", Value.initInt32(100)); - try test_obj.setPropertyStr(ctx, "y", Value.initInt32(200)); - try global.setPropertyStr(ctx, "testObj", test_obj); - - const sum = ctx.eval("testObj.x + testObj.y", "", .{}); - defer sum.deinit(ctx); - try testing.expectEqual(@as(i32, 300), try sum.toInt32(ctx)); -} - -test "comparison functions" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // isEqual vs isStrictEqual: 1 == "1" is true, 1 === "1" is false - const num_one = ctx.eval("1", "", .{}); - defer num_one.deinit(ctx); - const str_one = ctx.eval("'1'", "", .{}); - defer str_one.deinit(ctx); - - try testing.expect(try num_one.isEqual(ctx, str_one)); // 1 == "1" - try testing.expect(!num_one.isStrictEqual(ctx, str_one)); // 1 !== "1" - - // Same value comparison - const num_a = ctx.eval("42", "", .{}); - defer num_a.deinit(ctx); - const num_b = ctx.eval("42", "", .{}); - defer num_b.deinit(ctx); - - try testing.expect(try num_a.isEqual(ctx, num_b)); - try testing.expect(num_a.isStrictEqual(ctx, num_b)); - try testing.expect(num_a.isSameValue(ctx, num_b)); - - // isSameValue: NaN === NaN is false, but Object.is(NaN, NaN) is true - const nan_a = ctx.eval("NaN", "", .{}); - defer nan_a.deinit(ctx); - const nan_b = ctx.eval("NaN", "", .{}); - defer nan_b.deinit(ctx); - - try testing.expect(!nan_a.isStrictEqual(ctx, nan_b)); // NaN !== NaN - try testing.expect(nan_a.isSameValue(ctx, nan_b)); // Object.is(NaN, NaN) === true - - // isSameValueZero: +0 and -0 - const pos_zero = ctx.eval("+0", "", .{}); - defer pos_zero.deinit(ctx); - const neg_zero = ctx.eval("-0", "", .{}); - defer neg_zero.deinit(ctx); - - try testing.expect(pos_zero.isStrictEqual(ctx, neg_zero)); // +0 === -0 - try testing.expect(!pos_zero.isSameValue(ctx, neg_zero)); // Object.is(+0, -0) === false - try testing.expect(pos_zero.isSameValueZero(ctx, neg_zero)); // SameValueZero(+0, -0) === true -} - -test "function calls" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const global = ctx.getGlobalObject(); - defer global.deinit(ctx); - - // Create a function via eval - const add_func = ctx.eval("(function(a, b) { return a + b; })", "", .{}); - defer add_func.deinit(ctx); - try testing.expect(add_func.isFunction(ctx)); - - // Call the function with arguments - const arg1 = Value.initInt32(10); - const arg2 = Value.initInt32(32); - const result = add_func.call(ctx, Value.undefined, &.{ arg1, arg2 }); - defer result.deinit(ctx); - - try testing.expect(!result.isException()); - try testing.expectEqual(@as(i32, 42), try result.toInt32(ctx)); - - // Function with string concatenation - const concat_func = ctx.eval("(function(s1, s2) { return s1 + s2; })", "", .{}); - defer concat_func.deinit(ctx); - - const s1 = Value.initString(ctx, "Hello, "); - defer s1.deinit(ctx); - const s2 = Value.initString(ctx, "World!"); - defer s2.deinit(ctx); - const concat_result = concat_func.call(ctx, Value.undefined, &.{ s1.dup(ctx), s2.dup(ctx) }); - defer concat_result.deinit(ctx); - - const cstr = concat_result.toCString(ctx).?; - defer ctx.freeCString(cstr); - try testing.expectEqualStrings("Hello, World!", std.mem.span(cstr)); - - // Function that uses 'this' - const obj = Value.initObject(ctx); - defer obj.deinit(ctx); - try obj.setPropertyStr(ctx, "value", Value.initInt32(100)); - - const method = ctx.eval("(function() { return this.value * 2; })", "", .{}); - defer method.deinit(ctx); - - const method_result = method.call(ctx, obj, &.{}); - defer method_result.deinit(ctx); - try testing.expectEqual(@as(i32, 200), try method_result.toInt32(ctx)); -} - -test "array operations" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // Create array and add elements via setPropertyUint32 - const arr = Value.initArray(ctx); - defer arr.deinit(ctx); - - try arr.setPropertyUint32(ctx, 0, Value.initInt32(10)); - try arr.setPropertyUint32(ctx, 1, Value.initInt32(20)); - try arr.setPropertyUint32(ctx, 2, Value.initInt32(30)); - - try testing.expectEqual(@as(i64, 3), try arr.getLength(ctx)); - - // Read elements back via getPropertyUint32 - const elem0 = arr.getPropertyUint32(ctx, 0); - defer elem0.deinit(ctx); - try testing.expectEqual(@as(i32, 10), try elem0.toInt32(ctx)); - - const elem2 = arr.getPropertyUint32(ctx, 2); - defer elem2.deinit(ctx); - try testing.expectEqual(@as(i32, 30), try elem2.toInt32(ctx)); - - // initArrayFrom - const values = [_]Value{ - Value.initInt32(1), - Value.initInt32(2), - Value.initInt32(3), - }; - const arr2 = Value.initArrayFrom(ctx, &values); - defer arr2.deinit(ctx); - - try testing.expect(arr2.isArray()); - try testing.expectEqual(@as(i64, 3), try arr2.getLength(ctx)); -} - -test "JSON operations" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // parseJSON - const parsed = Value.parseJSON(ctx, "{\"a\": 1, \"b\": 2}", ""); - defer parsed.deinit(ctx); - - try testing.expect(parsed.isObject()); - const a = parsed.getPropertyStr(ctx, "a"); - defer a.deinit(ctx); - try testing.expectEqual(@as(i32, 1), try a.toInt32(ctx)); - - // jsonStringify - const obj = Value.initObject(ctx); - defer obj.deinit(ctx); - try obj.setPropertyStr(ctx, "x", Value.initInt32(42)); - - const json_str = obj.jsonStringify(ctx, Value.undefined, Value.undefined); - defer json_str.deinit(ctx); - - const cstr = json_str.toCString(ctx).?; - defer ctx.freeCString(cstr); - try testing.expectEqualStrings("{\"x\":42}", std.mem.span(cstr)); -} - -test "BigInt operations" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // initBigInt64 - const big1 = Value.initBigInt64(ctx, 9007199254740993); - defer big1.deinit(ctx); - try testing.expect(big1.isBigInt()); - - // initBigUint64 - const big2 = Value.initBigUint64(ctx, 18446744073709551615); - defer big2.deinit(ctx); - try testing.expect(big2.isBigInt()); - - // toBigInt64 - const big_val = ctx.eval("BigInt(12345)", "", .{}); - defer big_val.deinit(ctx); - try testing.expectEqual(@as(i64, 12345), try big_val.toBigInt64(ctx)); -} - -test "Date operations" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // initDate - const epoch_ms: f64 = 1609459200000; // 2021-01-01 00:00:00 UTC - const date = Value.initDate(ctx, epoch_ms); - defer date.deinit(ctx); - - try testing.expect(date.isDate()); - - // Verify via JavaScript - const global = ctx.getGlobalObject(); - defer global.deinit(ctx); - try global.setPropertyStr(ctx, "testDate", date.dup(ctx)); - - const year = ctx.eval("testDate.getUTCFullYear()", "", .{}); - defer year.deinit(ctx); - try testing.expectEqual(@as(i32, 2021), try year.toInt32(ctx)); -} - -test "Symbol operations" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // initSymbol (local) - const sym_local = Value.initSymbol(ctx, "mySymbol", false); - defer sym_local.deinit(ctx); - try testing.expect(sym_local.isSymbol()); - - // initSymbol (global) - const sym_global = Value.initSymbol(ctx, "globalSymbol", true); - defer sym_global.deinit(ctx); - try testing.expect(sym_global.isSymbol()); - - // Two global symbols with same name should be equal - const sym_global2 = Value.initSymbol(ctx, "globalSymbol", true); - defer sym_global2.deinit(ctx); - try testing.expect(sym_global.isStrictEqual(ctx, sym_global2)); - - // Two local symbols with same name should NOT be equal - const sym_local2 = Value.initSymbol(ctx, "mySymbol", false); - defer sym_local2.deinit(ctx); - try testing.expect(!sym_local.isStrictEqual(ctx, sym_local2)); -} - -test "Error operations" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // initError - const err = Value.initError(ctx); - defer err.deinit(ctx); - try testing.expect(err.isError()); - - // Exception handling via eval - const result = ctx.eval("throw new Error('test error')", "", .{}); - defer result.deinit(ctx); - try testing.expect(result.isException()); - - const exc = ctx.getException(); - defer exc.deinit(ctx); - try testing.expect(exc.isError()); -} - -test "Promise state" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // Pending promise - const pending = ctx.eval("new Promise(() => {})", "", .{}); - defer pending.deinit(ctx); - try testing.expect(pending.isPromise()); - try testing.expectEqual(PromiseState.pending, pending.promiseState(ctx)); - - // Fulfilled promise - const fulfilled = ctx.eval("Promise.resolve(42)", "", .{}); - defer fulfilled.deinit(ctx); - try testing.expectEqual(PromiseState.fulfilled, fulfilled.promiseState(ctx)); - - const result = fulfilled.promiseResult(ctx); - defer result.deinit(ctx); - try testing.expectEqual(@as(i32, 42), try result.toInt32(ctx)); - - // Non-promise value - const num = ctx.eval("42", "", .{}); - defer num.deinit(ctx); - try testing.expectEqual(PromiseState.not_a_promise, num.promiseState(ctx)); -} - -test "initPromiseCapability" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const promise: Value.Promise = Value.initPromiseCapability(ctx); - defer promise.resolve.deinit(ctx); - defer promise.reject.deinit(ctx); - defer promise.value.deinit(ctx); - - try testing.expect(promise.value.isPromise()); - try testing.expectEqual(PromiseState.pending, promise.value.promiseState(ctx)); - - // Resolve the promise - const val: Value = .initInt32(42); - const resolve_result = promise.resolve.call(ctx, .undefined, &.{val}); - defer resolve_result.deinit(ctx); - - try testing.expectEqual(PromiseState.fulfilled, promise.value.promiseState(ctx)); - - const result = promise.value.promiseResult(ctx); - defer result.deinit(ctx); - try testing.expectEqual(@as(i32, 42), try result.toInt32(ctx)); -} - -test "instanceof" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const arr = ctx.eval("[1, 2, 3]", "", .{}); - defer arr.deinit(ctx); - - const array_ctor = ctx.eval("Array", "", .{}); - defer array_ctor.deinit(ctx); - - const object_ctor = ctx.eval("Object", "", .{}); - defer object_ctor.deinit(ctx); - - const map_ctor = ctx.eval("Map", "", .{}); - defer map_ctor.deinit(ctx); - - try testing.expect(try arr.isInstanceOf(ctx, array_ctor)); - try testing.expect(try arr.isInstanceOf(ctx, object_ctor)); // Arrays are objects - try testing.expect(!try arr.isInstanceOf(ctx, map_ctor)); -} - -test "init with Value passthrough" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const original = Value.initInt32(42); - const result = Value.init(ctx, original); - - if (Value.is_nan_boxed) { - try testing.expectEqual(original.val, result.val); - } else { - try testing.expectEqual(original.tag, result.tag); - try testing.expectEqual(original.u.int32, result.u.int32); - } -} - -test "init with bool" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const true_val = Value.init(ctx, true); - try testing.expect(true_val.isBool()); - try testing.expect(try true_val.toBool(ctx)); - - const false_val = Value.init(ctx, false); - try testing.expect(false_val.isBool()); - try testing.expect(!try false_val.toBool(ctx)); -} - -test "init with void" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const result = Value.init(ctx, {}); - try testing.expect(result.isNull()); -} - -test "init with null" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const result = Value.init(ctx, null); - try testing.expect(result.isNull()); -} - -test "init with optional" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const some_val: ?i32 = 42; - const result = Value.init(ctx, some_val); - try testing.expect(!result.isNull()); - try testing.expectEqual(@as(i32, 42), try result.toInt32(ctx)); - - const none_val: ?i32 = null; - const result_null = Value.init(ctx, none_val); - try testing.expect(result_null.isNull()); -} - -test "init with signed integers" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // i8 - const i8_val: i8 = -42; - const result_i8 = Value.init(ctx, i8_val); - try testing.expectEqual(@as(i32, -42), try result_i8.toInt32(ctx)); - - // i16 - const i16_val: i16 = -1000; - const result_i16 = Value.init(ctx, i16_val); - try testing.expectEqual(@as(i32, -1000), try result_i16.toInt32(ctx)); - - // i32 - const i32_val: i32 = -100000; - const result_i32 = Value.init(ctx, i32_val); - try testing.expectEqual(@as(i32, -100000), try result_i32.toInt32(ctx)); - - // i64 that fits in i32 - const i64_small: i64 = -50000; - const result_i64_small = Value.init(ctx, i64_small); - try testing.expectEqual(@as(i32, -50000), try result_i64_small.toInt32(ctx)); - - // i64 that exceeds i32 range - const i64_large: i64 = 9007199254740992; - const result_i64_large = Value.init(ctx, i64_large); - try testing.expectEqual(@as(f64, 9007199254740992), try result_i64_large.toFloat64(ctx)); -} - -test "init with unsigned integers" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // u8 - const u8_val: u8 = 200; - const result_u8 = Value.init(ctx, u8_val); - try testing.expectEqual(@as(u32, 200), try result_u8.toUint32(ctx)); - - // u16 - const u16_val: u16 = 60000; - const result_u16 = Value.init(ctx, u16_val); - try testing.expectEqual(@as(u32, 60000), try result_u16.toUint32(ctx)); - - // u32 within i32 range - const u32_small: u32 = 1000000; - const result_u32_small = Value.init(ctx, u32_small); - try testing.expectEqual(@as(u32, 1000000), try result_u32_small.toUint32(ctx)); - - // u32 exceeding i32 max (becomes float) - const u32_large: u32 = 3000000000; - const result_u32_large = Value.init(ctx, u32_large); - try testing.expectEqual(@as(f64, 3000000000), try result_u32_large.toFloat64(ctx)); -} - -test "init with comptime_int" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const result = Value.init(ctx, 42); - try testing.expectEqual(@as(i32, 42), try result.toInt32(ctx)); - - const result_neg = Value.init(ctx, -100); - try testing.expectEqual(@as(i32, -100), try result_neg.toInt32(ctx)); -} - -test "init with floats" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // f32 - const f32_val: f32 = 3.14; - const result_f32 = Value.init(ctx, f32_val); - try testing.expect(result_f32.isNumber()); - try testing.expectApproxEqAbs(@as(f64, 3.14), try result_f32.toFloat64(ctx), 0.001); - - // f64 - const f64_val: f64 = 2.71828; - const result_f64 = Value.init(ctx, f64_val); - try testing.expectApproxEqAbs(@as(f64, 2.71828), try result_f64.toFloat64(ctx), 0.00001); -} - -test "init with comptime_float" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const result = Value.init(ctx, 3.14159); - try testing.expectApproxEqAbs(@as(f64, 3.14159), try result.toFloat64(ctx), 0.00001); -} - -test "init with string slice" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const str: []const u8 = "hello world"; - const result = Value.init(ctx, str); - defer result.deinit(ctx); - - try testing.expect(result.isString()); - const cstr = result.toCString(ctx).?; - defer ctx.freeCString(cstr); - try testing.expectEqualStrings("hello world", std.mem.span(cstr)); -} - -test "init with string pointer to array" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const str: *const [5]u8 = "hello"; - const result = Value.init(ctx, str); - defer result.deinit(ctx); - - try testing.expect(result.isString()); - const cstr = result.toCString(ctx).?; - defer ctx.freeCString(cstr); - try testing.expectEqualStrings("hello", std.mem.span(cstr)); -} - -test "init with string literal" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const result = Value.init(ctx, "test string"); - defer result.deinit(ctx); - - try testing.expect(result.isString()); - const cstr = result.toCString(ctx).?; - defer ctx.freeCString(cstr); - try testing.expectEqualStrings("test string", std.mem.span(cstr)); -} - -test "init with nested optional" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const optional_str: ?[]const u8 = "nested"; - const result = Value.init(ctx, optional_str); - defer result.deinit(ctx); - - try testing.expect(result.isString()); - const cstr = result.toCString(ctx).?; - defer ctx.freeCString(cstr); - try testing.expectEqualStrings("nested", std.mem.span(cstr)); -} - -test "Value isModule" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const loader = struct { - fn load( - _: void, - load_ctx: *Context, - name: [:0]const u8, - ) ?*ModuleDef { - if (!std.mem.eql(u8, name, "mod_test")) return null; - - const m = ModuleDef.init(load_ctx, name, initFn) orelse return null; - _ = m.addExport(load_ctx, "x"); - return m; - } - - fn initFn(init_ctx: *Context, m: *ModuleDef) bool { - if (!m.setExport(init_ctx, "x", Value.initInt32(1))) return false; - return true; - } - }; - - ctx.getRuntime().setModuleLoaderFunc(void, {}, null, loader.load); - - const result = ctx.eval( - \\import { x } from "mod_test"; x - , "", .{ .type = .module }); - defer result.deinit(ctx); - - try testing.expect(!result.isException()); - - const num = Value.initInt32(42); - try testing.expect(!num.isModule()); -} - -test "ArrayBuffer operations" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // initArrayBufferCopy - creates a copy of the data - const data = [_]u8{ 1, 2, 3, 4, 5 }; - const ab = Value.initArrayBufferCopy(ctx, &data); - defer ab.deinit(ctx); - - try testing.expect(ab.isArrayBuffer()); - try testing.expect(!ab.isException()); - - // getArrayBuffer - get the underlying data - const buf = ab.getArrayBuffer(ctx); - try testing.expect(buf != null); - try testing.expectEqual(@as(usize, 5), buf.?.len); - try testing.expectEqualSlices(u8, &data, buf.?); - - // Modify the buffer and verify it's the same underlying data - buf.?[0] = 42; - const buf2 = ab.getArrayBuffer(ctx); - try testing.expectEqual(@as(u8, 42), buf2.?[0]); -} - -test "ArrayBuffer from JavaScript" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // Create ArrayBuffer in JS - const ab = ctx.eval("new ArrayBuffer(8)", "", .{}); - defer ab.deinit(ctx); - - try testing.expect(!ab.isException()); - try testing.expect(ab.isArrayBuffer()); - - const buf = ab.getArrayBuffer(ctx); - try testing.expect(buf != null); - try testing.expectEqual(@as(usize, 8), buf.?.len); -} - -test "ArrayBuffer detach" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const data = [_]u8{ 1, 2, 3, 4 }; - const ab = Value.initArrayBufferCopy(ctx, &data); - defer ab.deinit(ctx); - - // Should have data before detach - try testing.expect(ab.getArrayBuffer(ctx) != null); - - // Detach the buffer - ab.detachArrayBuffer(ctx); - - // After detach, getArrayBuffer returns null - try testing.expect(ab.getArrayBuffer(ctx) == null); -} - -test "Uint8Array operations" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // initUint8ArrayCopy - creates a Uint8Array with copied data - const data = [_]u8{ 10, 20, 30, 40, 50 }; - const arr = Value.initUint8ArrayCopy(ctx, &data); - defer arr.deinit(ctx); - - try testing.expect(!arr.isException()); - - // getUint8Array - get the underlying data - const buf = arr.getUint8Array(ctx); - try testing.expect(buf != null); - try testing.expectEqual(@as(usize, 5), buf.?.len); - try testing.expectEqualSlices(u8, &data, buf.?); - - // getTypedArrayType - const arr_type = arr.getTypedArrayType(); - try testing.expect(arr_type != null); - try testing.expectEqual(typed_array.Type.uint8, arr_type.?); -} - -test "Uint8Array from JavaScript" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // Create Uint8Array in JS - const arr = ctx.eval("new Uint8Array([1, 2, 3, 4])", "", .{}); - defer arr.deinit(ctx); - - try testing.expect(!arr.isException()); - - const buf = arr.getUint8Array(ctx); - try testing.expect(buf != null); - try testing.expectEqual(@as(usize, 4), buf.?.len); - try testing.expectEqualSlices(u8, &[_]u8{ 1, 2, 3, 4 }, buf.?); - - // Verify type - const arr_type = arr.getTypedArrayType(); - try testing.expectEqual(typed_array.Type.uint8, arr_type.?); -} - -test "TypedArray types" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const TestCase = struct { - js_code: [:0]const u8, - expected_type: typed_array.Type, - }; - - const test_cases = [_]TestCase{ - .{ .js_code = "new Int8Array(4)", .expected_type = .int8 }, - .{ .js_code = "new Uint8Array(4)", .expected_type = .uint8 }, - .{ .js_code = "new Uint8ClampedArray(4)", .expected_type = .uint8_clamped }, - .{ .js_code = "new Int16Array(4)", .expected_type = .int16 }, - .{ .js_code = "new Uint16Array(4)", .expected_type = .uint16 }, - .{ .js_code = "new Int32Array(4)", .expected_type = .int32 }, - .{ .js_code = "new Uint32Array(4)", .expected_type = .uint32 }, - .{ .js_code = "new Float32Array(4)", .expected_type = .float32 }, - .{ .js_code = "new Float64Array(4)", .expected_type = .float64 }, - .{ .js_code = "new BigInt64Array(4)", .expected_type = .big_int64 }, - .{ .js_code = "new BigUint64Array(4)", .expected_type = .big_uint64 }, - }; - - for (test_cases) |tc| { - const arr = ctx.eval(tc.js_code, "", .{}); - defer arr.deinit(ctx); - - try testing.expect(!arr.isException()); - const arr_type = arr.getTypedArrayType(); - try testing.expect(arr_type != null); - try testing.expectEqual(tc.expected_type, arr_type.?); - } -} - -test "getTypedArrayType returns null for non-typed-arrays" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // Regular array - const arr = ctx.eval("[1, 2, 3]", "", .{}); - defer arr.deinit(ctx); - try testing.expect(arr.getTypedArrayType() == null); - - // Number - const num = Value.initInt32(42); - try testing.expect(num.getTypedArrayType() == null); - - // ArrayBuffer (not a TypedArray) - const ab = ctx.eval("new ArrayBuffer(8)", "", .{}); - defer ab.deinit(ctx); - try testing.expect(ab.getTypedArrayType() == null); -} - -test "getTypedArrayBuffer" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // Create a typed array with an offset and different element size - const arr = ctx.eval( - \\const buf = new ArrayBuffer(16); - \\new Int32Array(buf, 4, 2); - , "", .{}); - defer arr.deinit(ctx); - - try testing.expect(!arr.isException()); - - const info = arr.getTypedArrayBuffer(ctx); - try testing.expect(info != null); - defer info.?.value.deinit(ctx); - - try testing.expect(info.?.value.isArrayBuffer()); - try testing.expectEqual(@as(usize, 4), info.?.byte_offset); - try testing.expectEqual(@as(usize, 8), info.?.byte_length); // 2 Int32s = 8 bytes - try testing.expectEqual(@as(usize, 4), info.?.bytes_per_element); -} - -test "initTypedArray with length" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // Create typed array with length argument - const len = Value.initInt32(10); - const args = [_]Value{len}; - const arr = Value.initTypedArray(ctx, &args, .float64); - defer arr.deinit(ctx); - - try testing.expect(!arr.isException()); - try testing.expectEqual(typed_array.Type.float64, arr.getTypedArrayType().?); - - const info = arr.getTypedArrayBuffer(ctx); - try testing.expect(info != null); - defer info.?.value.deinit(ctx); - - try testing.expectEqual(@as(usize, 80), info.?.byte_length); // 10 Float64s = 80 bytes - try testing.expectEqual(@as(usize, 8), info.?.bytes_per_element); -} - -test "initCFunction basic" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const addFunc = Value.initCFunction(ctx, &testAddFunc, "add", 2); - defer addFunc.deinit(ctx); - - try testing.expect(!addFunc.isException()); - try testing.expect(addFunc.isFunction(ctx)); - - // Register as global and call from JS - const global = ctx.getGlobalObject(); - defer global.deinit(ctx); - try global.setPropertyStr(ctx, "add", addFunc.dup(ctx)); - - const result = ctx.eval("add(3, 4)", "", .{}); - defer result.deinit(ctx); - try testing.expect(!result.isException()); - try testing.expectEqual(@as(i32, 7), try result.toInt32(ctx)); -} - -fn testAddFunc( - ctx_opt: ?*Context, - _: Value, - args: []const c.JSValue, -) Value { - if (args.len < 2) return Value.undefined; - const ctx = ctx_opt.?; - const a = Value.fromCVal(args[0]).toInt32(ctx) catch return Value.exception; - const b = Value.fromCVal(args[1]).toInt32(ctx) catch return Value.exception; - return Value.initInt32(a + b); -} - -test "initCFunctionData with closure data" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // Create a function with closure data (a multiplier) - const multiplier = Value.initInt32(10); - const data = [_]Value{multiplier}; - const multiplyFunc = Value.initCFunctionData(ctx, &testMultiplyFunc, 1, 0, &data); - defer multiplyFunc.deinit(ctx); - - try testing.expect(!multiplyFunc.isException()); - try testing.expect(multiplyFunc.isFunction(ctx)); - - // Register as global and call from JS - const global = ctx.getGlobalObject(); - defer global.deinit(ctx); - try global.setPropertyStr(ctx, "multiplyByTen", multiplyFunc.dup(ctx)); - - const result = ctx.eval("multiplyByTen(5)", "", .{}); - defer result.deinit(ctx); - try testing.expect(!result.isException()); - try testing.expectEqual(@as(i32, 50), try result.toInt32(ctx)); -} - -fn testMultiplyFunc(ctx_opt: ?*Context, _: Value, args: []const c.JSValue, _: c_int, func_data: [*c]c.JSValue) Value { - if (args.len < 1) return Value.undefined; - const ctx = ctx_opt.?; - const val = Value.fromCVal(args[0]).toInt32(ctx) catch return Value.exception; - const multiplier = Value.fromCVal(func_data[0]).toInt32(ctx) catch return Value.exception; - return Value.initInt32(val * multiplier); -} - -test "setPropertyFunctionList" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const obj = Value.initObject(ctx); - defer obj.deinit(ctx); - - const list = [_]cfunc.FunctionListEntry{ - cfunc.FunctionListEntryHelpers.func("square", 1, &testSquareFunc), - cfunc.FunctionListEntryHelpers.propInt32("VERSION", 42, .{ .configurable = true }), - }; - - try obj.setPropertyFunctionList(ctx, &list); - - // Register object as global - const global = ctx.getGlobalObject(); - defer global.deinit(ctx); - try global.setPropertyStr(ctx, "myObj", obj.dup(ctx)); - - // Test function - const result1 = ctx.eval("myObj.square(7)", "", .{}); - defer result1.deinit(ctx); - try testing.expect(!result1.isException()); - try testing.expectEqual(@as(i32, 49), try result1.toInt32(ctx)); - - // Test constant - const result2 = ctx.eval("myObj.VERSION", "", .{}); - defer result2.deinit(ctx); - try testing.expect(!result2.isException()); - try testing.expectEqual(@as(i32, 42), try result2.toInt32(ctx)); -} - -fn testSquareFunc(ctx_opt: ?*Context, _: Value, args: []const c.JSValue) Value { - if (args.len < 1) return Value.undefined; - const ctx = ctx_opt.?; - const val = Value.fromCVal(args[0]).toInt32(ctx) catch return Value.exception; - return Value.initInt32(val * val); -} - -test "getset property accessors" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const obj: Value = .initObject(ctx); - defer obj.deinit(ctx); - - const list = [_]cfunc.FunctionListEntry{ - cfunc.FunctionListEntryHelpers.getset("readOnly", &testGetter, null), - cfunc.FunctionListEntryHelpers.getset("readWrite", &testGetter, &testSetter), - cfunc.FunctionListEntryHelpers.getsetMagic("magicProp", &testGetterMagic, &testSetterMagic, 42), - }; - - try obj.setPropertyFunctionList(ctx, &list); - - const global = ctx.getGlobalObject(); - defer global.deinit(ctx); - try global.setPropertyStr(ctx, "testObj", obj.dup(ctx)); - - // Test read-only getter - const result1 = ctx.eval("testObj.readOnly", "", .{}); - defer result1.deinit(ctx); - try testing.expect(!result1.isException()); - try testing.expectEqual(@as(i32, 123), try result1.toInt32(ctx)); - - // Test read-write getter - const result2 = ctx.eval("testObj.readWrite", "", .{}); - defer result2.deinit(ctx); - try testing.expect(!result2.isException()); - try testing.expectEqual(@as(i32, 123), try result2.toInt32(ctx)); - - // Test setter returns value - const result3 = ctx.eval("testObj.readWrite = 999", "", .{}); - defer result3.deinit(ctx); - try testing.expect(!result3.isException()); - - // Test magic getter (returns the magic value) - const result4 = ctx.eval("testObj.magicProp", "", .{}); - defer result4.deinit(ctx); - try testing.expect(!result4.isException()); - try testing.expectEqual(@as(i32, 42), try result4.toInt32(ctx)); - - // Test magic setter - const result5 = ctx.eval("testObj.magicProp = 100", "", .{}); - defer result5.deinit(ctx); - try testing.expect(!result5.isException()); -} - -fn testGetter(_: ?*Context, _: Value) Value { - return .initInt32(123); -} - -fn testSetter(_: ?*Context, _: Value, _: Value) Value { - return .undefined; -} - -fn testGetterMagic(_: ?*Context, _: Value, magic: c_int) Value { - return .initInt32(magic); -} - -fn testSetterMagic(_: ?*Context, _: Value, _: Value, _: c_int) Value { - return .undefined; -} - -test "setConstructor" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // Create a constructor function using generic (constructor_or_func allows both new and call) - const ctor = Value.initCFunction2(ctx, &testConstructor, "Point", 2, .constructor_or_func, 0); - defer ctor.deinit(ctx); - - // Create prototype with method - const proto = Value.initObject(ctx); - defer proto.deinit(ctx); - - const protoList = [_]cfunc.FunctionListEntry{ - cfunc.FunctionListEntryHelpers.func("toString", 0, &testPointToString), - }; - try proto.setPropertyFunctionList(ctx, &protoList); - - // Link constructor and prototype - ctor.setConstructor(ctx, proto); - - // Register as global - const global = ctx.getGlobalObject(); - defer global.deinit(ctx); - try global.setPropertyStr(ctx, "Point", ctor.dup(ctx)); - - // Test that function was registered and constructor link exists - const protoCheck = ctx.eval("Point.prototype.toString !== undefined", "", .{}); - defer protoCheck.deinit(ctx); - try testing.expect(!protoCheck.isException()); - try testing.expect(try protoCheck.toBool(ctx)); - - // Test that prototype.constructor points back to Point - const ctorCheck = ctx.eval("Point.prototype.constructor === Point", "", .{}); - defer ctorCheck.deinit(ctx); - try testing.expect(!ctorCheck.isException()); - try testing.expect(try ctorCheck.toBool(ctx)); -} - -fn testConstructor(ctx_opt: ?*Context, this_val: Value, args: []const c.JSValue) Value { - const ctx = ctx_opt.?; - var this = this_val; - - if (args.len >= 1) { - this.setPropertyStr(ctx, "x", Value.fromCVal(args[0]).dup(ctx)) catch return Value.exception; - } - if (args.len >= 2) { - this.setPropertyStr(ctx, "y", Value.fromCVal(args[1]).dup(ctx)) catch return Value.exception; - } - - return Value.undefined; -} - -fn testPointToString(_: ?*Context, _: Value, _: []const c.JSValue) Value { - return Value.initInt32(42); -} - -test "PropertyFlags constants match C" { - // Verify the packed struct matches C constants - try testing.expectEqual(@as(c_int, 0x1), (PropertyFlags{ .configurable = true }).toInt()); - try testing.expectEqual(@as(c_int, 0x2), (PropertyFlags{ .writable = true }).toInt()); - try testing.expectEqual(@as(c_int, 0x4), (PropertyFlags{ .enumerable = true }).toInt()); - try testing.expectEqual(@as(c_int, 0x7), PropertyFlags.default.toInt()); - - try testing.expectEqual(@as(c_int, c.JS_PROP_CONFIGURABLE), (PropertyFlags{ .configurable = true }).toInt()); - try testing.expectEqual(@as(c_int, c.JS_PROP_WRITABLE), (PropertyFlags{ .writable = true }).toInt()); - try testing.expectEqual(@as(c_int, c.JS_PROP_ENUMERABLE), (PropertyFlags{ .enumerable = true }).toInt()); - try testing.expectEqual(@as(c_int, c.JS_PROP_C_W_E), PropertyFlags.default.toInt()); -} - -test "GetPropertyNamesFlags constants match C" { - try testing.expectEqual(@as(c_int, c.JS_GPN_STRING_MASK), GetPropertyNamesFlags.strings.toInt()); - try testing.expectEqual(@as(c_int, c.JS_GPN_SYMBOL_MASK), (GetPropertyNamesFlags{ .symbol_mask = true }).toInt()); - try testing.expectEqual(@as(c_int, c.JS_GPN_ENUM_ONLY), (GetPropertyNamesFlags{ .enum_only = true }).toInt()); -} - -test "definePropertyValueStr" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const obj = Value.initObject(ctx); - defer obj.deinit(ctx); - - // Define a non-writable property - _ = try obj.definePropertyValueStr(ctx, "readOnly", Value.initInt32(42), .{ - .configurable = true, - .enumerable = true, - .writable = false, - }); - - // Read the value - const val = obj.getPropertyStr(ctx, "readOnly"); - defer val.deinit(ctx); - try testing.expectEqual(@as(i32, 42), try val.toInt32(ctx)); - - // Verify via JavaScript that it's not writable - const global = ctx.getGlobalObject(); - defer global.deinit(ctx); - try global.setPropertyStr(ctx, "testObj", obj.dup(ctx)); - - // Attempt to write should fail silently (not in strict mode) - const write_result = ctx.eval("testObj.readOnly = 100; testObj.readOnly", "", .{}); - defer write_result.deinit(ctx); - try testing.expectEqual(@as(i32, 42), try write_result.toInt32(ctx)); -} - -test "definePropertyValueUint32" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const arr = Value.initArray(ctx); - defer arr.deinit(ctx); - - // Define array elements with specific flags - _ = try arr.definePropertyValueUint32(ctx, 0, Value.initInt32(10), PropertyFlags.default); - _ = try arr.definePropertyValueUint32(ctx, 1, Value.initInt32(20), PropertyFlags.default); - _ = try arr.definePropertyValueUint32(ctx, 2, Value.initInt32(30), PropertyFlags.default); - - try testing.expectEqual(@as(i64, 3), try arr.getLength(ctx)); - - const elem = arr.getPropertyUint32(ctx, 1); - defer elem.deinit(ctx); - try testing.expectEqual(@as(i32, 20), try elem.toInt32(ctx)); -} - -test "definePropertyGetSet" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // Create an object with a getter/setter via eval for simplicity - const obj = ctx.eval( - \\(function() { - \\ var obj = { _value: 0 }; - \\ Object.defineProperty(obj, 'value', { - \\ get: function() { return this._value * 2; }, - \\ set: function(v) { this._value = v; }, - \\ configurable: true, - \\ enumerable: true - \\ }); - \\ return obj; - \\})() - , "", .{}); - defer obj.deinit(ctx); - - try testing.expect(!obj.isException()); - try testing.expect(obj.isObject()); - - // Test setter - try obj.setPropertyStr(ctx, "value", Value.initInt32(21)); - - // Test getter (should return 42 = 21 * 2) - const val = obj.getPropertyStr(ctx, "value"); - defer val.deinit(ctx); - try testing.expectEqual(@as(i32, 42), try val.toInt32(ctx)); -} - -test "getOwnPropertyNames" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const obj = Value.initObject(ctx); - defer obj.deinit(ctx); - - try obj.setPropertyStr(ctx, "a", Value.initInt32(1)); - try obj.setPropertyStr(ctx, "b", Value.initInt32(2)); - try obj.setPropertyStr(ctx, "c", Value.initInt32(3)); - - const props = try obj.getOwnPropertyNames(ctx, GetPropertyNamesFlags.strings); - defer Value.freePropertyEnum(ctx, props); - - try testing.expectEqual(@as(usize, 3), props.len); -} - -test "getOwnProperty" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const obj = ctx.eval( - \\(function() { - \\ var obj = {}; - \\ Object.defineProperty(obj, 'prop', { - \\ value: 42, - \\ writable: false, - \\ enumerable: true, - \\ configurable: true - \\ }); - \\ return obj; - \\})() - , "", .{}); - defer obj.deinit(ctx); - - const atom = Atom.init(ctx, "prop"); - defer atom.deinit(ctx); - - var desc = (try obj.getOwnProperty(ctx, atom)).?; - defer desc.deinit(ctx); - - try testing.expect(desc.flags.configurable); - try testing.expect(desc.flags.enumerable); - try testing.expect(!desc.flags.writable); - try testing.expectEqual(@as(i32, 42), try desc.value.toInt32(ctx)); -} - -test "getOwnProperty nonexistent" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const obj = Value.initObject(ctx); - defer obj.deinit(ctx); - - const atom = Atom.init(ctx, "nonexistent"); - defer atom.deinit(ctx); - - const desc = try obj.getOwnProperty(ctx, atom); - try testing.expect(desc == null); -} - -test "initObjectClass" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - // Class ID 1 is JS_CLASS_OBJECT - creates a plain object - const obj = Value.initObjectClass(ctx, @enumFromInt(1)); - defer obj.deinit(ctx); - - // Verify it's an object - try testing.expect(obj.isObject()); - - // Verify we can set properties on it - try obj.setPropertyStr(ctx, "test", Value.initInt32(42)); - const val = obj.getPropertyStr(ctx, "test"); - defer val.deinit(ctx); - try testing.expectEqual(@as(i32, 42), try val.toInt32(ctx)); -} - -test "invoke method by atom" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const obj = ctx.eval( - \\({ - \\ value: 10, - \\ add: function(x) { return this.value + x; } - \\}) - , "", .{}); - defer obj.deinit(ctx); - - const method_atom = Atom.init(ctx, "add"); - defer method_atom.deinit(ctx); - - const arg = Value.initInt32(5); - const result = obj.invoke(ctx, method_atom, &.{arg}); - defer result.deinit(ctx); - - try testing.expect(!result.isException()); - try testing.expectEqual(@as(i32, 15), try result.toInt32(ctx)); -} - -test "callConstructor2 with custom new.target" { - const rt: *Runtime = try .init(); - defer rt.deinit(); - - const ctx: *Context = try .init(rt); - defer ctx.deinit(); - - const ctor = ctx.eval( - \\(function MyClass(value) { - \\ this.value = value; - \\}) - , "", .{}); - defer ctor.deinit(ctx); - - const arg = Value.initInt32(42); - const result = ctor.callConstructor2(ctx, ctor, &.{arg}); - defer result.deinit(ctx); - - try testing.expect(!result.isException()); - try testing.expect(result.isObject()); - - const val = result.getPropertyStr(ctx, "value"); - defer val.deinit(ctx); - try testing.expectEqual(@as(i32, 42), try val.toInt32(ctx)); -} diff --git a/examples/pjs/pjs.h b/examples/pjs/pjs.h deleted file mode 100644 index 973b69e..0000000 --- a/examples/pjs/pjs.h +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef PJS_H -#define PJS_H - -#include "phpz.h" -#include "Zend/zend_interfaces.h" -#include "ext/spl/spl_exceptions.h" -#include "pjs_arginfo.h" - -#endif \ No newline at end of file diff --git a/examples/pjs/pjs.stub.php b/examples/pjs/pjs.stub.php deleted file mode 100644 index 4c6006d..0000000 --- a/examples/pjs/pjs.stub.php +++ /dev/null @@ -1,30 +0,0 @@ -ce_flags |= ZEND_ACC_FINAL; - - return class_entry; -} - -static zend_class_entry *register_class_Pjs_Context(void) -{ - zend_class_entry ce, *class_entry; - - INIT_NS_CLASS_ENTRY(ce, "Pjs", "Context", class_Pjs_Context_methods); - class_entry = zend_register_internal_class_ex(&ce, NULL); - class_entry->ce_flags |= ZEND_ACC_FINAL; - - return class_entry; -} - -static zend_class_entry *register_class_Pjs_Value(zend_class_entry *class_entry_Stringable) -{ - zend_class_entry ce, *class_entry; - - INIT_NS_CLASS_ENTRY(ce, "Pjs", "Value", class_Pjs_Value_methods); - class_entry = zend_register_internal_class_ex(&ce, NULL); - class_entry->ce_flags |= ZEND_ACC_FINAL; - zend_class_implements(class_entry, 1, class_entry_Stringable); - - zval property_value_default_value; - ZVAL_UNDEF(&property_value_default_value); - zend_string *property_value_name = zend_string_init("value", sizeof("value") - 1, 1); - zend_declare_typed_property(class_entry, property_value_name, &property_value_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ANY)); - zend_string_release(property_value_name); - - return class_entry; -} - -static zend_class_entry *register_class_Pjs_Exception(zend_class_entry *class_entry_RuntimeException) -{ - zend_class_entry ce, *class_entry; - - INIT_NS_CLASS_ENTRY(ce, "Pjs", "Exception", class_Pjs_Exception_methods); - class_entry = zend_register_internal_class_ex(&ce, class_entry_RuntimeException); - class_entry->ce_flags |= ZEND_ACC_FINAL; - - return class_entry; -} diff --git a/examples/pjs/run-tests.php b/examples/pjs/run-tests.php deleted file mode 100644 index 5ada2f9..0000000 --- a/examples/pjs/run-tests.php +++ /dev/null @@ -1,4081 +0,0 @@ -#!/usr/bin/env php - | - | Preston L. Bannister | - | Marcus Boerger | - | Derick Rethans | - | Sander Roobol | - | Andrea Faulds | - | (based on version by: Stig Bakken ) | - | (based on the PHP 3 test framework by Rasmus Lerdorf) | - +----------------------------------------------------------------------+ - */ - -/* $Id: 8dacaf5989572a162a322174a94059e245d6032c $ */ - -/* Temporary variables while this file is being refactored. */ -/** @var ?JUnit */ -$junit = null; - -/* End temporary variables. */ - -/* Let there be no top-level code beyond this point: - * Only functions and classes, thanks! - * - * Minimum required PHP version: 7.4.0 - */ - -function show_usage(): void -{ - echo << Run up to simultaneous testing processes in parallel for - quicker testing on systems with multiple logical processors. - Note that this is experimental feature. - - -l Read the testfiles to be executed from . After the test - has finished all failed tests are written to the same . - If the list is empty and no further test is specified then - all tests are executed (same as: -r -w ). - - -r Read the testfiles to be executed from . - - -w Write a list of all failed tests to . - - -a Same as -w but append rather then truncating . - - -W Write a list of all tests and their result status to . - - -c Look for php.ini in directory or use as ini. - - -n Pass -n option to the php binary (Do not use a php.ini). - - -d foo=bar Pass -d option to the php binary (Define INI entry foo - with value 'bar'). - - -g Comma separated list of groups to show during test run - (possible values: PASS, FAIL, XFAIL, XLEAK, SKIP, BORK, WARN, LEAK, REDIRECT). - - -m Test for memory leaks with Valgrind (equivalent to -M memcheck). - - -M Test for errors with Valgrind tool. - - -p Specify PHP executable to run. - - -P Use PHP_BINARY as PHP executable to run (default). - - -q Quiet, no user interaction (same as environment NO_INTERACTION). - - -s Write output to . - - -x Sets 'SKIP_SLOW_TESTS' environmental variable. - - --offline Sets 'SKIP_ONLINE_TESTS' environmental variable. - - --verbose - -v Verbose mode. - - --help - -h This Help. - - --temp-source --temp-target [--temp-urlbase ] - Write temporary files to by replacing from the - filenames to generate with . In general you want to make - the path to your source files and some patch in - your web page hierarchy with pointing to . - - --keep-[all|php|skip|clean] - Do not delete 'all' files, 'php' test file, 'skip' or 'clean' - file. - - --set-timeout - Set timeout for individual tests, where is the number of - seconds. The default value is 60 seconds, or 300 seconds when - testing for memory leaks. - - --context - Sets the number of lines of surrounding context to print for diffs. - The default value is 3. - - --show-[all|php|skip|clean|exp|diff|out|mem] - Show 'all' files, 'php' test file, 'skip' or 'clean' file. You - can also use this to show the output 'out', the expected result - 'exp', the difference between them 'diff' or the valgrind log - 'mem'. The result types get written independent of the log format, - however 'diff' only exists when a test fails. - - --show-slow - Show all tests that took longer than milliseconds to run. - - --no-clean Do not execute clean section if any. - - --color - --no-color Do/Don't colorize the result type in the test result. - - --progress - --no-progress Do/Don't show the current progress. - - --repeat [n] - Run the tests multiple times in the same process and check the - output of the last execution (CLI SAPI only). - - --bless Bless failed tests using scripts/dev/bless_tests.php. - -HELP; -} - -/** - * One function to rule them all, one function to find them, one function to - * bring them all and in the darkness bind them. - * This is the entry point and exit point überfunction. It contains all the - * code that was previously found at the top level. It could and should be - * refactored to be smaller and more manageable. - */ -function main(): void -{ - /* This list was derived in a naïve mechanical fashion. If a member - * looks like it doesn't belong, it probably doesn't; cull at will. - */ - global $DETAILED, $PHP_FAILED_TESTS, $SHOW_ONLY_GROUPS, $argc, $argv, $cfg, - $cfgfiles, $cfgtypes, $conf_passed, $end_time, $environment, - $exts_skipped, $exts_tested, $exts_to_test, $failed_tests_file, - $ignored_by_ext, $ini_overwrites, $is_switch, $colorize, - $log_format, $matches, $no_clean, $no_file_cache, - $optionals, $pass_option_n, $pass_options, - $pattern_match, $php, $php_cgi, $phpdbg, $preload, $redir_tests, - $repeat, $result_tests_file, $slow_min_ms, $start_time, $switch, - $temp_source, $temp_target, $test_cnt, $test_dirs, - $test_files, $test_idx, $test_list, $test_results, $testfile, - $user_tests, $valgrind, $sum_results, $shuffle, $file_cache, $num_repeats, - $bless, $show_progress; - // Parallel testing - global $workers, $workerID; - global $context_line_count; - - // Temporary for the duration of refactoring - /** @var JUnit */ - global $junit; - - define('IS_WINDOWS', substr(PHP_OS, 0, 3) == "WIN"); - - $workerID = 0; - if (getenv("TEST_PHP_WORKER")) { - $workerID = intval(getenv("TEST_PHP_WORKER")); - run_worker(); - return; - } - - define('INIT_DIR', getcwd()); - - // Change into the PHP source directory. - if (getenv('TEST_PHP_SRCDIR')) { - @chdir(getenv('TEST_PHP_SRCDIR')); - } - - define('TEST_PHP_SRCDIR', getcwd()); - - check_proc_open_function_exists(); - - // If timezone is not set, use UTC. - if (ini_get('date.timezone') == '') { - date_default_timezone_set('UTC'); - } - - // Delete some security related environment variables - putenv('SSH_CLIENT=deleted'); - putenv('SSH_AUTH_SOCK=deleted'); - putenv('SSH_TTY=deleted'); - putenv('SSH_CONNECTION=deleted'); - - set_time_limit(0); - - ini_set('pcre.backtrack_limit', PHP_INT_MAX); - - init_output_buffers(); - - error_reporting(E_ALL); - - $environment = $_ENV ?? []; - - // Some configurations like php.ini-development set variables_order="GPCS" - // not "EGPCS", in which case $_ENV is NOT populated. Detect if the $_ENV - // was empty and handle it by explicitly populating through getenv(). - if (empty($environment)) { - $environment = getenv(); - } - - if (empty($environment['TEMP'])) { - $environment['TEMP'] = sys_get_temp_dir(); - - if (empty($environment['TEMP'])) { - // For example, OpCache on Windows will fail in this case because - // child processes (for tests) will not get a TEMP variable, so - // GetTempPath() will fallback to c:\windows, while GetTempPath() - // will return %TEMP% for parent (likely a different path). The - // parent will initialize the OpCache in that path, and child will - // fail to reattach to the OpCache because it will be using the - // wrong path. - die("TEMP environment is NOT set"); - } - - if (count($environment) == 1) { - // Not having other environment variables, only having TEMP, is - // probably ok, but strange and may make a difference in the - // test pass rate, so warn the user. - echo "WARNING: Only 1 environment variable will be available to tests(TEMP environment variable)" , PHP_EOL; - } - } - - if (IS_WINDOWS && empty($environment["SystemRoot"])) { - $environment["SystemRoot"] = getenv("SystemRoot"); - } - - $php = null; - $php_cgi = null; - $phpdbg = null; - - if (getenv('TEST_PHP_LOG_FORMAT')) { - $log_format = strtoupper(getenv('TEST_PHP_LOG_FORMAT')); - } else { - $log_format = 'LEODS'; - } - - // Check whether a detailed log is wanted. - if (getenv('TEST_PHP_DETAILED')) { - $DETAILED = getenv('TEST_PHP_DETAILED'); - } else { - $DETAILED = 0; - } - - $junit = new JUnit($environment, $workerID); - - if (getenv('SHOW_ONLY_GROUPS')) { - $SHOW_ONLY_GROUPS = explode(",", getenv('SHOW_ONLY_GROUPS')); - } else { - $SHOW_ONLY_GROUPS = []; - } - - // Check whether user test dirs are requested. - if (getenv('TEST_PHP_USER')) { - $user_tests = explode(',', getenv('TEST_PHP_USER')); - } else { - $user_tests = []; - } - - $exts_to_test = []; - $ini_overwrites = [ - 'output_handler=', - 'open_basedir=', - 'disable_functions=', - 'output_buffering=Off', - 'error_reporting=' . E_ALL, - 'display_errors=1', - 'display_startup_errors=1', - 'log_errors=0', - 'html_errors=0', - 'track_errors=0', - 'report_memleaks=1', - 'report_zend_debug=0', - 'docref_root=', - 'docref_ext=.html', - 'error_prepend_string=', - 'error_append_string=', - 'auto_prepend_file=', - 'auto_append_file=', - 'ignore_repeated_errors=0', - 'precision=14', - 'serialize_precision=-1', - 'memory_limit=128M', - 'opcache.fast_shutdown=0', - 'opcache.file_update_protection=0', - 'opcache.revalidate_freq=0', - 'opcache.jit_hot_loop=1', - 'opcache.jit_hot_func=1', - 'opcache.jit_hot_return=1', - 'opcache.jit_hot_side_exit=1', - 'zend.assertions=1', - 'zend.exception_ignore_args=0', - 'zend.exception_string_param_max_len=15', - 'short_open_tag=0', - ]; - - $no_file_cache = '-d opcache.file_cache= -d opcache.file_cache_only=0'; - - define('TRAVIS_CI', (bool) getenv('TRAVIS')); - - // Determine the tests to be run. - - $test_files = []; - $redir_tests = []; - $test_results = []; - $PHP_FAILED_TESTS = [ - 'BORKED' => [], - 'FAILED' => [], - 'WARNED' => [], - 'LEAKED' => [], - 'XFAILED' => [], - 'XLEAKED' => [], - 'SLOW' => [] - ]; - - // If parameters given assume they represent selected tests to run. - $result_tests_file = false; - $failed_tests_file = false; - $pass_option_n = false; - $pass_options = ''; - - $output_file = INIT_DIR . '/php_test_results_' . date('Ymd_Hi') . '.txt'; - - $just_save_results = false; - $valgrind = null; - $temp_source = null; - $temp_target = null; - $conf_passed = null; - $no_clean = false; - $colorize = true; - if (function_exists('sapi_windows_vt100_support') && !sapi_windows_vt100_support(STDOUT, true)) { - $colorize = false; - } - if (array_key_exists('NO_COLOR', $environment)) { - $colorize = false; - } - $selected_tests = false; - $slow_min_ms = INF; - $preload = false; - $file_cache = null; - $shuffle = false; - $bless = false; - $workers = null; - $context_line_count = 3; - $num_repeats = 1; - $show_progress = true; - - $cfgtypes = ['show', 'keep']; - $cfgfiles = ['skip', 'php', 'clean', 'out', 'diff', 'exp', 'mem']; - $cfg = []; - - foreach ($cfgtypes as $type) { - $cfg[$type] = []; - - foreach ($cfgfiles as $file) { - $cfg[$type][$file] = false; - } - } - - if (!isset($argc, $argv) || !$argc) { - $argv = [__FILE__]; - $argc = 1; - } - - if (getenv('TEST_PHP_ARGS')) { - $argv = array_merge($argv, explode(' ', getenv('TEST_PHP_ARGS'))); - $argc = count($argv); - } - - for ($i = 1; $i < $argc; $i++) { - $is_switch = false; - $switch = substr($argv[$i], 1, 1); - $repeat = substr($argv[$i], 0, 1) == '-'; - - while ($repeat) { - if (!$is_switch) { - $switch = substr($argv[$i], 1, 1); - } - - $is_switch = true; - - if ($repeat) { - foreach ($cfgtypes as $type) { - if (strpos($switch, '--' . $type) === 0) { - foreach ($cfgfiles as $file) { - if ($switch == '--' . $type . '-' . $file) { - $cfg[$type][$file] = true; - $is_switch = false; - break; - } - } - } - } - } - - if (!$is_switch) { - $is_switch = true; - break; - } - - $repeat = false; - - switch ($switch) { - case 'j': - $workers = substr($argv[$i], 2); - if ($workers == 0 || !preg_match('/^\d+$/', $workers)) { - error("'$workers' is not a valid number of workers, try e.g. -j16 for 16 workers"); - } - $workers = intval($workers, 10); - // Don't use parallel testing infrastructure if there is only one worker. - if ($workers === 1) { - $workers = null; - } - break; - case 'r': - case 'l': - $test_list = file($argv[++$i]); - if ($test_list) { - foreach ($test_list as $test) { - $matches = []; - if (preg_match('/^#.*\[(.*)\]\:\s+(.*)$/', $test, $matches)) { - $redir_tests[] = [$matches[1], $matches[2]]; - } elseif (strlen($test)) { - $test_files[] = trim($test); - } - } - } - if ($switch != 'l') { - break; - } - $i--; - // no break - case 'w': - $failed_tests_file = fopen($argv[++$i], 'w+t'); - break; - case 'a': - $failed_tests_file = fopen($argv[++$i], 'a+t'); - break; - case 'W': - $result_tests_file = fopen($argv[++$i], 'w+t'); - break; - case 'c': - $conf_passed = $argv[++$i]; - break; - case 'd': - $ini_overwrites[] = $argv[++$i]; - break; - case 'g': - $SHOW_ONLY_GROUPS = explode(",", $argv[++$i]); - break; - //case 'h' - case '--keep-all': - foreach ($cfgfiles as $file) { - $cfg['keep'][$file] = true; - } - break; - //case 'l' - case 'm': - $valgrind = new RuntestsValgrind($environment); - break; - case 'M': - $valgrind = new RuntestsValgrind($environment, $argv[++$i]); - break; - case 'n': - if (!$pass_option_n) { - $pass_options .= ' -n'; - } - $pass_option_n = true; - break; - case 'e': - $pass_options .= ' -e'; - break; - case '--preload': - $preload = true; - $environment['SKIP_PRELOAD'] = 1; - break; - case '--file-cache-prime': - $file_cache = 'prime'; - break; - case '--file-cache-use': - $file_cache = 'use'; - break; - case '--no-clean': - $no_clean = true; - break; - case '--color': - $colorize = true; - break; - case '--no-color': - $colorize = false; - break; - case 'p': - $php = $argv[++$i]; - putenv("TEST_PHP_EXECUTABLE=$php"); - $environment['TEST_PHP_EXECUTABLE'] = $php; - break; - case 'P': - $php = PHP_BINARY; - putenv("TEST_PHP_EXECUTABLE=$php"); - $environment['TEST_PHP_EXECUTABLE'] = $php; - break; - case 'q': - putenv('NO_INTERACTION=1'); - $environment['NO_INTERACTION'] = 1; - break; - //case 'r' - case 's': - $output_file = $argv[++$i]; - $just_save_results = true; - break; - case '--set-timeout': - $timeout = $argv[++$i] ?? ''; - if (!preg_match('/^\d+$/', $timeout)) { - error("'$timeout' is not a valid number of seconds, try e.g. --set-timeout 60 for 1 minute"); - } - $environment['TEST_TIMEOUT'] = intval($timeout, 10); - break; - case '--context': - $context_line_count = $argv[++$i] ?? ''; - if (!preg_match('/^\d+$/', $context_line_count)) { - error("'$context_line_count' is not a valid number of lines of context, try e.g. --context 3 for 3 lines"); - } - $context_line_count = intval($context_line_count, 10); - break; - case '--show-all': - foreach ($cfgfiles as $file) { - $cfg['show'][$file] = true; - } - break; - case '--show-slow': - $slow_min_ms = $argv[++$i] ?? ''; - if (!preg_match('/^\d+$/', $slow_min_ms)) { - error("'$slow_min_ms' is not a valid number of milliseconds, try e.g. --show-slow 1000 for 1 second"); - } - $slow_min_ms = intval($slow_min_ms, 10); - break; - case '--temp-source': - $temp_source = $argv[++$i]; - break; - case '--temp-target': - $temp_target = $argv[++$i]; - break; - case 'v': - case '--verbose': - $DETAILED = true; - break; - case 'x': - $environment['SKIP_SLOW_TESTS'] = 1; - break; - case '--offline': - $environment['SKIP_ONLINE_TESTS'] = 1; - break; - case '--shuffle': - $shuffle = true; - break; - case '--asan': - case '--msan': - $environment['USE_ZEND_ALLOC'] = 0; - $environment['USE_TRACKED_ALLOC'] = 1; - $environment['SKIP_ASAN'] = 1; - $environment['SKIP_PERF_SENSITIVE'] = 1; - if ($switch === '--msan') { - $environment['SKIP_MSAN'] = 1; - $environment['MSAN_OPTIONS'] = 'intercept_tls_get_addr=0'; - } - - $lsanSuppressions = __DIR__ . '/.github/lsan-suppressions.txt'; - if (file_exists($lsanSuppressions)) { - $environment['LSAN_OPTIONS'] = 'suppressions=' . $lsanSuppressions - . ':print_suppressions=0'; - } - break; - case '--repeat': - $num_repeats = (int) $argv[++$i]; - $environment['SKIP_REPEAT'] = 1; - break; - case '--bless': - $bless = true; - break; - //case 'w' - case '-': - // repeat check with full switch - $switch = $argv[$i]; - if ($switch != '-') { - $repeat = true; - } - break; - case '--progress': - $show_progress = true; - break; - case '--no-progress': - $show_progress = false; - break; - case '--version': - echo '$Id: 8dacaf5989572a162a322174a94059e245d6032c $' . "\n"; - exit(1); - - default: - echo "Illegal switch '$switch' specified!\n"; - // no break - case 'h': - case '-help': - case '--help': - show_usage(); - exit(1); - } - } - - if (!$is_switch) { - $selected_tests = true; - $testfile = realpath($argv[$i]); - - if (!$testfile && strpos($argv[$i], '*') !== false && function_exists('glob')) { - if (substr($argv[$i], -5) == '.phpt') { - $pattern_match = glob($argv[$i]); - } elseif (preg_match("/\*$/", $argv[$i])) { - $pattern_match = glob($argv[$i] . '.phpt'); - } else { - die('Cannot find test file "' . $argv[$i] . '".' . PHP_EOL); - } - - if (is_array($pattern_match)) { - $test_files = array_merge($test_files, $pattern_match); - } - } elseif (is_dir($testfile)) { - find_files($testfile); - } elseif (substr($testfile, -5) == '.phpt') { - $test_files[] = $testfile; - } else { - die('Cannot find test file "' . $argv[$i] . '".' . PHP_EOL); - } - } - } - - if ($selected_tests && count($test_files) === 0) { - echo "No tests found.\n"; - return; - } - - if (!$php) { - $php = getenv('TEST_PHP_EXECUTABLE'); - } - if (!$php) { - $php = PHP_BINARY; - } - - if (!$php_cgi) { - $php_cgi = getenv('TEST_PHP_CGI_EXECUTABLE'); - } - if (!$php_cgi) { - $php_cgi = get_binary($php, 'php-cgi', 'sapi/cgi/php-cgi'); - } - - if (!$phpdbg) { - $phpdbg = getenv('TEST_PHPDBG_EXECUTABLE'); - } - if (!$phpdbg) { - $phpdbg = get_binary($php, 'phpdbg', 'sapi/phpdbg/phpdbg'); - } - - putenv("TEST_PHP_EXECUTABLE=$php"); - $environment['TEST_PHP_EXECUTABLE'] = $php; - putenv("TEST_PHP_CGI_EXECUTABLE=$php_cgi"); - $environment['TEST_PHP_CGI_EXECUTABLE'] = $php_cgi; - putenv("TEST_PHPDBG_EXECUTABLE=$phpdbg"); - $environment['TEST_PHPDBG_EXECUTABLE'] = $phpdbg; - - if ($conf_passed !== null) { - if (IS_WINDOWS) { - $pass_options .= " -c " . escapeshellarg($conf_passed); - } else { - $pass_options .= " -c '" . realpath($conf_passed) . "'"; - } - } - - $test_files = array_unique($test_files); - $test_files = array_merge($test_files, $redir_tests); - - // Run selected tests. - $test_cnt = count($test_files); - - verify_config(); - write_information(); - - if ($test_cnt) { - putenv('NO_INTERACTION=1'); - usort($test_files, "test_sort"); - $start_time = time(); - - echo "Running selected tests.\n"; - - $test_idx = 0; - run_all_tests($test_files, $environment); - $end_time = time(); - - if ($failed_tests_file) { - fclose($failed_tests_file); - } - - if ($result_tests_file) { - fclose($result_tests_file); - } - - if (0 == count($test_results)) { - echo "No tests were run.\n"; - return; - } - - compute_summary(); - echo "====================================================================="; - echo get_summary(false); - - if ($output_file != '' && $just_save_results) { - save_results($output_file, /* prompt_to_save_results: */ false); - } - } else { - // Compile a list of all test files (*.phpt). - $test_files = []; - $exts_tested = count($exts_to_test); - $exts_skipped = 0; - $ignored_by_ext = 0; - sort($exts_to_test); - $test_dirs = []; - $optionals = ['Zend', 'tests', 'ext', 'sapi']; - - foreach ($optionals as $dir) { - if (is_dir($dir)) { - $test_dirs[] = $dir; - } - } - - // Convert extension names to lowercase - foreach ($exts_to_test as $key => $val) { - $exts_to_test[$key] = strtolower($val); - } - - foreach ($test_dirs as $dir) { - find_files(TEST_PHP_SRCDIR . "/{$dir}", $dir == 'ext'); - } - - foreach ($user_tests as $dir) { - find_files($dir, $dir == 'ext'); - } - - $test_files = array_unique($test_files); - usort($test_files, "test_sort"); - - $start_time = time(); - show_start($start_time); - - $test_cnt = count($test_files); - $test_idx = 0; - run_all_tests($test_files, $environment); - $end_time = time(); - - if ($failed_tests_file) { - fclose($failed_tests_file); - } - - if ($result_tests_file) { - fclose($result_tests_file); - } - - // Summarize results - - if (0 == count($test_results)) { - echo "No tests were run.\n"; - return; - } - - compute_summary(); - - show_end($end_time); - show_summary(); - - save_results($output_file, /* prompt_to_save_results: */ true); - } - - $junit->saveXML(); - if ($bless) { - bless_failed_tests($PHP_FAILED_TESTS['FAILED']); - } - if (getenv('REPORT_EXIT_STATUS') !== '0' && getenv('REPORT_EXIT_STATUS') !== 'no' && - ($sum_results['FAILED'] || $sum_results['BORKED'] || $sum_results['LEAKED'])) { - exit(1); - } -} - -if (!function_exists("hrtime")) { - /** - * @return array|float|int - */ - function hrtime(bool $as_num = false) - { - $t = microtime(true); - - if ($as_num) { - return $t * 1000000000; - } - - $s = floor($t); - return [0 => $s, 1 => ($t - $s) * 1000000000]; - } -} - -function verify_config(): void -{ - global $php; - - if (empty($php) || !file_exists($php)) { - error('environment variable TEST_PHP_EXECUTABLE must be set to specify PHP executable!'); - } - - if (!is_executable($php)) { - error("invalid PHP executable specified by TEST_PHP_EXECUTABLE = $php"); - } -} - -function write_information(): void -{ - global $php, $php_cgi, $phpdbg, $php_info, $user_tests, $ini_overwrites, $pass_options, $exts_to_test, $valgrind, $no_file_cache; - - // Get info from php - $info_file = __DIR__ . '/run-test-info.php'; - @unlink($info_file); - $php_info = ''; - save_text($info_file, $php_info); - $info_params = []; - settings2array($ini_overwrites, $info_params); - $info_params = settings2params($info_params); - $php_info = shell_exec("$php $pass_options $info_params $no_file_cache \"$info_file\""); - define('TESTED_PHP_VERSION', shell_exec("$php -n -r \"echo PHP_VERSION;\"")); - - if ($php_cgi && $php != $php_cgi) { - $php_info_cgi = shell_exec("$php_cgi $pass_options $info_params $no_file_cache -q \"$info_file\""); - $php_info_sep = "\n---------------------------------------------------------------------"; - $php_cgi_info = "$php_info_sep\nPHP : $php_cgi $php_info_cgi$php_info_sep"; - } else { - $php_cgi_info = ''; - } - - if ($phpdbg) { - $phpdbg_info = shell_exec("$phpdbg $pass_options $info_params $no_file_cache -qrr \"$info_file\""); - $php_info_sep = "\n---------------------------------------------------------------------"; - $phpdbg_info = "$php_info_sep\nPHP : $phpdbg $phpdbg_info$php_info_sep"; - } else { - $phpdbg_info = ''; - } - - if (function_exists('opcache_invalidate')) { - opcache_invalidate($info_file, true); - } - @unlink($info_file); - - // load list of enabled and loadable extensions - save_text($info_file, <<<'PHP' - - PHP); - $exts_to_test = explode(',', shell_exec("$php $pass_options $info_params $no_file_cache \"$info_file\"")); - // check for extensions that need special handling and regenerate - $info_params_ex = [ - 'session' => ['session.auto_start=0'], - 'tidy' => ['tidy.clean_output=0'], - 'zlib' => ['zlib.output_compression=Off'], - 'xdebug' => ['xdebug.mode=off'], - ]; - - foreach ($info_params_ex as $ext => $ini_overwrites_ex) { - if (in_array($ext, $exts_to_test)) { - $ini_overwrites = array_merge($ini_overwrites, $ini_overwrites_ex); - } - } - - if (function_exists('opcache_invalidate')) { - opcache_invalidate($info_file, true); - } - @unlink($info_file); - - // Write test context information. - echo " -===================================================================== -PHP : $php $php_info $php_cgi_info $phpdbg_info -CWD : " . TEST_PHP_SRCDIR . " -Extra dirs : "; - foreach ($user_tests as $test_dir) { - echo "{$test_dir}\n "; - } - echo " -VALGRIND : " . ($valgrind ? $valgrind->getHeader() : 'Not used') . " -===================================================================== -"; -} - -function save_results(string $output_file, bool $prompt_to_save_results): void -{ - global $sum_results, $failed_test_summary, - $PHP_FAILED_TESTS, $php; - - if (getenv('NO_INTERACTION') || TRAVIS_CI) { - return; - } - - if ($prompt_to_save_results) { - /* We got failed Tests, offer the user to save a QA report */ - $fp = fopen("php://stdin", "r+"); - if ($sum_results['FAILED'] || $sum_results['BORKED'] || $sum_results['WARNED'] || $sum_results['LEAKED']) { - echo "\nYou may have found a problem in PHP."; - } - echo "\nThis report can be saved and used to open an issue on the bug tracker at\n"; - echo "https://github.com/php/php-src/issues\n"; - echo "This gives us a better understanding of PHP's behavior.\n"; - echo "Do you want to save this report in a file? [Yn]: "; - flush(); - - $user_input = fgets($fp, 10); - fclose($fp); - if (!(strlen(trim($user_input)) == 0 || strtolower($user_input[0]) == 'y')) { - return; - } - } - /** - * Collect information about the host system for our report - * Fetch phpinfo() output so that we can see the PHP environment - * Make an archive of all the failed tests - */ - $failed_tests_data = ''; - $sep = "\n" . str_repeat('=', 80) . "\n"; - $failed_tests_data .= $failed_test_summary . "\n"; - $failed_tests_data .= get_summary(true) . "\n"; - - if ($sum_results['FAILED']) { - foreach ($PHP_FAILED_TESTS['FAILED'] as $test_info) { - $failed_tests_data .= $sep . $test_info['name'] . $test_info['info']; - $failed_tests_data .= $sep . file_get_contents(realpath($test_info['output'])); - $failed_tests_data .= $sep . file_get_contents(realpath($test_info['diff'])); - $failed_tests_data .= $sep . "\n\n"; - } - } - - $failed_tests_data .= "\n" . $sep . 'BUILD ENVIRONMENT' . $sep; - $failed_tests_data .= "OS:\n" . PHP_OS . " - " . php_uname() . "\n\n"; - $ldd = $autoconf = $sys_libtool = $libtool = $compiler = 'N/A'; - - if (!IS_WINDOWS) { - /* If PHP_AUTOCONF is set, use it; otherwise, use 'autoconf'. */ - if (getenv('PHP_AUTOCONF')) { - $autoconf = shell_exec(getenv('PHP_AUTOCONF') . ' --version'); - } else { - $autoconf = shell_exec('autoconf --version'); - } - - /* Always use the generated libtool - Mac OSX uses 'glibtool' */ - $libtool = shell_exec(INIT_DIR . '/libtool --version'); - - /* Use shtool to find out if there is glibtool present (MacOSX) */ - $sys_libtool_path = shell_exec(__DIR__ . '/build/shtool path glibtool libtool'); - - if ($sys_libtool_path) { - $sys_libtool = shell_exec(str_replace("\n", "", $sys_libtool_path) . ' --version'); - } - - /* Try the most common flags for 'version' */ - $flags = ['-v', '-V', '--version']; - $cc_status = 0; - - foreach ($flags as $flag) { - system(getenv('CC') . " $flag >/dev/null 2>&1", $cc_status); - if ($cc_status == 0) { - $compiler = shell_exec(getenv('CC') . " $flag 2>&1"); - break; - } - } - - $ldd = shell_exec("ldd $php 2>/dev/null"); - } - - $failed_tests_data .= "Autoconf:\n$autoconf\n"; - $failed_tests_data .= "Bundled Libtool:\n$libtool\n"; - $failed_tests_data .= "System Libtool:\n$sys_libtool\n"; - $failed_tests_data .= "Compiler:\n$compiler\n"; - $failed_tests_data .= "Bison:\n" . shell_exec('bison --version 2>/dev/null') . "\n"; - $failed_tests_data .= "Libraries:\n$ldd\n"; - $failed_tests_data .= "\n"; - $failed_tests_data .= $sep . "PHPINFO" . $sep; - $failed_tests_data .= shell_exec($php . ' -ddisplay_errors=stderr -dhtml_errors=0 -i 2> /dev/null'); - - file_put_contents($output_file, $failed_tests_data); - echo "Report saved to: ", $output_file, "\n"; -} - -function get_binary(string $php, string $sapi, string $sapi_path): ?string -{ - $dir = dirname($php); - if (IS_WINDOWS && file_exists("$dir/$sapi.exe")) { - return realpath("$dir/$sapi.exe"); - } - // Sources tree - if (file_exists("$dir/../../$sapi_path")) { - return realpath("$dir/../../$sapi_path"); - } - // Installation tree, preserve command prefix/suffix - $inst = str_replace('php', $sapi, basename($php)); - if (file_exists("$dir/$inst")) { - return realpath("$dir/$inst"); - } - return null; -} - -function find_files(string $dir, bool $is_ext_dir = false, bool $ignore = false): void -{ - global $test_files, $exts_to_test, $ignored_by_ext, $exts_skipped; - - $o = opendir($dir) or error("cannot open directory: $dir"); - - while (($name = readdir($o)) !== false) { - if (is_dir("{$dir}/{$name}") && !in_array($name, ['.', '..', '.svn'])) { - $skip_ext = ($is_ext_dir && !in_array(strtolower($name), $exts_to_test)); - if ($skip_ext) { - $exts_skipped++; - } - find_files("{$dir}/{$name}", false, $ignore || $skip_ext); - } - - // Cleanup any left-over tmp files from last run. - if (substr($name, -4) == '.tmp') { - @unlink("$dir/$name"); - continue; - } - - // Otherwise we're only interested in *.phpt files. - // (but not those starting with a dot, which are hidden on - // many platforms) - if (substr($name, -5) == '.phpt' && substr($name, 0, 1) !== '.') { - if ($ignore) { - $ignored_by_ext++; - } else { - $testfile = realpath("{$dir}/{$name}"); - $test_files[] = $testfile; - } - } - } - - closedir($o); -} - -/** - * @param array|string $name - */ -function test_name($name): string -{ - if (is_array($name)) { - return $name[0] . ':' . $name[1]; - } - - return $name; -} -/** - * @param array|string $a - * @param array|string $b - */ -function test_sort($a, $b): int -{ - $a = test_name($a); - $b = test_name($b); - - $ta = strpos($a, TEST_PHP_SRCDIR . "/tests") === 0 ? 1 + (strpos($a, - TEST_PHP_SRCDIR . "/tests/run-test") === 0 ? 1 : 0) : 0; - $tb = strpos($b, TEST_PHP_SRCDIR . "/tests") === 0 ? 1 + (strpos($b, - TEST_PHP_SRCDIR . "/tests/run-test") === 0 ? 1 : 0) : 0; - - if ($ta == $tb) { - return strcmp($a, $b); - } - - return $tb - $ta; -} - -// -// Write the given text to a temporary file, and return the filename. -// - -function save_text(string $filename, string $text, ?string $filename_copy = null): void -{ - global $DETAILED; - - if ($filename_copy && $filename_copy != $filename && file_put_contents($filename_copy, $text) === false) { - error("Cannot open file '" . $filename_copy . "' (save_text)"); - } - - if (file_put_contents($filename, $text) === false) { - error("Cannot open file '" . $filename . "' (save_text)"); - } - - if (1 < $DETAILED) { - echo " -FILE $filename {{{ -$text -}}} -"; - } -} - -// -// Write an error in a format recognizable to Emacs or MSVC. -// - -function error_report(string $testname, string $logname, string $tested): void -{ - $testname = realpath($testname); - $logname = realpath($logname); - - switch (strtoupper(getenv('TEST_PHP_ERROR_STYLE'))) { - case 'MSVC': - echo $testname . "(1) : $tested\n"; - echo $logname . "(1) : $tested\n"; - break; - case 'EMACS': - echo $testname . ":1: $tested\n"; - echo $logname . ":1: $tested\n"; - break; - } -} - -/** - * @return false|string - */ -function system_with_timeout( - string $commandline, - ?array $env = null, - ?string $stdin = null, - bool $captureStdIn = true, - bool $captureStdOut = true, - bool $captureStdErr = true -) { - global $valgrind; - - $data = ''; - - $bin_env = []; - foreach ((array) $env as $key => $value) { - $bin_env[$key] = $value; - } - - $descriptorspec = []; - if ($captureStdIn) { - $descriptorspec[0] = ['pipe', 'r']; - } - if ($captureStdOut) { - $descriptorspec[1] = ['pipe', 'w']; - } - if ($captureStdErr) { - $descriptorspec[2] = ['pipe', 'w']; - } - $proc = proc_open($commandline, $descriptorspec, $pipes, TEST_PHP_SRCDIR, $bin_env, ['suppress_errors' => true]); - - if (!$proc) { - return false; - } - - if ($captureStdIn) { - if (!is_null($stdin)) { - fwrite($pipes[0], $stdin); - } - fclose($pipes[0]); - unset($pipes[0]); - } - - $timeout = $valgrind ? 300 : ($env['TEST_TIMEOUT'] ?? 60); - /* ASAN can cause a ~2-3x slowdown. */ - if (isset($env['SKIP_ASAN'])) { - $timeout *= 3; - } - - while (true) { - /* hide errors from interrupted syscalls */ - $r = $pipes; - $w = null; - $e = null; - - $n = @stream_select($r, $w, $e, $timeout); - - if ($n === false) { - break; - } - - if ($n === 0) { - /* timed out */ - $data .= "\n ** ERROR: process timed out **\n"; - proc_terminate($proc, 9); - return $data; - } - - if ($n > 0) { - if ($captureStdOut) { - $line = fread($pipes[1], 8192); - } elseif ($captureStdErr) { - $line = fread($pipes[2], 8192); - } else { - $line = ''; - } - if (strlen($line) == 0) { - /* EOF */ - break; - } - $data .= $line; - } - } - - $stat = proc_get_status($proc); - - if ($stat['signaled']) { - $data .= "\nTermsig=" . $stat['stopsig'] . "\n"; - } - if ($stat["exitcode"] > 128 && $stat["exitcode"] < 160) { - $data .= "\nTermsig=" . ($stat["exitcode"] - 128) . "\n"; - } else if (defined('PHP_WINDOWS_VERSION_MAJOR') && (($stat["exitcode"] >> 28) & 0b1111) === 0b1100) { - // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/87fba13e-bf06-450e-83b1-9241dc81e781 - $data .= "\nTermsig=" . $stat["exitcode"] . "\n"; - } - - proc_close($proc); - return $data; -} - -/** - * @param string|array|null $redir_tested - */ -function run_all_tests(array $test_files, array $env, $redir_tested = null): void -{ - global $test_results, $failed_tests_file, $result_tests_file, $php, $test_idx, $file_cache; - global $preload; - // Parallel testing - global $PHP_FAILED_TESTS, $workers, $workerID, $workerSock; - - if ($file_cache !== null || $preload) { - /* Automatically skip opcache tests in --file-cache and --preload mode, - * because opcache generally expects these to run under a default configuration. */ - $test_files = array_filter($test_files, function($test) use($preload) { - if (!is_string($test)) { - return true; - } - if (false !== strpos($test, 'ext/opcache')) { - return false; - } - if ($preload && false !== strpos($test, 'ext/zend_test/tests/observer')) { - return false; - } - return true; - }); - } - - /* Ignore -jN if there is only one file to analyze. */ - if ($workers !== null && count($test_files) > 1 && !$workerID) { - run_all_tests_parallel($test_files, $env, $redir_tested); - return; - } - - foreach ($test_files as $name) { - if (is_array($name)) { - $index = "# $name[1]: $name[0]"; - - if ($redir_tested) { - $name = $name[0]; - } - } elseif ($redir_tested) { - $index = "# $redir_tested: $name"; - } else { - $index = $name; - } - $test_idx++; - - if ($workerID) { - $PHP_FAILED_TESTS = ['BORKED' => [], 'FAILED' => [], 'WARNED' => [], 'LEAKED' => [], 'XFAILED' => [], 'XLEAKED' => [], 'SLOW' => []]; - ob_start(); - } - - $result = run_test($php, $name, $env); - if ($workerID) { - $resultText = ob_get_clean(); - } - - if (!is_array($name) && $result != 'REDIR') { - if ($workerID) { - send_message($workerSock, [ - "type" => "test_result", - "name" => $name, - "index" => $index, - "result" => $result, - "text" => $resultText, - "PHP_FAILED_TESTS" => $PHP_FAILED_TESTS - ]); - continue; - } - - $test_results[$index] = $result; - if ($failed_tests_file && ($result == 'XFAILED' || $result == 'XLEAKED' || $result == 'FAILED' || $result == 'WARNED' || $result == 'LEAKED')) { - fwrite($failed_tests_file, "$index\n"); - } - if ($result_tests_file) { - fwrite($result_tests_file, "$result\t$index\n"); - } - } - } -} - -/** The heart of parallel testing. - * @param string|array|null $redir_tested - */ -function run_all_tests_parallel(array $test_files, array $env, $redir_tested): void -{ - global $workers, $test_idx, $test_cnt, $test_results, $failed_tests_file, $result_tests_file, $PHP_FAILED_TESTS, $shuffle, $SHOW_ONLY_GROUPS, $valgrind, $show_progress; - - global $junit; - - // The PHP binary running run-tests.php, and run-tests.php itself - // This PHP executable is *not* necessarily the same as the tested version - $thisPHP = PHP_BINARY; - $thisScript = __FILE__; - - $workerProcs = []; - $workerSocks = []; - - // Each test may specify a list of conflict keys. While a test that conflicts with - // key K is running, no other test that conflicts with K may run. Conflict keys are - // specified either in the --CONFLICTS-- section, or CONFLICTS file inside a directory. - $dirConflictsWith = []; - $fileConflictsWith = []; - $sequentialTests = []; - foreach ($test_files as $i => $file) { - $contents = file_get_contents($file); - if (preg_match('/^--CONFLICTS--(.+?)^--/ms', $contents, $matches)) { - $conflicts = parse_conflicts($matches[1]); - } else { - // Cache per-directory conflicts in a separate map, so we compute these only once. - $dir = dirname($file); - if (!isset($dirConflictsWith[$dir])) { - $dirConflicts = []; - if (file_exists($dir . '/CONFLICTS')) { - $contents = file_get_contents($dir . '/CONFLICTS'); - $dirConflicts = parse_conflicts($contents); - } - $dirConflictsWith[$dir] = $dirConflicts; - } - $conflicts = $dirConflictsWith[$dir]; - } - - // For tests conflicting with "all", no other tests may run in parallel. We'll run these - // tests separately at the end, when only one worker is left. - if (in_array('all', $conflicts, true)) { - $sequentialTests[] = $file; - unset($test_files[$i]); - } - - $fileConflictsWith[$file] = $conflicts; - } - - // Some tests assume that they are executed in a certain order. We will be popping from - // $test_files, so reverse its order here. This makes sure that order is preserved at least - // for tests with a common conflict key. - $test_files = array_reverse($test_files); - - // To discover parallelization issues it is useful to randomize the test order. - if ($shuffle) { - shuffle($test_files); - } - - // Don't start more workers than test files. - $workers = max(1, min($workers, count($test_files))); - - echo "Spawning $workers workers... "; - - // We use sockets rather than STDIN/STDOUT for comms because on Windows, - // those can't be non-blocking for some reason. - $listenSock = stream_socket_server("tcp://127.0.0.1:0") or error("Couldn't create socket on localhost."); - $sockName = stream_socket_get_name($listenSock, false); - // PHP is terrible and returns IPv6 addresses not enclosed by [] - $portPos = strrpos($sockName, ":"); - $sockHost = substr($sockName, 0, $portPos); - if (false !== strpos($sockHost, ":")) { - $sockHost = "[$sockHost]"; - } - $sockPort = substr($sockName, $portPos + 1); - $sockUri = "tcp://$sockHost:$sockPort"; - $totalFileCount = count($test_files); - - $startTime = microtime(true); - for ($i = 1; $i <= $workers; $i++) { - $proc = proc_open( - [$thisPHP, $thisScript], - [], // Inherit our stdin, stdout and stderr - $pipes, - null, - $GLOBALS['environment'] + [ - "TEST_PHP_WORKER" => $i, - "TEST_PHP_URI" => $sockUri, - ], - [ - "suppress_errors" => true, - 'create_new_console' => true, - ] - ); - if ($proc === false) { - kill_children($workerProcs); - error("Failed to spawn worker $i"); - } - $workerProcs[$i] = $proc; - } - - for ($i = 1; $i <= $workers; $i++) { - $workerSock = stream_socket_accept($listenSock, 5); - if ($workerSock === false) { - kill_children($workerProcs); - error("Failed to accept connection from worker."); - } - - $greeting = base64_encode(serialize([ - "type" => "hello", - "GLOBALS" => $GLOBALS, - "constants" => [ - "INIT_DIR" => INIT_DIR, - "TEST_PHP_SRCDIR" => TEST_PHP_SRCDIR, - "TRAVIS_CI" => TRAVIS_CI - ] - ])) . "\n"; - - stream_set_timeout($workerSock, 5); - if (fwrite($workerSock, $greeting) === false) { - kill_children($workerProcs); - error("Failed to send greeting to worker."); - } - - $rawReply = fgets($workerSock); - if ($rawReply === false) { - kill_children($workerProcs); - error("Failed to read greeting reply from worker."); - } - - $reply = unserialize(base64_decode($rawReply)); - if (!$reply || $reply["type"] !== "hello_reply") { - kill_children($workerProcs); - error("Greeting reply from worker unexpected or could not be decoded: '$rawReply'"); - } - - stream_set_timeout($workerSock, 0); - stream_set_blocking($workerSock, false); - - $workerID = $reply["workerID"]; - $workerSocks[$workerID] = $workerSock; - } - printf("Done in %.2fs\n", microtime(true) - $startTime); - echo "=====================================================================\n"; - echo "\n"; - - $rawMessageBuffers = []; - $testsInProgress = 0; - - // Map from conflict key to worker ID. - $activeConflicts = []; - // Tests waiting due to conflicts. Map from conflict key to array. - $waitingTests = []; - -escape: - while ($test_files || $sequentialTests || $testsInProgress > 0) { - $toRead = array_values($workerSocks); - $toWrite = null; - $toExcept = null; - if (stream_select($toRead, $toWrite, $toExcept, 10)) { - foreach ($toRead as $workerSock) { - $i = array_search($workerSock, $workerSocks); - if ($i === false) { - kill_children($workerProcs); - error("Could not find worker stdout in array of worker stdouts, THIS SHOULD NOT HAPPEN."); - } - if (feof($workerSock)) { - kill_children($workerProcs); - error("Worker $i died unexpectedly"); - } - while (false !== ($rawMessage = fgets($workerSock))) { - // work around fgets truncating things - if (($rawMessageBuffers[$i] ?? '') !== '') { - $rawMessage = $rawMessageBuffers[$i] . $rawMessage; - $rawMessageBuffers[$i] = ''; - } - if (substr($rawMessage, -1) !== "\n") { - $rawMessageBuffers[$i] = $rawMessage; - continue; - } - - $message = unserialize(base64_decode($rawMessage)); - if (!$message) { - kill_children($workerProcs); - $stuff = fread($workerSock, 65536); - error("Could not decode message from worker $i: '$rawMessage$stuff'"); - } - - switch ($message["type"]) { - case "tests_finished": - $testsInProgress--; - foreach ($activeConflicts as $key => $workerId) { - if ($workerId === $i) { - unset($activeConflicts[$key]); - if (isset($waitingTests[$key])) { - while ($test = array_pop($waitingTests[$key])) { - $test_files[] = $test; - } - unset($waitingTests[$key]); - } - } - } - $junit->mergeResults($message["junit"]); - // no break - case "ready": - // Schedule sequential tests only once we are down to one worker. - if (count($workerProcs) === 1 && $sequentialTests) { - $test_files = array_merge($test_files, $sequentialTests); - $sequentialTests = []; - } - // Batch multiple tests to reduce communication overhead. - // - When valgrind is used, communication overhead is relatively small, - // so just use a batch size of 1. - // - If this is running a small enough number of tests, - // reduce the batch size to give batches to more workers. - $files = []; - $maxBatchSize = $valgrind ? 1 : ($shuffle ? 4 : 32); - $averageFilesPerWorker = max(1, (int) ceil($totalFileCount / count($workerProcs))); - $batchSize = min($maxBatchSize, $averageFilesPerWorker); - while (count($files) <= $batchSize && $file = array_pop($test_files)) { - foreach ($fileConflictsWith[$file] as $conflictKey) { - if (isset($activeConflicts[$conflictKey])) { - $waitingTests[$conflictKey][] = $file; - continue 2; - } - } - $files[] = $file; - } - if ($files) { - foreach ($files as $file) { - foreach ($fileConflictsWith[$file] as $conflictKey) { - $activeConflicts[$conflictKey] = $i; - } - } - $testsInProgress++; - send_message($workerSocks[$i], [ - "type" => "run_tests", - "test_files" => $files, - "env" => $env, - "redir_tested" => $redir_tested - ]); - } else { - proc_terminate($workerProcs[$i]); - unset($workerProcs[$i], $workerSocks[$i]); - goto escape; - } - break; - case "test_result": - list($name, $index, $result, $resultText) = [$message["name"], $message["index"], $message["result"], $message["text"]]; - foreach ($message["PHP_FAILED_TESTS"] as $category => $tests) { - $PHP_FAILED_TESTS[$category] = array_merge($PHP_FAILED_TESTS[$category], $tests); - } - $test_idx++; - - if ($show_progress) { - clear_show_test(); - } - - echo $resultText; - - if ($show_progress) { - show_test($test_idx, count($workerProcs) . "/$workers concurrent test workers running"); - } - - if (!is_array($name) && $result != 'REDIR') { - $test_results[$index] = $result; - - if ($failed_tests_file && ($result == 'XFAILED' || $result == 'XLEAKED' || $result == 'FAILED' || $result == 'WARNED' || $result == 'LEAKED')) { - fwrite($failed_tests_file, "$index\n"); - } - if ($result_tests_file) { - fwrite($result_tests_file, "$result\t$index\n"); - } - } - break; - case "error": - kill_children($workerProcs); - error("Worker $i reported error: $message[msg]"); - break; - case "php_error": - kill_children($workerProcs); - $error_consts = [ - 'E_ERROR', - 'E_WARNING', - 'E_PARSE', - 'E_NOTICE', - 'E_CORE_ERROR', - 'E_CORE_WARNING', - 'E_COMPILE_ERROR', - 'E_COMPILE_WARNING', - 'E_USER_ERROR', - 'E_USER_WARNING', - 'E_USER_NOTICE', - 'E_STRICT', // TODO Cleanup when removed from Zend Engine. - 'E_RECOVERABLE_ERROR', - 'E_DEPRECATED', - 'E_USER_DEPRECATED' - ]; - $error_consts = array_combine(array_map('constant', $error_consts), $error_consts); - error("Worker $i reported unexpected {$error_consts[$message['errno']]}: $message[errstr] in $message[errfile] on line $message[errline]"); - // no break - default: - kill_children($workerProcs); - error("Unrecognised message type '$message[type]' from worker $i"); - } - } - } - } - } - - if ($show_progress) { - clear_show_test(); - } - - kill_children($workerProcs); - - if ($testsInProgress < 0) { - error("$testsInProgress test batches “in progress”, which is less than zero. THIS SHOULD NOT HAPPEN."); - } -} - -function send_message($stream, array $message): void -{ - $blocking = stream_get_meta_data($stream)["blocked"]; - stream_set_blocking($stream, true); - fwrite($stream, base64_encode(serialize($message)) . "\n"); - stream_set_blocking($stream, $blocking); -} - -function kill_children(array $children): void -{ - foreach ($children as $child) { - if ($child) { - proc_terminate($child); - } - } -} - -function run_worker(): void -{ - global $workerID, $workerSock; - - global $junit; - - $sockUri = getenv("TEST_PHP_URI"); - - $workerSock = stream_socket_client($sockUri, $_, $_, 5) or error("Couldn't connect to $sockUri"); - - $greeting = fgets($workerSock); - $greeting = unserialize(base64_decode($greeting)) or die("Could not decode greeting\n"); - if ($greeting["type"] !== "hello") { - error("Unexpected greeting of type $greeting[type]"); - } - - set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use ($workerSock): bool { - if (error_reporting() & $errno) { - send_message($workerSock, compact('errno', 'errstr', 'errfile', 'errline') + [ - 'type' => 'php_error' - ]); - } - - return true; - }); - - foreach ($greeting["GLOBALS"] as $var => $value) { - if ($var !== "workerID" && $var !== "workerSock" && $var !== "GLOBALS") { - $GLOBALS[$var] = $value; - } - } - foreach ($greeting["constants"] as $const => $value) { - define($const, $value); - } - - send_message($workerSock, [ - "type" => "hello_reply", - "workerID" => $workerID - ]); - - send_message($workerSock, [ - "type" => "ready" - ]); - - while (($command = fgets($workerSock))) { - $command = unserialize(base64_decode($command)); - - switch ($command["type"]) { - case "run_tests": - run_all_tests($command["test_files"], $command["env"], $command["redir_tested"]); - send_message($workerSock, [ - "type" => "tests_finished", - "junit" => $junit->isEnabled() ? $junit : null, - ]); - $junit->clear(); - break; - default: - send_message($workerSock, [ - "type" => "error", - "msg" => "Unrecognised message type: $command[type]" - ]); - break 2; - } - } -} - -// -// Show file or result block -// -function show_file_block(string $file, string $block, ?string $section = null): void -{ - global $cfg; - global $colorize; - - if ($cfg['show'][$file]) { - if (is_null($section)) { - $section = strtoupper($file); - } - if ($section === 'DIFF' && $colorize) { - // '-' is Light Red for removal, '+' is Light Green for addition - $block = preg_replace('/^[0-9]+\-\s.*$/m', "\e[1;31m\\0\e[0m", $block); - $block = preg_replace('/^[0-9]+\+\s.*$/m', "\e[1;32m\\0\e[0m", $block); - } - - echo "\n========" . $section . "========\n"; - echo rtrim($block); - echo "\n========DONE========\n"; - } -} - -function skip_test(string $tested, string $tested_file, string $shortname, string $reason) { - global $junit; - - show_result('SKIP', $tested, $tested_file, "reason: $reason"); - $junit->initSuite($junit->getSuiteName($shortname)); - $junit->markTestAs('SKIP', $shortname, $tested, 0, $reason); - return 'SKIPPED'; -} - -// -// Run an individual test case. -// -/** - * @param string|array $file - */ -function run_test(string $php, $file, array $env): string -{ - global $log_format, $ini_overwrites, $PHP_FAILED_TESTS; - global $pass_options, $DETAILED, $IN_REDIRECT, $test_cnt, $test_idx; - global $valgrind, $temp_source, $temp_target, $cfg, $environment; - global $no_clean; - global $SHOW_ONLY_GROUPS; - global $no_file_cache; - global $slow_min_ms; - global $preload, $file_cache; - global $num_repeats; - // Parallel testing - global $workerID; - global $show_progress; - - // Temporary - /** @var JUnit */ - global $junit; - - static $skipCache; - if (!$skipCache) { - $enableSkipCache = !($env['DISABLE_SKIP_CACHE'] ?? '0'); - $skipCache = new SkipCache($enableSkipCache, $cfg['keep']['skip']); - } - - $retried = false; -retry: - - $temp_filenames = null; - $org_file = $file; - $orig_php = $php; - - $php_cgi = $env['TEST_PHP_CGI_EXECUTABLE'] ?? null; - $phpdbg = $env['TEST_PHPDBG_EXECUTABLE'] ?? null; - - if (is_array($file)) { - $file = $file[0]; - } - - if ($DETAILED) { - echo " -================= -TEST $file -"; - } - - $shortname = str_replace(TEST_PHP_SRCDIR . '/', '', $file); - $tested_file = $shortname; - - try { - $test = new TestFile($file, (bool)$IN_REDIRECT); - } catch (BorkageException $ex) { - show_result("BORK", $ex->getMessage(), $tested_file); - $PHP_FAILED_TESTS['BORKED'][] = [ - 'name' => $file, - 'test_name' => '', - 'output' => '', - 'diff' => '', - 'info' => "{$ex->getMessage()} [$file]", - ]; - - $junit->markTestAs('BORK', $shortname, $tested_file, 0, $ex->getMessage()); - return 'BORKED'; - } - - $tested = $test->getName(); - - if ($test->hasSection('FILE_EXTERNAL')) { - if ($num_repeats > 1) { - return skip_test($tested, $tested_file, $shortname, 'Test with FILE_EXTERNAL might not be repeatable'); - } - } - - if ($test->hasSection('CAPTURE_STDIO')) { - $capture = $test->getSection('CAPTURE_STDIO'); - $captureStdIn = stripos($capture, 'STDIN') !== false; - $captureStdOut = stripos($capture, 'STDOUT') !== false; - $captureStdErr = stripos($capture, 'STDERR') !== false; - } else { - $captureStdIn = true; - $captureStdOut = true; - $captureStdErr = true; - } - if ($captureStdOut && $captureStdErr) { - $cmdRedirect = ' 2>&1'; - } else { - $cmdRedirect = ''; - } - - /* For GET/POST/PUT tests, check if cgi sapi is available and if it is, use it. */ - if ($test->isCGI()) { - if (!$php_cgi) { - return skip_test($tested, $tested_file, $shortname, 'CGI not available'); - } - $php = $php_cgi . ' -C '; - $uses_cgi = true; - if ($num_repeats > 1) { - return skip_test($tested, $tested_file, $shortname, 'CGI does not support --repeat'); - } - } - - /* For phpdbg tests, check if phpdbg sapi is available and if it is, use it. */ - $extra_options = ''; - if ($test->hasSection('PHPDBG')) { - if (isset($phpdbg)) { - $php = $phpdbg . ' -qIb'; - - // Additional phpdbg command line options for sections that need to - // be run straight away. For example, EXTENSIONS, SKIPIF, CLEAN. - $extra_options = '-rr'; - } else { - return skip_test($tested, $tested_file, $shortname, 'phpdbg not available'); - } - if ($num_repeats > 1) { - return skip_test($tested, $tested_file, $shortname, 'phpdbg does not support --repeat'); - } - } - - foreach (['CLEAN', 'STDIN', 'CAPTURE_STDIO'] as $section) { - if ($test->hasSection($section)) { - if ($num_repeats > 1) { - return skip_test($tested, $tested_file, $shortname, "Test with $section might not be repeatable"); - } - } - } - - if ($show_progress && !$workerID) { - show_test($test_idx, $shortname); - } - - if (is_array($IN_REDIRECT)) { - $temp_dir = $test_dir = $IN_REDIRECT['dir']; - } else { - $temp_dir = $test_dir = realpath(dirname($file)); - } - - if ($temp_source && $temp_target) { - $temp_dir = str_replace($temp_source, $temp_target, $temp_dir); - } - - $main_file_name = basename($file, 'phpt'); - - $diff_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'diff'; - $log_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'log'; - $exp_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'exp'; - $output_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'out'; - $memcheck_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'mem'; - $sh_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'sh'; - $temp_file = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'php'; - $test_file = $test_dir . DIRECTORY_SEPARATOR . $main_file_name . 'php'; - $temp_skipif = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'skip.php'; - $test_skipif = $test_dir . DIRECTORY_SEPARATOR . $main_file_name . 'skip.php'; - $temp_clean = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'clean.php'; - $test_clean = $test_dir . DIRECTORY_SEPARATOR . $main_file_name . 'clean.php'; - $preload_filename = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'preload.php'; - $tmp_post = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'post'; - $tmp_relative_file = str_replace(__DIR__ . DIRECTORY_SEPARATOR, '', $test_file) . 't'; - - if ($temp_source && $temp_target) { - $temp_skipif .= 's'; - $temp_file .= 's'; - $temp_clean .= 's'; - $copy_file = $temp_dir . DIRECTORY_SEPARATOR . basename(is_array($file) ? $file[1] : $file) . '.phps'; - - if (!is_dir(dirname($copy_file))) { - mkdir(dirname($copy_file), 0777, true) or error("Cannot create output directory - " . dirname($copy_file)); - } - - if ($test->hasSection('FILE')) { - save_text($copy_file, $test->getSection('FILE')); - } - - $temp_filenames = [ - 'file' => $copy_file, - 'diff' => $diff_filename, - 'log' => $log_filename, - 'exp' => $exp_filename, - 'out' => $output_filename, - 'mem' => $memcheck_filename, - 'sh' => $sh_filename, - 'php' => $temp_file, - 'skip' => $temp_skipif, - 'clean' => $temp_clean - ]; - } - - if (is_array($IN_REDIRECT)) { - $tested = $IN_REDIRECT['prefix'] . ' ' . $tested; - $tested_file = $tmp_relative_file; - $shortname = str_replace(TEST_PHP_SRCDIR . '/', '', $tested_file); - } - - // unlink old test results - @unlink($diff_filename); - @unlink($log_filename); - @unlink($exp_filename); - @unlink($output_filename); - @unlink($memcheck_filename); - @unlink($sh_filename); - @unlink($temp_file); - @unlink($test_file); - @unlink($temp_skipif); - @unlink($test_skipif); - @unlink($tmp_post); - @unlink($temp_clean); - @unlink($test_clean); - @unlink($preload_filename); - - // Reset environment from any previous test. - $env['REDIRECT_STATUS'] = ''; - $env['QUERY_STRING'] = ''; - $env['PATH_TRANSLATED'] = ''; - $env['SCRIPT_FILENAME'] = ''; - $env['REQUEST_METHOD'] = ''; - $env['CONTENT_TYPE'] = ''; - $env['CONTENT_LENGTH'] = ''; - $env['TZ'] = ''; - - if ($test->sectionNotEmpty('ENV')) { - $env_str = str_replace('{PWD}', dirname($file), $test->getSection('ENV')); - foreach (explode("\n", $env_str) as $e) { - $e = explode('=', trim($e), 2); - - if (!empty($e[0]) && isset($e[1])) { - $env[$e[0]] = $e[1]; - } - } - } - - // Default ini settings - $ini_settings = $workerID ? ['opcache.cache_id' => "worker$workerID"] : []; - - // Additional required extensions - $extensions = []; - if ($test->hasSection('EXTENSIONS')) { - $extensions = preg_split("/[\n\r]+/", trim($test->getSection('EXTENSIONS'))); - } - if (is_array($IN_REDIRECT) && $IN_REDIRECT['EXTENSIONS'] != []) { - $extensions = array_merge($extensions, $IN_REDIRECT['EXTENSIONS']); - } - - /* Load required extensions */ - if ($extensions != []) { - $ext_params = []; - settings2array($ini_overwrites, $ext_params); - $ext_params = settings2params($ext_params); - [$ext_dir, $loaded] = $skipCache->getExtensions("$orig_php $pass_options $extra_options $ext_params $no_file_cache"); - $ext_prefix = IS_WINDOWS ? "php_" : ""; - $missing = []; - foreach ($extensions as $req_ext) { - if (!in_array(strtolower($req_ext), $loaded)) { - if ($req_ext == 'opcache' || $req_ext == 'xdebug') { - $ext_file = $ext_dir . DIRECTORY_SEPARATOR . $ext_prefix . $req_ext . '.' . PHP_SHLIB_SUFFIX; - $ini_settings['zend_extension'][] = $ext_file; - } else { - $ext_file = $ext_dir . DIRECTORY_SEPARATOR . $ext_prefix . $req_ext . '.' . PHP_SHLIB_SUFFIX; - $ini_settings['extension'][] = $ext_file; - } - if (!is_readable($ext_file)) { - $missing[] = $req_ext; - } - } - } - if ($missing) { - $message = 'Required extension' . (count($missing) > 1 ? 's' : '') - . ' missing: ' . implode(', ', $missing); - return skip_test($tested, $tested_file, $shortname, $message); - } - } - - // additional ini overwrites - //$ini_overwrites[] = 'setting=value'; - settings2array($ini_overwrites, $ini_settings); - - $orig_ini_settings = settings2params($ini_settings); - - if ($file_cache !== null) { - $ini_settings['opcache.file_cache'] = '/tmp'; - // Make sure warnings still show up on the second run. - $ini_settings['opcache.record_warnings'] = '1'; - // File cache is currently incompatible with JIT. - $ini_settings['opcache.jit'] = '0'; - if ($file_cache === 'use') { - // Disable timestamp validation in order to fetch from file cache, - // even though all the files are re-created. - $ini_settings['opcache.validate_timestamps'] = '0'; - } - } else if ($num_repeats > 1) { - // Make sure warnings still show up on the second run. - $ini_settings['opcache.record_warnings'] = '1'; - } - if (extension_loaded('posix') && posix_getuid() === 0) { - $ini_settings['opcache.preload_user'] = 'root'; - } - - // Any special ini settings - // these may overwrite the test defaults... - if ($test->hasSection('INI')) { - $ini = str_replace('{PWD}', dirname($file), $test->getSection('INI')); - $ini = str_replace('{TMP}', sys_get_temp_dir(), $ini); - $replacement = IS_WINDOWS ? '"' . PHP_BINARY . ' -r \"while ($in = fgets(STDIN)) echo $in;\" > $1"' : 'tee $1 >/dev/null'; - $ini = preg_replace('/{MAIL:(\S+)}/', $replacement, $ini); - settings2array(preg_split("/[\n\r]+/", $ini), $ini_settings); - - if (isset($ini_settings['opcache.opt_debug_level'])) { - if ($num_repeats > 1) { - return skip_test($tested, $tested_file, $shortname, 'opt_debug_level tests are not repeatable'); - } - } - } - - $ini_settings = settings2params($ini_settings); - - $env['TEST_PHP_EXTRA_ARGS'] = $pass_options . ' ' . $ini_settings; - - // Check if test should be skipped. - $info = ''; - $warn = false; - - if ($test->sectionNotEmpty('SKIPIF')) { - show_file_block('skip', $test->getSection('SKIPIF')); - $extra = !IS_WINDOWS ? - "unset REQUEST_METHOD; unset QUERY_STRING; unset PATH_TRANSLATED; unset SCRIPT_FILENAME; unset REQUEST_METHOD;" : ""; - - if ($valgrind) { - $env['USE_ZEND_ALLOC'] = '0'; - $env['ZEND_DONT_UNLOAD_MODULES'] = 1; - } - - $junit->startTimer($shortname); - - $startTime = microtime(true); - $commandLine = "$extra $php $pass_options $extra_options -q $orig_ini_settings $no_file_cache -d display_errors=1 -d display_startup_errors=0"; - $output = $skipCache->checkSkip($commandLine, $test->getSection('SKIPIF'), $test_skipif, $temp_skipif, $env); - - $time = microtime(true) - $startTime; - $junit->stopTimer($shortname); - - if ($time > $slow_min_ms / 1000) { - $PHP_FAILED_TESTS['SLOW'][] = [ - 'name' => $file, - 'test_name' => 'SKIPIF of ' . $tested . " [$tested_file]", - 'output' => '', - 'diff' => '', - 'info' => $time, - ]; - } - - if (!$cfg['keep']['skip']) { - @unlink($test_skipif); - } - - if (!strncasecmp('skip', $output, 4)) { - if (preg_match('/^skip\s*(.+)/i', $output, $m)) { - show_result('SKIP', $tested, $tested_file, "reason: $m[1]", $temp_filenames); - } else { - show_result('SKIP', $tested, $tested_file, '', $temp_filenames); - } - - $message = !empty($m[1]) ? $m[1] : ''; - $junit->markTestAs('SKIP', $shortname, $tested, null, $message); - return 'SKIPPED'; - } - - - if (!strncasecmp('info', $output, 4) && preg_match('/^info\s*(.+)/i', $output, $m)) { - $info = " (info: $m[1])"; - } elseif (!strncasecmp('warn', $output, 4) && preg_match('/^warn\s+(.+)/i', $output, $m)) { - $warn = true; /* only if there is a reason */ - $info = " (warn: $m[1])"; - } elseif (!strncasecmp('xfail', $output, 5)) { - // Pretend we have an XFAIL section - $test->setSection('XFAIL', ltrim(substr($output, 5))); - } elseif (!strncasecmp('flaky', $output, 5)) { - // Pretend we have a FLAKY section - $test->setSection('FLAKY', ltrim(substr($output, 5))); - } elseif ($output !== '') { - show_result("BORK", $output, $tested_file, 'reason: invalid output from SKIPIF', $temp_filenames); - $PHP_FAILED_TESTS['BORKED'][] = [ - 'name' => $file, - 'test_name' => '', - 'output' => '', - 'diff' => '', - 'info' => "$output [$file]", - ]; - - $junit->markTestAs('BORK', $shortname, $tested, null, $output); - return 'BORKED'; - } - } - - if (!extension_loaded("zlib") && $test->hasAnySections("GZIP_POST", "DEFLATE_POST")) { - $message = "ext/zlib required"; - show_result('SKIP', $tested, $tested_file, "reason: $message", $temp_filenames); - $junit->markTestAs('SKIP', $shortname, $tested, null, $message); - return 'SKIPPED'; - } - - if ($test->hasSection('REDIRECTTEST')) { - $test_files = []; - - $IN_REDIRECT = eval($test->getSection('REDIRECTTEST')); - $IN_REDIRECT['via'] = "via [$shortname]\n\t"; - $IN_REDIRECT['dir'] = realpath(dirname($file)); - $IN_REDIRECT['prefix'] = $tested; - $IN_REDIRECT['EXTENSIONS'] = $extensions; - - if (!empty($IN_REDIRECT['TESTS'])) { - if (is_array($org_file)) { - $test_files[] = $org_file[1]; - } else { - $GLOBALS['test_files'] = $test_files; - find_files($IN_REDIRECT['TESTS']); - - foreach ($GLOBALS['test_files'] as $f) { - $test_files[] = [$f, $file]; - } - } - $test_cnt += count($test_files) - 1; - $test_idx--; - - show_redirect_start($IN_REDIRECT['TESTS'], $tested, $tested_file); - - // set up environment - $redirenv = array_merge($environment, $IN_REDIRECT['ENV']); - $redirenv['REDIR_TEST_DIR'] = realpath($IN_REDIRECT['TESTS']) . DIRECTORY_SEPARATOR; - - usort($test_files, "test_sort"); - run_all_tests($test_files, $redirenv, $tested); - - show_redirect_ends($IN_REDIRECT['TESTS'], $tested, $tested_file); - - // a redirected test never fails - $IN_REDIRECT = false; - - $junit->markTestAs('PASS', $shortname, $tested); - return 'REDIR'; - } - - $bork_info = "Redirect info must contain exactly one TEST string to be used as redirect directory."; - show_result("BORK", $bork_info, '', '', $temp_filenames); - $PHP_FAILED_TESTS['BORKED'][] = [ - 'name' => $file, - 'test_name' => '', - 'output' => '', - 'diff' => '', - 'info' => "$bork_info [$file]", - ]; - } - - if (is_array($org_file) || $test->hasSection('REDIRECTTEST')) { - if (is_array($org_file)) { - $file = $org_file[0]; - } - - $bork_info = "Redirected test did not contain redirection info"; - show_result("BORK", $bork_info, '', '', $temp_filenames); - $PHP_FAILED_TESTS['BORKED'][] = [ - 'name' => $file, - 'test_name' => '', - 'output' => '', - 'diff' => '', - 'info' => "$bork_info [$file]", - ]; - - $junit->markTestAs('BORK', $shortname, $tested, null, $bork_info); - - return 'BORKED'; - } - - // We've satisfied the preconditions - run the test! - if ($test->hasSection('FILE')) { - show_file_block('php', $test->getSection('FILE'), 'TEST'); - save_text($test_file, $test->getSection('FILE'), $temp_file); - } else { - $test_file = $temp_file = ""; - } - - if ($test->hasSection('GET')) { - $query_string = trim($test->getSection('GET')); - } else { - $query_string = ''; - } - - $env['REDIRECT_STATUS'] = '1'; - if (empty($env['QUERY_STRING'])) { - $env['QUERY_STRING'] = $query_string; - } - if (empty($env['PATH_TRANSLATED'])) { - $env['PATH_TRANSLATED'] = $test_file; - } - if (empty($env['SCRIPT_FILENAME'])) { - $env['SCRIPT_FILENAME'] = $test_file; - } - - if ($test->hasSection('COOKIE')) { - $env['HTTP_COOKIE'] = trim($test->getSection('COOKIE')); - } else { - $env['HTTP_COOKIE'] = ''; - } - - $args = $test->hasSection('ARGS') ? ' -- ' . $test->getSection('ARGS') : ''; - - if ($preload && !empty($test_file)) { - save_text($preload_filename, "sectionNotEmpty('POST_RAW')) { - $post = trim($test->getSection('POST_RAW')); - $raw_lines = explode("\n", $post); - - $request = ''; - $started = false; - - foreach ($raw_lines as $line) { - if (empty($env['CONTENT_TYPE']) && preg_match('/^Content-Type:(.*)/i', $line, $res)) { - $env['CONTENT_TYPE'] = trim(str_replace("\r", '', $res[1])); - continue; - } - - if ($started) { - $request .= "\n"; - } - - $started = true; - $request .= $line; - } - - $env['CONTENT_LENGTH'] = strlen($request); - $env['REQUEST_METHOD'] = 'POST'; - - if (empty($request)) { - $junit->markTestAs('BORK', $shortname, $tested, null, 'empty $request'); - return 'BORKED'; - } - - save_text($tmp_post, $request); - $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; - } elseif ($test->sectionNotEmpty('PUT')) { - $post = trim($test->getSection('PUT')); - $raw_lines = explode("\n", $post); - - $request = ''; - $started = false; - - foreach ($raw_lines as $line) { - if (empty($env['CONTENT_TYPE']) && preg_match('/^Content-Type:(.*)/i', $line, $res)) { - $env['CONTENT_TYPE'] = trim(str_replace("\r", '', $res[1])); - continue; - } - - if ($started) { - $request .= "\n"; - } - - $started = true; - $request .= $line; - } - - $env['CONTENT_LENGTH'] = strlen($request); - $env['REQUEST_METHOD'] = 'PUT'; - - if (empty($request)) { - $junit->markTestAs('BORK', $shortname, $tested, null, 'empty $request'); - return 'BORKED'; - } - - save_text($tmp_post, $request); - $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; - } elseif ($test->sectionNotEmpty('POST')) { - $post = trim($test->getSection('POST')); - $content_length = strlen($post); - save_text($tmp_post, $post); - - $env['REQUEST_METHOD'] = 'POST'; - if (empty($env['CONTENT_TYPE'])) { - $env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; - } - - if (empty($env['CONTENT_LENGTH'])) { - $env['CONTENT_LENGTH'] = $content_length; - } - - $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; - } elseif ($test->sectionNotEmpty('GZIP_POST')) { - $post = trim($test->getSection('GZIP_POST')); - $post = gzencode($post, 9, FORCE_GZIP); - $env['HTTP_CONTENT_ENCODING'] = 'gzip'; - - save_text($tmp_post, $post); - $content_length = strlen($post); - - $env['REQUEST_METHOD'] = 'POST'; - $env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; - $env['CONTENT_LENGTH'] = $content_length; - - $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; - } elseif ($test->sectionNotEmpty('DEFLATE_POST')) { - $post = trim($test->getSection('DEFLATE_POST')); - $post = gzcompress($post, 9); - $env['HTTP_CONTENT_ENCODING'] = 'deflate'; - save_text($tmp_post, $post); - $content_length = strlen($post); - - $env['REQUEST_METHOD'] = 'POST'; - $env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; - $env['CONTENT_LENGTH'] = $content_length; - - $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; - } else { - $env['REQUEST_METHOD'] = 'GET'; - $env['CONTENT_TYPE'] = ''; - $env['CONTENT_LENGTH'] = ''; - - $repeat_option = $num_repeats > 1 ? "--repeat $num_repeats" : ""; - $cmd = "$php $pass_options $repeat_option $ini_settings -f \"$test_file\" $args$cmdRedirect"; - } - - $orig_cmd = $cmd; - if ($valgrind) { - $env['USE_ZEND_ALLOC'] = '0'; - $env['ZEND_DONT_UNLOAD_MODULES'] = 1; - - $cmd = $valgrind->wrapCommand($cmd, $memcheck_filename, strpos($test_file, "pcre") !== false); - } - - if ($DETAILED) { - echo " -CONTENT_LENGTH = " . $env['CONTENT_LENGTH'] . " -CONTENT_TYPE = " . $env['CONTENT_TYPE'] . " -PATH_TRANSLATED = " . $env['PATH_TRANSLATED'] . " -QUERY_STRING = " . $env['QUERY_STRING'] . " -REDIRECT_STATUS = " . $env['REDIRECT_STATUS'] . " -REQUEST_METHOD = " . $env['REQUEST_METHOD'] . " -SCRIPT_FILENAME = " . $env['SCRIPT_FILENAME'] . " -HTTP_COOKIE = " . $env['HTTP_COOKIE'] . " -COMMAND $cmd -"; - } - - $junit->startTimer($shortname); - $hrtime = hrtime(); - $startTime = $hrtime[0] * 1000000000 + $hrtime[1]; - - $stdin = $test->hasSection('STDIN') ? $test->getSection('STDIN') : null; - $out = system_with_timeout($cmd, $env, $stdin, $captureStdIn, $captureStdOut, $captureStdErr); - - $junit->stopTimer($shortname); - $hrtime = hrtime(); - $time = $hrtime[0] * 1000000000 + $hrtime[1] - $startTime; - if ($time >= $slow_min_ms * 1000000) { - $PHP_FAILED_TESTS['SLOW'][] = [ - 'name' => $file, - 'test_name' => $tested . " [$tested_file]", - 'output' => '', - 'diff' => '', - 'info' => $time / 1000000000, - ]; - } - - // Remember CLEAN output to report borked test if it otherwise passes. - $clean_output = null; - if ((!$no_clean || $cfg['keep']['clean']) && $test->sectionNotEmpty('CLEAN')) { - show_file_block('clean', $test->getSection('CLEAN')); - save_text($test_clean, trim($test->getSection('CLEAN')), $temp_clean); - - if (!$no_clean) { - $extra = !IS_WINDOWS ? - "unset REQUEST_METHOD; unset QUERY_STRING; unset PATH_TRANSLATED; unset SCRIPT_FILENAME; unset REQUEST_METHOD;" : ""; - $clean_output = system_with_timeout("$extra $orig_php $pass_options -q $orig_ini_settings $no_file_cache \"$test_clean\"", $env); - } - - if (!$cfg['keep']['clean']) { - @unlink($test_clean); - } - } - - $leaked = false; - $passed = false; - - if ($valgrind) { // leak check - $leaked = filesize($memcheck_filename) > 0; - - if (!$leaked) { - @unlink($memcheck_filename); - } - } - - if ($num_repeats > 1) { - // In repeat mode, retain the output before the first execution, - // and of the last execution. Do this early, because the trimming below - // makes the newline handling complicated. - $separator1 = "Executing for the first time...\n"; - $separator1_pos = strpos($out, $separator1); - if ($separator1_pos !== false) { - $separator2 = "Finished execution, repeating...\n"; - $separator2_pos = strrpos($out, $separator2); - if ($separator2_pos !== false) { - $out = substr($out, 0, $separator1_pos) - . substr($out, $separator2_pos + strlen($separator2)); - } else { - $out = substr($out, 0, $separator1_pos) - . substr($out, $separator1_pos + strlen($separator1)); - } - } - } - - // Does the output match what is expected? - $output = preg_replace("/\r\n/", "\n", trim($out)); - - /* when using CGI, strip the headers from the output */ - $headers = []; - - if (!empty($uses_cgi) && preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $out, $match)) { - $output = trim($match[2]); - $rh = preg_split("/[\n\r]+/", $match[1]); - - foreach ($rh as $line) { - if (strpos($line, ':') !== false) { - $line = explode(':', $line, 2); - $headers[trim($line[0])] = trim($line[1]); - } - } - } - - $wanted_headers = null; - $output_headers = null; - $failed_headers = false; - - if ($test->hasSection('EXPECTHEADERS')) { - $want = []; - $wanted_headers = []; - $lines = preg_split("/[\n\r]+/", $test->getSection('EXPECTHEADERS')); - - foreach ($lines as $line) { - if (strpos($line, ':') !== false) { - $line = explode(':', $line, 2); - $want[trim($line[0])] = trim($line[1]); - $wanted_headers[] = trim($line[0]) . ': ' . trim($line[1]); - } - } - - $output_headers = []; - - foreach ($want as $k => $v) { - if (isset($headers[$k])) { - $output_headers[] = $k . ': ' . $headers[$k]; - } - - if (!isset($headers[$k]) || $headers[$k] != $v) { - $failed_headers = true; - } - } - - ksort($wanted_headers); - $wanted_headers = implode("\n", $wanted_headers); - ksort($output_headers); - $output_headers = implode("\n", $output_headers); - } - - show_file_block('out', $output); - - if ($preload) { - $output = trim(preg_replace("/\n?Warning: Can't preload [^\n]*\n?/", "", $output)); - } - - if ($test->hasAnySections('EXPECTF', 'EXPECTREGEX')) { - if ($test->hasSection('EXPECTF')) { - $wanted = trim($test->getSection('EXPECTF')); - } else { - $wanted = trim($test->getSection('EXPECTREGEX')); - } - - show_file_block('exp', $wanted); - $wanted_re = preg_replace('/\r\n/', "\n", $wanted); - - if ($test->hasSection('EXPECTF')) { - // do preg_quote, but miss out any %r delimited sections - $temp = ""; - $r = "%r"; - $startOffset = 0; - $length = strlen($wanted_re); - while ($startOffset < $length) { - $start = strpos($wanted_re, $r, $startOffset); - if ($start !== false) { - // we have found a start tag - $end = strpos($wanted_re, $r, $start + 2); - if ($end === false) { - // unbalanced tag, ignore it. - $end = $start = $length; - } - } else { - // no more %r sections - $start = $end = $length; - } - // quote a non re portion of the string - $temp .= preg_quote(substr($wanted_re, $startOffset, $start - $startOffset), '/'); - // add the re unquoted. - if ($end > $start) { - $temp .= '(' . substr($wanted_re, $start + 2, $end - $start - 2) . ')'; - } - $startOffset = $end + 2; - } - $wanted_re = $temp; - - // Stick to basics - $wanted_re = strtr($wanted_re, [ - '%e' => preg_quote(DIRECTORY_SEPARATOR, '/'), - '%s' => '[^\r\n]+', - '%S' => '[^\r\n]*', - '%a' => '.+', - '%A' => '.*', - '%w' => '\s*', - '%i' => '[+-]?\d+', - '%d' => '\d+', - '%x' => '[0-9a-fA-F]+', - '%f' => '[+-]?(?:\d+|(?=\.\d))(?:\.\d+)?(?:[Ee][+-]?\d+)?', - '%c' => '.', - '%0' => '\x00', - ]); - } - - if (preg_match('/^' . $wanted_re . '$/s', $output)) { - $passed = true; - } - } else { - $wanted = trim($test->getSection('EXPECT')); - $wanted = preg_replace('/\r\n/', "\n", $wanted); - show_file_block('exp', $wanted); - - // compare and leave on success - if (!strcmp($output, $wanted)) { - $passed = true; - } - - $wanted_re = null; - } - if (!$passed && !$retried && error_may_be_retried($test, $output)) { - $retried = true; - goto retry; - } - - if ($passed) { - if (!$cfg['keep']['php'] && !$leaked) { - @unlink($test_file); - @unlink($preload_filename); - } - @unlink($tmp_post); - - if (!$leaked && !$failed_headers) { - // If the test passed and CLEAN produced output, report test as borked. - if ($clean_output) { - show_result("BORK", $output, $tested_file, 'reason: invalid output from CLEAN', $temp_filenames); - $PHP_FAILED_TESTS['BORKED'][] = [ - 'name' => $file, - 'test_name' => '', - 'output' => '', - 'diff' => '', - 'info' => "$clean_output [$file]", - ]; - - $junit->markTestAs('BORK', $shortname, $tested, null, $clean_output); - return 'BORKED'; - } - - if ($test->hasSection('XFAIL')) { - $warn = true; - $info = " (warn: XFAIL section but test passes)"; - } elseif ($test->hasSection('XLEAK')) { - $warn = true; - $info = " (warn: XLEAK section but test passes)"; - } elseif ($retried) { - $warn = true; - $info = " (warn: Test passed on retry attempt)"; - } else { - show_result("PASS", $tested, $tested_file, '', $temp_filenames); - $junit->markTestAs('PASS', $shortname, $tested); - return 'PASSED'; - } - } - } - - // Test failed so we need to report details. - if ($failed_headers) { - $passed = false; - $wanted = $wanted_headers . "\n--HEADERS--\n" . $wanted; - $output = $output_headers . "\n--HEADERS--\n" . $output; - - if (isset($wanted_re)) { - $wanted_re = preg_quote($wanted_headers . "\n--HEADERS--\n", '/') . $wanted_re; - } - } - - $restype = []; - - if ($leaked) { - $restype[] = $test->hasSection('XLEAK') ? - 'XLEAK' : 'LEAK'; - } - - if ($warn) { - $restype[] = 'WARN'; - } - - if (!$passed) { - if ($test->hasSection('XFAIL')) { - $restype[] = 'XFAIL'; - $info = ' XFAIL REASON: ' . rtrim($test->getSection('XFAIL')); - } elseif ($test->hasSection('XLEAK')) { - $restype[] = 'XLEAK'; - $info = ' XLEAK REASON: ' . rtrim($test->getSection('XLEAK')); - } else { - $restype[] = 'FAIL'; - } - } - - if (!$passed) { - // write .exp - if (strpos($log_format, 'E') !== false && file_put_contents($exp_filename, $wanted) === false) { - error("Cannot create expected test output - $exp_filename"); - } - - // write .out - if (strpos($log_format, 'O') !== false && file_put_contents($output_filename, $output) === false) { - error("Cannot create test output - $output_filename"); - } - - // write .diff - if (!empty($environment['TEST_PHP_DIFF_CMD'])) { - $diff = generate_diff_external($environment['TEST_PHP_DIFF_CMD'], $exp_filename, $output_filename); - } else { - $diff = generate_diff($wanted, $wanted_re, $output); - } - - if (is_array($IN_REDIRECT)) { - $orig_shortname = str_replace(TEST_PHP_SRCDIR . '/', '', $file); - $diff = "# original source file: $orig_shortname\n" . $diff; - } - if (!$SHOW_ONLY_GROUPS || array_intersect($restype, $SHOW_ONLY_GROUPS)) { - show_file_block('diff', $diff); - } - if (strpos($log_format, 'D') !== false && file_put_contents($diff_filename, $diff) === false) { - error("Cannot create test diff - $diff_filename"); - } - - // write .log - if (strpos($log_format, 'L') !== false && file_put_contents($log_filename, " ----- EXPECTED OUTPUT -$wanted ----- ACTUAL OUTPUT -$output ----- FAILED -") === false) { - error("Cannot create test log - $log_filename"); - error_report($file, $log_filename, $tested); - } - } - - if (!$passed || $leaked) { - // write .sh - if (strpos($log_format, 'S') !== false) { - $env_lines = []; - foreach ($env as $env_var => $env_val) { - $env_lines[] = "export $env_var=" . escapeshellarg($env_val ?? ""); - } - $exported_environment = $env_lines ? "\n" . implode("\n", $env_lines) . "\n" : ""; - $sh_script = << $file, - 'test_name' => (is_array($IN_REDIRECT) ? $IN_REDIRECT['via'] : '') . $tested . " [$tested_file]", - 'output' => $output_filename, - 'diff' => $diff_filename, - 'info' => $info, - ]; - } - - $diff = empty($diff) ? '' : preg_replace('/\e/', '', $diff); - - $junit->markTestAs($restype, $shortname, $tested, null, $info, $diff); - - return $restype[0] . 'ED'; -} - -function is_flaky(TestFile $test): bool -{ - if ($test->hasSection('FLAKY')) { - return true; - } - if ($test->hasSection('SKIPIF')) { - if (strpos($test->getSection('SKIPIF'), 'SKIP_PERF_SENSITIVE') !== false) { - return true; - } - } - if (!$test->hasSection('FILE')) { - return false; - } - $file = $test->getSection('FILE'); - $flaky_functions = [ - 'disk_free_space', - 'hrtime', - 'microtime', - 'sleep', - 'usleep', - ]; - $regex = '(\b(' . implode('|', $flaky_functions) . ')\()i'; - return preg_match($regex, $file) === 1; -} - -function is_flaky_output(string $output): bool -{ - $messages = [ - '404: page not found', - 'address already in use', - 'connection refused', - 'deadlock', - 'mailbox already exists', - 'timed out', - ]; - $regex = '(\b(' . implode('|', $messages) . ')\b)i'; - return preg_match($regex, $output) === 1; -} - -function error_may_be_retried(TestFile $test, string $output): bool -{ - return is_flaky_output($output) - || is_flaky($test); -} - -/** - * @return bool|int - */ -function comp_line(string $l1, string $l2, bool $is_reg) -{ - if ($is_reg) { - return preg_match('/^' . $l1 . '$/s', $l2); - } - - return !strcmp($l1, $l2); -} - -function count_array_diff( - array $ar1, - array $ar2, - bool $is_reg, - array $w, - int $idx1, - int $idx2, - int $cnt1, - int $cnt2, - int $steps -): int { - $equal = 0; - - while ($idx1 < $cnt1 && $idx2 < $cnt2 && comp_line($ar1[$idx1], $ar2[$idx2], $is_reg)) { - $idx1++; - $idx2++; - $equal++; - $steps--; - } - if (--$steps > 0) { - $eq1 = 0; - $st = $steps / 2; - - for ($ofs1 = $idx1 + 1; $ofs1 < $cnt1 && $st-- > 0; $ofs1++) { - $eq = @count_array_diff($ar1, $ar2, $is_reg, $w, $ofs1, $idx2, $cnt1, $cnt2, $st); - - if ($eq > $eq1) { - $eq1 = $eq; - } - } - - $eq2 = 0; - $st = $steps; - - for ($ofs2 = $idx2 + 1; $ofs2 < $cnt2 && $st-- > 0; $ofs2++) { - $eq = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1, $ofs2, $cnt1, $cnt2, $st); - if ($eq > $eq2) { - $eq2 = $eq; - } - } - - if ($eq1 > $eq2) { - $equal += $eq1; - } elseif ($eq2 > 0) { - $equal += $eq2; - } - } - - return $equal; -} - -function generate_array_diff(array $ar1, array $ar2, bool $is_reg, array $w): array -{ - global $context_line_count; - $idx1 = 0; - $cnt1 = @count($ar1); - $idx2 = 0; - $cnt2 = @count($ar2); - $diff = []; - $old1 = []; - $old2 = []; - $number_len = max(3, strlen((string)max($cnt1 + 1, $cnt2 + 1))); - $line_number_spec = '%0' . $number_len . 'd'; - - /** Mapping from $idx2 to $idx1, including indexes of idx2 that are identical to idx1 as well as entries that don't have matches */ - $mapping = []; - - while ($idx1 < $cnt1 && $idx2 < $cnt2) { - $mapping[$idx2] = $idx1; - if (comp_line($ar1[$idx1], $ar2[$idx2], $is_reg)) { - $idx1++; - $idx2++; - continue; - } - - $c1 = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1 + 1, $idx2, $cnt1, $cnt2, 10); - $c2 = @count_array_diff($ar1, $ar2, $is_reg, $w, $idx1, $idx2 + 1, $cnt1, $cnt2, 10); - - if ($c1 > $c2) { - $old1[$idx1] = sprintf("{$line_number_spec}- ", $idx1 + 1) . $w[$idx1++]; - } elseif ($c2 > 0) { - $old2[$idx2] = sprintf("{$line_number_spec}+ ", $idx2 + 1) . $ar2[$idx2++]; - } else { - $old1[$idx1] = sprintf("{$line_number_spec}- ", $idx1 + 1) . $w[$idx1++]; - $old2[$idx2] = sprintf("{$line_number_spec}+ ", $idx2 + 1) . $ar2[$idx2++]; - } - $last_printed_context_line = $idx1; - } - $mapping[$idx2] = $idx1; - - reset($old1); - $k1 = key($old1); - $l1 = -2; - reset($old2); - $k2 = key($old2); - $l2 = -2; - $old_k1 = -1; - $add_context_lines = function (int $new_k1) use (&$old_k1, &$diff, $w, $context_line_count, $number_len) { - if ($old_k1 >= $new_k1 || !$context_line_count) { - return; - } - $end = $new_k1 - 1; - $range_end = min($end, $old_k1 + $context_line_count); - if ($old_k1 >= 0) { - while ($old_k1 < $range_end) { - $diff[] = str_repeat(' ', $number_len + 2) . $w[$old_k1++]; - } - } - if ($end - $context_line_count > $old_k1) { - $old_k1 = $end - $context_line_count; - if ($old_k1 > 0) { - // Add a '--' to mark sections where the common areas were truncated - $diff[] = '--'; - } - } - $old_k1 = max($old_k1, 0); - while ($old_k1 < $end) { - $diff[] = str_repeat(' ', $number_len + 2) . $w[$old_k1++]; - } - $old_k1 = $new_k1; - }; - - while ($k1 !== null || $k2 !== null) { - if ($k1 == $l1 + 1 || $k2 === null) { - $add_context_lines($k1); - $l1 = $k1; - $diff[] = current($old1); - $old_k1 = $k1; - $k1 = next($old1) ? key($old1) : null; - } elseif ($k2 == $l2 + 1 || $k1 === null) { - $add_context_lines($mapping[$k2]); - $l2 = $k2; - $diff[] = current($old2); - $k2 = next($old2) ? key($old2) : null; - } elseif ($k1 < $mapping[$k2]) { - $add_context_lines($k1); - $l1 = $k1; - $diff[] = current($old1); - $k1 = next($old1) ? key($old1) : null; - } else { - $add_context_lines($mapping[$k2]); - $l2 = $k2; - $diff[] = current($old2); - $k2 = next($old2) ? key($old2) : null; - } - } - - while ($idx1 < $cnt1) { - $add_context_lines($idx1 + 1); - $diff[] = sprintf("{$line_number_spec}- ", $idx1 + 1) . $w[$idx1++]; - } - - while ($idx2 < $cnt2) { - if (isset($mapping[$idx2])) { - $add_context_lines($mapping[$idx2] + 1); - } - $diff[] = sprintf("{$line_number_spec}+ ", $idx2 + 1) . $ar2[$idx2++]; - } - $add_context_lines(min($old_k1 + $context_line_count + 1, $cnt1 + 1)); - if ($context_line_count && $old_k1 < $cnt1 + 1) { - // Add a '--' to mark sections where the common areas were truncated - $diff[] = '--'; - } - - return $diff; -} - -function generate_diff_external(string $diff_cmd, string $exp_file, string $output_file): string -{ - $retval = shell_exec("{$diff_cmd} {$exp_file} {$output_file}"); - - return is_string($retval) ? $retval : 'Could not run external diff tool set through TEST_PHP_DIFF_CMD environment variable'; -} - -function generate_diff(string $wanted, ?string $wanted_re, string $output): string -{ - $w = explode("\n", $wanted); - $o = explode("\n", $output); - $r = is_null($wanted_re) ? $w : explode("\n", $wanted_re); - $diff = generate_array_diff($r, $o, !is_null($wanted_re), $w); - - return implode(PHP_EOL, $diff); -} - -function error(string $message): void -{ - echo "ERROR: {$message}\n"; - exit(1); -} - -function settings2array(array $settings, &$ini_settings): void -{ - foreach ($settings as $setting) { - if (strpos($setting, '=') !== false) { - $setting = explode("=", $setting, 2); - $name = trim($setting[0]); - $value = trim($setting[1]); - - if ($name == 'extension' || $name == 'zend_extension') { - if (!isset($ini_settings[$name])) { - $ini_settings[$name] = []; - } - - $ini_settings[$name][] = $value; - } else { - $ini_settings[$name] = $value; - } - } - } -} - -function settings2params(array $ini_settings): string -{ - $settings = ''; - - foreach ($ini_settings as $name => $value) { - if (is_array($value)) { - foreach ($value as $val) { - $val = addslashes($val); - $settings .= " -d \"$name=$val\""; - } - } else { - if (IS_WINDOWS && !empty($value) && $value[0] == '"') { - $len = strlen($value); - - if ($value[$len - 1] == '"') { - $value[0] = "'"; - $value[$len - 1] = "'"; - } - } else { - $value = addslashes($value); - } - - $settings .= " -d \"$name=$value\""; - } - } - - return $settings; -} - -function compute_summary(): void -{ - global $n_total, $test_results, $ignored_by_ext, $sum_results, $percent_results; - - $n_total = count($test_results); - $n_total += $ignored_by_ext; - $sum_results = [ - 'PASSED' => 0, - 'WARNED' => 0, - 'SKIPPED' => 0, - 'FAILED' => 0, - 'BORKED' => 0, - 'LEAKED' => 0, - 'XFAILED' => 0, - 'XLEAKED' => 0 - ]; - - foreach ($test_results as $v) { - $sum_results[$v]++; - } - - $sum_results['SKIPPED'] += $ignored_by_ext; - $percent_results = []; - - foreach ($sum_results as $v => $n) { - $percent_results[$v] = (100.0 * $n) / $n_total; - } -} - -function get_summary(bool $show_ext_summary): string -{ - global $exts_skipped, $exts_tested, $n_total, $sum_results, $percent_results, $end_time, $start_time, $failed_test_summary, $PHP_FAILED_TESTS, $valgrind; - - $x_total = $n_total - $sum_results['SKIPPED'] - $sum_results['BORKED']; - - if ($x_total) { - $x_warned = (100.0 * $sum_results['WARNED']) / $x_total; - $x_failed = (100.0 * $sum_results['FAILED']) / $x_total; - $x_xfailed = (100.0 * $sum_results['XFAILED']) / $x_total; - $x_xleaked = (100.0 * $sum_results['XLEAKED']) / $x_total; - $x_leaked = (100.0 * $sum_results['LEAKED']) / $x_total; - $x_passed = (100.0 * $sum_results['PASSED']) / $x_total; - } else { - $x_warned = $x_failed = $x_passed = $x_leaked = $x_xfailed = $x_xleaked = 0; - } - - $summary = ''; - - if ($show_ext_summary) { - $summary .= ' -===================================================================== -TEST RESULT SUMMARY ---------------------------------------------------------------------- -Exts skipped : ' . sprintf('%4d', $exts_skipped) . ' -Exts tested : ' . sprintf('%4d', $exts_tested) . ' ---------------------------------------------------------------------- -'; - } - - $summary .= ' -Number of tests : ' . sprintf('%4d', $n_total) . ' ' . sprintf('%8d', $x_total); - - if ($sum_results['BORKED']) { - $summary .= ' -Tests borked : ' . sprintf('%4d (%5.1f%%)', $sum_results['BORKED'], $percent_results['BORKED']) . ' --------'; - } - - $summary .= ' -Tests skipped : ' . sprintf('%4d (%5.1f%%)', $sum_results['SKIPPED'], $percent_results['SKIPPED']) . ' -------- -Tests warned : ' . sprintf('%4d (%5.1f%%)', $sum_results['WARNED'], $percent_results['WARNED']) . ' ' . sprintf('(%5.1f%%)', $x_warned) . ' -Tests failed : ' . sprintf('%4d (%5.1f%%)', $sum_results['FAILED'], $percent_results['FAILED']) . ' ' . sprintf('(%5.1f%%)', $x_failed); - - if ($sum_results['XFAILED']) { - $summary .= ' -Expected fail : ' . sprintf('%4d (%5.1f%%)', $sum_results['XFAILED'], $percent_results['XFAILED']) . ' ' . sprintf('(%5.1f%%)', $x_xfailed); - } - - if ($valgrind) { - $summary .= ' -Tests leaked : ' . sprintf('%4d (%5.1f%%)', $sum_results['LEAKED'], $percent_results['LEAKED']) . ' ' . sprintf('(%5.1f%%)', $x_leaked); - if ($sum_results['XLEAKED']) { - $summary .= ' -Expected leak : ' . sprintf('%4d (%5.1f%%)', $sum_results['XLEAKED'], $percent_results['XLEAKED']) . ' ' . sprintf('(%5.1f%%)', $x_xleaked); - } - } - - $summary .= ' -Tests passed : ' . sprintf('%4d (%5.1f%%)', $sum_results['PASSED'], $percent_results['PASSED']) . ' ' . sprintf('(%5.1f%%)', $x_passed) . ' ---------------------------------------------------------------------- -Time taken : ' . sprintf('%4d seconds', $end_time - $start_time) . ' -===================================================================== -'; - $failed_test_summary = ''; - - if (count($PHP_FAILED_TESTS['SLOW'])) { - usort($PHP_FAILED_TESTS['SLOW'], function (array $a, array $b): int { - return $a['info'] < $b['info'] ? 1 : -1; - }); - - $failed_test_summary .= ' -===================================================================== -SLOW TEST SUMMARY ---------------------------------------------------------------------- -'; - foreach ($PHP_FAILED_TESTS['SLOW'] as $failed_test_data) { - $failed_test_summary .= sprintf('(%.3f s) ', $failed_test_data['info']) . $failed_test_data['test_name'] . "\n"; - } - $failed_test_summary .= "=====================================================================\n"; - } - - if (count($PHP_FAILED_TESTS['XFAILED'])) { - $failed_test_summary .= ' -===================================================================== -EXPECTED FAILED TEST SUMMARY ---------------------------------------------------------------------- -'; - foreach ($PHP_FAILED_TESTS['XFAILED'] as $failed_test_data) { - $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n"; - } - $failed_test_summary .= "=====================================================================\n"; - } - - if (count($PHP_FAILED_TESTS['BORKED'])) { - $failed_test_summary .= ' -===================================================================== -BORKED TEST SUMMARY ---------------------------------------------------------------------- -'; - foreach ($PHP_FAILED_TESTS['BORKED'] as $failed_test_data) { - $failed_test_summary .= $failed_test_data['info'] . "\n"; - } - - $failed_test_summary .= "=====================================================================\n"; - } - - if (count($PHP_FAILED_TESTS['FAILED'])) { - $failed_test_summary .= ' -===================================================================== -FAILED TEST SUMMARY ---------------------------------------------------------------------- -'; - foreach ($PHP_FAILED_TESTS['FAILED'] as $failed_test_data) { - $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n"; - } - $failed_test_summary .= "=====================================================================\n"; - } - if (count($PHP_FAILED_TESTS['WARNED'])) { - $failed_test_summary .= ' -===================================================================== -WARNED TEST SUMMARY ---------------------------------------------------------------------- -'; - foreach ($PHP_FAILED_TESTS['WARNED'] as $failed_test_data) { - $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n"; - } - - $failed_test_summary .= "=====================================================================\n"; - } - - if (count($PHP_FAILED_TESTS['LEAKED'])) { - $failed_test_summary .= ' -===================================================================== -LEAKED TEST SUMMARY ---------------------------------------------------------------------- -'; - foreach ($PHP_FAILED_TESTS['LEAKED'] as $failed_test_data) { - $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n"; - } - - $failed_test_summary .= "=====================================================================\n"; - } - - if (count($PHP_FAILED_TESTS['XLEAKED'])) { - $failed_test_summary .= ' -===================================================================== -EXPECTED LEAK TEST SUMMARY ---------------------------------------------------------------------- -'; - foreach ($PHP_FAILED_TESTS['XLEAKED'] as $failed_test_data) { - $failed_test_summary .= $failed_test_data['test_name'] . $failed_test_data['info'] . "\n"; - } - - $failed_test_summary .= "=====================================================================\n"; - } - - if ($failed_test_summary && !getenv('NO_PHPTEST_SUMMARY')) { - $summary .= $failed_test_summary; - } - - return $summary; -} - -function show_start($start_time): void -{ - echo "TIME START " . date('Y-m-d H:i:s', $start_time) . "\n=====================================================================\n"; -} - -function show_end($end_time): void -{ - echo "=====================================================================\nTIME END " . date('Y-m-d H:i:s', $end_time) . "\n"; -} - -function show_summary(): void -{ - echo get_summary(true); -} - -function show_redirect_start(string $tests, string $tested, string $tested_file): void -{ - global $SHOW_ONLY_GROUPS, $show_progress; - - if (!$SHOW_ONLY_GROUPS || in_array('REDIRECT', $SHOW_ONLY_GROUPS)) { - echo "REDIRECT $tests ($tested [$tested_file]) begin\n"; - } elseif ($show_progress) { - clear_show_test(); - } -} - -function show_redirect_ends(string $tests, string $tested, string $tested_file): void -{ - global $SHOW_ONLY_GROUPS, $show_progress; - - if (!$SHOW_ONLY_GROUPS || in_array('REDIRECT', $SHOW_ONLY_GROUPS)) { - echo "REDIRECT $tests ($tested [$tested_file]) done\n"; - } elseif ($show_progress) { - clear_show_test(); - } -} - -function show_test(int $test_idx, string $shortname): void -{ - global $test_cnt; - global $line_length; - - $str = "TEST $test_idx/$test_cnt [$shortname]\r"; - $line_length = strlen($str); - echo $str; - flush(); -} - -function clear_show_test(): void -{ - global $line_length; - // Parallel testing - global $workerID; - - if (!$workerID && isset($line_length)) { - // Write over the last line to avoid random trailing chars on next echo - echo str_repeat(" ", $line_length), "\r"; - } -} - -function parse_conflicts(string $text): array -{ - // Strip comments - $text = preg_replace('/#.*/', '', $text); - return array_map('trim', explode("\n", trim($text))); -} - -function show_result( - string $result, - string $tested, - string $tested_file, - string $extra = '', - ?array $temp_filenames = null -): void { - global $SHOW_ONLY_GROUPS, $colorize, $show_progress; - - if (!$SHOW_ONLY_GROUPS || in_array($result, $SHOW_ONLY_GROUPS)) { - if ($colorize) { - /* Use ANSI escape codes for coloring test result */ - switch ( $result ) { - case 'PASS': // Light Green - $color = "\e[1;32m{$result}\e[0m"; break; - case 'FAIL': - case 'BORK': - case 'LEAK': - case 'LEAK&FAIL': - // Light Red - $color = "\e[1;31m{$result}\e[0m"; break; - default: // Yellow - $color = "\e[1;33m{$result}\e[0m"; break; - } - - echo "$color $tested [$tested_file] $extra\n"; - } else { - echo "$result $tested [$tested_file] $extra\n"; - } - } elseif ($show_progress) { - clear_show_test(); - } -} - -class BorkageException extends Exception -{ -} - -class JUnit -{ - private bool $enabled = true; - private $fp = null; - private array $suites = []; - private array $rootSuite = self::EMPTY_SUITE + ['name' => 'php']; - - private const EMPTY_SUITE = [ - 'test_total' => 0, - 'test_pass' => 0, - 'test_fail' => 0, - 'test_error' => 0, - 'test_skip' => 0, - 'test_warn' => 0, - 'files' => [], - 'execution_time' => 0, - ]; - - /** - * @throws Exception - */ - public function __construct(array $env, int $workerID) - { - // Check whether a junit log is wanted. - $fileName = $env['TEST_PHP_JUNIT'] ?? null; - if (empty($fileName)) { - $this->enabled = false; - return; - } - if (!$workerID && !$this->fp = fopen($fileName, 'w')) { - throw new Exception("Failed to open $fileName for writing."); - } - } - - public function isEnabled(): bool - { - return $this->enabled; - } - - public function clear(): void - { - $this->rootSuite = self::EMPTY_SUITE + ['name' => 'php']; - $this->suites = []; - } - - public function saveXML(): void - { - if (!$this->enabled) { - return; - } - - $xml = '<' . '?' . 'xml version="1.0" encoding="UTF-8"' . '?' . '>' . PHP_EOL; - $xml .= sprintf( - '' . PHP_EOL, - $this->rootSuite['name'], - $this->rootSuite['test_total'], - $this->rootSuite['test_fail'], - $this->rootSuite['test_error'], - $this->rootSuite['test_skip'], - $this->rootSuite['execution_time'] - ); - $xml .= $this->getSuitesXML(); - $xml .= ''; - fwrite($this->fp, $xml); - } - - private function getSuitesXML(string $suite_name = '') - { - // FIXME: $suite_name gets overwritten - $result = ''; - - foreach ($this->suites as $suite_name => $suite) { - $result .= sprintf( - '' . PHP_EOL, - $suite['name'], - $suite['test_total'], - $suite['test_fail'], - $suite['test_error'], - $suite['test_skip'], - $suite['execution_time'] - ); - - if (!empty($suite_name)) { - foreach ($suite['files'] as $file) { - $result .= $this->rootSuite['files'][$file]['xml']; - } - } - - $result .= '' . PHP_EOL; - } - - return $result; - } - - public function markTestAs( - $type, - string $file_name, - string $test_name, - ?int $time = null, - string $message = '', - string $details = '' - ): void { - if (!$this->enabled) { - return; - } - - $suite = $this->getSuiteName($file_name); - - $this->record($suite, 'test_total'); - - $time = $time ?? $this->getTimer($file_name); - $this->record($suite, 'execution_time', $time); - - $escaped_details = htmlspecialchars($details, ENT_QUOTES, 'UTF-8'); - $escaped_details = preg_replace_callback('/[\0-\x08\x0B\x0C\x0E-\x1F]/', function ($c) { - return sprintf('[[0x%02x]]', ord($c[0])); - }, $escaped_details); - $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8'); - - $escaped_test_name = htmlspecialchars($file_name . ' (' . $test_name . ')', ENT_QUOTES); - $this->rootSuite['files'][$file_name]['xml'] = "\n"; - - if (is_array($type)) { - $output_type = $type[0] . 'ED'; - $temp = array_intersect(['XFAIL', 'XLEAK', 'FAIL', 'WARN'], $type); - $type = reset($temp); - } else { - $output_type = $type . 'ED'; - } - - if ('PASS' == $type || 'XFAIL' == $type || 'XLEAK' == $type) { - $this->record($suite, 'test_pass'); - } elseif ('BORK' == $type) { - $this->record($suite, 'test_error'); - $this->rootSuite['files'][$file_name]['xml'] .= "\n"; - } elseif ('SKIP' == $type) { - $this->record($suite, 'test_skip'); - $this->rootSuite['files'][$file_name]['xml'] .= "$escaped_message\n"; - } elseif ('WARN' == $type) { - $this->record($suite, 'test_warn'); - $this->rootSuite['files'][$file_name]['xml'] .= "$escaped_message\n"; - } elseif ('FAIL' == $type) { - $this->record($suite, 'test_fail'); - $this->rootSuite['files'][$file_name]['xml'] .= "$escaped_details\n"; - } else { - $this->record($suite, 'test_error'); - $this->rootSuite['files'][$file_name]['xml'] .= "$escaped_details\n"; - } - - $this->rootSuite['files'][$file_name]['xml'] .= "\n"; - } - - private function record(string $suite, string $param, $value = 1): void - { - $this->rootSuite[$param] += $value; - $this->suites[$suite][$param] += $value; - } - - private function getTimer(string $file_name) - { - if (!$this->enabled) { - return 0; - } - - if (isset($this->rootSuite['files'][$file_name]['total'])) { - return number_format($this->rootSuite['files'][$file_name]['total'], 4); - } - - return 0; - } - - public function startTimer(string $file_name): void - { - if (!$this->enabled) { - return; - } - - if (!isset($this->rootSuite['files'][$file_name]['start'])) { - $this->rootSuite['files'][$file_name]['start'] = microtime(true); - - $suite = $this->getSuiteName($file_name); - $this->initSuite($suite); - $this->suites[$suite]['files'][$file_name] = $file_name; - } - } - - public function getSuiteName(string $file_name): string - { - return $this->pathToClassName(dirname($file_name)); - } - - private function pathToClassName(string $file_name): string - { - if (!$this->enabled) { - return ''; - } - - $ret = $this->rootSuite['name']; - $_tmp = []; - - // lookup whether we're in the PHP source checkout - $max = 5; - if (is_file($file_name)) { - $dir = dirname(realpath($file_name)); - } else { - $dir = realpath($file_name); - } - do { - array_unshift($_tmp, basename($dir)); - $chk = $dir . DIRECTORY_SEPARATOR . "main" . DIRECTORY_SEPARATOR . "php_version.h"; - $dir = dirname($dir); - } while (!file_exists($chk) && --$max > 0); - if (file_exists($chk)) { - if ($max) { - array_shift($_tmp); - } - foreach ($_tmp as $p) { - $ret .= "." . preg_replace(",[^a-z0-9]+,i", ".", $p); - } - return $ret; - } - - return $this->rootSuite['name'] . '.' . str_replace([DIRECTORY_SEPARATOR, '-'], '.', $file_name); - } - - public function initSuite(string $suite_name): void - { - if (!$this->enabled) { - return; - } - - if (!empty($this->suites[$suite_name])) { - return; - } - - $this->suites[$suite_name] = self::EMPTY_SUITE + ['name' => $suite_name]; - } - - /** - * @throws Exception - */ - public function stopTimer(string $file_name): void - { - if (!$this->enabled) { - return; - } - - if (!isset($this->rootSuite['files'][$file_name]['start'])) { - throw new Exception("Timer for $file_name was not started!"); - } - - if (!isset($this->rootSuite['files'][$file_name]['total'])) { - $this->rootSuite['files'][$file_name]['total'] = 0; - } - - $start = $this->rootSuite['files'][$file_name]['start']; - $this->rootSuite['files'][$file_name]['total'] += microtime(true) - $start; - unset($this->rootSuite['files'][$file_name]['start']); - } - - public function mergeResults(?JUnit $other): void - { - if (!$this->enabled || !$other) { - return; - } - - $this->mergeSuites($this->rootSuite, $other->rootSuite); - foreach ($other->suites as $name => $suite) { - if (!isset($this->suites[$name])) { - $this->suites[$name] = $suite; - continue; - } - - $this->mergeSuites($this->suites[$name], $suite); - } - } - - private function mergeSuites(array &$dest, array $source): void - { - $dest['test_total'] += $source['test_total']; - $dest['test_pass'] += $source['test_pass']; - $dest['test_fail'] += $source['test_fail']; - $dest['test_error'] += $source['test_error']; - $dest['test_skip'] += $source['test_skip']; - $dest['test_warn'] += $source['test_warn']; - $dest['execution_time'] += $source['execution_time']; - $dest['files'] += $source['files']; - } -} - -class SkipCache -{ - private bool $enable; - private bool $keepFile; - - private array $skips = []; - private array $extensions = []; - - private int $hits = 0; - private int $misses = 0; - private int $extHits = 0; - private int $extMisses = 0; - - public function __construct(bool $enable, bool $keepFile) - { - $this->enable = $enable; - $this->keepFile = $keepFile; - } - - public function checkSkip(string $php, string $code, string $checkFile, string $tempFile, array $env): string - { - // Extension tests frequently use something like $dir"; - - if (isset($this->skips[$key][$code])) { - $this->hits++; - if ($this->keepFile) { - save_text($checkFile, $code, $tempFile); - } - return $this->skips[$key][$code]; - } - - save_text($checkFile, $code, $tempFile); - $result = trim(system_with_timeout("$php \"$checkFile\"", $env)); - if (strpos($result, 'nocache') === 0) { - $result = ''; - } else if ($this->enable) { - $this->skips[$key][$code] = $result; - } - $this->misses++; - - if (!$this->keepFile) { - @unlink($checkFile); - } - - return $result; - } - - public function getExtensions(string $php): array - { - if (isset($this->extensions[$php])) { - $this->extHits++; - return $this->extensions[$php]; - } - - $extDir = shell_exec("$php -d display_errors=0 -r \"echo ini_get('extension_dir');\""); - $extensions = explode(",", shell_exec("$php -d display_errors=0 -r \"echo implode(',', get_loaded_extensions());\"")); - $extensions = array_map('strtolower', $extensions); - if (in_array('zend opcache', $extensions)) { - $extensions[] = 'opcache'; - } - - $result = [$extDir, $extensions]; - $this->extensions[$php] = $result; - $this->extMisses++; - - return $result; - } - -// public function __destruct() -// { -// echo "Skips: {$this->hits} hits, {$this->misses} misses.\n"; -// echo "Extensions: {$this->extHits} hits, {$this->extMisses} misses.\n"; -// echo "Cache distribution:\n"; -// -// foreach ($this->skips as $php => $cache) { -// echo "$php: " . count($cache) . "\n"; -// } -// } -} - -class RuntestsValgrind -{ - protected $version = ''; - protected $header = ''; - protected $version_3_8_0 = false; - protected $tool = null; - - public function getVersion(): string - { - return $this->version; - } - - public function getHeader(): string - { - return $this->header; - } - - public function __construct(array $environment, string $tool = 'memcheck') - { - $this->tool = $tool; - $header = system_with_timeout("valgrind --tool={$this->tool} --version", $environment); - if (!$header) { - error("Valgrind returned no version info for {$this->tool}, cannot proceed.\n". - "Please check if Valgrind is installed and the tool is named correctly."); - } - $count = 0; - $version = preg_replace("/valgrind-(\d+)\.(\d+)\.(\d+)([.\w_-]+)?(\s+)/", '$1.$2.$3', $header, 1, $count); - if ($count != 1) { - error("Valgrind returned invalid version info (\"{$header}\") for {$this->tool}, cannot proceed."); - } - $this->version = $version; - $this->header = sprintf( - "%s (%s)", trim($header), $this->tool); - $this->version_3_8_0 = version_compare($version, '3.8.0', '>='); - } - - public function wrapCommand(string $cmd, string $memcheck_filename, bool $check_all): string - { - $vcmd = "valgrind -q --tool={$this->tool} --trace-children=yes"; - if ($check_all) { - $vcmd .= ' --smc-check=all'; - } - - /* --vex-iropt-register-updates=allregs-at-mem-access is necessary for phpdbg watchpoint tests */ - if ($this->version_3_8_0) { - return "$vcmd --vex-iropt-register-updates=allregs-at-mem-access --log-file=$memcheck_filename $cmd"; - } - return "$vcmd --vex-iropt-precise-memory-exns=yes --log-file=$memcheck_filename $cmd"; - } -} - -class TestFile -{ - private string $fileName; - - private array $sections = ['TEST' => '']; - - private const ALLOWED_SECTIONS = [ - 'EXPECT', 'EXPECTF', 'EXPECTREGEX', 'EXPECTREGEX_EXTERNAL', 'EXPECT_EXTERNAL', 'EXPECTF_EXTERNAL', 'EXPECTHEADERS', - 'POST', 'POST_RAW', 'GZIP_POST', 'DEFLATE_POST', 'PUT', 'GET', 'COOKIE', 'ARGS', - 'FILE', 'FILEEOF', 'FILE_EXTERNAL', 'REDIRECTTEST', - 'CAPTURE_STDIO', 'STDIN', 'CGI', 'PHPDBG', - 'INI', 'ENV', 'EXTENSIONS', - 'SKIPIF', 'XFAIL', 'XLEAK', 'CLEAN', - 'CREDITS', 'DESCRIPTION', 'CONFLICTS', 'WHITESPACE_SENSITIVE', - 'FLAKY', - ]; - - /** - * @throws BorkageException - */ - public function __construct(string $fileName, bool $inRedirect) - { - $this->fileName = $fileName; - - $this->readFile(); - $this->validateAndProcess($inRedirect); - } - - public function hasSection(string $name): bool - { - return isset($this->sections[$name]); - } - - public function hasAllSections(string ...$names): bool - { - foreach ($names as $section) { - if (!isset($this->sections[$section])) { - return false; - } - } - - return true; - } - - public function hasAnySections(string ...$names): bool - { - foreach ($names as $section) { - if (isset($this->sections[$section])) { - return true; - } - } - - return false; - } - - public function sectionNotEmpty(string $name): bool - { - return !empty($this->sections[$name]); - } - - /** - * @throws Exception - */ - public function getSection(string $name): string - { - if (!isset($this->sections[$name])) { - throw new Exception("Section $name not found"); - } - return $this->sections[$name]; - } - - public function getName(): string - { - return trim($this->getSection('TEST')); - } - - public function isCGI(): bool - { - return $this->hasSection('CGI') - || $this->sectionNotEmpty('GET') - || $this->sectionNotEmpty('POST') - || $this->sectionNotEmpty('GZIP_POST') - || $this->sectionNotEmpty('DEFLATE_POST') - || $this->sectionNotEmpty('POST_RAW') - || $this->sectionNotEmpty('PUT') - || $this->sectionNotEmpty('COOKIE') - || $this->sectionNotEmpty('EXPECTHEADERS'); - } - - /** - * TODO Refactor to make it not needed - */ - public function setSection(string $name, string $value): void - { - $this->sections[$name] = $value; - } - - /** - * Load the sections of the test file - * @throws BorkageException - */ - private function readFile(): void - { - $fp = fopen($this->fileName, "rb") or error("Cannot open test file: {$this->fileName}"); - - if (!feof($fp)) { - $line = fgets($fp); - - if ($line === false) { - throw new BorkageException("cannot read test"); - } - } else { - throw new BorkageException("empty test [{$this->fileName}]"); - } - if (strncmp('--TEST--', $line, 8)) { - throw new BorkageException("tests must start with --TEST-- [{$this->fileName}]"); - } - - $section = 'TEST'; - $secfile = false; - $secdone = false; - - while (!feof($fp)) { - $line = fgets($fp); - - if ($line === false) { - break; - } - - // Match the beginning of a section. - if (preg_match('/^--([_A-Z]+)--/', $line, $r)) { - $section = (string) $r[1]; - - if (isset($this->sections[$section]) && $this->sections[$section]) { - throw new BorkageException("duplicated $section section"); - } - - // check for unknown sections - if (!in_array($section, self::ALLOWED_SECTIONS)) { - throw new BorkageException('Unknown section "' . $section . '"'); - } - - $this->sections[$section] = ''; - $secfile = $section == 'FILE' || $section == 'FILEEOF' || $section == 'FILE_EXTERNAL'; - $secdone = false; - continue; - } - - // Add to the section text. - if (!$secdone) { - $this->sections[$section] .= $line; - } - - // End of actual test? - if ($secfile && preg_match('/^===DONE===\s*$/', $line)) { - $secdone = true; - } - } - - fclose($fp); - } - - /** - * @throws BorkageException - */ - private function validateAndProcess(bool $inRedirect): void - { - // the redirect section allows a set of tests to be reused outside of - // a given test dir - if ($this->hasSection('REDIRECTTEST')) { - if ($inRedirect) { - throw new BorkageException("Can't redirect a test from within a redirected test"); - } - return; - } - if (!$this->hasSection('PHPDBG') && $this->hasSection('FILE') + $this->hasSection('FILEEOF') + $this->hasSection('FILE_EXTERNAL') != 1) { - throw new BorkageException("missing section --FILE--"); - } - - if ($this->hasSection('FILEEOF')) { - $this->sections['FILE'] = preg_replace("/[\r\n]+$/", '', $this->sections['FILEEOF']); - unset($this->sections['FILEEOF']); - } - - foreach (['FILE', 'EXPECT', 'EXPECTF', 'EXPECTREGEX'] as $prefix) { - // For grepping: FILE_EXTERNAL, EXPECT_EXTERNAL, EXPECTF_EXTERNAL, EXPECTREGEX_EXTERNAL - $key = $prefix . '_EXTERNAL'; - - if ($this->hasSection($key)) { - // don't allow tests to retrieve files from anywhere but this subdirectory - $dir = dirname($this->fileName); - $fileName = $dir . '/' . trim(str_replace('..', '', $this->getSection($key))); - - if (file_exists($fileName)) { - $this->sections[$prefix] = file_get_contents($fileName); - } else { - throw new BorkageException("could not load --" . $key . "-- " . $dir . '/' . trim($fileName)); - } - } - } - - if (($this->hasSection('EXPECT') + $this->hasSection('EXPECTF') + $this->hasSection('EXPECTREGEX')) != 1) { - throw new BorkageException("missing section --EXPECT--, --EXPECTF-- or --EXPECTREGEX--"); - } - - if ($this->hasSection('PHPDBG') && !$this->hasSection('STDIN')) { - $this->sections['STDIN'] = $this->sections['PHPDBG'] . "\n"; - } - } -} - -function init_output_buffers(): void -{ - // Delete as much output buffers as possible. - while (@ob_end_clean()) { - } - - if (ob_get_level()) { - echo "Not all buffers were deleted.\n"; - } -} - -function check_proc_open_function_exists(): void -{ - if (!function_exists('proc_open')) { - echo <<", .{}); - errdefer result.deinit(inner); - - if (result.isException()) { - const exc = inner.getException(); - defer exc.deinit(inner); - if (exc.toZigSlice(inner)) |msg| { - defer inner.freeCString(msg.ptr); - exception.throw(msg); - } else { - exception.throw("unknown error"); - } - return error.JavaScriptException; - } - - const ctx_obj: *Class = .from(.impl, self); - const val: *value.Class = .new(); - val.impl.ctx = ctx_obj; - val.impl.inner = result; - try val.impl.updateValueFromJsValue(result); - ctx_obj.addref(); - - ctx.ret.set(.object, phpz.zend.Object.from(&val.std)); - } -}; - -pub const Class = phpz.Class("Pjs\\Context", Context); - -comptime { - Class.method("__construct", .construct); - Class.method("eval", .eval); -} - -const std = @import("std"); - -const phpz = @import("phpz"); -const c = phpz.c; -const quickjs = @import("quickjs"); - -const exception = @import("exception.zig"); -const runtime = @import("runtime.zig"); -const value = @import("value.zig"); diff --git a/examples/pjs/src/classes/exception.zig b/examples/pjs/src/classes/exception.zig deleted file mode 100644 index 595f034..0000000 --- a/examples/pjs/src/classes/exception.zig +++ /dev/null @@ -1,13 +0,0 @@ -pub fn register(extends: anytype) *phpz.ClassEntry { - return .from(extends(c.spl_ce_RuntimeException)); -} - -pub fn throw(message: [:0]const u8) void { - _ = errors.throwException(Class.entry, message, 0); -} - -pub const Class = phpz.SimpleClass("Pjs\\Exception", @This()); - -const phpz = @import("phpz"); -const c = phpz.c; -const errors = phpz.errors; diff --git a/examples/pjs/src/classes/runtime.zig b/examples/pjs/src/classes/runtime.zig deleted file mode 100644 index 83a4895..0000000 --- a/examples/pjs/src/classes/runtime.zig +++ /dev/null @@ -1,16 +0,0 @@ -pub const Runtime = extern struct { - inner: *quickjs.Runtime, - - pub fn init(self: *Runtime) void { - self.inner = quickjs.Runtime.init() catch unreachable; - } - - pub fn deinit(self: *Runtime) void { - self.inner.deinit(); - } -}; - -pub const Class = phpz.Class("Pjs\\Runtime", Runtime); - -const phpz = @import("phpz"); -const quickjs = @import("quickjs"); diff --git a/examples/pjs/src/classes/value.zig b/examples/pjs/src/classes/value.zig deleted file mode 100644 index 24a485e..0000000 --- a/examples/pjs/src/classes/value.zig +++ /dev/null @@ -1,136 +0,0 @@ -pub const Value = extern struct { - ctx: ?*context.Class, - inner: quickjs.Value, - - pub fn register(impl: anytype) *phpz.ClassEntry { - return .from(impl(c.zend_ce_stringable)); - } - - pub fn init(self: *Value) void { - self.inner = .undefined; - } - - pub fn deinit(self: *Value) void { - if (self.ctx) |ctx_obj| { - if (ctx_obj.impl.inner) |js_ctx| self.inner.deinit(js_ctx); - ctx_obj.release(); - } - } - - pub fn construct(self: *Value, ctx: phpz.Ctx) !void { - self.ctx = null; - - const ctx_obj: *phpz.zend.Object, const value_zv: ?*c.zval = try ctx.call.expectArgs(&.{ - .{ .object = .{ .instanceof = true } }, - .{ .mixed = .{ .optional = true } }, - }, .{ - .{ .class = context.Class.entry }, {}, - }); - - self.ctx = .from(.std, ctx_obj.ptr()); - self.ctx.?.addref(); - - if (value_zv) |value| { - self.updateValue(.from(value)) catch return errors.argumentTypeError( - 2, - "must be int|float|string|bool|null, unsupported value type '%s'", - .{phpz.Zval.native.kind(value).cstr()}, - ); - } else { - const php_obj: *Class = .from(.impl, self); - try php_obj.updateProperty(.null, "value", {}); - self.inner = .null; - } - } - - /// Returns the inner quickjs Context from the linked Pjs\Context object. - /// Only valid for fully-constructed Value instances. - fn innerCtx(self: *Value) *quickjs.Context { - return self.ctx.?.impl.inner.?; - } - - fn updateValue(self: *Value, zv: *phpz.Zval) !void { - const php_obj: *Class = .from(.impl, self); - switch (zv.kind()) { - .null => { - try php_obj.updateProperty(.null, "value", {}); - self.inner = .null; - }, - .bool => { - try php_obj.updateProperty(.bool, "value", zv.asUnchecked(.bool)); - self.inner = .initBool(zv.asUnchecked(.bool)); - }, - .int => { - try php_obj.updateProperty(.int, "value", zv.asUnchecked(.int)); - self.inner = .initInt64(zv.asUnchecked(.int)); - }, - .float => { - try php_obj.updateProperty(.float, "value", zv.asUnchecked(.float)); - self.inner = .initFloat64(zv.asUnchecked(.float)); - }, - .string => { - try php_obj.updateProperty(.string, "value", zv.asUnchecked(.string)); - self.inner = .initStringLen(self.innerCtx(), zv.asUnchecked(.string)); - }, - // TODO: more types - else => return error.Unsupported, - } - } - - pub fn updateValueFromJsValue(self: *Value, js_value: quickjs.Value) !void { - const php_obj: *Class = .from(.impl, self); - if (js_value.isNull()) { - try php_obj.updateProperty(.null, "value", {}); - return; - } - if (js_value.isNumber()) { - try php_obj.updateProperty(.float, "value", try js_value.toFloat64(self.innerCtx())); - return; - } - - if (js_value.isBool()) { - try php_obj.updateProperty(.bool, "value", try js_value.toBool(self.innerCtx())); - return; - } - - // fallback to string - // TODO: more type... - const js_ctx = self.innerCtx(); - var str_val = js_value.toStringValue(js_ctx); - defer str_val.deinit(js_ctx); - if (str_val.toZigSlice(js_ctx)) |msg| { - defer js_ctx.freeCString(msg.ptr); - try php_obj.updateProperty(.string, "value", msg); - } - } - - pub fn toString(self: *Value, ctx: phpz.Ctx) !void { - const js_ctx = self.innerCtx(); - var str_val = self.inner.toStringValue(js_ctx); - defer str_val.deinit(js_ctx); - - if (str_val.toZigSlice(js_ctx)) |msg| { - defer js_ctx.freeCString(msg.ptr); - ctx.ret.set(.string, msg); - return; - } - - ctx.ret.set(.string, ""); - } -}; - -pub const Class = phpz.Class("Pjs\\Value", Value); - -comptime { - Class.method("__construct", .construct); - Class.method("__toString", .toString); -} - -const std = @import("std"); - -const phpz = @import("phpz"); -const c = phpz.c; -const errors = phpz.errors; -const quickjs = @import("quickjs"); - -const context = @import("context.zig"); diff --git a/examples/pjs/src/root.zig b/examples/pjs/src/root.zig deleted file mode 100644 index 7453d99..0000000 --- a/examples/pjs/src/root.zig +++ /dev/null @@ -1,24 +0,0 @@ -fn startup() !void { - classes.runtime.Class.register(); - classes.context.Class.register(); - classes.exception.Class.register(); - classes.value.Class.register(); -} - -fn shutdown() !void { - allocator.deinit(); -} - -comptime { - phpz.module(.{ - .name = "pjs", - .version = "0.1.0", - .module_startup_fn = startup, - .module_shutdown_fn = shutdown, - }); -} - -const phpz = @import("phpz"); - -const allocator = @import("allocator.zig"); -const classes = @import("classes.zig"); diff --git a/examples/pjs/tests/eval_date.phpt b/examples/pjs/tests/eval_date.phpt deleted file mode 100644 index 5329a04..0000000 --- a/examples/pjs/tests/eval_date.phpt +++ /dev/null @@ -1,18 +0,0 @@ ---TEST-- -eval: new Date ---FILE-- -eval("new Date")); - -?> ---EXPECTF-- -object(Pjs\Value)%A (1) { - ["value"]=> - string(%d) "%s" -} diff --git a/examples/pjs/tests/eval_error.phpt b/examples/pjs/tests/eval_error.phpt deleted file mode 100644 index 3876aa0..0000000 --- a/examples/pjs/tests/eval_error.phpt +++ /dev/null @@ -1,23 +0,0 @@ ---TEST-- -eval: a() - ReferenceError exception ---FILE-- -eval("a()"); -} catch (Exception $e) { - echo 'class: ', $e::class, "\n"; - echo 'error: ', $e->getMessage(), "\n"; -} - -?> ---EXPECT-- -class: Pjs\Exception -error: ReferenceError: a is not defined diff --git a/examples/pjs/tests/eval_math.phpt b/examples/pjs/tests/eval_math.phpt deleted file mode 100644 index 2cb77fe..0000000 --- a/examples/pjs/tests/eval_math.phpt +++ /dev/null @@ -1,18 +0,0 @@ ---TEST-- -eval: 1+1 ---FILE-- -eval("1+1")); - -?> ---EXPECTF-- -object(Pjs\Value)%A (1) { - ["value"]=> - float(2) -} diff --git a/examples/pjs/tests/eval_testjs.phpt b/examples/pjs/tests/eval_testjs.phpt deleted file mode 100644 index c107f65..0000000 --- a/examples/pjs/tests/eval_testjs.phpt +++ /dev/null @@ -1,36 +0,0 @@ ---TEST-- -eval: test.js functions ---FILE-- -eval(file_get_contents('tests/test.js')); - -echo "greet: "; -var_dump($ctx->eval('greet("World")')); - -echo "fibonacci: "; -var_dump($ctx->eval('fibonacci(8)')); - -echo "introduce: "; -var_dump($ctx->eval('testResults.person.introduce()')); - -?> ---EXPECTF-- -greet: object(Pjs\Value)%A (1) { - ["value"]=> - string(13) "Hello, World!" -} -fibonacci: object(Pjs\Value)%A (1) { - ["value"]=> - float(21) -} -introduce: object(Pjs\Value)%A (1) { - ["value"]=> - string(21) "Alice is 25 years old" -} diff --git a/examples/pjs/tests/eval_testjs_full.phpt b/examples/pjs/tests/eval_testjs_full.phpt deleted file mode 100644 index 2d6feed..0000000 --- a/examples/pjs/tests/eval_testjs_full.phpt +++ /dev/null @@ -1,19 +0,0 @@ ---TEST-- -eval: test.js full result ---FILE-- -eval(file_get_contents('tests/test.js'))); - -?> ---EXPECTF-- -object(Pjs\Value)%A (1) { - ["value"]=> - string(%d) "%A" -} diff --git a/examples/pjs/tests/eval_types.phpt b/examples/pjs/tests/eval_types.phpt deleted file mode 100644 index ad486af..0000000 --- a/examples/pjs/tests/eval_types.phpt +++ /dev/null @@ -1,44 +0,0 @@ ---TEST-- -Context::eval with different value types ---FILE-- -eval("42")); -var_dump($ctx->eval("'hello'")); -var_dump($ctx->eval("true")); -var_dump($ctx->eval("false")); -var_dump($ctx->eval("null")); -var_dump($ctx->eval("[1, 2, 3]")); - -?> ---EXPECTF-- -object(Pjs\Value)%A (1) { - ["value"]=> - float(42) -} -object(Pjs\Value)%A (1) { - ["value"]=> - string(5) "hello" -} -object(Pjs\Value)%A (1) { - ["value"]=> - bool(true) -} -object(Pjs\Value)%A (1) { - ["value"]=> - bool(false) -} -object(Pjs\Value)%A (1) { - ["value"]=> - NULL -} -object(Pjs\Value)%A (1) { - ["value"]=> - string(5) "1,2,3" -} diff --git a/examples/pjs/tests/init_error.phpt b/examples/pjs/tests/init_error.phpt deleted file mode 100644 index 05a1833..0000000 --- a/examples/pjs/tests/init_error.phpt +++ /dev/null @@ -1,21 +0,0 @@ ---TEST-- -Init Context with invalid Runtime ---FILE-- -getMessage() . PHP_EOL; -} - -?> ---EXPECTF-- -object(Pjs\Context)#%A (0) { -} -TypeError: Pjs\Context::__construct(): Argument #1 ($runtime) must be instance of Pjs\Runtime, stdClass given diff --git a/examples/pjs/tests/test.js b/examples/pjs/tests/test.js deleted file mode 100644 index 4b6b36b..0000000 --- a/examples/pjs/tests/test.js +++ /dev/null @@ -1,54 +0,0 @@ -// test.js - QuickJS test script - -// 1. Simple greeting function -function greet(name) { - return 'Hello, ' + name + '!'; -} - -// 2. Mathematical calculation -function fibonacci(n) { - if (n <= 1) return n; - return fibonacci(n - 1) + fibonacci(n - 2); -} - -// 3. Array and object operations -function processData(arr) { - return arr - .filter(x => x > 0) - .map(x => x * 2) - .reduce((sum, x) => sum + x, 0); -} - -// 4. Date handling -function getCurrentTime() { - return new Date().toISOString(); -} - -// 5. String manipulation -function reverseString(str) { - return str.split('').reverse().join(''); -} - -// 6. Object factory -function createPerson(name, age) { - return { - name: name, - age: age, - introduce: function () { - return this.name + ' is ' + this.age + ' years old'; - } - }; -} - -// Export test results -var testResults = { - greeting: greet('Zig-PHP'), - fibonacci: fibonacci(10), - dataProcessing: processData([1, -2, 3, -4, 5, 6]), - currentTime: getCurrentTime(), - reversed: reverseString('QuickJS'), - person: createPerson('Alice', 25) -}; - -// Return test results -JSON.stringify(testResults);; diff --git a/examples/skeleton/build.zig b/examples/skeleton/build.zig index 58e3b11..5797abb 100644 --- a/examples/skeleton/build.zig +++ b/examples/skeleton/build.zig @@ -21,7 +21,7 @@ pub fn build(b: *std.Build) void { }); const ext_name = "skeleton"; - const ext_lib = b.addLibrary(.{ + const ext_lib = phpz.addExtension(b, .{ .name = ext_name, .root_module = b.createModule(.{ .root_source_file = b.path("src/root.zig"), @@ -30,8 +30,6 @@ pub fn build(b: *std.Build) void { }), .linkage = .dynamic, }); - ext_lib.root_module.addImport("phpz", phpz.mod); - phpz.apply(ext_lib); const ext_filename = if (target.result.os.tag == .windows) "php_" ++ ext_name ++ ".dll" diff --git a/examples/skeleton/src/root.zig b/examples/skeleton/src/root.zig index e62bde5..56a063d 100644 --- a/examples/skeleton/src/root.zig +++ b/examples/skeleton/src/root.zig @@ -4,10 +4,6 @@ const phpz = @import("phpz"); const counter = @import("classes/counter.zig"); -fn startup() !void { - counter.Class.register(); -} - comptime { phpz.function("hello", hello); phpz.function("greet", greet); @@ -15,7 +11,7 @@ comptime { phpz.module(.{ .name = "skeleton", .version = "0.1.0", - .module_startup_fn = startup, + .classes = &.{counter.Class}, }); } diff --git a/scripts/test-msvc.sh b/scripts/test-msvc.sh new file mode 100755 index 0000000..d7d4e24 --- /dev/null +++ b/scripts/test-msvc.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env sh +set -eu + +root_dir=$(cd "$(dirname "$0")/.." && pwd) +cd "$root_dir" + +usage() { + echo "usage: $0 [--php-version VERSION] [--zts]" +} + +php_version=8.5.8 +php_thread_safety=nts + +while [ "$#" -gt 0 ]; do + case "$1" in + --php-version) + if [ "$#" -lt 2 ]; then + usage >&2 + exit 2 + fi + php_version=$2 + shift 2 + ;; + --zts) + php_thread_safety=zts + shift + ;; + -h | --help) + usage + exit 0 + ;; + *) + usage >&2 + exit 2 + ;; + esac +done + +tmp_dir=$root_dir/tmp +xwin=$tmp_dir/xwin +win_sdk=$tmp_dir/win_sdk +libc_file=$tmp_dir/libc.txt + +case "$php_thread_safety" in + nts) + php_archive_name=php-devel-pack-${php_version}-nts-Win32-vs17-x64.zip + ;; + zts) + php_archive_name=php-devel-pack-${php_version}-Win32-vs17-x64.zip + ;; + *) + echo "usage: $0 [PHP_VERSION] [nts|zts]" >&2 + exit 2 + ;; +esac + +php_cache_dir=$tmp_dir/php-${php_version}-${php_thread_safety} +php_sdk=$php_cache_dir/php-${php_version}-devel-vs17-x64 + +mkdir -p "$tmp_dir" + +if [ ! -x "$xwin" ]; then + xwin_archive=$tmp_dir/xwin-0.9.0-x86_64-unknown-linux-musl.tar.gz + curl -fsSL \ + https://github.com/Jake-Shadle/xwin/releases/download/0.9.0/xwin-0.9.0-x86_64-unknown-linux-musl.tar.gz \ + -o "$xwin_archive" + tar -xzf "$xwin_archive" \ + -C "$tmp_dir" \ + --strip-components=1 \ + xwin-0.9.0-x86_64-unknown-linux-musl/xwin + rm "$xwin_archive" +fi + +if [ ! -d "$win_sdk" ]; then + "$xwin" \ + --accept-license \ + --arch x86_64,aarch64 \ + --variant desktop \ + --cache-dir "$HOME/.cache/xwin" \ + splat \ + --include-debug-libs \ + --include-debug-symbols \ + --preserve-ms-arch-notation \ + --output "$win_sdk" +fi + +if [ ! -e "$win_sdk/sdk/include/um/Ws2tcpip.h" ]; then + ln -s WS2tcpip.h "$win_sdk/sdk/include/um/Ws2tcpip.h" +fi + +php_archive=$tmp_dir/$php_archive_name +if [ ! -d "$php_sdk" ]; then + curl -fsSL \ + "https://downloads.php.net/~windows/releases/archives/$php_archive_name" \ + -o "$php_archive" + mkdir -p "$php_cache_dir" + unzip -q "$php_archive" -d "$php_cache_dir" + rm "$php_archive" +fi + +cat >"$libc_file" < .{ .x86_64_vectorcall = .{} }, + .x86 => .{ .x86_vectorcall = .{} }, + else => .c, + } +else + .c; + +const Fn = fn (?*c.zend_execute_data, ?*c.zval) callconv(fn_cc) void; /// Register a Zig function as a PHP function. /// @@ -91,7 +102,7 @@ pub fn methodWithClass(comptime Class: anytype, comptime class_name: [:0]const u fn wrapFn(comptime func_desc: [:0]const u8, comptime func: anytype) Fn { const Args = std.meta.ArgsTuple(@TypeOf(func)); return struct { - fn handle(execute_data: ?*c.zend_execute_data, return_value: ?*c.zval) callconv(.c) void { + fn handle(execute_data: ?*c.zend_execute_data, return_value: ?*c.zval) callconv(fn_cc) void { var ctx: Ctx = .{ .call = .from(execute_data.?), .ret = .from(return_value.?) }; const args = switch (Args) { @Tuple(&.{Ctx}) => .{ctx}, @@ -121,7 +132,7 @@ fn wrapMethod(comptime Class: type, comptime func_desc: [:0]const u8, comptime f } else .static; const impl_offset = comptime @intFromBool(kind == .object); return struct { - fn handle(execute_data: ?*c.zend_execute_data, return_value: ?*c.zval) callconv(.c) void { + fn handle(execute_data: ?*c.zend_execute_data, return_value: ?*c.zval) callconv(fn_cc) void { var ctx: Ctx = .{ .call = .from(execute_data.?), .ret = .from(return_value.?) }; const args: Args = (if (kind == .object) blk: { const obj: *Class = .from(.std, ctx.call.thisObject().?.ptr()); diff --git a/src/globals.zig b/src/globals.zig new file mode 100644 index 0000000..db61356 --- /dev/null +++ b/src/globals.zig @@ -0,0 +1,100 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const c = @import("c.zig").c; + +const use_dll_import = builtin.os.tag == .windows and builtin.abi == .msvc; + +pub const GlobalAccess = enum { ptr, value }; + +fn Global(comptime access: GlobalAccess, comptime T: type, comptime name: []const u8) type { + return switch (access) { + .ptr => if (use_dll_import) *T else @TypeOf(&@field(c, name)), + .value => T, + }; +} + +pub inline fn global( + comptime access: GlobalAccess, + comptime T: type, + comptime name: []const u8, +) Global(access, T, name) { + return switch (access) { + .ptr => if (comptime use_dll_import) + @extern(*T, .{ .name = name, .is_dll_import = true }) + else + &@field(c, name), + .value => if (comptime use_dll_import) + @extern(*T, .{ .name = name, .is_dll_import = true }).* + else + @field(c, name), + }; +} + +pub const class = struct { + fn symbolName(comptime prefix: []const u8, comptime class_name: []const u8, comptime lowercase: bool) []const u8 { + const name = if (class_name.len > 0 and class_name[0] == '\\') class_name[1..] else class_name; + if (name.len == 0) @compileError("class entry name cannot be empty"); + + comptime var result: [prefix.len + name.len]u8 = undefined; + inline for (prefix, 0..) |ch, i| { + result[i] = ch; + } + inline for (name, 0..) |ch, i| { + result[prefix.len + i] = comptime switch (ch) { + '\\' => '_', + else => if (lowercase) std.ascii.toLower(ch) else ch, + }; + } + + const final = result; + return &final; + } + + fn symbol(comptime class_name: []const u8) []const u8 { + return comptime blk: { + const zend_original = symbolName("zend_ce_", class_name, false); + if (@hasDecl(c, zend_original)) break :blk zend_original; + + const zend_lowercase = symbolName("zend_ce_", class_name, true); + if (@hasDecl(c, zend_lowercase)) break :blk zend_lowercase; + + const spl_original = symbolName("spl_ce_", class_name, false); + if (@hasDecl(c, spl_original)) break :blk spl_original; + + const spl_lowercase = symbolName("spl_ce_", class_name, true); + if (@hasDecl(c, spl_lowercase)) break :blk spl_lowercase; + + @compileError("class entry symbol not found: " ++ zend_original ++ ", " ++ zend_lowercase ++ ", " ++ spl_original ++ ", or " ++ spl_lowercase); + }; + } + + pub inline fn rawEntry(comptime name: []const u8) *c.zend_class_entry { + return global(.value, *c.zend_class_entry, name); + } + + pub inline fn entry(comptime class_name: []const u8) *c.zend_class_entry { + return rawEntry(symbol(class_name)); + } +}; + +pub inline fn executor() *c.zend_executor_globals { + return @ptrCast(c.phpz_executor_globals()); +} + +pub inline fn compiler() *c.zend_compiler_globals { + return @ptrCast(c.phpz_compiler_globals()); +} + +pub inline fn core() *c.php_core_globals { + return @ptrCast(c.phpz_core_globals()); +} + +pub inline fn sapi() *c.sapi_globals_struct { + return @ptrCast(c.phpz_sapi_globals()); +} + +test { + std.testing.refAllDecls(@This()); + std.testing.refAllDecls(@This().class); +} diff --git a/src/root.zig b/src/root.zig index 9b1057e..44acb27 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,6 +1,7 @@ const std = @import("std"); pub const c = @import("c.zig").c; +pub const globals = @import("globals.zig"); pub const heap = @import("heap.zig"); pub const ini = @import("ini.zig"); @@ -30,11 +31,19 @@ pub fn printf(fmt: [:0]const u8, args: anytype) usize { return @call(.auto, c.php_printf, .{fmt.ptr} ++ args); } +comptime { + const builtin = @import("builtin"); + if (builtin.os.tag == .windows and builtin.abi != .msvc) { + @compileError("phpz only supports the MSVC ABI on Windows"); + } +} + test { _ = @import("Ctx.zig"); _ = @import("class.zig"); _ = @import("errors.zig"); _ = @import("function.zig"); + _ = @import("globals.zig"); _ = @import("heap.zig"); _ = @import("ini.zig"); _ = @import("info.zig"); diff --git a/src/zend/function.zig b/src/zend/function.zig index b59c2d5..32c590c 100644 --- a/src/zend/function.zig +++ b/src/zend/function.zig @@ -1,6 +1,7 @@ const errors = @import("../errors.zig"); const phpz = @import("../root.zig"); const c = phpz.c; +const globals = phpz.globals; const ClassEntry = @import("class_entry.zig").ClassEntry; const Object = @import("object.zig").Object; const try_catch = @import("try_catch.zig"); @@ -42,7 +43,7 @@ pub const Function = opaque { /// Look up a function in the global function table pub fn find(func_name: []const u8) ?*Function { return @ptrCast(@alignCast(c.zend_hash_str_find_ptr( - c.phpz_executor_globals().*.function_table, + globals.executor().function_table, func_name.ptr, func_name.len, ))); diff --git a/src/zend/object.zig b/src/zend/object.zig index fd42f7e..b47a767 100644 --- a/src/zend/object.zig +++ b/src/zend/object.zig @@ -1,6 +1,7 @@ const errors = @import("../errors.zig"); const phpz = @import("../root.zig"); const c = phpz.c; +const globals = phpz.globals; const native = @import("../zval.zig").Zval.native; const Array = @import("array.zig").Array; const ClassEntry = @import("class_entry.zig").ClassEntry; @@ -15,9 +16,10 @@ pub const Object = opaque { /// /// Ownership: caller owns the returned object; call `release()` when done. pub fn init() InitError!*Object { - const obj = c.zend_objects_new(c.zend_standard_class_def); + const std_class = globals.class.rawEntry("zend_standard_class_def"); + const obj = c.zend_objects_new(std_class); if (obj == null) return error.InitFailed; - c.object_properties_init(obj, c.zend_standard_class_def); + c.object_properties_init(obj, std_class); return @ptrCast(obj); } @@ -112,7 +114,7 @@ pub const Object = opaque { /// (private/protected), triggers `__call` when the method is absent, /// and respects inheritance. Use `findMethod` for a direct table lookup. pub fn resolveMethod(self: *Object, method_name: []const u8) ?*Function { - const zstr = String.init(method_name); + const zstr = String.init(method_name, false); defer zstr.release(); var obj_ptr = self.ptr(); @@ -167,7 +169,7 @@ pub const Object = opaque { comptime read: PropertyRead, scratch: *c.zval, ) Function.Error!*c.zval { - const zstr = String.init(name); + const zstr = String.init(name, false); defer zstr.release(); const result = c.zend_std_read_property( @@ -193,7 +195,7 @@ pub const Object = opaque { name: []const u8, value: *c.zval, ) Function.Error!?*c.zval { - const zstr = String.init(name); + const zstr = String.init(name, false); defer zstr.release(); const result = c.zend_std_write_property( @@ -225,7 +227,7 @@ pub const Object = opaque { name: []const u8, comptime check: PropertyCheck, ) Function.Error!bool { - const zstr = String.init(name); + const zstr = String.init(name, false); defer zstr.release(); const result = c.zend_std_has_property(self.ptr(), zstr.ptr(), @intFromEnum(check), null); @@ -237,14 +239,14 @@ pub const Object = opaque { /// /// Returns `error.PhpException` if a magic `__unset` handler throws. pub fn unsetProperty(self: *Object, name: []const u8) Function.Error!void { - const zstr = String.init(name); + const zstr = String.init(name, false); defer zstr.release(); c.zend_std_unset_property(self.ptr(), zstr.ptr(), null); if (errors.hasException()) return error.PhpException; } - /// Check if object is an instance of a class + /// Check if object is an instance of a class or implements an interface. pub inline fn instanceof(self: *Object, ce: *ClassEntry) bool { return c.instanceof_function(self.class().ptr(), ce.ptr()); } @@ -326,7 +328,7 @@ pub const Object = opaque { retval: ?*c.zval, params: []c.zval, ) CallIfExistsError!void { - const zstr = String.init(method_name); + const zstr = String.init(method_name, false); defer zstr.release(); const result = c.zend_call_method_if_exists( diff --git a/src/zend/string.zig b/src/zend/string.zig index 5e1468f..b626dbe 100644 --- a/src/zend/string.zig +++ b/src/zend/string.zig @@ -1,28 +1,31 @@ -const c = @import("../root.zig").c; +const phpz = @import("../root.zig"); +const c = phpz.c; +const globals = phpz.globals; pub const String = opaque { /// Create an empty string. /// /// Ownership: borrowed interned singleton. pub inline fn empty() *String { - return @ptrCast(@alignCast(c.zend_empty_string)); + return @ptrCast(globals.global(.value, *c.zend_string, "zend_empty_string")); } /// Create a single character string. /// /// Ownership: borrowed interned singleton. pub inline fn char(ch: u8) *String { - return @ptrCast(@alignCast(c.zend_one_char_string[ch])); + const index: usize = ch; + return @ptrCast(globals.global(.ptr, [256]*c.zend_string, "zend_one_char_string")[index]); } /// Create a string from a byte slice. /// /// Ownership: caller owns one logical reference; call `release()` when done. /// Empty and one-byte strings may be interned, but `release()` is still valid. - pub fn init(str: []const u8) *String { + pub fn init(str: []const u8, persistent: bool) *String { if (str.len == 0) return empty(); if (str.len == 1) return char(str[0]); - return @ptrCast(c.zend_string_init(str.ptr, str.len, false)); + return @ptrCast(c.zend_string_init(str.ptr, str.len, persistent)); } /// Create a string from an existing zend_string pointer. diff --git a/src/zval.zig b/src/zval.zig index b12f001..9d91601 100644 --- a/src/zval.zig +++ b/src/zval.zig @@ -564,7 +564,7 @@ pub const Zval = opaque { zv.u1.type_info = c.IS_DOUBLE; }, .string => { - const str: *zend.String = .init(val); + const str: *zend.String = .init(val, false); zv.value.str = str.ptr(); zv.u1.type_info = if (str.isInterned()) c.IS_INTERNED_STRING_EX else c.IS_STRING_EX; }, diff --git a/tools/phpz_skel.php b/tools/phpz_skel.php index 349e3bb..4c43201 100755 --- a/tools/phpz_skel.php +++ b/tools/phpz_skel.php @@ -10,7 +10,7 @@ const MIN_PHP_VERSION = '8.2.0'; const MIN_PHP_VERSION_ID = 80200; -const MIN_ZIG_VERSION = '0.16.0'; +const MIN_ZIG_VERSION = '0.17.0'; const DEFAULT_PHPZ_SPEC = 'git+https://github.com/happystraw/phpz'; const DEFAULT_PHP_INCLUDE_DIR = '/usr/include/php'; const REQUIRED_PHP_EXTENSIONS = ['pcre', 'standard']; diff --git a/tools/windows_patcher.zig b/tools/windows_patcher.zig new file mode 100644 index 0000000..5872a9f --- /dev/null +++ b/tools/windows_patcher.zig @@ -0,0 +1,328 @@ +const std = @import("std"); + +const default_dllimport_data_symbols = [_][]const u8{ + "std_object_handlers", + "zend_empty_string", + "zend_one_char_string", + "zend_string_init_interned", + "executor_globals", + "compiler_globals", + "core_globals", + "sapi_globals", + "executor_globals_offset", + "compiler_globals_offset", + "core_globals_offset", + "sapi_globals_offset", +}; + +const default_dllimport_data_prefixes = [_][]const u8{ + "zend_ce_", + "spl_ce_", +}; + +pub fn main(init: std.process.Init) !void { + const gpa = init.gpa; + + var args = try std.process.Args.Iterator.initAllocator(init.minimal.args, gpa); + defer args.deinit(); + + const cmd = args.next() orelse "windows-patcher"; + const input_path = args.next() orelse fatal("usage: {s} ", .{cmd}); + const output_path = args.next() orelse fatal("usage: {s} ", .{cmd}); + if (args.next() != null) { + fatal("usage: {s} ", .{cmd}); + } + + const cwd = std.Io.Dir.cwd(); + const source = try cwd.readFileAllocOptions(init.io, input_path, gpa, .unlimited, .of(u8), 0); + defer gpa.free(source); + + var result = std.ArrayList(u8).empty; + defer result.deinit(gpa); + + try patch(gpa, &result, source, .{ + .dllimport_data_symbols = &default_dllimport_data_symbols, + .dllimport_data_prefixes = &default_dllimport_data_prefixes, + }); + try cwd.writeFile(init.io, .{ .sub_path = output_path, .data = result.items }); +} + +fn patch( + gpa: std.mem.Allocator, + result: *std.ArrayList(u8), + source: [:0]const u8, + options: Patcher.Options, +) !void { + var tree = try std.zig.Ast.parse(gpa, source, .zig); + defer tree.deinit(gpa); + + if (tree.errors.len != 0) { + const parse_error = tree.errors[0]; + const loc = tree.tokenLocation(0, parse_error.token); + fatal("unable to parse translated Zig at {d}:{d}", .{ loc.line + 1, loc.column + 1 }); + } + + var patcher = Patcher.init(gpa, tree); + defer patcher.deinit(); + + const changed = try patcher.patch(options); + if (!changed) { + try result.appendSlice(gpa, source); + return; + } + + try patcher.render(result); +} + +const Patcher = struct { + gpa: std.mem.Allocator, + tree: std.zig.Ast, + fixups: std.zig.Ast.Render.Fixups = .{}, + dllimport_data_symbols: std.StringHashMapUnmanaged(DllImportDataSymbol) = .empty, + allocated_replacements: std.ArrayList([]u8) = .empty, + + const Options = struct { + dllimport_data_symbols: []const []const u8 = &.{}, + dllimport_data_prefixes: []const []const u8 = &.{}, + }; + + const DllImportDataSymbol = struct { + name: []const u8, + type_expr: []const u8, + is_const: bool, + }; + + const TokenRange = struct { first: std.zig.Ast.TokenIndex, last: std.zig.Ast.TokenIndex }; + + fn init(gpa: std.mem.Allocator, tree: std.zig.Ast) Patcher { + return .{ .gpa = gpa, .tree = tree }; + } + + fn deinit(self: *Patcher) void { + for (self.allocated_replacements.items) |replacement| { + self.gpa.free(replacement); + } + self.allocated_replacements.deinit(self.gpa); + self.dllimport_data_symbols.deinit(self.gpa); + self.fixups.deinit(self.gpa); + } + + fn patch(self: *Patcher, options: Options) !bool { + var changed = false; + + try self.collectDllImportDataSymbols(options); + changed = try self.patchDllImportDataReferences() or changed; + + return changed; + } + + fn collectDllImportDataSymbols(self: *Patcher, options: Options) !void { + for (self.tree.rootDecls()) |node| { + const var_decl = self.tree.fullVarDecl(node) orelse continue; + const extern_token = var_decl.extern_export_token orelse continue; + if (self.tree.tokenTag(extern_token) != .keyword_extern) continue; + if (var_decl.lib_name != null) continue; + + const name_token = var_decl.ast.mut_token + 1; + if (self.tree.tokenTag(name_token) != .identifier) continue; + + const name = self.tree.tokenSlice(name_token); + if (!isDllImportDataSymbolName(name, options)) continue; + + const type_node = var_decl.ast.type_node.unwrap() orelse continue; + try self.dllimport_data_symbols.put(self.gpa, name, .{ + .name = name, + .type_expr = self.tree.getNodeSource(type_node), + .is_const = self.tree.tokenTag(var_decl.ast.mut_token) == .keyword_const, + }); + } + } + + fn patchDllImportDataReferences(self: *Patcher) !bool { + if (self.dllimport_data_symbols.count() == 0) return false; + + var body_ranges = std.ArrayList(TokenRange).empty; + defer body_ranges.deinit(self.gpa); + try self.collectFunctionBodyRanges(&body_ranges); + if (body_ranges.items.len == 0) return false; + + var addressed_identifiers: std.AutoHashMapUnmanaged(std.zig.Ast.Node.Index, void) = .empty; + defer addressed_identifiers.deinit(self.gpa); + + var changed = false; + for (0..self.tree.nodes.len) |node_i| { + const node: std.zig.Ast.Node.Index = @enumFromInt(node_i); + if (self.tree.nodeTag(node) != .address_of) continue; + if (!self.nodeInTokenRanges(node, body_ranges.items)) continue; + + const child = self.tree.nodeData(node).node; + const symbol = self.identifierDllImportDataSymbol(child) orelse continue; + + const replacement = try self.dllImportPointerExpr(symbol); + try self.fixups.replace_nodes_with_string.put(self.gpa, node, replacement); + try addressed_identifiers.put(self.gpa, child, {}); + changed = true; + } + + for (0..self.tree.nodes.len) |node_i| { + const node: std.zig.Ast.Node.Index = @enumFromInt(node_i); + if (self.tree.nodeTag(node) != .identifier) continue; + if (addressed_identifiers.contains(node)) continue; + if (!self.nodeInTokenRanges(node, body_ranges.items)) continue; + + const symbol = self.identifierDllImportDataSymbol(node) orelse continue; + const replacement = try self.dllImportValueExpr(symbol); + try self.fixups.replace_nodes_with_string.put(self.gpa, node, replacement); + changed = true; + } + + return changed; + } + + fn collectFunctionBodyRanges( + self: *Patcher, + ranges: *std.ArrayList(TokenRange), + ) !void { + for (self.tree.rootDecls()) |node| { + if (self.tree.nodeTag(node) != .fn_decl) continue; + const fn_parts = self.tree.nodeData(node).node_and_node; + const body_node = fn_parts[1]; + try ranges.append(self.gpa, .{ + .first = self.tree.firstToken(body_node), + .last = self.tree.lastToken(body_node), + }); + } + } + + fn identifierDllImportDataSymbol( + self: *Patcher, + node: std.zig.Ast.Node.Index, + ) ?DllImportDataSymbol { + if (self.tree.nodeTag(node) != .identifier) return null; + const name = self.tree.tokenSlice(self.tree.nodeMainToken(node)); + return self.dllimport_data_symbols.get(name); + } + + fn dllImportPointerExpr(self: *Patcher, symbol: DllImportDataSymbol) ![]const u8 { + const const_kw = if (symbol.is_const) "const " else ""; + return self.allocReplacement( + \\@extern(*{s}{s}, .{{ .name = "{s}", .is_dll_import = true }}) + , .{ const_kw, symbol.type_expr, symbol.name }); + } + + fn dllImportValueExpr(self: *Patcher, symbol: DllImportDataSymbol) ![]const u8 { + const const_kw = if (symbol.is_const) "const " else ""; + return self.allocReplacement( + \\(@extern(*{s}{s}, .{{ .name = "{s}", .is_dll_import = true }}).*) + , .{ const_kw, symbol.type_expr, symbol.name }); + } + + fn allocReplacement(self: *Patcher, comptime fmt: []const u8, args: anytype) ![]const u8 { + const replacement = try std.fmt.allocPrint(self.gpa, fmt, args); + errdefer self.gpa.free(replacement); + try self.allocated_replacements.append(self.gpa, replacement); + return replacement; + } + + fn render(self: *Patcher, result: *std.ArrayList(u8)) !void { + var aw: std.Io.Writer.Allocating = .init(self.gpa); + defer aw.deinit(); + + try self.tree.render(self.gpa, &aw.writer, self.fixups); + try result.appendSlice(self.gpa, aw.written()); + } + + fn nodeInTokenRanges(self: *const Patcher, node: std.zig.Ast.Node.Index, ranges: []const TokenRange) bool { + const first_token = self.tree.firstToken(node); + for (ranges) |range| { + if (first_token >= range.first and first_token <= range.last) return true; + } + return false; + } + + fn isDllImportDataSymbolName(name: []const u8, options: Options) bool { + if (std.mem.startsWith(u8, name, "__imp_")) return false; + if (std.mem.endsWith(u8, name, "_module_entry")) return false; + + for (options.dllimport_data_symbols) |symbol| { + if (std.mem.eql(u8, name, symbol)) return true; + } + for (options.dllimport_data_prefixes) |prefix| { + if (std.mem.startsWith(u8, name, prefix)) return true; + } + + return false; + } +}; + +fn fatal(comptime format: []const u8, args: anytype) noreturn { + std.debug.print(format ++ "\n", args); + std.process.exit(1); +} + +test "patches address-of dllimport data references in function bodies" { + const raw_source = + \\pub extern const std_object_handlers: zend_object_handlers; + \\pub const untouched = &std_object_handlers; + \\pub fn register() callconv(.c) void { + \\ ce.default_object_handlers = &std_object_handlers; + \\} + \\ + ; + const source = try std.testing.allocator.dupeSentinel(u8, raw_source, 0); + defer std.testing.allocator.free(source); + var result = std.ArrayList(u8).empty; + defer result.deinit(std.testing.allocator); + const dllimport_data_symbols = [_][]const u8{"std_object_handlers"}; + + try patch(std.testing.allocator, &result, source, .{ + .dllimport_data_symbols = &dllimport_data_symbols, + }); + + try std.testing.expect(std.mem.indexOf(u8, result.items, "untouched = &std_object_handlers") != null); + try std.testing.expect(std.mem.indexOf(u8, result.items, "ce.default_object_handlers = @extern(*const zend_object_handlers, .{ .name = \"std_object_handlers\", .is_dll_import = true })") != null); +} + +test "patches value dllimport data references in function bodies" { + const raw_source = + \\pub extern var zend_ce_exception: [*c]zend_class_entry; + \\pub fn register() callconv(.c) void { + \\ var class_entry = zend_ce_exception; + \\ _ = class_entry; + \\} + \\ + ; + const source = try std.testing.allocator.dupeSentinel(u8, raw_source, 0); + defer std.testing.allocator.free(source); + var result = std.ArrayList(u8).empty; + defer result.deinit(std.testing.allocator); + const dllimport_data_prefixes = [_][]const u8{"zend_ce_"}; + + try patch(std.testing.allocator, &result, source, .{ + .dllimport_data_prefixes = &dllimport_data_prefixes, + }); + + try std.testing.expect(std.mem.indexOf(u8, result.items, "var class_entry = (@extern(*[*c]zend_class_entry, .{ .name = \"zend_ce_exception\", .is_dll_import = true }).*)") != null); +} + +test "patches dllimport function pointer data calls in function bodies" { + const raw_source = + \\pub const zend_string_init_interned_func_t = ?*const fn (str: [*c]const u8, size: usize, permanent: bool) callconv(.c) [*c]zend_string; + \\pub extern var zend_string_init_interned: zend_string_init_interned_func_t; + \\pub fn register() callconv(.c) void { + \\ ce.name = zend_string_init_interned.?("Example", 7, true); + \\} + \\ + ; + const source = try std.testing.allocator.dupeSentinel(u8, raw_source, 0); + defer std.testing.allocator.free(source); + var result = std.ArrayList(u8).empty; + defer result.deinit(std.testing.allocator); + const dllimport_data_symbols = [_][]const u8{"zend_string_init_interned"}; + + try patch(std.testing.allocator, &result, source, .{ + .dllimport_data_symbols = &dllimport_data_symbols, + }); + + try std.testing.expect(std.mem.indexOf(u8, result.items, "(@extern(*zend_string_init_interned_func_t, .{ .name = \"zend_string_init_interned\", .is_dll_import = true }).*).?(\"Example\", 7, true)") != null); +} From 42d542344242c150f34919252e058be9c20585c3 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Thu, 9 Jul 2026 22:11:03 +0800 Subject: [PATCH 08/17] feat(tools): add Windows skeleton build support Generate platform-aware build scripts and module filenames, propagate Windows ZTS/debug options to examples, and register generated classes through module metadata. --- build.zig | 2 + examples/my_php_extension/build.zig | 2 + examples/skeleton/build.zig | 6 +- tools/phpz_skel.php | 139 +++++++++++++++++++++++++--- 4 files changed, 135 insertions(+), 14 deletions(-) diff --git a/build.zig b/build.zig index f086fee..27c6dca 100644 --- a/build.zig +++ b/build.zig @@ -120,6 +120,8 @@ fn addTestExamplesStep(b: *std.Build, options: BuildOptions) void { if (options.php_lib_dir) |lib_dir| { test_cmd.addArg(b.fmt("-Dphp-lib-dir={s}", .{lib_dir})); } + test_cmd.addArg(b.fmt("-Dwindows-zts={}", .{options.windows_zts})); + test_cmd.addArg(b.fmt("-Dwindows-debug={}", .{options.windows_debug})); } test_cmd.setCwd(b.path("examples").join(b.allocator, test_example) catch unreachable); step.dependOn(&test_cmd.step); diff --git a/examples/my_php_extension/build.zig b/examples/my_php_extension/build.zig index c5bba8d..af6fd99 100644 --- a/examples/my_php_extension/build.zig +++ b/examples/my_php_extension/build.zig @@ -15,6 +15,7 @@ pub fn build(b: *std.Build) void { const libc_file = b.option([]const u8, "libc", "Libc paths file for cross-compilation"); const libc_file_path: ?std.Build.LazyPath = if (libc_file) |path| .{ .cwd_relative = path } else null; const windows_zts = b.option(bool, "windows-zts", "Windows only: link against the thread-safe PHP library") orelse false; + const windows_debug = b.option(bool, "windows-debug", "Windows only: build against a debug PHP SDK") orelse false; // Fetch the phpz dependency declared in build.zig.zon const phpz_dep = b.dependency("phpz", .{}); @@ -29,6 +30,7 @@ pub fn build(b: *std.Build) void { .php_include_dir = .{ .cwd_relative = php_include_dir }, .php_lib_dir = if (php_lib_dir) |d| .{ .cwd_relative = d } else null, .windows_zts = windows_zts, + .windows_debug = windows_debug, }); // Create the PHP extension as a dynamic library (.so / .dll / .dylib) diff --git a/examples/skeleton/build.zig b/examples/skeleton/build.zig index 5797abb..2afbff5 100644 --- a/examples/skeleton/build.zig +++ b/examples/skeleton/build.zig @@ -7,7 +7,9 @@ pub fn build(b: *std.Build) void { const optimize = b.standardOptimizeOption(.{}); const php_include_dir = b.option([]const u8, "php-include-dir", "PHP include directory (main/, Zend/, TSRM/, ext/)") orelse "/usr/include/php"; - const php_lib_dir = b.option([]const u8, "php-lib-dir", "PHP SDK library directory (Windows only, contains php8.lib)"); + const php_lib_dir = b.option([]const u8, "php-lib-dir", "PHP SDK library directory (Windows only, contains php8*.lib)"); + const windows_zts = b.option(bool, "windows-zts", "Windows only: link against the thread-safe PHP library") orelse false; + const windows_debug = b.option(bool, "windows-debug", "Windows only: build against a debug PHP SDK") orelse false; const phpz_dep = b.dependency("phpz", .{}); const phpz = Phpz.init(phpz_dep, .{ @@ -18,6 +20,8 @@ pub fn build(b: *std.Build) void { }, .php_include_dir = .{ .cwd_relative = php_include_dir }, .php_lib_dir = if (php_lib_dir) |d| .{ .cwd_relative = d } else null, + .windows_zts = windows_zts, + .windows_debug = windows_debug, }); const ext_name = "skeleton"; diff --git a/tools/phpz_skel.php b/tools/phpz_skel.php index 4c43201..47b5df3 100755 --- a/tools/phpz_skel.php +++ b/tools/phpz_skel.php @@ -7,10 +7,10 @@ // phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols -- CLI script entry point calls Command::run(). // phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses -- CLI script is distributed as one file. +// phpcs:disable Generic.Files.LineLength.TooLong -- Embedded Zig templates use Zig formatting. const MIN_PHP_VERSION = '8.2.0'; -const MIN_PHP_VERSION_ID = 80200; -const MIN_ZIG_VERSION = '0.17.0'; +const MIN_ZIG_VERSION = '0.17.0-dev.1282+c0f9b51d8'; const DEFAULT_PHPZ_SPEC = 'git+https://github.com/happystraw/phpz'; const DEFAULT_PHP_INCLUDE_DIR = '/usr/include/php'; const REQUIRED_PHP_EXTENSIONS = ['pcre', 'standard']; @@ -26,6 +26,8 @@ public function __construct( public PhpConfig $phpConfig, public ToolOption $genStub, public ToolOption $runTests, + public bool $unix, + public bool $windows, public bool $force, public bool $verbose, ) { @@ -44,6 +46,8 @@ public static function parse(array $argv): self $phpConfig = PhpConfig::auto(); $genStub = ToolOption::auto(); $runTests = ToolOption::auto(); + $unix = true; + $windows = true; $force = false; $verbose = false; @@ -65,6 +69,14 @@ public static function parse(array $argv): self $runTests = ToolOption::disabled(); continue; } + if ($arg === '--onlyunix') { + $windows = false; + continue; + } + if ($arg === '--onlywindows') { + $unix = false; + continue; + } $name = null; $value = null; @@ -125,6 +137,9 @@ public static function parse(array $argv): self if ($ext === null) { Console::fail('Missing --ext '); } + if (!$unix && !$windows) { + Console::fail('Cannot pass both --onlyunix and --onlywindows'); + } if (!preg_match('/^[A-Za-z][A-Za-z0-9_]*$/', $ext)) { Console::fail('Invalid extension name. Use letters, numbers and underscores, starting with a letter.'); } @@ -138,6 +153,8 @@ public static function parse(array $argv): self $phpConfig, $genStub, $runTests, + $unix, + $windows, $force, $verbose, ); @@ -567,7 +584,7 @@ public static function checkZigVersion(string $zig): string public static function checkPhpVersion(): string { $version = PHP_VERSION; - if (PHP_VERSION_ID < MIN_PHP_VERSION_ID || version_compare($version, MIN_PHP_VERSION, '<')) { + if (version_compare($version, MIN_PHP_VERSION, '<')) { Console::fail("Unsupported PHP version $version. Expected >= " . MIN_PHP_VERSION . '.'); } @@ -836,6 +853,55 @@ public static function runGenStub( test_step.dependOn(&test_phpt_cmd.step); ZIG; +const PHPZ_WINDOWS_BUILD_OPTIONS_TEMPLATE = <<<'ZIG' + const php_lib_dir = b.option([]const u8, "php-lib-dir", "PHP SDK library directory (Windows only, contains php8*.lib)"); + const windows_zts = b.option(bool, "windows-zts", "Windows only: link against the thread-safe PHP library") orelse false; + const windows_debug = b.option(bool, "windows-debug", "Windows only: build against a debug PHP SDK") orelse false; +ZIG; + +const PHPZ_WINDOWS_INIT_OPTIONS_TEMPLATE = <<<'ZIG' + .php_lib_dir = if (php_lib_dir) |dir| .{ .cwd_relative = dir } else null, + .windows_zts = windows_zts, + .windows_debug = windows_debug, +ZIG; + +const PHPZ_ALL_PLATFORM_FILENAME_TEMPLATE = <<<'ZIG' + const ext_filename = if (target.result.os.tag == .windows) + "php_" ++ ext_name ++ ".dll" + else + ext_name ++ ".so"; +ZIG; + +const PHPZ_UNIX_FILENAME_TEMPLATE = <<<'ZIG' + const ext_filename = ext_name ++ ".so"; +ZIG; + +const PHPZ_WINDOWS_FILENAME_TEMPLATE = <<<'ZIG' + const ext_filename = "php_" ++ ext_name ++ ".dll"; +ZIG; + +const PHPZ_UNIX_TARGET_CHECK_TEMPLATE = <<<'ZIG' + if (target.result.os.tag == .windows) { + @panic("This extension only supports Unix targets"); + } +ZIG; + +const PHPZ_WINDOWS_TARGET_CHECK_TEMPLATE = <<<'ZIG' + if (target.result.os.tag != .windows) { + @panic("This extension only supports Windows targets"); + } +ZIG; + +const PHPZ_UNIX_MANUAL_COMMANDS_TEMPLATE = <<<'MD' +php -dextension=./modules/{{EXT_NAME}}.so -r 'hello();' +php -dextension=./modules/{{EXT_NAME}}.so -r 'echo greet("World");' +MD; + +const PHPZ_WINDOWS_MANUAL_COMMANDS_TEMPLATE = <<<'MD' +php -dextension=./modules/php_{{EXT_NAME}}.dll -r 'hello();' +php -dextension=./modules/php_{{EXT_NAME}}.dll -r 'echo greet("World");' +MD; + const PHPZ_GEN_STUB_REGEN_TEMPLATE = <<<'MD' After changing `{{EXT_NAME}}.stub.php`, regenerate C arginfo: @@ -869,8 +935,10 @@ public static function runGenStub( pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); +{{PHPZ_PLATFORM_TARGET_CHECK}} const php_include_dir = b.option([]const u8, "php-include-dir", "PHP include dir") orelse "{{PHP_INCLUDE_DIR}}"; +{{PHPZ_WINDOWS_BUILD_OPTIONS}} const phpz_dep = b.dependency("phpz", .{}); const phpz = Phpz.init(phpz_dep, .{ @@ -880,6 +948,7 @@ public static function runGenStub( .optimize = optimize, }, .php_include_dir = .{ .cwd_relative = php_include_dir }, +{{PHPZ_WINDOWS_INIT_OPTIONS}} }); const ext_name = "{{EXT_NAME}}"; @@ -893,7 +962,7 @@ public static function runGenStub( .linkage = .dynamic, }); - const ext_filename = ext_name ++ ".so"; +{{PHPZ_EXT_FILENAME}} const ext_file = std.Build.Step.UpdateSourceFiles.create(b); ext_file.addCopyFileToSource(ext_lib.getEmittedBin(), b.fmt("modules/{s}", .{ext_filename})); @@ -925,8 +994,7 @@ public static function runGenStub( ```bash zig build -php -dextension=./modules/{{EXT_NAME}}.so -r 'hello();' -php -dextension=./modules/{{EXT_NAME}}.so -r 'echo greet("World");' +{{PHPZ_MANUAL_COMMANDS}} ``` MD, '{{EXT_NAME}}.h' => <<<'C' @@ -1029,10 +1097,6 @@ class_entry = zend_register_internal_class_ex(&ce, NULL); const counter = @import("classes/counter.zig"); -fn startup() !void { - counter.Class.register(); -} - comptime { phpz.function("hello", hello); phpz.function("greet", greet); @@ -1040,7 +1104,7 @@ class_entry = zend_register_internal_class_ex(&ce, NULL); phpz.module(.{ .name = "{{EXT_NAME}}", .version = "0.1.0", - .module_startup_fn = startup, + .classes = &.{counter.Class}, }); } @@ -1175,8 +1239,48 @@ public static function render( string $phpIncludeDir, bool $hasRunTests, bool $hasGenStub, + bool $unix, + bool $windows, ): string { - $contents = str_replace('{{PHPZ_TEST_STEP}}', $hasRunTests ? PHPZ_TEST_STEP_TEMPLATE : '', $contents); + $targetCheck = ''; + $extFilename = PHPZ_ALL_PLATFORM_FILENAME_TEMPLATE; + if (!$windows) { + $targetCheck = PHPZ_UNIX_TARGET_CHECK_TEMPLATE; + $extFilename = PHPZ_UNIX_FILENAME_TEMPLATE; + } elseif (!$unix) { + $targetCheck = PHPZ_WINDOWS_TARGET_CHECK_TEMPLATE; + $extFilename = PHPZ_WINDOWS_FILENAME_TEMPLATE; + } + + $contents = str_replace( + "{{PHPZ_PLATFORM_TARGET_CHECK}}\n", + $targetCheck === '' ? '' : $targetCheck . "\n", + $contents, + ); + $contents = str_replace( + "{{PHPZ_WINDOWS_BUILD_OPTIONS}}\n", + $windows ? PHPZ_WINDOWS_BUILD_OPTIONS_TEMPLATE . "\n" : '', + $contents, + ); + $contents = str_replace( + "{{PHPZ_WINDOWS_INIT_OPTIONS}}\n", + $windows ? PHPZ_WINDOWS_INIT_OPTIONS_TEMPLATE . "\n" : '', + $contents, + ); + $contents = str_replace("{{PHPZ_EXT_FILENAME}}\n", $extFilename . "\n", $contents); + $manualCommands = PHPZ_UNIX_MANUAL_COMMANDS_TEMPLATE; + if ($unix && $windows) { + $manualCommands = "# Unix\n" . PHPZ_UNIX_MANUAL_COMMANDS_TEMPLATE + . "\n\n# Windows\n" . PHPZ_WINDOWS_MANUAL_COMMANDS_TEMPLATE; + } elseif ($windows) { + $manualCommands = PHPZ_WINDOWS_MANUAL_COMMANDS_TEMPLATE; + } + $contents = str_replace('{{PHPZ_MANUAL_COMMANDS}}', $manualCommands, $contents); + $contents = str_replace( + "{{PHPZ_TEST_STEP}}\n", + $hasRunTests ? PHPZ_TEST_STEP_TEMPLATE . "\n" : '', + $contents, + ); $contents = str_replace('{{PHPZ_TEST_COMMAND}}', $hasRunTests ? 'zig build test' : 'zig build', $contents); $contents = str_replace('{{PHP_INCLUDE_DIR}}', TemplateVars::escapeZigString($phpIncludeDir), $contents); $contents = str_replace( @@ -1202,6 +1306,8 @@ public static function write( bool $force, bool $hasRunTests, bool $hasGenStub, + bool $unix, + bool $windows, ): array { $written = 0; $skippedTests = 0; @@ -1223,7 +1329,7 @@ public static function write( if ( file_put_contents( $destPath, - self::render($contents, $ext, $phpIncludeDir, $hasRunTests, $hasGenStub), + self::render($contents, $ext, $phpIncludeDir, $hasRunTests, $hasGenStub, $unix, $windows), ) === false ) { Console::fail("Unable to write $destPath"); @@ -1283,6 +1389,8 @@ public static function run(mixed $argv): void $args->force, $runTestsPath !== null, $genStubPath !== null, + $args->unix, + $args->windows, ); $templateResult = $templateCount . ' files'; if ($skippedTestTemplateCount > 0) { @@ -1356,6 +1464,8 @@ private static function usage(): never --with-run-tests [path] Use run-tests.php; without path, auto-detect via php-config or zig-pkg/phpz-*/examples/skeleton/run-tests.php. --without-run-tests Disable run-tests.php; omit tests/ and the build test step. + --onlyunix Only include Unix support in build.zig. + --onlywindows Only include Windows support in build.zig. --force Overwrite generated template files in an existing target. -v, --verbose Print successful command output. -h, --help Show this help. @@ -1414,6 +1524,9 @@ private static function printSummary( echo "Project:\n"; echo ' extension: ' . $args->ext . "\n"; echo " directory: $targetDir\n"; + echo ' platforms: ' . ($args->unix && $args->windows + ? 'Unix, Windows' + : ($args->unix ? 'Unix' : 'Windows')) . "\n"; echo " arginfo: $arginfoStatus\n"; echo " PHPT tests: $testsStatus\n"; echo "\nQuick start:\n"; From 4bb9131f227b8b91be73be26c7be93d730969137 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Fri, 10 Jul 2026 23:15:38 +0800 Subject: [PATCH 09/17] feat(windows): improve php windows setup and msvc overflow compatibility --- .github/scripts/setup-php-windows.ps1 | 414 ++++++++++++++++++++++++++ .github/workflows/test-windows.yml | 155 ++++++---- build.zig | 10 +- build/Phpz.zig | 8 +- build/phpz.h | 2 + build/phpz_compat.h | 24 ++ build/phpz_translate_c_compat.h | 79 +++-- examples/my_php_extension/build.zig | 6 +- examples/skeleton/build.zig | 2 + scripts/test-msvc.sh | 2 +- tools/phpz_skel.php | 2 + 11 files changed, 606 insertions(+), 98 deletions(-) create mode 100644 .github/scripts/setup-php-windows.ps1 create mode 100644 build/phpz_compat.h diff --git a/.github/scripts/setup-php-windows.ps1 b/.github/scripts/setup-php-windows.ps1 new file mode 100644 index 0000000..bd351ea --- /dev/null +++ b/.github/scripts/setup-php-windows.ps1 @@ -0,0 +1,414 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidatePattern('^\d+\.\d+$')] + [string] $PhpVersion, + + [Parameter(Mandatory = $true)] + [ValidateSet('x64', 'x86')] + [string] $Arch, + + [Parameter(Mandatory = $true)] + [ValidateSet('nts', 'ts')] + [string] $ThreadSafety, + + [Parameter(Mandatory = $true)] + [string] $Destination +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +$PSNativeCommandUseErrorActionPreference = $true + +trap { + Write-Host "setup-php-windows.ps1 failed at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)" + Write-Host $_.ScriptStackTrace + throw +} + +$vsVersion = switch ($PhpVersion) { + '8.2' { 'vs16' } + '8.3' { 'vs16' } + '8.4' { 'vs17' } + '8.5' { 'vs17' } + default { throw "Unsupported PHP version $PhpVersion" } +} + +# Keep toolset ranges and component IDs aligned with php-windows-builder. +$vsConfiguration = switch ($vsVersion) { + 'vs16' { + [PSCustomObject]@{ + ToolsetMajor = 14 + ToolsetMinorMinimum = 20 + ToolsetMinorMaximum = 29 + Components = @( + 'Microsoft.VisualStudio.Component.CoreBuildTools' + 'Microsoft.VisualStudio.ComponentGroup.VC.Tools.142.x86.x64' + 'Microsoft.VisualStudio.Component.VC.ATL' + 'Microsoft.VisualStudio.Component.Windows10SDK.19041' + ) + } + } + 'vs17' { + [PSCustomObject]@{ + ToolsetMajor = 14 + ToolsetMinorMinimum = 30 + ToolsetMinorMaximum = 49 + Components = @( + 'Microsoft.VisualStudio.Component.CoreBuildTools' + 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64' + 'Microsoft.VisualStudio.Component.VC.ATL' + 'Microsoft.VisualStudio.Component.Windows10SDK.19041' + ) + } + } +} + +$baseUrl = 'https://downloads.php.net/~windows/releases' +$releases = Invoke-RestMethod -Uri "$baseUrl/releases.json" +$releaseProperty = $releases.PSObject.Properties[$PhpVersion] +if ($null -eq $releaseProperty) { + throw "PHP $PhpVersion is missing from releases.json" +} + +$release = $releaseProperty.Value +$buildKey = "$ThreadSafety-$vsVersion-$Arch" +$buildProperty = $release.PSObject.Properties[$buildKey] +if ($null -eq $buildProperty) { + throw "PHP $PhpVersion has no $buildKey build in releases.json" +} + +$build = $buildProperty.Value +$destinationPath = [System.IO.Path]::GetFullPath($Destination) +$downloadDirectory = Join-Path $destinationPath 'downloads' +$phpBinDirectory = Join-Path $destinationPath 'php-bin' +$phpDevDirectory = Join-Path $destinationPath 'php-dev' + +if (Test-Path -LiteralPath $destinationPath) { + Remove-Item -LiteralPath $destinationPath -Recurse -Force +} +New-Item -Path $downloadDirectory -ItemType Directory -Force | Out-Null + +function Get-VsWherePath { + $installedPath = Join-Path "${env:ProgramFiles(x86)}\Microsoft Visual Studio" 'Installer\vswhere.exe' + if (Test-Path -LiteralPath $installedPath) { + return $installedPath + } + + $downloadPath = Join-Path $downloadDirectory 'vswhere.exe' + Write-Host 'Downloading vswhere.exe' + Invoke-WebRequest ` + -Uri 'https://github.com/microsoft/vswhere/releases/latest/download/vswhere.exe' ` + -OutFile $downloadPath ` + -MaximumRetryCount 3 ` + -RetryIntervalSec 2 + return $downloadPath +} + +function Get-VisualStudioInstance { + param( + [Parameter(Mandatory = $true)] + [string] $VsWherePath + ) + + $json = ((& $VsWherePath -latest -products '*' -format json 2>$null) -join [Environment]::NewLine) + if ([string]::IsNullOrWhiteSpace($json)) { + return $null + } + + return @($json | ConvertFrom-Json) | Select-Object -First 1 +} + +function Get-MsvcToolset { + param( + [Parameter(Mandatory = $true)] + [object] $VsInstance, + + [Parameter(Mandatory = $true)] + [object] $Configuration + ) + + $msvcDirectory = Join-Path ([string] $VsInstance.installationPath) 'VC\Tools\MSVC' + if (!(Test-Path -LiteralPath $msvcDirectory)) { + return $null + } + + $toolsets = @( + Get-ChildItem -LiteralPath $msvcDirectory -Directory | ForEach-Object { + $version = $null + if ( + [System.Version]::TryParse($_.Name, [ref] $version) -and + $version.Major -eq $Configuration.ToolsetMajor -and + $version.Minor -ge $Configuration.ToolsetMinorMinimum -and + $version.Minor -le $Configuration.ToolsetMinorMaximum + ) { + [PSCustomObject]@{ + Name = $_.Name + Path = $_.FullName + Version = $version + } + } + } | Sort-Object -Property Version -Descending + ) + + return $toolsets | Select-Object -First 1 +} + +function Install-VisualStudioComponents { + param( + [Parameter(Mandatory = $true)] + [string] $VsVersion, + + [AllowNull()] + [object] $VsInstance, + + [Parameter(Mandatory = $true)] + [string[]] $Components + ) + + $channel = $VsVersion.Substring(2) + $installerName = 'vs_buildtools.exe' + $installerArguments = @() + + if ($null -ne $VsInstance) { + $channel = ([string] $VsInstance.installationVersion).Split('.')[0] + $productId = $null + if ($VsInstance.PSObject.Properties['catalog']) { + $catalog = $VsInstance.catalog + if ($null -ne $catalog -and $catalog.PSObject.Properties['productId']) { + $productId = [string] $catalog.productId + } + } + if ($null -eq $productId -and $VsInstance.PSObject.Properties['productId']) { + $productId = [string] $VsInstance.productId + } + if ($productId -match '(Enterprise|Professional|Community)$') { + $installerName = "vs_$($Matches[1].ToLowerInvariant()).exe" + } + $installerArguments += @('modify', '--installPath', [string] $VsInstance.installationPath) + } + + $installerPath = Join-Path $downloadDirectory $installerName + $installerUrl = "https://aka.ms/vs/$channel/release/$installerName" + Write-Host "Downloading $installerUrl" + Invoke-WebRequest ` + -Uri $installerUrl ` + -OutFile $installerPath ` + -MaximumRetryCount 3 ` + -RetryIntervalSec 2 + + $installerArguments += @('--quiet', '--wait', '--norestart', '--nocache') + foreach ($component in $Components) { + $installerArguments += @('--add', $component) + } + + Write-Host "Installing Visual Studio components for $VsVersion" + $previousNativeErrorPreference = $PSNativeCommandUseErrorActionPreference + $installerExitCode = $null + try { + $PSNativeCommandUseErrorActionPreference = $false + & $installerPath @installerArguments 2>&1 | ForEach-Object { Write-Host $_ } + $installerExitCode = $LASTEXITCODE + } finally { + $PSNativeCommandUseErrorActionPreference = $previousNativeErrorPreference + } + + if ($installerExitCode -notin @(0, 3010)) { + throw "Visual Studio installer failed with exit code $installerExitCode" + } +} + +function Get-ZigLibCFields { + param( + [Parameter(Mandatory = $true)] + [string] $ZigExe + ) + + $fields = [ordered]@{} + foreach ($line in @(& $ZigExe libc)) { + if ($line -match '^([a-z0-9_]+)=(.*)$') { + $fields[$Matches[1]] = $Matches[2] + } + } + + foreach ($name in @('include_dir', 'sys_include_dir', 'crt_dir', 'msvc_lib_dir', 'kernel32_lib_dir', 'gcc_dir')) { + if (!$fields.Contains($name)) { + throw "zig libc did not provide $name" + } + } + + return $fields +} + +function Save-PhpAsset { + param( + [Parameter(Mandatory = $true)] + [object] $Asset + ) + + $assetName = [string] $Asset.path + $archivePath = Join-Path $downloadDirectory ([System.IO.Path]::GetFileName($assetName)) + $downloaded = $false + $lastError = $null + + foreach ($url in @("$baseUrl/$assetName", "$baseUrl/archives/$assetName")) { + try { + Write-Host "Downloading $url" + Invoke-WebRequest -Uri $url -OutFile $archivePath -MaximumRetryCount 3 -RetryIntervalSec 2 + $downloaded = $true + break + } catch { + $lastError = $_ + Remove-Item -LiteralPath $archivePath -Force -ErrorAction SilentlyContinue + } + } + + if (!$downloaded) { + throw "Failed to download $assetName`: $lastError" + } + + $expectedHash = ([string] $Asset.sha256).ToLowerInvariant() + $actualHash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actualHash -ne $expectedHash) { + throw "SHA256 mismatch for $assetName`: expected $expectedHash, got $actualHash" + } + + return $archivePath +} + +$runtimeArchive = Save-PhpAsset -Asset $build.zip +$develArchive = Save-PhpAsset -Asset $build.devel_pack + +Expand-Archive -LiteralPath $runtimeArchive -DestinationPath $phpBinDirectory -Force +Expand-Archive -LiteralPath $develArchive -DestinationPath $phpDevDirectory -Force + +$phpDevRoot = if ( + (Test-Path -LiteralPath (Join-Path $phpDevDirectory 'include')) -and + (Test-Path -LiteralPath (Join-Path $phpDevDirectory 'lib')) +) { + Get-Item -LiteralPath $phpDevDirectory +} else { + Get-ChildItem -LiteralPath $phpDevDirectory -Directory | + Where-Object { + (Test-Path -LiteralPath (Join-Path $_.FullName 'include')) -and + (Test-Path -LiteralPath (Join-Path $_.FullName 'lib')) + } | + Select-Object -First 1 +} + +if ($null -eq $phpDevRoot) { + throw "Unable to locate include and lib directories in $phpDevDirectory" +} + +$phpExe = Join-Path $phpBinDirectory 'php.exe' +$phpIncludeDirectory = Join-Path $phpDevRoot.FullName 'include' +$phpLibDirectory = Join-Path $phpDevRoot.FullName 'lib' +$phpImportLibrary = if ($ThreadSafety -eq 'ts') { 'php8ts.lib' } else { 'php8.lib' } + +if (!(Test-Path -LiteralPath $phpExe)) { + throw "Missing PHP executable $phpExe" +} +if (!(Test-Path -LiteralPath (Join-Path $phpIncludeDirectory 'main\php.h'))) { + throw "Missing PHP headers in $phpIncludeDirectory" +} +if (!(Test-Path -LiteralPath (Join-Path $phpLibDirectory $phpImportLibrary))) { + throw "Missing $phpImportLibrary in $phpLibDirectory" +} + +$actualVersion = ([string] (& $phpExe -n -r 'echo PHP_VERSION;')).Trim() +$actualThreadSafety = ([string] (& $phpExe -n -r 'echo PHP_ZTS ? "ts" : "nts";')).Trim() +$actualDebug = ([string] (& $phpExe -n -r 'echo PHP_DEBUG ? "1" : "0";')).Trim() +if ($actualVersion -ne [string] $release.version) { + throw "Expected PHP $($release.version), got $actualVersion" +} +if ($actualThreadSafety -ne $ThreadSafety) { + throw "Expected PHP $ThreadSafety, got $actualThreadSafety" +} +if ($actualDebug -ne '0') { + throw "Expected a release PHP build, got PHP_DEBUG=$actualDebug" +} + +$vsWherePath = Get-VsWherePath +$vsInstance = Get-VisualStudioInstance -VsWherePath $vsWherePath +$toolset = if ($null -ne $vsInstance) { + Get-MsvcToolset -VsInstance $vsInstance -Configuration $vsConfiguration +} else { + $null +} +if ($null -eq $toolset) { + Install-VisualStudioComponents ` + -VsVersion $vsVersion ` + -VsInstance $vsInstance ` + -Components $vsConfiguration.Components + $vsInstance = Get-VisualStudioInstance -VsWherePath $vsWherePath + if ($null -eq $vsInstance) { + throw 'Visual Studio installation is not available after installing components' + } + $toolset = Get-MsvcToolset -VsInstance $vsInstance -Configuration $vsConfiguration +} +if ($null -eq $toolset) { + throw "Unable to locate an MSVC $vsVersion toolset after installing components" +} + +$zigExe = (Get-Command zig -CommandType Application -ErrorAction Stop).Source +$libcTarget = if ($Arch -eq 'x64') { 'x86_64-windows-msvc' } else { 'x86-windows-msvc' } +$msvcArch = if ($Arch -eq 'x64') { 'x64' } else { 'x86' } +$libcFields = Get-ZigLibCFields -ZigExe $zigExe +if ($Arch -eq 'x86') { + $libcFields['crt_dir'] = Join-Path (Split-Path -Parent $libcFields['crt_dir']) 'x86' + $libcFields['kernel32_lib_dir'] = Join-Path (Split-Path -Parent $libcFields['kernel32_lib_dir']) 'x86' +} +$toolsetIncludeDirectory = Join-Path $toolset.Path 'include' +$toolsetLibDirectory = Join-Path (Join-Path $toolset.Path 'lib') $msvcArch +if (!(Test-Path -LiteralPath (Join-Path $toolsetIncludeDirectory 'vcruntime.h'))) { + throw "Missing vcruntime.h in $toolsetIncludeDirectory" +} +if (!(Test-Path -LiteralPath (Join-Path $toolsetLibDirectory 'vcruntime.lib'))) { + throw "Missing vcruntime.lib in $toolsetLibDirectory" +} +if (!(Test-Path -LiteralPath (Join-Path $libcFields['include_dir'] 'stdlib.h'))) { + throw "Missing stdlib.h in $($libcFields['include_dir'])" +} +if (!(Test-Path -LiteralPath (Join-Path $libcFields['crt_dir'] 'ucrt.lib'))) { + throw "Missing ucrt.lib in $($libcFields['crt_dir'])" +} +if (!(Test-Path -LiteralPath (Join-Path $libcFields['kernel32_lib_dir'] 'kernel32.lib'))) { + throw "Missing kernel32.lib in $($libcFields['kernel32_lib_dir'])" +} +$libcFields['sys_include_dir'] = $toolsetIncludeDirectory +$libcFields['msvc_lib_dir'] = $toolsetLibDirectory +$libcFile = Join-Path $destinationPath 'libc.txt' +@( + '# The directory that contains stdlib.h.' + "include_dir=$($libcFields['include_dir'])" + "sys_include_dir=$($libcFields['sys_include_dir'])" + "crt_dir=$($libcFields['crt_dir'])" + "msvc_lib_dir=$($libcFields['msvc_lib_dir'])" + "kernel32_lib_dir=$($libcFields['kernel32_lib_dir'])" + "gcc_dir=$($libcFields['gcc_dir'])" +) | Set-Content -LiteralPath $libcFile -Encoding utf8 +& $zigExe libc -target $libcTarget $libcFile | Out-Null + +$windowsZts = if ($ThreadSafety -eq 'ts') { 'true' } else { 'false' } +@( + "PHP_VERSION=$actualVersion" + "PHP_BIN_DIR=$phpBinDirectory" + "PHP_INCLUDE_DIR=$phpIncludeDirectory" + "PHP_LIB_DIR=$phpLibDirectory" + "PHP_THREAD_SAFETY=$actualThreadSafety" + "WINDOWS_ZTS=$windowsZts" + "PHP_VS_VERSION=$vsVersion" + "MSVC_TOOLSET=$($toolset.Name)" + "LIBC_TARGET=$libcTarget" + "LIBC_FILE=$libcFile" +) | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append +$phpBinDirectory | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + +Write-Host "PHP version: $actualVersion" +Write-Host "PHP mode: $actualThreadSafety" +Write-Host "PHP include: $phpIncludeDirectory" +Write-Host "PHP lib: $phpLibDirectory" +Write-Host "PHP Visual Studio: $vsVersion" +Write-Host "MSVC toolset: $($toolset.Name)" +Write-Host "Libc file: $libcFile" diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 270982f..a8650ec 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -1,30 +1,52 @@ name: Windows Test on: - # push: - # branches: ["dev"] - # paths: - # - "src/**" - # - "build.zig" - # - "build.zig.zon" - # - ".github/workflows/test-windows.yml" - # pull_request: - # branches: ["dev"] - # paths: - # - "src/**" - # - "build.zig" - # - "build.zig.zon" - # - ".github/workflows/test-windows.yml" + push: + branches: ["dev"] + paths: + - "src/**" + - "build/**" + - "examples/**" + - "tools/**" + - "build.zig" + - "build.zig.zon" + - ".github/scripts/setup-php-windows.ps1" + - ".github/workflows/test-windows.yml" + pull_request: + branches: ["dev"] + paths: + - "src/**" + - "build/**" + - "examples/**" + - "tools/**" + - "build.zig" + - "build.zig.zon" + - ".github/scripts/setup-php-windows.ps1" + - ".github/workflows/test-windows.yml" workflow_dispatch: jobs: + matrix: + name: Generate Windows test matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.generate.outputs.matrix }} + steps: + - name: Generate matrix + id: generate + uses: php/php-windows-builder/extension-matrix@v1 + with: + php-version-list: "8.2,8.3,8.4,8.5" + arch-list: "x64" + ts-list: "nts,ts" + test: - runs-on: windows-2025 + name: test (PHP ${{ matrix['php-version'] }}, ${{ matrix.arch }}, ${{ matrix.ts }}) + needs: matrix + runs-on: ${{ matrix.os }} strategy: fail-fast: false - matrix: - # php: ["8.2", "8.3", "8.4", "8.5"] - php: ["8.5"] + matrix: ${{ fromJSON(needs.matrix.outputs.matrix) }} steps: - name: Checkout uses: actions/checkout@v4 @@ -32,63 +54,64 @@ jobs: - name: Install pkg-config shell: pwsh run: | - # Strawberry Perl's pkg-config.BAT cannot be executed directly by - # Zig's ChildProcess. Install a native .exe to shadow it. choco install pkgconfiglite --no-progress -y - where pkg-config - - name: Setup PHP SDK with dev headers - id: setup-php-sdk - uses: php/setup-php-sdk@v0.12 - with: - version: ${{ matrix.php }} - arch: x64 - ts: nts - - name: Resolve PHP dev paths - id: php-paths - shell: bash + where.exe pkg-config + - name: Setup PHP runtime and development files + shell: pwsh run: | - # setup-php-sdk lays out: - # /php-bin -> PHP runtime (php.exe), already on PATH - # /php-dev -> dev pack with include/ and lib/ - DEV_DIR="$(cygpath -u "${{ github.workspace }}")/php-dev" - INCLUDE_DIR="$DEV_DIR/include" - LIB_DIR="$DEV_DIR/lib" - [ -d "$INCLUDE_DIR" ] || { echo "missing $INCLUDE_DIR"; ls -la "$DEV_DIR" || true; exit 1; } - [ -d "$LIB_DIR" ] || { echo "missing $LIB_DIR"; ls -la "$DEV_DIR" || true; exit 1; } - # Pass native (backslash) paths to zig - INCLUDE_WIN="$(cygpath -w "$INCLUDE_DIR")" - LIB_WIN="$(cygpath -w "$LIB_DIR")" - echo "include=$INCLUDE_WIN" >> "$GITHUB_OUTPUT" - echo "lib=$LIB_WIN" >> "$GITHUB_OUTPUT" - echo "PHP include: $INCLUDE_WIN" - echo "PHP lib: $LIB_WIN" + .\.github\scripts\setup-php-windows.ps1 ` + -PhpVersion "${{ matrix['php-version'] }}" ` + -Arch "${{ matrix.arch }}" ` + -ThreadSafety "${{ matrix.ts }}" ` + -Destination (Join-Path $env:RUNNER_TEMP "php-sdk") + - name: Verify PHP installation + shell: pwsh + run: | + php -n -v + php -n -r 'printf("PHP %s (%s, PHP_DEBUG=%d)\n", PHP_VERSION, PHP_ZTS ? "ZTS" : "NTS", PHP_DEBUG);' + Get-ChildItem -LiteralPath $env:PHP_LIB_DIR -Filter "php8*.lib" + if (!(Test-Path -LiteralPath $env:LIBC_FILE)) { + throw "Missing libc file $env:LIBC_FILE" + } + Get-Content -LiteralPath $env:LIBC_FILE + zig libc -target $env:LIBC_TARGET $env:LIBC_FILE + Write-Host "PHP Visual Studio: $env:PHP_VS_VERSION" + Write-Host "MSVC toolset: $env:MSVC_TOOLSET" - name: Run unit tests - shell: bash + shell: pwsh run: | - zig build test \ - -Dtarget=native-native-msvc \ - -Dphp-include-dir="${{ steps.php-paths.outputs.include }}" \ - -Dphp-lib-dir="${{ steps.php-paths.outputs.lib }}" + zig build test ` + -Dtarget=native-native-msvc ` + "-Dlibc-file=$env:LIBC_FILE" ` + "-Dphp-include-dir=$env:PHP_INCLUDE_DIR" ` + "-Dphp-lib-dir=$env:PHP_LIB_DIR" ` + "-Dwindows-zts=$env:WINDOWS_ZTS" - name: Run example tests - shell: bash + shell: pwsh run: | - zig build test-examples \ - -Dtarget=native-native-msvc \ - -Dphp-include-dir="${{ steps.php-paths.outputs.include }}" \ - -Dphp-lib-dir="${{ steps.php-paths.outputs.lib }}" + zig build test-examples ` + -Dtarget=native-native-msvc ` + "-Dlibc-file=$env:LIBC_FILE" ` + "-Dphp-include-dir=$env:PHP_INCLUDE_DIR" ` + "-Dphp-lib-dir=$env:PHP_LIB_DIR" ` + "-Dwindows-zts=$env:WINDOWS_ZTS" - name: Run example tests (ReleaseSafe) - shell: bash + shell: pwsh run: | - zig build test-examples \ - -Dtarget=native-native-msvc \ - -Dphp-include-dir="${{ steps.php-paths.outputs.include }}" \ - -Dphp-lib-dir="${{ steps.php-paths.outputs.lib }}" \ + zig build test-examples ` + -Dtarget=native-native-msvc ` + "-Dlibc-file=$env:LIBC_FILE" ` + "-Dphp-include-dir=$env:PHP_INCLUDE_DIR" ` + "-Dphp-lib-dir=$env:PHP_LIB_DIR" ` + "-Dwindows-zts=$env:WINDOWS_ZTS" ` -Doptimize=ReleaseSafe - name: Run example tests (ReleaseFast) - shell: bash + shell: pwsh run: | - zig build test-examples \ - -Dtarget=native-native-msvc \ - -Dphp-include-dir="${{ steps.php-paths.outputs.include }}" \ - -Dphp-lib-dir="${{ steps.php-paths.outputs.lib }}" \ + zig build test-examples ` + -Dtarget=native-native-msvc ` + "-Dlibc-file=$env:LIBC_FILE" ` + "-Dphp-include-dir=$env:PHP_INCLUDE_DIR" ` + "-Dphp-lib-dir=$env:PHP_LIB_DIR" ` + "-Dwindows-zts=$env:WINDOWS_ZTS" ` -Doptimize=ReleaseFast diff --git a/build.zig b/build.zig index 27c6dca..6266b50 100644 --- a/build.zig +++ b/build.zig @@ -20,7 +20,7 @@ pub fn build(b: *std.Build) void { const options = BuildOptions{ .php_include_dir = b.option([]const u8, "php-include-dir", "PHP include directory (main/, Zend/, TSRM/, ext/)") orelse "/usr/include/php", .php_lib_dir = b.option([]const u8, "php-lib-dir", "PHP SDK library directory (Windows only, contains php8*.lib)"), - .libc_file = b.option([]const u8, "libc", "Libc paths file for local cross-compilation tests"), + .libc_file = b.option([]const u8, "libc-file", "Libc paths file for C translation and extension compilation"), .windows_zts = b.option(bool, "windows-zts", "Windows only: link against the thread-safe PHP library") orelse false, .windows_debug = b.option(bool, "windows-debug", "Windows only: build against a debug PHP SDK") orelse false, .target = b.standardTargetOptions(.{}), @@ -40,8 +40,8 @@ fn createPhpzModule(b: *std.Build, options: BuildOptions) *std.Build.Module { .c_source_file = b.path("build/phpz.h"), .target = options.target, .optimize = options.optimize, - .libc_file = options.getLibCFilePath(), }, + .libc_file = options.getLibCFilePath(), .php_include_dir = .{ .cwd_relative = options.php_include_dir }, .php_lib_dir = if (options.php_lib_dir) |d| .{ .cwd_relative = d } else null, .windows_zts = options.windows_zts, @@ -110,9 +110,13 @@ fn addTestExamplesStep(b: *std.Build, options: BuildOptions) void { "my_php_extension", }; inline for (examples) |test_example| { - const test_cmd = b.addSystemCommand(&[_][]const u8{ b.graph.zig_exe, "build", "test" }); + const test_cmd = b.addSystemCommand(&[_][]const u8{ "zig", "build", "test" }); test_cmd.addArg(b.fmt("-Dphp-include-dir={s}", .{options.php_include_dir})); test_cmd.addArg(b.fmt("-Doptimize={s}", .{@tagName(options.optimize)})); + if (options.libc_file) |libc_file| { + const absolute_path = std.fs.path.resolve(b.allocator, &.{libc_file}) catch @panic("OOM"); + test_cmd.addArg(b.fmt("-Dlibc-file={s}", .{absolute_path})); + } if (!options.target.query.isNativeTriple()) { test_cmd.addArg(b.fmt("-Dtarget={s}", .{options.target.query.zigTriple(b.allocator) catch unreachable})); } diff --git a/build/Phpz.zig b/build/Phpz.zig index 5fe6f14..efd8d15 100644 --- a/build/Phpz.zig +++ b/build/Phpz.zig @@ -18,6 +18,9 @@ pub const Options = struct { /// Phpz forces `strict_flex_arrays` to `.@"1"`. translator: Translator.Options, + /// Libc paths file used by both C translation and extension compilation. + libc_file: ?Build.LazyPath = null, + /// Directory containing PHP header files (main/, Zend/, TSRM/, ext/). /// Linux: /usr/include/php /// macOS: /usr/local/php/include @@ -82,7 +85,6 @@ pub fn initInner(b: *Build, options: Options) Phpz { mod.addCMacro("PHP_WIN32", "1"); mod.addCMacro("WINDOWS", "1"); mod.addCMacro("WIN32", "1"); - mod.addCMacro("ENABLE_INTSAFE_SIGNED_FUNCTIONS", "1"); }, else => {}, } @@ -100,7 +102,10 @@ pub fn initInner(b: *Build, options: Options) Phpz { fn createPhpCTranslator(b: *Build, options: Options) Translator { const translate_c_dep = b.dependency("translate_c", .{}); var translator_options = options.translator; + + // Override translator options managed by Phpz. translator_options.strict_flex_arrays = .@"1"; + translator_options.libc_file = options.libc_file; var c: Translator = .init(translate_c_dep, translator_options); // phpz.h @@ -149,6 +154,7 @@ fn createPhpCTranslator(b: *Build, options: Options) Translator { pub fn addExtension(self: Phpz, b: *Build, options: Build.LibraryOptions) *Build.Step.Compile { options.root_module.addImport("phpz", self.mod); const lib = b.addLibrary(options); + lib.setLibCFile(self.options.libc_file); switch (self.options.translator.target.result.os.tag) { // macOS: allows undefined symbols to be resolved at runtime by PHP .macos => lib.linker_allow_shlib_undefined = true, diff --git a/build/phpz.h b/build/phpz.h index fb7c16b..de2edc9 100644 --- a/build/phpz.h +++ b/build/phpz.h @@ -4,6 +4,8 @@ // compatibility #ifdef PHPZ_TRANSLATE_C #include "phpz_translate_c_compat.h" +#else +#include "phpz_compat.h" #endif // core diff --git a/build/phpz_compat.h b/build/phpz_compat.h new file mode 100644 index 0000000..d831eb8 --- /dev/null +++ b/build/phpz_compat.h @@ -0,0 +1,24 @@ +#ifndef PHPZ_COMPAT_H +#define PHPZ_COMPAT_H + +#if defined(_MSC_VER) && defined(__clang__) && !defined(ZEND_PORTABILITY_H) +#include "main/php_version.h" + +#if PHP_VERSION_ID >= 80500 +/* Match PHP's Clang toolset by using compiler overflow builtins. */ +#define PHP_HAVE_BUILTIN_SADDL_OVERFLOW 1 +#define PHP_HAVE_BUILTIN_SADDLL_OVERFLOW 1 +#define PHP_HAVE_BUILTIN_SSUBL_OVERFLOW 1 +#define PHP_HAVE_BUILTIN_SSUBLL_OVERFLOW 1 +#define PHP_HAVE_BUILTIN_SMULL_OVERFLOW 1 +#define PHP_HAVE_BUILTIN_SMULLL_OVERFLOW 1 +#endif + +/* PHP's Windows headers disable parts of the MSVC path, including + * ZEND_FASTCALL in PHP <= 8.3 and , when __clang__ is defined. + * Parse zend_portability.h as MSVC to match PHP's ABI, then restore __clang__. */ +#undef __clang__ +#include "Zend/zend_portability.h" +#define __clang__ +#endif +#endif // PHPZ_COMPAT_H diff --git a/build/phpz_translate_c_compat.h b/build/phpz_translate_c_compat.h index 746ebcb..2017269 100644 --- a/build/phpz_translate_c_compat.h +++ b/build/phpz_translate_c_compat.h @@ -3,39 +3,72 @@ // Workaround for windows.h: https://codeberg.org/ziglang/translate-c/issues/307 #if defined(PHP_WIN32) || defined(_WIN32) || defined(WIN32) -# ifndef WIN32_LEAN_AND_MEAN -# define WIN32_LEAN_AND_MEAN -# endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + +#ifndef NOMINMAX +#define NOMINMAX +#endif -# ifndef NOMINMAX -# define NOMINMAX -# endif +#ifndef DECLSPEC_NOINITALL +#define DECLSPEC_NOINITALL +#endif -# ifndef DECLSPEC_NOINITALL -# define DECLSPEC_NOINITALL -# endif +#ifndef __assume +#define __assume(expr) ((void)0) +#endif + +#include +#ifdef _CLASS_ARG +#undef _CLASS_ARG +#define _CLASS_ARG(_Val) (sizeof((_Val) + (float)0) == sizeof(float) ? 'f' : sizeof((_Val) + (double)0) == sizeof(double) ? 'd' : 'l') +#endif +#endif -# ifndef ENABLE_INTSAFE_SIGNED_FUNCTIONS -# define ENABLE_INTSAFE_SIGNED_FUNCTIONS -# endif +#if defined(PHP_WIN32) +#include "main/php_version.h" -# ifndef __assume -# define __assume(expr) ((void)0) -# endif +#if PHP_VERSION_ID >= 80500 +/* Match PHP's Clang toolset by using compiler overflow builtins. translate-c + * currently lowers only their generic forms, so map the typed variants. */ +#define PHP_HAVE_BUILTIN_SADDL_OVERFLOW 1 +#define PHP_HAVE_BUILTIN_SADDLL_OVERFLOW 1 +#define PHP_HAVE_BUILTIN_SSUBL_OVERFLOW 1 +#define PHP_HAVE_BUILTIN_SSUBLL_OVERFLOW 1 +#define PHP_HAVE_BUILTIN_SMULL_OVERFLOW 1 +#define PHP_HAVE_BUILTIN_SMULLL_OVERFLOW 1 +#define __builtin_saddl_overflow __builtin_add_overflow +#define __builtin_saddll_overflow __builtin_add_overflow +#define __builtin_ssubl_overflow __builtin_sub_overflow +#define __builtin_ssubll_overflow __builtin_sub_overflow +#define __builtin_smull_overflow __builtin_mul_overflow +#define __builtin_smulll_overflow __builtin_mul_overflow +#endif -# include -# ifdef _CLASS_ARG -# undef _CLASS_ARG -# define _CLASS_ARG(_Val) (sizeof((_Val) + (float)0) == sizeof(float) ? 'f' : sizeof((_Val) + (double)0) == sizeof(double) ? 'd' : 'l') -# endif +#if PHP_VERSION_ID >= 80400 && PHP_VERSION_ID < 80500 \ + && defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L +/* PHP 8.4 assumes C11 stddef.h provides max_align_t, but Windows UCRT does not. + * Match PHP 8.5 by making zend_max_align_t use the Windows fallback union. */ +#define max_align_t union { \ + char c; short s; int i; long l; long long ll; \ + float f; double d; long double ld; void *p; void (*fun)(void); \ +} +#endif #endif +#if defined(_MSC_VER) && defined(__clang__) +#undef __clang__ +#include "Zend/zend_portability.h" +#define __clang__ +#else +#include "Zend/zend_portability.h" +#endif // FIXME: regression in translate_c -#include "Zend/zend_portability.h" #ifdef ZEND_ATTRIBUTE_COLD_LABEL -# undef ZEND_ATTRIBUTE_COLD_LABEL -# define ZEND_ATTRIBUTE_COLD_LABEL +#undef ZEND_ATTRIBUTE_COLD_LABEL +#define ZEND_ATTRIBUTE_COLD_LABEL #endif #endif // PHPZ_TRANSLATE_C_COMPAT_H diff --git a/examples/my_php_extension/build.zig b/examples/my_php_extension/build.zig index af6fd99..201fa35 100644 --- a/examples/my_php_extension/build.zig +++ b/examples/my_php_extension/build.zig @@ -12,7 +12,7 @@ pub fn build(b: *std.Build) void { const php_include_dir = b.option([]const u8, "php-include-dir", "PHP include directory (main/, Zend/, TSRM/, ext/)") orelse "/usr/include/php"; // Windows only: PHP SDK lib directory containing php8*.lib const php_lib_dir = b.option([]const u8, "php-lib-dir", "PHP SDK library directory (Windows only, contains php8*.lib)"); - const libc_file = b.option([]const u8, "libc", "Libc paths file for cross-compilation"); + const libc_file = b.option([]const u8, "libc-file", "Libc paths file for C translation and extension compilation"); const libc_file_path: ?std.Build.LazyPath = if (libc_file) |path| .{ .cwd_relative = path } else null; const windows_zts = b.option(bool, "windows-zts", "Windows only: link against the thread-safe PHP library") orelse false; const windows_debug = b.option(bool, "windows-debug", "Windows only: build against a debug PHP SDK") orelse false; @@ -25,8 +25,8 @@ pub fn build(b: *std.Build) void { .c_source_file = b.path("my_php_extension.h"), .target = target, .optimize = optimize, - .libc_file = libc_file_path, }, + .libc_file = libc_file_path, .php_include_dir = .{ .cwd_relative = php_include_dir }, .php_lib_dir = if (php_lib_dir) |d| .{ .cwd_relative = d } else null, .windows_zts = windows_zts, @@ -44,8 +44,6 @@ pub fn build(b: *std.Build) void { }), .linkage = .dynamic, }); - php_ext_lib.setLibCFile(libc_file_path); - // PHP expects extensions at a specific naming convention const php_ext_filename = if (target.result.os.tag == .windows) "php_" ++ php_ext_name ++ ".dll" diff --git a/examples/skeleton/build.zig b/examples/skeleton/build.zig index 2afbff5..2c92c2a 100644 --- a/examples/skeleton/build.zig +++ b/examples/skeleton/build.zig @@ -8,6 +8,7 @@ pub fn build(b: *std.Build) void { const php_include_dir = b.option([]const u8, "php-include-dir", "PHP include directory (main/, Zend/, TSRM/, ext/)") orelse "/usr/include/php"; const php_lib_dir = b.option([]const u8, "php-lib-dir", "PHP SDK library directory (Windows only, contains php8*.lib)"); + const libc_file = b.option([]const u8, "libc-file", "Libc paths file for C translation and extension compilation"); const windows_zts = b.option(bool, "windows-zts", "Windows only: link against the thread-safe PHP library") orelse false; const windows_debug = b.option(bool, "windows-debug", "Windows only: build against a debug PHP SDK") orelse false; @@ -18,6 +19,7 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }, + .libc_file = if (libc_file) |path| .{ .cwd_relative = path } else null, .php_include_dir = .{ .cwd_relative = php_include_dir }, .php_lib_dir = if (php_lib_dir) |d| .{ .cwd_relative = d } else null, .windows_zts = windows_zts, diff --git a/scripts/test-msvc.sh b/scripts/test-msvc.sh index d7d4e24..3eb3049 100755 --- a/scripts/test-msvc.sh +++ b/scripts/test-msvc.sh @@ -108,7 +108,7 @@ gcc_dir= EOF set -- -Dtarget=native-windows-msvc \ - -Dlibc="$libc_file" \ + -Dlibc-file="$libc_file" \ -Dphp-include-dir="$php_sdk/include" \ -Dphp-lib-dir="$php_sdk/lib" diff --git a/tools/phpz_skel.php b/tools/phpz_skel.php index 47b5df3..521780f 100755 --- a/tools/phpz_skel.php +++ b/tools/phpz_skel.php @@ -938,6 +938,7 @@ public static function runGenStub( {{PHPZ_PLATFORM_TARGET_CHECK}} const php_include_dir = b.option([]const u8, "php-include-dir", "PHP include dir") orelse "{{PHP_INCLUDE_DIR}}"; + const libc_file = b.option([]const u8, "libc-file", "Libc paths file for C translation and extension compilation"); {{PHPZ_WINDOWS_BUILD_OPTIONS}} const phpz_dep = b.dependency("phpz", .{}); @@ -947,6 +948,7 @@ public static function runGenStub( .target = target, .optimize = optimize, }, + .libc_file = if (libc_file) |path| .{ .cwd_relative = path } else null, .php_include_dir = .{ .cwd_relative = php_include_dir }, {{PHPZ_WINDOWS_INIT_OPTIONS}} }); From d3ce2cfb53f40d074b446f939288504133776550 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Fri, 10 Jul 2026 23:30:38 +0800 Subject: [PATCH 10/17] chore(ci): remove pull_request trigger from docs workflow --- .github/workflows/docs.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3d928db..d31b2ef 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,13 +8,6 @@ on: - "build.zig" - "build.zig.zon" - ".github/workflows/docs.yml" - pull_request: - branches: ["dev"] - paths: - - "src/**" - - "build.zig" - - "build.zig.zon" - - ".github/workflows/docs.yml" workflow_dispatch: permissions: From e50d4ebc2d297640c3ea18576ae46db6b70b854c Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Sun, 12 Jul 2026 22:13:26 +0800 Subject: [PATCH 11/17] feat(tools): add arginfo validation and improve Windows patcher --- build.zig | 19 +++++++- build.zig.zon | 1 + build/Phpz.zig | 19 ++++++++ tools/check_arginfo.zig | 98 +++++++++++++++++++++++++++++++++++++++ tools/windows_patcher.zig | 47 ++++++++++++------- 5 files changed, 164 insertions(+), 20 deletions(-) create mode 100644 tools/check_arginfo.zig diff --git a/build.zig b/build.zig index 6266b50..3c1d855 100644 --- a/build.zig +++ b/build.zig @@ -19,10 +19,10 @@ const BuildOptions = struct { pub fn build(b: *std.Build) void { const options = BuildOptions{ .php_include_dir = b.option([]const u8, "php-include-dir", "PHP include directory (main/, Zend/, TSRM/, ext/)") orelse "/usr/include/php", - .php_lib_dir = b.option([]const u8, "php-lib-dir", "PHP SDK library directory (Windows only, contains php8*.lib)"), - .libc_file = b.option([]const u8, "libc-file", "Libc paths file for C translation and extension compilation"), + .php_lib_dir = b.option([]const u8, "php-lib-dir", "Windows only: required PHP SDK library directory containing php8*.lib"), .windows_zts = b.option(bool, "windows-zts", "Windows only: link against the thread-safe PHP library") orelse false, .windows_debug = b.option(bool, "windows-debug", "Windows only: build against a debug PHP SDK") orelse false, + .libc_file = b.option([]const u8, "libc-file", "Libc paths file for C translation and extension compilation"), .target = b.standardTargetOptions(.{}), .optimize = b.standardOptimizeOption(.{}), }; @@ -101,6 +101,21 @@ fn addTestStep(b: *std.Build, mod: *std.Build.Module, options: BuildOptions) voi test_lib.setLibCFile(options.getLibCFilePath()); const step = b.step("test", "Run unit tests"); step.dependOn(&test_lib.step); + + for ([_][]const u8{ + "tools/check_arginfo.zig", + "tools/windows_patcher.zig", + }) |path| { + const tool_tests = b.addTest(.{ + .name = b.fmt("{s}-tests", .{std.fs.path.stem(path)}), + .root_module = b.createModule(.{ + .root_source_file = b.path(path), + .target = b.graph.host, + .optimize = options.optimize, + }), + }); + step.dependOn(&b.addRunArtifact(tool_tests).step); + } } fn addTestExamplesStep(b: *std.Build, options: BuildOptions) void { diff --git a/build.zig.zon b/build.zig.zon index 5584239..5d8454c 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -3,6 +3,7 @@ .version = "0.0.0", .fingerprint = 0x935004cc3861700f, .minimum_zig_version = "0.17.0-dev.1282+c0f9b51d8", + .minimum_php_version = "8.2", .dependencies = .{ .translate_c = .{ .url = "git+https://codeberg.org/happystraw/translate-c?ref=vectorcall#6d456ba6a90d3a63b56b85a6ad7c295d5707def3", diff --git a/build/Phpz.zig b/build/Phpz.zig index efd8d15..316ce3a 100644 --- a/build/Phpz.zig +++ b/build/Phpz.zig @@ -160,9 +160,28 @@ pub fn addExtension(self: Phpz, b: *Build, options: Build.LibraryOptions) *Build .macos => lib.linker_allow_shlib_undefined = true, else => {}, } + if (b.option(bool, "check-arginfo", "Check that the generated arginfo header matches the extension stub") orelse true) { + lib.step.dependOn(addArginfoCheckStep(self, b, options.name)); + } return lib; } +fn addArginfoCheckStep(self: Phpz, b: *Build, extension_name: []const u8) *Build.Step { + const checker = b.addExecutable(.{ + .name = "check-arginfo", + .root_module = b.createModule(.{ + .root_source_file = self.mod.owner.path("tools/check_arginfo.zig"), + .target = b.graph.host, + .optimize = .ReleaseSafe, + }), + }); + const run = b.addRunArtifact(checker); + run.setName(b.fmt("check {s} arginfo", .{extension_name})); + run.addFileArg(b.path(b.fmt("{s}.stub.php", .{extension_name}))); + run.addFileArg(b.path(b.fmt("{s}_arginfo.h", .{extension_name}))); + return &run.step; +} + fn patchWindowsBindings(b: *Build, translate_c: *Translator, options: Translator.Options) void { const patcher = b.addExecutable(.{ .name = "windows-patcher", diff --git a/tools/check_arginfo.zig b/tools/check_arginfo.zig new file mode 100644 index 0000000..f4470f8 --- /dev/null +++ b/tools/check_arginfo.zig @@ -0,0 +1,98 @@ +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + const arena = init.arena.allocator(); + + var args = init.minimal.args.iterate(); + + const cmd = args.next() orelse "check-arginfo"; + const stub_path = args.next() orelse fatal("usage: {s} ", .{cmd}); + const arginfo_path = args.next() orelse fatal("usage: {s} ", .{cmd}); + if (args.next() != null) { + fatal("usage: {s} ", .{cmd}); + } + + const cwd = std.Io.Dir.cwd(); + const stub = cwd.readFileAllocOptions(init.io, stub_path, arena, .unlimited, .of(u8), 0) catch |err| { + fatal("unable to read {s}: {s}", .{ stub_path, @errorName(err) }); + }; + const arginfo = cwd.readFileAllocOptions(init.io, arginfo_path, arena, .unlimited, .of(u8), 0) catch |err| { + fatal("unable to read {s}: {s}", .{ arginfo_path, @errorName(err) }); + }; + + const expected = computeStubHash(stub); + const actual = parseArginfoHash(arginfo) catch |err| switch (err) { + error.MissingStubHash => fatal("{s} has no gen_stub.php hash", .{arginfo_path}), + error.InvalidStubHash => fatal("{s} has an invalid gen_stub.php hash", .{arginfo_path}), + }; + if (!std.mem.eql(u8, &expected, actual)) { + fatal( + "{s} does not match {s}\nexpected Stub hash: {s}\nfound Stub hash: {s}\nregenerate it with: php /path/to/php-src/build/gen_stub.php {s}\nor disable this check with -Dcheck-arginfo=false", + .{ arginfo_path, stub_path, &expected, actual, stub_path }, + ); + } +} + +const stub_hash_length = std.crypto.hash.Sha1.digest_length * 2; + +fn computeStubHash(stub: []const u8) [stub_hash_length]u8 { + var sha1 = std.crypto.hash.Sha1.init(.{}); + var offset: usize = 0; + while (std.mem.indexOfPos(u8, stub, offset, "\r\n")) |index| { + sha1.update(stub[offset..index]); + sha1.update("\n"); + offset = index + 2; + } + sha1.update(stub[offset..]); + + var digest: [std.crypto.hash.Sha1.digest_length]u8 = undefined; + sha1.final(&digest); + return std.fmt.bytesToHex(digest, .lower); +} + +const ParseArginfoHashError = error{ + MissingStubHash, + InvalidStubHash, +}; + +fn parseArginfoHash(arginfo: []const u8) ParseArginfoHashError![]const u8 { + const marker = "* Stub hash: "; + const marker_index = std.mem.indexOf(u8, arginfo, marker) orelse return error.MissingStubHash; + const hash_index = marker_index + marker.len; + if (hash_index + stub_hash_length + " */".len > arginfo.len or + !std.mem.eql(u8, arginfo[hash_index + stub_hash_length ..][0.." */".len], " */")) + { + return error.InvalidStubHash; + } + return arginfo[hash_index..][0..stub_hash_length]; +} + +fn fatal(comptime format: []const u8, args: anytype) noreturn { + std.log.err(format, args); + std.process.exit(1); +} + +test "computeStubHash computes the normalized SHA-1 hash" { + const actual = computeStubHash("hello\n"); + try std.testing.expectEqualStrings("f572d396fae9206628714fb2ce00f72e94f2258f", actual[0..]); +} + +test "computeStubHash normalizes CRLF line endings" { + const lf = computeStubHash("first\nsecond\n"); + const crlf = computeStubHash("first\r\nsecond\r\n"); + try std.testing.expectEqual(lf, crlf); +} + +test "parseArginfoHash extracts the generated stub hash" { + const expected = "d84374eb3cc93ebb1e1cf6f0dfa2ee49381bec07"; + const arginfo = "/* Stub hash: " ++ expected ++ " */"; + try std.testing.expectEqualStrings(expected, try parseArginfoHash(arginfo)); +} + +test "parseArginfoHash rejects a missing stub hash" { + try std.testing.expectError(error.MissingStubHash, parseArginfoHash("/* generated */")); +} + +test "parseArginfoHash rejects a malformed stub hash" { + try std.testing.expectError(error.InvalidStubHash, parseArginfoHash("/* Stub hash: short */")); +} diff --git a/tools/windows_patcher.zig b/tools/windows_patcher.zig index 5872a9f..3d1e0c6 100644 --- a/tools/windows_patcher.zig +++ b/tools/windows_patcher.zig @@ -5,10 +5,20 @@ const default_dllimport_data_symbols = [_][]const u8{ "zend_empty_string", "zend_one_char_string", "zend_string_init_interned", + + // NTS "executor_globals", "compiler_globals", "core_globals", "sapi_globals", + "file_globals", + + // ZTS + "executor_globals_id", + "compiler_globals_id", + "core_globals_id", + "sapi_globals_id", + "file_globals_id", "executor_globals_offset", "compiler_globals_offset", "core_globals_offset", @@ -21,10 +31,9 @@ const default_dllimport_data_prefixes = [_][]const u8{ }; pub fn main(init: std.process.Init) !void { - const gpa = init.gpa; + const arena = init.arena.allocator(); - var args = try std.process.Args.Iterator.initAllocator(init.minimal.args, gpa); - defer args.deinit(); + var args = init.minimal.args.iterate(); const cmd = args.next() orelse "windows-patcher"; const input_path = args.next() orelse fatal("usage: {s} ", .{cmd}); @@ -34,13 +43,11 @@ pub fn main(init: std.process.Init) !void { } const cwd = std.Io.Dir.cwd(); - const source = try cwd.readFileAllocOptions(init.io, input_path, gpa, .unlimited, .of(u8), 0); - defer gpa.free(source); + const source = try cwd.readFileAllocOptions(init.io, input_path, arena, .unlimited, .of(u8), 0); var result = std.ArrayList(u8).empty; - defer result.deinit(gpa); - try patch(gpa, &result, source, .{ + try patch(arena, &result, source, .{ .dllimport_data_symbols = &default_dllimport_data_symbols, .dllimport_data_prefixes = &default_dllimport_data_prefixes, }); @@ -108,12 +115,8 @@ const Patcher = struct { } fn patch(self: *Patcher, options: Options) !bool { - var changed = false; - try self.collectDllImportDataSymbols(options); - changed = try self.patchDllImportDataReferences() or changed; - - return changed; + return try self.patchDllImportDataReferences(); } fn collectDllImportDataSymbols(self: *Patcher, options: Options) !void { @@ -129,6 +132,10 @@ const Patcher = struct { const name = self.tree.tokenSlice(name_token); if (!isDllImportDataSymbolName(name, options)) continue; + // pub extern var executor_globals: zend_executor_globals; + // - name: executor_globals + // - type_expr: zend_executor_globals + // - is_const: false const type_node = var_decl.ast.type_node.unwrap() orelse continue; try self.dllimport_data_symbols.put(self.gpa, name, .{ .name = name, @@ -141,10 +148,10 @@ const Patcher = struct { fn patchDllImportDataReferences(self: *Patcher) !bool { if (self.dllimport_data_symbols.count() == 0) return false; - var body_ranges = std.ArrayList(TokenRange).empty; - defer body_ranges.deinit(self.gpa); - try self.collectFunctionBodyRanges(&body_ranges); - if (body_ranges.items.len == 0) return false; + var fn_body_ranges = std.ArrayList(TokenRange).empty; + defer fn_body_ranges.deinit(self.gpa); + try self.collectFunctionBodyRanges(&fn_body_ranges); + if (fn_body_ranges.items.len == 0) return false; var addressed_identifiers: std.AutoHashMapUnmanaged(std.zig.Ast.Node.Index, void) = .empty; defer addressed_identifiers.deinit(self.gpa); @@ -153,11 +160,12 @@ const Patcher = struct { for (0..self.tree.nodes.len) |node_i| { const node: std.zig.Ast.Node.Index = @enumFromInt(node_i); if (self.tree.nodeTag(node) != .address_of) continue; - if (!self.nodeInTokenRanges(node, body_ranges.items)) continue; + if (!self.nodeInTokenRanges(node, fn_body_ranges.items)) continue; const child = self.tree.nodeData(node).node; const symbol = self.identifierDllImportDataSymbol(child) orelse continue; + // &executor_globals -> @extern(*zend_executor_globals, ...) const replacement = try self.dllImportPointerExpr(symbol); try self.fixups.replace_nodes_with_string.put(self.gpa, node, replacement); try addressed_identifiers.put(self.gpa, child, {}); @@ -167,10 +175,13 @@ const Patcher = struct { for (0..self.tree.nodes.len) |node_i| { const node: std.zig.Ast.Node.Index = @enumFromInt(node_i); if (self.tree.nodeTag(node) != .identifier) continue; + // Its parent &identifier node already has a replacement; avoid overlapping fixups. if (addressed_identifiers.contains(node)) continue; - if (!self.nodeInTokenRanges(node, body_ranges.items)) continue; + if (!self.nodeInTokenRanges(node, fn_body_ranges.items)) continue; const symbol = self.identifierDllImportDataSymbol(node) orelse continue; + + // executor_globals -> (@extern(*zend_executor_globals, ...).*) const replacement = try self.dllImportValueExpr(symbol); try self.fixups.replace_nodes_with_string.put(self.gpa, node, replacement); changed = true; From d6d8c385c8302fa5ce20e01905cf209b98deaf2a Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Sun, 12 Jul 2026 22:13:27 +0800 Subject: [PATCH 12/17] feat(zend): add file globals and hash table accessors --- build/phpz.h | 1 + build/phpz_wrapper.h | 18 +++++++++++++++++- src/globals.zig | 4 ++++ src/zend/array.zig | 8 ++++---- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/build/phpz.h b/build/phpz.h index de2edc9..124ba9f 100644 --- a/build/phpz.h +++ b/build/phpz.h @@ -13,6 +13,7 @@ #include "Zend/zend_API.h" #include "Zend/zend_exceptions.h" #include "Zend/zend_enum.h" +#include "ext/standard/file.h" #include "ext/standard/info.h" #include "main/SAPI.h" diff --git a/build/phpz_wrapper.h b/build/phpz_wrapper.h index 741d7a3..913cf9e 100644 --- a/build/phpz_wrapper.h +++ b/build/phpz_wrapper.h @@ -53,6 +53,18 @@ static zend_always_inline sapi_globals_struct *phpz_sapi_globals(void) { #endif } +static zend_always_inline php_file_globals *phpz_file_globals(void) { +#ifdef ZTS +#ifdef ZEND_ENABLE_STATIC_TSRMLS_CACHE + return TSRMG_BULK_STATIC(file_globals_id, php_file_globals *); +#else + return TSRMG_BULK(file_globals_id, php_file_globals *); +#endif +#else + return &file_globals; +#endif +} + typedef void (*phpz_zend_try_callback)(void *); bool phpz_zend_try_catch(phpz_zend_try_callback callback, void *ctx); @@ -66,11 +78,15 @@ static zend_always_inline void phpz_zval_zval(zval *z, zval *src, bool copy, boo ZVAL_ZVAL(z, src, copy, dtor_src); } -/* Bridge helpers for zend_class_entry anonymous unions. +/* Bridge helpers for anonymous unions. * translate-c numbers unnamed unions (unnamed_0, unnamed_1, ...), * which break when PHP headers change union layout. These inline * wrappers give translate-c stable symbol names to bind against. */ +static zend_always_inline zval *phpz_hash_table_get_ar_packed(HashTable *ht) { + return ht->arPacked; +} + static zend_always_inline void phpz_class_entry_set_create_object(zend_class_entry *ce, zend_object* (*handler)(zend_class_entry *)) { ce->create_object = handler; } diff --git a/src/globals.zig b/src/globals.zig index db61356..38b5ddf 100644 --- a/src/globals.zig +++ b/src/globals.zig @@ -94,6 +94,10 @@ pub inline fn sapi() *c.sapi_globals_struct { return @ptrCast(c.phpz_sapi_globals()); } +pub inline fn file() *c.php_file_globals { + return @ptrCast(c.phpz_file_globals()); +} + test { std.testing.refAllDecls(@This()); std.testing.refAllDecls(@This().class); diff --git a/src/zend/array.zig b/src/zend/array.zig index eb70ef2..f38907b 100644 --- a/src/zend/array.zig +++ b/src/zend/array.zig @@ -274,7 +274,7 @@ pub const Array = opaque { const ht = self.ptr(); const count = ht.nNumUsed; const el_size: usize = c.ZEND_HASH_ELEMENT_SIZE(ht); - var el: [*]u8 = @ptrCast(ht.unnamed_0.arPacked); + var el: [*]u8 = @ptrCast(c.phpz_hash_table_get_ar_packed(ht)); var i: u32 = 0; while (i < count) : (i += 1) { @@ -323,7 +323,7 @@ pub const Array = opaque { const count = ht.nNumUsed; const is_packed = (ht.u.flags & c.HASH_FLAG_PACKED) != 0; const el_size: usize = c.ZEND_HASH_ELEMENT_SIZE(ht); - var el: [*]u8 = @ptrCast(ht.unnamed_0.arPacked); + var el: [*]u8 = @ptrCast(c.phpz_hash_table_get_ar_packed(ht)); if (is_packed) { // Packed @@ -574,7 +574,7 @@ pub const Array = opaque { pub fn init(array: *Array) FastValueIterator { const ht = array.ptr(); return .{ - .el = @ptrCast(ht.unnamed_0.arPacked), + .el = @ptrCast(c.phpz_hash_table_get_ar_packed(ht)), .el_size = c.ZEND_HASH_ELEMENT_SIZE(ht), .pos = 0, .count = ht.nNumUsed, @@ -640,7 +640,7 @@ pub const Array = opaque { pub fn init(array: *Array) FastIterator { const ht = array.ptr(); return .{ - .el = @ptrCast(ht.unnamed_0.arPacked), + .el = @ptrCast(c.phpz_hash_table_get_ar_packed(ht)), .el_size = c.ZEND_HASH_ELEMENT_SIZE(ht), .pos = 0, .count = ht.nNumUsed, From 30802020c6ff096c0ba0e33877e1c4660aef7c50 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Sun, 12 Jul 2026 22:13:28 +0800 Subject: [PATCH 13/17] refactor: clarify call and module helpers --- src/Ctx.zig | 26 ++++++++++++-------------- src/module.zig | 10 +++++++--- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/Ctx.zig b/src/Ctx.zig index d800bf7..28c097b 100644 --- a/src/Ctx.zig +++ b/src/Ctx.zig @@ -75,7 +75,7 @@ pub const Call = opaque { } /// Get all arguments as a slice of zvals. - pub inline fn args(self: *Call) []c.zval { + pub fn args(self: *Call) []c.zval { const count = self.argCount(); const base: [*]c.zval = @ptrCast(self.ptr()); return base[c.ZEND_CALL_FRAME_SLOT..][0..count]; @@ -83,7 +83,7 @@ pub const Call = opaque { /// Validate the total number of arguments against expected min/max. /// Call once at the top of each function, before accessing individual args. - pub inline fn expectArgCount(self: *Call, min: u32, max: u32) errors.WrongParameterCountError!void { + pub fn expectArgCount(self: *Call, min: u32, max: u32) errors.WrongParameterCountError!void { const count = self.argCount(); if (count < min or count > max) { return errors.wrongParameterCount(min, max); @@ -91,7 +91,7 @@ pub const Call = opaque { } /// Expect zero arguments. Calls zend_wrong_parameters_none_error on failure. - pub inline fn expectNoArgs(self: *Call) errors.WrongParameterCountError!void { + pub fn expectNoArgs(self: *Call) errors.WrongParameterCountError!void { if (self.argCount() != 0) { return errors.wrongParametersNone(); } @@ -163,12 +163,13 @@ pub const Call = opaque { /// .string => |name| _ = name, /// } /// ``` - pub inline fn expectArgs( + pub fn expectArgs( self: *Call, comptime specs: []const ExpectArgKind.Spec, runtime: ExpectArgsRuntime(specs), ) ExpectArgsError!ExpectArgResults(specs) { - comptime { + const min = comptime min: { + var count: u32 = 0; var seen_optional = false; for (specs, 0..) |spec, i| { if (seen_optional and !spec.isOptional()) { @@ -177,17 +178,14 @@ pub const Call = opaque { .{ i + 1, @tagName(spec) }, )); } - if (spec.isOptional()) seen_optional = true; - } - } - - const min = comptime min: { - var count: u32 = 0; - for (specs) |spec| { - if (!spec.isOptional()) count += 1; + if (spec.isOptional()) + seen_optional = true + else + count += 1; } break :min count; }; + const max: u32 = @intCast(specs.len); try self.expectArgCount(min, max); @@ -383,7 +381,7 @@ pub const Call = opaque { /// - scalar types → `{}` /// /// Prefer `expectArgs` for multi-arg cases. - pub inline fn expectArg( + pub fn expectArg( self: *Call, n: u32, comptime spec: ExpectArgKind.Spec, diff --git a/src/module.zig b/src/module.zig index aa3e4dc..aacdbdb 100644 --- a/src/module.zig +++ b/src/module.zig @@ -93,7 +93,9 @@ pub fn module(comptime cfg: Config) void { fn makePhpHookFn(comptime hook_fn: PhpHookFn) *const fn (c_int, c_int) callconv(.c) c.zend_result { return struct { - fn handle(_: c_int, _: c_int) callconv(.c) c.zend_result { + fn handle(module_type: c_int, module_number: c_int) callconv(.c) c.zend_result { + _ = module_type; + _ = module_number; hook_fn() catch |err| { errors.err(.err, "Module hook function failed: %s", .{@errorName(err).ptr}); return c.FAILURE; @@ -138,7 +140,8 @@ fn makePhpModuleStartupFn( const has_symbols = comptime @hasDecl(c, symbols_fn_name); if (!has_symbols and hook_fn == null and ini_defs == null and classes == null) return null; return struct { - fn handle(_: c_int, module_number: c_int) callconv(.c) c.zend_result { + fn handle(module_type: c_int, module_number: c_int) callconv(.c) c.zend_result { + _ = module_type; if (comptime has_symbols) { // Constants: register_{name}_symbols is generated by gen_stub.php @field(c, symbols_fn_name)(module_number); @@ -168,7 +171,8 @@ fn makePhpModuleStartupFn( fn makePhpModuleShutdownFn(comptime hook_fn: ?PhpHookFn, comptime ini_defs: ?[]const c.zend_ini_entry_def) ?*const fn (c_int, c_int) callconv(.c) c.zend_result { if (hook_fn == null and ini_defs == null) return null; return struct { - fn handle(_: c_int, module_number: c_int) callconv(.c) c.zend_result { + fn handle(module_type: c_int, module_number: c_int) callconv(.c) c.zend_result { + _ = module_type; if (comptime ini_defs != null) { c.zend_unregister_ini_entries(module_number); } From 59f813af9c8ded0e633b69c9beab6790033e5e79 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Sun, 12 Jul 2026 22:13:29 +0800 Subject: [PATCH 14/17] feat(windows): validate SDK inputs and expand MSVC tests --- build/Phpz.zig | 30 +++++++++----- scripts/test-msvc.sh | 95 +++++++++++++++++++++++++++++--------------- src/root.zig | 7 ---- 3 files changed, 84 insertions(+), 48 deletions(-) diff --git a/build/Phpz.zig b/build/Phpz.zig index 316ce3a..4245b5c 100644 --- a/build/Phpz.zig +++ b/build/Phpz.zig @@ -138,10 +138,19 @@ fn createPhpCTranslator(b: *Build, options: Options) Translator { }, else => {}, } + } else { + c.run.step.dependOn(&b.addFail("PHP headers are required; pass -Dphp-include-dir=").step); } - if (translator_options.target.result.os.tag == .windows and translator_options.target.result.abi == .msvc) { - patchWindowsBindings(b, &c, translator_options); + if (translator_options.target.result.os.tag == .windows) { + if (translator_options.target.result.abi == .msvc) { + patchWindowsBindings(b, &c, translator_options); + } else { + c.run.step.dependOn(&b.addFail("Windows PHP extensions require the MSVC ABI; use -Dtarget=native-windows-msvc").step); + } + if (options.php_lib_dir == null) { + c.run.step.dependOn(&b.addFail("Windows PHP extensions require the PHP SDK library directory; pass -Dphp-lib-dir=").step); + } } return c; @@ -152,15 +161,18 @@ fn createPhpCTranslator(b: *Build, options: Options) Translator { /// This also applies platform-specific linker settings required for PHP to /// load the resulting shared library. pub fn addExtension(self: Phpz, b: *Build, options: Build.LibraryOptions) *Build.Step.Compile { - options.root_module.addImport("phpz", self.mod); const lib = b.addLibrary(options); - lib.setLibCFile(self.options.libc_file); - switch (self.options.translator.target.result.os.tag) { - // macOS: allows undefined symbols to be resolved at runtime by PHP - .macos => lib.linker_allow_shlib_undefined = true, - else => {}, + lib.root_module.addImport("phpz", self.mod); + + if (self.options.libc_file) |libc_file| lib.setLibCFile(libc_file); + if (lib.root_module.resolved_target) |target| { + switch (target.result.os.tag) { + // macOS: allows undefined symbols to be resolved at runtime by PHP + .macos => lib.linker_allow_shlib_undefined = true, + else => {}, + } } - if (b.option(bool, "check-arginfo", "Check that the generated arginfo header matches the extension stub") orelse true) { + if (b.option(bool, "check-arginfo", "Check that the generated arginfo header matches the extension stub (default: true)") orelse true) { lib.step.dependOn(addArginfoCheckStep(self, b, options.name)); } return lib; diff --git a/scripts/test-msvc.sh b/scripts/test-msvc.sh index 3eb3049..41b9a39 100755 --- a/scripts/test-msvc.sh +++ b/scripts/test-msvc.sh @@ -13,52 +13,71 @@ php_thread_safety=nts while [ "$#" -gt 0 ]; do case "$1" in - --php-version) - if [ "$#" -lt 2 ]; then - usage >&2 - exit 2 - fi - php_version=$2 - shift 2 - ;; - --zts) - php_thread_safety=zts - shift - ;; - -h | --help) - usage - exit 0 - ;; - *) + --php-version) + if [ "$#" -lt 2 ]; then usage >&2 exit 2 - ;; + fi + php_version=$2 + shift 2 + ;; + --zts) + php_thread_safety=zts + shift + ;; + -h | --help) + usage + exit 0 + ;; + *) + usage >&2 + exit 2 + ;; esac done +# Match the Visual Studio toolset used by official PHP for Windows builds. +case "$php_version" in +8.2.* | 8.3.*) + vs_version=16 + ;; +8.4.* | 8.5.*) + vs_version=17 + ;; +8.6.*) + vs_version=18 + ;; +*) + echo "unsupported PHP version: $php_version (supported: 8.2-8.6)" >&2 + exit 2 + ;; +esac +vs_toolset=vs$vs_version + tmp_dir=$root_dir/tmp xwin=$tmp_dir/xwin -win_sdk=$tmp_dir/win_sdk -libc_file=$tmp_dir/libc.txt +win_sdk=$tmp_dir/win_sdk-$vs_toolset +libc_file=$tmp_dir/libc-$vs_toolset.txt case "$php_thread_safety" in - nts) - php_archive_name=php-devel-pack-${php_version}-nts-Win32-vs17-x64.zip - ;; - zts) - php_archive_name=php-devel-pack-${php_version}-Win32-vs17-x64.zip - ;; - *) - echo "usage: $0 [PHP_VERSION] [nts|zts]" >&2 - exit 2 - ;; +nts) + php_archive_name=php-devel-pack-${php_version}-nts-Win32-${vs_toolset}-x64.zip + ;; +zts) + php_archive_name=php-devel-pack-${php_version}-Win32-${vs_toolset}-x64.zip + ;; +*) + usage >&2 + exit 2 + ;; esac php_cache_dir=$tmp_dir/php-${php_version}-${php_thread_safety} -php_sdk=$php_cache_dir/php-${php_version}-devel-vs17-x64 +php_sdk=$php_cache_dir/php-${php_version}-devel-${vs_toolset}-x64 mkdir -p "$tmp_dir" +echo "==> Preparing xwin" if [ ! -x "$xwin" ]; then xwin_archive=$tmp_dir/xwin-0.9.0-x86_64-unknown-linux-musl.tar.gz curl -fsSL \ @@ -71,12 +90,14 @@ if [ ! -x "$xwin" ]; then rm "$xwin_archive" fi +echo "==> Preparing Windows SDK and MSVC toolset ($vs_toolset)" if [ ! -d "$win_sdk" ]; then "$xwin" \ --accept-license \ + --manifest-version "$vs_version" \ --arch x86_64,aarch64 \ --variant desktop \ - --cache-dir "$HOME/.cache/xwin" \ + --cache-dir "$HOME/.cache/xwin/$vs_toolset" \ splat \ --include-debug-libs \ --include-debug-symbols \ @@ -89,6 +110,7 @@ if [ ! -e "$win_sdk/sdk/include/um/Ws2tcpip.h" ]; then fi php_archive=$tmp_dir/$php_archive_name +echo "==> Preparing PHP $php_version development files ($php_thread_safety, $vs_toolset)" if [ ! -d "$php_sdk" ]; then curl -fsSL \ "https://downloads.php.net/~windows/releases/archives/$php_archive_name" \ @@ -98,6 +120,7 @@ if [ ! -d "$php_sdk" ]; then rm "$php_archive" fi +echo "==> Writing Zig libc configuration ($libc_file)" cat >"$libc_file" < Running phpz tests" zig build test "$@" +echo "==> Building skeleton example" +( + cd examples/skeleton + zig build "$@" +) + +echo "==> Building my_php_extension example" ( cd examples/my_php_extension zig build "$@" diff --git a/src/root.zig b/src/root.zig index 44acb27..3ce5926 100644 --- a/src/root.zig +++ b/src/root.zig @@ -31,13 +31,6 @@ pub fn printf(fmt: [:0]const u8, args: anytype) usize { return @call(.auto, c.php_printf, .{fmt.ptr} ++ args); } -comptime { - const builtin = @import("builtin"); - if (builtin.os.tag == .windows and builtin.abi != .msvc) { - @compileError("phpz only supports the MSVC ABI on Windows"); - } -} - test { _ = @import("Ctx.zig"); _ = @import("class.zig"); From 95709c795a117abae965f6319148779b8ccd12c3 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Sun, 12 Jul 2026 22:13:30 +0800 Subject: [PATCH 15/17] chore: refine examples and benchmark setup --- bench/README.md | 25 +++++------ bench/run.sh | 2 +- bench/zig_ext/build.zig | 2 +- examples/my_php_extension/build.zig | 6 +-- examples/skeleton/build.zig | 4 +- examples/skeleton/src/classes/counter.zig | 44 ------------------- examples/skeleton/src/root.zig | 51 ++++++++++++++++++++++- 7 files changed, 69 insertions(+), 65 deletions(-) delete mode 100644 examples/skeleton/src/classes/counter.zig diff --git a/bench/README.md b/bench/README.md index 184e568..2e58eac 100644 --- a/bench/README.md +++ b/bench/README.md @@ -12,23 +12,22 @@ Runs: C build + test, Zig build + test, then benchmarks. Default 1,000,000 itera ## Results -Linux, PHP 8.5.7, Zig 0.16.0, 1M iterations × 5 runs averaged, ReleaseFast. Lower is better. +Linux, PHP 8.5.8, Zig 0.17.0-dev.1282, 1M iterations × 10 runs averaged. C uses `-O3`; Zig uses `ReleaseFast`. Lower is better. | Benchmark | C ns/call | Zig ns/call | Winner | |---|---|---|---| -| Empty function call | 29.7 | 30.4 | — (tie) | -| Parse 8 params (macro/fast) | 62.9 | 63.1 | — (tie) | -| Parse 8 params (parse/zpp) | 134.3 | **125.7** | Zig 6.4% | -| Array sum 1k (macro/fast) | 449.7 | **336.2** | Zig 25.2% | -| Array sum 1k (parse/zpp) | 462.6 | **338.9** | Zig 26.7% | -| **TOTAL** | 1139.2 | **894.3** | **Zig 21.5%** | +| Empty function call | 28.2 | 29.3 | — (tie) | +| Parse 8 params (macro/fast) | 60.6 | 61.7 | — (tie) | +| Parse 8 params (parse/zpp) | **139.1** | 141.1 | C 1.4% | +| Array sum 1k (macro/fast) | 448.3 | **316.5** | Zig 29.4% | +| Array sum 1k (parse/zpp) | 463.9 | **339.8** | Zig 26.8% | +| **TOTAL** | 1140.1 | **888.3** | **Zig 22.1%** | ### Takeaways -- **Function call overhead**: identical (~1ns diff, within noise). -- **Parameter parsing**: Zig's `expectArgs` matches C's fast `Z_PARAM` macros (tie). - The traditional `zend_parse_parameters` path is ~6% faster in Zig. -- **Array iteration**: Zig's inline `eachValue` beats C's `ZEND_HASH_FOREACH_VAL` by ~26%. +- **Function call overhead**: no meaningful difference (~1.1ns per call). +- **Parameter parsing**: the fast paths are effectively tied (~1.1ns); C's traditional path is ~2ns faster. +- **Array iteration**: Zig's inline `eachValue` beats C's `ZEND_HASH_FOREACH_VAL` by 27–29%. The IS_UNDEF check and user type check are in the same compiler scope, eliminating redundant branch evaluation. @@ -48,7 +47,9 @@ Linux, PHP 8.5.7, Zig 0.16.0, 1M iterations × 5 runs averaged, ReleaseFast. Low ```bash cd c_ext -phpize && ./configure && make +phpize +CFLAGS="-O3" ./configure +make make test ``` diff --git a/bench/run.sh b/bench/run.sh index fa7b86e..0b459df 100755 --- a/bench/run.sh +++ b/bench/run.sh @@ -9,7 +9,7 @@ echo "=== Building C extension ===" cd "$SCRIPT_DIR/c_ext" git clean -fdX phpize - ./configure --with-php-config="$(which php-config)" + CFLAGS="-O3" ./configure --with-php-config="$(which php-config)" make echo " C extension built" php run-tests.php -d "extension=$SCRIPT_DIR/c_ext/modules/bench_c.so" --show-diff -q diff --git a/bench/zig_ext/build.zig b/bench/zig_ext/build.zig index c7839dc..f6601bb 100644 --- a/bench/zig_ext/build.zig +++ b/bench/zig_ext/build.zig @@ -8,7 +8,7 @@ pub fn build(b: *std.Build) void { const php_include_dir = b.option([]const u8, "php-include-dir", "PHP include directory (main/, Zend/, TSRM/, ext/)") orelse "/usr/include/php"; - const php_lib_dir = b.option([]const u8, "php-lib-dir", "PHP SDK library directory (Windows only)"); + const php_lib_dir = b.option([]const u8, "php-lib-dir", "Windows only: required PHP SDK library directory containing php8*.lib"); const phpz_dep = b.dependency("phpz", .{}); const phpz = Phpz.init(phpz_dep, .{ diff --git a/examples/my_php_extension/build.zig b/examples/my_php_extension/build.zig index 201fa35..4547845 100644 --- a/examples/my_php_extension/build.zig +++ b/examples/my_php_extension/build.zig @@ -11,11 +11,11 @@ pub fn build(b: *std.Build) void { // Directory containing PHP header files (main/, Zend/, TSRM/, ext/) const php_include_dir = b.option([]const u8, "php-include-dir", "PHP include directory (main/, Zend/, TSRM/, ext/)") orelse "/usr/include/php"; // Windows only: PHP SDK lib directory containing php8*.lib - const php_lib_dir = b.option([]const u8, "php-lib-dir", "PHP SDK library directory (Windows only, contains php8*.lib)"); - const libc_file = b.option([]const u8, "libc-file", "Libc paths file for C translation and extension compilation"); - const libc_file_path: ?std.Build.LazyPath = if (libc_file) |path| .{ .cwd_relative = path } else null; + const php_lib_dir = b.option([]const u8, "php-lib-dir", "Windows only: required PHP SDK library directory containing php8*.lib"); const windows_zts = b.option(bool, "windows-zts", "Windows only: link against the thread-safe PHP library") orelse false; const windows_debug = b.option(bool, "windows-debug", "Windows only: build against a debug PHP SDK") orelse false; + const libc_file = b.option([]const u8, "libc-file", "Libc paths file for C translation and extension compilation"); + const libc_file_path: ?std.Build.LazyPath = if (libc_file) |path| .{ .cwd_relative = path } else null; // Fetch the phpz dependency declared in build.zig.zon const phpz_dep = b.dependency("phpz", .{}); diff --git a/examples/skeleton/build.zig b/examples/skeleton/build.zig index 2c92c2a..f742981 100644 --- a/examples/skeleton/build.zig +++ b/examples/skeleton/build.zig @@ -7,10 +7,10 @@ pub fn build(b: *std.Build) void { const optimize = b.standardOptimizeOption(.{}); const php_include_dir = b.option([]const u8, "php-include-dir", "PHP include directory (main/, Zend/, TSRM/, ext/)") orelse "/usr/include/php"; - const php_lib_dir = b.option([]const u8, "php-lib-dir", "PHP SDK library directory (Windows only, contains php8*.lib)"); - const libc_file = b.option([]const u8, "libc-file", "Libc paths file for C translation and extension compilation"); + const php_lib_dir = b.option([]const u8, "php-lib-dir", "Windows only: required PHP SDK library directory containing php8*.lib"); const windows_zts = b.option(bool, "windows-zts", "Windows only: link against the thread-safe PHP library") orelse false; const windows_debug = b.option(bool, "windows-debug", "Windows only: build against a debug PHP SDK") orelse false; + const libc_file = b.option([]const u8, "libc-file", "Libc paths file for C translation and extension compilation"); const phpz_dep = b.dependency("phpz", .{}); const phpz = Phpz.init(phpz_dep, .{ diff --git a/examples/skeleton/src/classes/counter.zig b/examples/skeleton/src/classes/counter.zig deleted file mode 100644 index fe0e876..0000000 --- a/examples/skeleton/src/classes/counter.zig +++ /dev/null @@ -1,44 +0,0 @@ -const phpz = @import("phpz"); - -/// class Counter -pub const Counter = extern struct { - n: i64, - - /// public function __construct(int $n = 0): void - pub fn construct(self: *Counter, ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .int = .{ .optional = true } }, - }, {}); - self.n = args[0] orelse 0; - } - - /// public function add(int $n): void - pub fn add(self: *Counter, ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .int = .{} }, - }, {}); - self.n +|= args[0]; - } - - /// public function dec(int $n): void - pub fn dec(self: *Counter, ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .int = .{} }, - }, {}); - self.n -|= args[0]; - } - - /// public function value(): int - pub fn value(self: Counter, ctx: phpz.Ctx) void { - ctx.ret.set(.int, self.n); - } -}; - -pub const Class = phpz.Class("Counter", Counter); - -comptime { - Class.method("__construct", .construct); - Class.method("add", .add); - Class.method("dec", .dec); - Class.method("value", .value); -} diff --git a/examples/skeleton/src/root.zig b/examples/skeleton/src/root.zig index 56a063d..423ea50 100644 --- a/examples/skeleton/src/root.zig +++ b/examples/skeleton/src/root.zig @@ -2,16 +2,27 @@ const std = @import("std"); const phpz = @import("phpz"); -const counter = @import("classes/counter.zig"); +// Create the PHP class wrapper for the Zig implementation. +const CounterClass = phpz.Class("Counter", Counter); comptime { + // Bind Zig functions as methods of the PHP class wrapper. + CounterClass.method("__construct", .construct); + CounterClass.method("add", .add); + CounterClass.method("dec", .dec); + CounterClass.method("value", .value); +} + +comptime { + // Export Zig functions as PHP global functions. phpz.function("hello", hello); phpz.function("greet", greet); + // Create and export the PHP module entry. Listed classes are registered during MINIT. phpz.module(.{ .name = "skeleton", .version = "0.1.0", - .classes = &.{counter.Class}, + .classes = &.{CounterClass}, }); } @@ -33,3 +44,39 @@ fn greet(ctx: phpz.Ctx) !void { const result = try std.fmt.bufPrint(&buffer, "Hello, {s}!", .{name}); ctx.ret.set(.string, result); } + +// --- Classes --- + +/// class Counter +pub const Counter = extern struct { + n: i64, + + /// public function __construct(int $n = 0): void + pub fn construct(self: *Counter, ctx: phpz.Ctx) !void { + const args = try ctx.call.expectArgs(&.{ + .{ .int = .{ .optional = true } }, + }, {}); + self.n = args[0] orelse 0; + } + + /// public function add(int $n): void + pub fn add(self: *Counter, ctx: phpz.Ctx) !void { + const args = try ctx.call.expectArgs(&.{ + .{ .int = .{} }, + }, {}); + self.n +|= args[0]; + } + + /// public function dec(int $n): void + pub fn dec(self: *Counter, ctx: phpz.Ctx) !void { + const args = try ctx.call.expectArgs(&.{ + .{ .int = .{} }, + }, {}); + self.n -|= args[0]; + } + + /// public function value(): int + pub fn value(self: Counter, ctx: phpz.Ctx) void { + ctx.ret.set(.int, self.n); + } +}; From 28bd1db4945259889624de8ffdc57b70fec09386 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Sun, 12 Jul 2026 22:13:31 +0800 Subject: [PATCH 16/17] feat(module): add typed module globals --- build/phpz_wrapper.h | 19 +++ .../my_php_extension.stub.php | 8 + .../my_php_extension_arginfo.h | 22 ++- examples/my_php_extension/src/functions.zig | 26 ++++ examples/my_php_extension/src/root.zig | 4 +- .../tests/module_globals.phpt | 14 ++ src/module.zig | 145 +++++++++++++++++- src/root.zig | 1 + 8 files changed, 226 insertions(+), 13 deletions(-) create mode 100644 examples/my_php_extension/tests/module_globals.phpt diff --git a/build/phpz_wrapper.h b/build/phpz_wrapper.h index 913cf9e..738ca53 100644 --- a/build/phpz_wrapper.h +++ b/build/phpz_wrapper.h @@ -65,6 +65,25 @@ static zend_always_inline php_file_globals *phpz_file_globals(void) { #endif } +#ifdef ZTS +typedef ts_rsrc_id phpz_rsrc_id; +#else +typedef int phpz_rsrc_id; +#endif + +static zend_always_inline void *phpz_tsrmg_bulk(phpz_rsrc_id id) { +#ifdef ZTS +#ifdef ZEND_ENABLE_STATIC_TSRMLS_CACHE + return TSRMG_BULK_STATIC(id, void *); +#else + return TSRMG_BULK(id, void *); +#endif +#else + (void) id; + return NULL; +#endif +} + typedef void (*phpz_zend_try_callback)(void *); bool phpz_zend_try_catch(phpz_zend_try_callback callback, void *ctx); diff --git a/examples/my_php_extension/my_php_extension.stub.php b/examples/my_php_extension/my_php_extension.stub.php index ea4b233..fc9f070 100644 --- a/examples/my_php_extension/my_php_extension.stub.php +++ b/examples/my_php_extension/my_php_extension.stub.php @@ -22,6 +22,14 @@ function greet(string $name): string { } + function moduleGlobalsGet(): int + { + } + + function moduleGlobalsIncrement(): int + { + } + function iniGetGreeting(): string { } diff --git a/examples/my_php_extension/my_php_extension_arginfo.h b/examples/my_php_extension/my_php_extension_arginfo.h index 744c31e..0a602eb 100644 --- a/examples/my_php_extension/my_php_extension_arginfo.h +++ b/examples/my_php_extension/my_php_extension_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 34215a54f14dd37a56a161859166f9ee56368b8d */ + * Stub hash: 8174e4e61bb73c9c1ec4ce6f84107a80a784c4fe */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_hello, 0, 0, IS_VOID, 0) ZEND_END_ARG_INFO() @@ -8,12 +8,16 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_greet, 0, 1, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_iniGetGreeting, 0, 0, IS_STRING, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_moduleGlobalsGet, 0, 0, IS_LONG, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_iniGetMaxUsers, 0, 0, IS_LONG, 0) +#define arginfo_moduleGlobalsIncrement arginfo_moduleGlobalsGet + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_iniGetGreeting, 0, 0, IS_STRING, 0) ZEND_END_ARG_INFO() +#define arginfo_iniGetMaxUsers arginfo_moduleGlobalsGet + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_iniGetDebug, 0, 0, _IS_BOOL, 0) ZEND_END_ARG_INFO() @@ -67,7 +71,7 @@ ZEND_END_ARG_INFO() #define arginfo_MyPHPExt_testHeapAllocatorBailout arginfo_MyPHPExt_listStatuses -#define arginfo_class_MyPHPExt_Identifiable_getId arginfo_iniGetMaxUsers +#define arginfo_class_MyPHPExt_Identifiable_getId arginfo_moduleGlobalsGet ZEND_BEGIN_ARG_INFO_EX(arginfo_class_MyPHPExt_ExampleAttribute___construct, 0, 0, 1) ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) @@ -85,7 +89,7 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_MyPHPExt_User___construct, 0, 0, 1) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, age, IS_LONG, 1, "null") ZEND_END_ARG_INFO() -#define arginfo_class_MyPHPExt_User_getId arginfo_iniGetMaxUsers +#define arginfo_class_MyPHPExt_User_getId arginfo_moduleGlobalsGet #define arginfo_class_MyPHPExt_User_handle arginfo_class_MyPHPExt_AbstractEntity_handle @@ -105,7 +109,7 @@ ZEND_END_ARG_INFO() #define arginfo_class_MyPHPExt_Counter_dec arginfo_class_MyPHPExt_Counter_add -#define arginfo_class_MyPHPExt_Counter_value arginfo_iniGetMaxUsers +#define arginfo_class_MyPHPExt_Counter_value arginfo_moduleGlobalsGet ZEND_BEGIN_ARG_INFO_EX(arginfo_class_MyPHPExt_ArrayLike___construct, 0, 0, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, data, IS_ARRAY, 0, "[]") @@ -130,7 +134,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_ArrayLike_offsetU ZEND_ARG_TYPE_INFO(0, offset, IS_MIXED, 0) ZEND_END_ARG_INFO() -#define arginfo_class_MyPHPExt_ArrayLike_count arginfo_iniGetMaxUsers +#define arginfo_class_MyPHPExt_ArrayLike_count arginfo_moduleGlobalsGet ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_ArrayLike_current, 0, 0, IS_MIXED, 0) ZEND_END_ARG_INFO() @@ -146,6 +150,8 @@ ZEND_END_ARG_INFO() ZEND_FUNCTION(hello); ZEND_FUNCTION(greet); +ZEND_FUNCTION(moduleGlobalsGet); +ZEND_FUNCTION(moduleGlobalsIncrement); ZEND_FUNCTION(iniGetGreeting); ZEND_FUNCTION(iniGetMaxUsers); ZEND_FUNCTION(iniGetDebug); @@ -189,6 +195,8 @@ ZEND_METHOD(MyPHPExt_ArrayLike, valid); static const zend_function_entry ext_functions[] = { ZEND_FE(hello, arginfo_hello) ZEND_FE(greet, arginfo_greet) + ZEND_FE(moduleGlobalsGet, arginfo_moduleGlobalsGet) + ZEND_FE(moduleGlobalsIncrement, arginfo_moduleGlobalsIncrement) ZEND_FE(iniGetGreeting, arginfo_iniGetGreeting) ZEND_FE(iniGetMaxUsers, arginfo_iniGetMaxUsers) ZEND_FE(iniGetDebug, arginfo_iniGetDebug) diff --git a/examples/my_php_extension/src/functions.zig b/examples/my_php_extension/src/functions.zig index 5fd09df..0159ebf 100644 --- a/examples/my_php_extension/src/functions.zig +++ b/examples/my_php_extension/src/functions.zig @@ -8,6 +8,18 @@ const ini_config = @import("ini.zig"); const StatusEnum = @import("classes.zig").status.Enum; const UserClass = @import("classes.zig").user.Class; +pub const Globals = phpz.ModuleGlobals(struct { + count: i64 = 40, + + pub fn init(self: *@This()) void { + self.count += 2; + } + + pub fn deinit(self: *@This()) void { + self.count = 0; + } +}); + /// hello(): void fn hello() void { _ = phpz.printf("Hello from ZIG!\n", .{}); @@ -26,6 +38,18 @@ fn greet(ctx: phpz.Ctx) !void { ctx.ret.set(.string, result); } +/// moduleGlobalsGet(): int +fn moduleGlobalsGet(ctx: phpz.Ctx) void { + ctx.ret.set(.int, Globals.get().count); +} + +/// moduleGlobalsIncrement(): int +fn moduleGlobalsIncrement(ctx: phpz.Ctx) void { + const globals = Globals.get(); + globals.count += 1; + ctx.ret.set(.int, globals.count); +} + /// increment(int &$value): void fn increment(ctx: phpz.Ctx) !void { const args = try ctx.call.expectArgs(&.{ @@ -324,6 +348,8 @@ fn iniGetMode(ctx: phpz.Ctx) !void { comptime { phpz.function("hello", hello); phpz.function("greet", greet); + phpz.function("moduleGlobalsGet", moduleGlobalsGet); + phpz.function("moduleGlobalsIncrement", moduleGlobalsIncrement); phpz.function("MyPHPExt\\increment", increment); phpz.function("MyPHPExt\\findById", findById); phpz.function("MyPHPExt\\getDefaultUser", getDefaultUser); diff --git a/examples/my_php_extension/src/root.zig b/examples/my_php_extension/src/root.zig index b8efe17..f9d173b 100644 --- a/examples/my_php_extension/src/root.zig +++ b/examples/my_php_extension/src/root.zig @@ -2,6 +2,7 @@ const phpz = @import("phpz"); const allocator = @import("allocator.zig"); const classes = @import("classes.zig"); +const functions = @import("functions.zig"); const ini_config = @import("ini.zig"); fn shutdown() !void { @@ -20,11 +21,10 @@ fn info(entry: *phpz.ModuleEntry) void { } comptime { - _ = @import("functions.zig"); - phpz.module(.{ .name = "my_php_extension", .version = "0.1.0", + .globals = functions.Globals, .classes = &.{ classes.identifiable.Interface, classes.status.Enum, diff --git a/examples/my_php_extension/tests/module_globals.phpt b/examples/my_php_extension/tests/module_globals.phpt new file mode 100644 index 0000000..46dd133 --- /dev/null +++ b/examples/my_php_extension/tests/module_globals.phpt @@ -0,0 +1,14 @@ +--TEST-- +Module globals +--FILE-- + +--EXPECT-- +int(42) +int(43) +int(43) diff --git a/src/module.zig b/src/module.zig index aacdbdb..8959830 100644 --- a/src/module.zig +++ b/src/module.zig @@ -6,6 +6,7 @@ const c = @import("root.zig").c; pub const ModuleEntry = c.zend_module_entry; const errors = @import("errors.zig"); const ini_helper = @import("ini.zig"); + /// Configuration for creating a PHP extension module. /// /// This structure defines the metadata and lifecycle hooks for a PHP extension. @@ -32,6 +33,10 @@ pub const Config = struct { /// INI declarations to register during module startup. ini: []const type = &.{}, + /// Typed module globals managed by PHP for the extension lifetime. + /// In ZTS builds, each TSRM thread receives a separate instance. + globals: ?type = null, + /// Module startup hook, called when PHP starts (e.g., when Apache starts). /// Use this to initialize resources that persist across requests (e.g., register classes). /// Called once per PHP process/thread pool initialization. @@ -230,16 +235,148 @@ inline fn createModuleEntry(comptime cfg: Config) ModuleEntry { entry.module_number = 0; entry.build_id = c.phpz_module_build_id(); - if (@hasField(ModuleEntry, "globals_id_ptr")) { - // ZTS - entry.globals_id_ptr = null; + if (cfg.globals) |g| { + g.configure(&entry); } else { - entry.globals_ptr = null; + if (c.USING_ZTS == 1) { + entry.globals_id_ptr = null; + } else { + entry.globals_ptr = null; + } } return entry; } +fn checkModuleGlobalsHook(comptime T: type, comptime name: []const u8) void { + if (!@hasDecl(T, name)) return; + + const hook_info = @typeInfo(@TypeOf(@field(T, name))); + if (hook_info != .@"fn") { + @compileError(@typeName(T) ++ "." ++ name ++ " must be a function"); + } + + const fn_info = hook_info.@"fn"; + if (fn_info.param_types.len != 1) { + @compileError(@typeName(T) ++ "." ++ name ++ " must have the signature fn (*" ++ @typeName(T) ++ ") void"); + } + const param_type = fn_info.param_types[0] orelse { + @compileError(@typeName(T) ++ "." ++ name ++ " must have a concrete receiver type"); + }; + const return_type = fn_info.return_type orelse { + @compileError(@typeName(T) ++ "." ++ name ++ " must have a concrete return type"); + }; + if (param_type != *T or return_type != void) { + @compileError(@typeName(T) ++ "." ++ name ++ " must have the signature fn (*" ++ @typeName(T) ++ ") void"); + } +} + +/// Creates a typed namespace for PHP module globals. +/// +/// The globals type must be a non-zero-sized struct whose fields can be +/// initialized with `.{}`. Optional `init(*T) void` and `deinit(*T) void` +/// methods run from PHP's globals constructor and destructor respectively. +pub fn ModuleGlobals(comptime T: type) type { + comptime { + if (@typeInfo(T) != .@"struct") { + @compileError("phpz.ModuleGlobals requires a struct type"); + } + if (@sizeOf(T) == 0) { + @compileError("phpz.ModuleGlobals does not support zero-sized globals"); + } + _ = @as(T, .{}); + checkModuleGlobalsHook(T, "init"); + checkModuleGlobalsHook(T, "deinit"); + } + + if (comptime c.USING_ZTS != 0) { + return struct { + var id: c.phpz_rsrc_id = 0; + + /// Returns the current thread's module globals. + pub inline fn get() *T { + std.debug.assert(id != 0); + const ptr = c.phpz_tsrmg_bulk(id) orelse unreachable; + return @ptrCast(@alignCast(ptr)); + } + + fn construct(raw: ?*anyopaque) callconv(.c) void { + const value: *T = @ptrCast(@alignCast(raw orelse unreachable)); + value.* = .{}; + if (comptime @hasDecl(T, "init")) value.init(); + } + + fn destruct(raw: ?*anyopaque) callconv(.c) void { + const value: *T = @ptrCast(@alignCast(raw orelse unreachable)); + if (comptime @hasDecl(T, "deinit")) value.deinit(); + } + + fn configure(entry: *ModuleEntry) void { + entry.globals_size = @sizeOf(T); + entry.globals_ctor = &construct; + entry.globals_dtor = &destruct; + entry.globals_id_ptr = &id; + } + }; + } + + return struct { + var storage: T = undefined; + + /// Returns the module globals. + pub inline fn get() *T { + return &storage; + } + + fn construct(raw: ?*anyopaque) callconv(.c) void { + const value: *T = @ptrCast(@alignCast(raw orelse unreachable)); + value.* = .{}; + if (comptime @hasDecl(T, "init")) value.init(); + } + + fn destruct(raw: ?*anyopaque) callconv(.c) void { + const value: *T = @ptrCast(@alignCast(raw orelse unreachable)); + if (comptime @hasDecl(T, "deinit")) value.deinit(); + } + + fn configure(entry: *ModuleEntry) void { + entry.globals_size = @sizeOf(T); + entry.globals_ctor = &construct; + entry.globals_dtor = &destruct; + entry.globals_ptr = &storage; + } + }; +} + +test "module globals lifecycle and access types" { + const TestGlobals = struct { + count: usize = 40, + initialized: bool = false, + + pub fn init(self: *@This()) void { + self.count += 2; + self.initialized = true; + } + + pub fn deinit(self: *@This()) void { + self.count = 0; + self.initialized = false; + } + }; + const Globals = ModuleGlobals(TestGlobals); + + try std.testing.expect(@TypeOf(Globals.get()) == *TestGlobals); + + var value: TestGlobals = undefined; + Globals.construct(&value); + try std.testing.expectEqual(@as(usize, 42), value.count); + try std.testing.expect(value.initialized); + + Globals.destruct(&value); + try std.testing.expectEqual(@as(usize, 0), value.count); + try std.testing.expect(!value.initialized); +} + test { std.testing.refAllDecls(@This()); } diff --git a/src/root.zig b/src/root.zig index 3ce5926..f5bddad 100644 --- a/src/root.zig +++ b/src/root.zig @@ -10,6 +10,7 @@ pub const info = @import("info.zig"); const mod_helper = @import("module.zig"); pub const module = mod_helper.module; pub const ModuleEntry = mod_helper.ModuleEntry; +pub const ModuleGlobals = mod_helper.ModuleGlobals; pub const zend = @import("zend.zig"); From f8d735d16ab5c8b341430901be659602775a5f3a Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Mon, 13 Jul 2026 18:31:47 +0800 Subject: [PATCH 17/17] fix(tools): support Windows hosts and compute stub hashes --- tools/check_arginfo.zig | 2 +- tools/phpz_skel.php | 8 +++++++- tools/windows_patcher.zig | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/tools/check_arginfo.zig b/tools/check_arginfo.zig index f4470f8..9adff36 100644 --- a/tools/check_arginfo.zig +++ b/tools/check_arginfo.zig @@ -3,7 +3,7 @@ const std = @import("std"); pub fn main(init: std.process.Init) !void { const arena = init.arena.allocator(); - var args = init.minimal.args.iterate(); + var args = try init.minimal.args.iterateAllocator(arena); const cmd = args.next() orelse "check-arginfo"; const stub_path = args.next() orelse fatal("usage: {s} ", .{cmd}); diff --git a/tools/phpz_skel.php b/tools/phpz_skel.php index 521780f..fbbd78c 100755 --- a/tools/phpz_skel.php +++ b/tools/phpz_skel.php @@ -1036,7 +1036,7 @@ public function value(): int {} PHP, '{{EXT_NAME}}_arginfo.h' => <<<'C' /* This is a generated file, edit the .stub.php file instead. - * Stub hash: d84374eb3cc93ebb1e1cf6f0dfa2ee49381bec07 */ + * Stub hash: {{PHPZ_STUB_HASH}} */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_hello, 0, 0, IS_VOID, 0) ZEND_END_ARG_INFO() @@ -1295,6 +1295,12 @@ public static function render( $hasGenStub ? PHPZ_GEN_STUB_REGEN_TEMPLATE : PHPZ_GEN_STUB_MISSING_TEMPLATE, $contents, ); + $stub = TemplateVars::replace(PHPZ_TEMPLATE_FILES['{{EXT_NAME}}.stub.php'], $ext); + $contents = str_replace( + '{{PHPZ_STUB_HASH}}', + sha1(str_replace("\r\n", "\n", $stub)), + $contents, + ); return TemplateVars::replace($contents, $ext); } diff --git a/tools/windows_patcher.zig b/tools/windows_patcher.zig index 3d1e0c6..8ceffee 100644 --- a/tools/windows_patcher.zig +++ b/tools/windows_patcher.zig @@ -33,7 +33,7 @@ const default_dllimport_data_prefixes = [_][]const u8{ pub fn main(init: std.process.Init) !void { const arena = init.arena.allocator(); - var args = init.minimal.args.iterate(); + var args = try init.minimal.args.iterateAllocator(arena); const cmd = args.next() orelse "windows-patcher"; const input_path = args.next() orelse fatal("usage: {s} ", .{cmd});