From a7ae4d186e304890111959119391cb3132a067d8 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Thu, 9 Jul 2026 22:10:47 +0800 Subject: [PATCH 01/36] 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..8b6f63a 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#a7cd3fd81b074c1318060fa1985098bfda571aa8", + .hash = "translate_c-0.0.0-Q_BUWqlCBwBpiytivz8S8gbHOGHiKuYMQLkySmMO_1Ls", }, }, .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 315dcd3b160376427c8e90fa7c01d6f24a6396b8 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Thu, 9 Jul 2026 22:10:50 +0800 Subject: [PATCH 02/36] 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 f835ede5249abb22aa6d451d6f57e6edcb60680b Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Thu, 9 Jul 2026 22:10:52 +0800 Subject: [PATCH 03/36] 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 4c914f16653ccdc3f4ce37211db4449b82c60948 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Thu, 9 Jul 2026 22:10:55 +0800 Subject: [PATCH 04/36] 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 359834d633718d613c8bb1ab3f8c476447404b8d Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Thu, 9 Jul 2026 22:10:57 +0800 Subject: [PATCH 05/36] 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 1b6d9263abdb0e3155f74290f27169f917ba7f05 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Thu, 9 Jul 2026 22:10:59 +0800 Subject: [PATCH 06/36] 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 952f0a1cf9d1af667a0cb8f808137b5b579e82a6 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Thu, 9 Jul 2026 22:11:01 +0800 Subject: [PATCH 07/36] 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 97541ad2db5af27e9884c355bfd8ab1c872b7e4f Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Thu, 9 Jul 2026 22:11:03 +0800 Subject: [PATCH 08/36] 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 1b79b0265e6dd9d54dfc35aa7e56a3723fcc410f Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Fri, 10 Jul 2026 23:15:38 +0800 Subject: [PATCH 09/36] 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 1b463ce40ceab8209e0092595004cb4ce60edf07 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Fri, 10 Jul 2026 23:30:38 +0800 Subject: [PATCH 10/36] 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 4eaddf5cd5a8c3d2a3cb3095bafdbf93770278cb Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Sun, 12 Jul 2026 22:13:26 +0800 Subject: [PATCH 11/36] 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 8b6f63a..d2d4e8e 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#a7cd3fd81b074c1318060fa1985098bfda571aa8", 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 ce1766d2f9bb89e73b82879009486077a0de88c2 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Sun, 12 Jul 2026 22:13:27 +0800 Subject: [PATCH 12/36] 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 04ad69f25b7b6521b19412bec230d50235275174 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Sun, 12 Jul 2026 22:13:28 +0800 Subject: [PATCH 13/36] 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 b75a9bfa8f9acd29432810f6995f9191d1644597 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Sun, 12 Jul 2026 22:13:29 +0800 Subject: [PATCH 14/36] 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 4aab3800c525cfee49be39e39356b55c24915bd1 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Sun, 12 Jul 2026 22:13:30 +0800 Subject: [PATCH 15/36] 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 7cc8a2735c13f4d15ae27541533871a31312cd68 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Sun, 12 Jul 2026 22:13:31 +0800 Subject: [PATCH 16/36] 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 9535c71d4be61313b230c060d08c855c14761ab1 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Mon, 13 Jul 2026 22:13:20 +0800 Subject: [PATCH 17/36] 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}); From 42273a34d94d8bd6a8c4c98f212abfadb4ff6e94 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Mon, 13 Jul 2026 22:13:26 +0800 Subject: [PATCH 18/36] =?UTF-8?q?refactor(ini):=20rename=20typed=20?= =?UTF-8?q?=E2=86=92=20Typed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/my_php_extension/src/ini.zig | 2 +- src/ini.zig | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/my_php_extension/src/ini.zig b/examples/my_php_extension/src/ini.zig index 697c0de..a7f32c9 100644 --- a/examples/my_php_extension/src/ini.zig +++ b/examples/my_php_extension/src/ini.zig @@ -7,7 +7,7 @@ pub const debug = phpz.ini.boolean.new("my_php_extension.debug", false, .user); pub const Mode = enum { safe, fast }; -pub const mode = phpz.ini.typed(Mode).new(.{ +pub const mode = phpz.ini.Typed(Mode).new(.{ .name = "my_php_extension.mode", .default = .safe, .default_text = "safe", diff --git a/src/ini.zig b/src/ini.zig index 8e847a7..ff7c79a 100644 --- a/src/ini.zig +++ b/src/ini.zig @@ -44,7 +44,7 @@ pub const SetError = error{ AlterIniFailed, FormatIniValueFailed }; /// ```zig /// const Mode = enum { safe, fast }; /// -/// const mode = phpz.ini.typed(Mode).new(.{ +/// const mode = phpz.ini.Typed(Mode).new(.{ /// .name = "ext.mode", /// .default = .safe, /// .default_text = "safe", @@ -53,7 +53,7 @@ pub const SetError = error{ AlterIniFailed, FormatIniValueFailed }; /// .format = formatMode, /// }); /// ``` -pub fn typed(comptime T: type) type { +pub fn Typed(comptime T: type) type { return struct { pub const ParseFn = fn ([:0]const u8) anyerror!T; pub const ParseWithEntryFn = fn (*c.zend_ini_entry, [:0]const u8) anyerror!T; @@ -83,7 +83,7 @@ pub fn typed(comptime T: type) type { 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()", + "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; @@ -129,7 +129,7 @@ pub fn typed(comptime T: type) type { 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", + "ini.Typed(" ++ @typeName(T) ++ ").new(...) requires .parse or .parse_with_entry", ); } }; @@ -143,7 +143,7 @@ pub const string = struct { comptime default_value: [:0]const u8, comptime access: Access, ) type { - return typed([:0]const u8).new(.{ + return Typed([:0]const u8).new(.{ .name = name, .default = default_value, .default_text = default_value, @@ -168,7 +168,7 @@ pub const int = struct { comptime default_value: i64, comptime access: Access, ) type { - return typed(i64).new(.{ + return Typed(i64).new(.{ .name = name, .default = default_value, .default_text = std.fmt.comptimePrint("{d}", .{default_value}), @@ -195,7 +195,7 @@ pub const float = struct { comptime default_value: f64, comptime access: Access, ) type { - return typed(f64).new(.{ + return Typed(f64).new(.{ .name = name, .default = default_value, .default_text = std.fmt.comptimePrint("{d}", .{default_value}), @@ -220,7 +220,7 @@ pub const boolean = struct { comptime default_value: bool, comptime access: Access, ) type { - return typed(bool).new(.{ + return Typed(bool).new(.{ .name = name, .default = default_value, .default_text = if (default_value) "1" else "0", @@ -408,7 +408,7 @@ 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(...)", + "pass a value returned by phpz.ini.string/int/float/boolean/Typed.new(...) or phpz.ini.custom(...)", ); } @@ -461,7 +461,7 @@ test { 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(.{ + const mode = Typed(Mode).new(.{ .name = "test.mode", .default = .safe, .default_text = "safe", From d29186773af0efd16e42a1079a74c93afd91579b Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Mon, 13 Jul 2026 22:29:00 +0800 Subject: [PATCH 19/36] refactor(build): remove macos _Nullable macro workaround The translate_c regression that required these defines on macOS has been fixed, so the workaround is no longer needed. --- build/Phpz.zig | 6 ------ 1 file changed, 6 deletions(-) diff --git a/build/Phpz.zig b/build/Phpz.zig index 4245b5c..6ec9f5d 100644 --- a/build/Phpz.zig +++ b/build/Phpz.zig @@ -130,12 +130,6 @@ fn createPhpCTranslator(b: *Build, options: Options) Translator { 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 => {}, } } else { From 50bfd9c26d4f9867ea7bc04e5b0852c431d97320 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Mon, 13 Jul 2026 23:40:19 +0800 Subject: [PATCH 20/36] refactor: use [:0]const u8 for all zend_string slices --- src/globals.zig | 16 ++++++++++++++-- src/zend/function.zig | 7 ++++--- src/zend/object.zig | 6 +++--- src/zend/property_info.zig | 6 +++--- src/zend/string.zig | 20 ++++++++++---------- 5 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/globals.zig b/src/globals.zig index 38b5ddf..33d4a89 100644 --- a/src/globals.zig +++ b/src/globals.zig @@ -86,8 +86,8 @@ 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 core() *CoreGlobals { + return .from(c.phpz_core_globals()); } pub inline fn sapi() *c.sapi_globals_struct { @@ -98,7 +98,19 @@ pub inline fn file() *c.php_file_globals { return @ptrCast(c.phpz_file_globals()); } +pub const CoreGlobals = opaque { + pub inline fn from(core_globals: *c.php_core_globals) *CoreGlobals { + return @ptrCast(core_globals); + } + pub inline fn ptr(self: *CoreGlobals) *c.php_core_globals { + return @ptrCast(@alignCast(self)); + } + + // TODO: $_GET/$_POST/... +}; + test { std.testing.refAllDecls(@This()); std.testing.refAllDecls(@This().class); + std.testing.refAllDecls(CoreGlobals); } diff --git a/src/zend/function.zig b/src/zend/function.zig index 32c590c..c6b457e 100644 --- a/src/zend/function.zig +++ b/src/zend/function.zig @@ -13,11 +13,12 @@ pub const Function = opaque { pub const TryCallError = Error || try_catch.TryCatchError; /// Function type classification. - pub const Kind = enum(u8) { + pub const Kind = enum(@TypeOf(c.ZEND_INTERNAL_FUNCTION)) { /// PHP internal function (written in C/Zig) internal = c.ZEND_INTERNAL_FUNCTION, /// PHP userland function (written in PHP) user = c.ZEND_USER_FUNCTION, + _, }; /// Call context classification. @@ -72,11 +73,11 @@ pub const Function = opaque { } /// Get the function name - pub fn name(self: *Function) []const u8 { + pub fn name(self: *Function) [:0]const u8 { const zfn = self.ptr(); const fname = zfn.*.common.function_name; if (fname == null) return ""; - return fname.*.val()[0..fname.*.len]; + return fname.*.val()[0..fname.*.len :0]; } /// Call as a global function (no object, no scope). diff --git a/src/zend/object.zig b/src/zend/object.zig index b47a767..8f54a7d 100644 --- a/src/zend/object.zig +++ b/src/zend/object.zig @@ -2,7 +2,6 @@ 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; const Function = @import("function.zig").Function; @@ -408,9 +407,10 @@ pub const Object = opaque { /// Get the case name from an enum case object. /// /// Ownership: borrowed string view owned by the enum case object. - pub fn enumCaseName(self: *Object) []const u8 { + pub fn enumCaseName(self: *Object) [:0]const u8 { const zv = c.zend_enum_fetch_case_name(self.ptr()); - return native.asUnchecked(zv, .string); + const zstr = zv.*.value.str; + return zstr.*.val()[0..zstr.*.len :0]; } /// Get the backing value from a backed enum case, or null if pure enum. diff --git a/src/zend/property_info.zig b/src/zend/property_info.zig index f757025..f732a79 100644 --- a/src/zend/property_info.zig +++ b/src/zend/property_info.zig @@ -1,6 +1,6 @@ const c = @import("../root.zig").c; -const ClassEntry = @import("class_entry.zig").ClassEntry; const Array = @import("array.zig").Array; +const ClassEntry = @import("class_entry.zig").ClassEntry; pub const PropertyInfo = opaque { pub const Error = error{ @@ -27,9 +27,9 @@ pub const PropertyInfo = opaque { } /// Get the property name as a byte slice - pub fn name(self: *PropertyInfo) []const u8 { + pub fn name(self: *PropertyInfo) [:0]const u8 { const n = self.ptr().name; - return n.*.val()[0..n.*.len]; + return n.*.val()[0..n.*.len :0]; } /// Get the declaring class entry. diff --git a/src/zend/string.zig b/src/zend/string.zig index b626dbe..9bf68fa 100644 --- a/src/zend/string.zig +++ b/src/zend/string.zig @@ -43,13 +43,6 @@ pub const String = opaque { return @ptrCast(@alignCast(self)); } - /// Get C string pointer (null-terminated). - /// - /// Ownership: borrowed view into this string. - pub inline fn cstr(self: *String) [*:0]const u8 { - return @ptrCast(self.ptr().val()); - } - /// Release one owned string reference. pub fn release(self: *String) void { c.zend_string_release(self.ptr()); @@ -60,11 +53,18 @@ pub const String = opaque { return self.ptr().len; } - /// Get string as a slice. + /// Get C string pointer (null-terminated). + /// + /// Ownership: borrowed raw pointer. + pub inline fn cstr(self: *String) [*:0]const u8 { + return @ptrCast(self.ptr().val()); + } + + /// Get string as a null-terminated slice. /// /// Ownership: borrowed view into this string. - pub fn slice(self: *String) []const u8 { - return self.cstr()[0..self.len()]; + pub fn slice(self: *String) [:0]const u8 { + return self.cstr()[0..self.len() :0]; } /// Check if string is empty From c644a57c8daf89831570dc84b1003e4faedaee90 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Tue, 14 Jul 2026 02:43:35 +0800 Subject: [PATCH 21/36] chore: update zig and translate-c --- bench/zig_ext/build.zig.zon | 2 +- build.zig.zon | 6 +++--- examples/my_php_extension/build.zig.zon | 2 +- examples/skeleton/build.zig.zon | 2 +- scripts/test-msvc.sh | 2 +- tools/windows_patcher.zig | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bench/zig_ext/build.zig.zon b/bench/zig_ext/build.zig.zon index a6a46a1..40b55fd 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.17.0-dev.1282+c0f9b51d8", + .minimum_zig_version = "0.17.0-dev.1387+01b60634c", .dependencies = .{ .phpz = .{ .path = "../..", diff --git a/build.zig.zon b/build.zig.zon index d2d4e8e..735401f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,12 +2,12 @@ .name = .phpz, .version = "0.0.0", .fingerprint = 0x935004cc3861700f, - .minimum_zig_version = "0.17.0-dev.1282+c0f9b51d8", + .minimum_zig_version = "0.17.0-dev.1387+01b60634c", .minimum_php_version = "8.2", .dependencies = .{ .translate_c = .{ - .url = "git+https://codeberg.org/happystraw/translate-c?ref=vectorcall#a7cd3fd81b074c1318060fa1985098bfda571aa8", - .hash = "translate_c-0.0.0-Q_BUWqlCBwBpiytivz8S8gbHOGHiKuYMQLkySmMO_1Ls", + .url = "git+https://codeberg.org/happystraw/translate-c?ref=fn-callconv-stuff#e487ee52747cf308af3cc26b34e6e1bbae896a17", + .hash = "translate_c-0.0.0-Q_BUWiQ7BwC08JMURgqiXUf_j-jbG-ALr-e3ZHBsw8uj", }, }, .paths = .{ diff --git a/examples/my_php_extension/build.zig.zon b/examples/my_php_extension/build.zig.zon index 61447e8..af0b3c5 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.17.0-dev.1282+c0f9b51d8", + .minimum_zig_version = "0.17.0-dev.1387+01b60634c", .dependencies = .{ .phpz = .{ .path = "../..", diff --git a/examples/skeleton/build.zig.zon b/examples/skeleton/build.zig.zon index e5cc786..b07976d 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.17.0-dev.1282+c0f9b51d8", + .minimum_zig_version = "0.17.0-dev.1387+01b60634c", .dependencies = .{ .phpz = .{ .path = "../..", diff --git a/scripts/test-msvc.sh b/scripts/test-msvc.sh index 41b9a39..f9d2b7c 100755 --- a/scripts/test-msvc.sh +++ b/scripts/test-msvc.sh @@ -95,7 +95,7 @@ if [ ! -d "$win_sdk" ]; then "$xwin" \ --accept-license \ --manifest-version "$vs_version" \ - --arch x86_64,aarch64 \ + --arch x86_64 \ --variant desktop \ --cache-dir "$HOME/.cache/xwin/$vs_toolset" \ splat \ diff --git a/tools/windows_patcher.zig b/tools/windows_patcher.zig index 8ceffee..716bc15 100644 --- a/tools/windows_patcher.zig +++ b/tools/windows_patcher.zig @@ -60,7 +60,7 @@ fn patch( source: [:0]const u8, options: Patcher.Options, ) !void { - var tree = try std.zig.Ast.parse(gpa, source, .zig); + var tree = try std.zig.Ast.parse(gpa, source, .{ .mode = .zig }); defer tree.deinit(gpa); if (tree.errors.len != 0) { From 94916d405c71730f6960183aa3657e984b91c347 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Tue, 14 Jul 2026 02:43:36 +0800 Subject: [PATCH 22/36] refactor(zend): organize bailout APIs --- src/function.zig | 4 +- src/heap.zig | 4 +- src/root.zig | 2 +- src/zend.zig | 19 +----- src/zend/bailout.zig | 136 +++++++++++++++++++++++++++++++++++++++++ src/zend/callable.zig | 8 +-- src/zend/function.zig | 8 +-- src/zend/object.zig | 4 +- src/zend/try_catch.zig | 129 -------------------------------------- 9 files changed, 152 insertions(+), 162 deletions(-) create mode 100644 src/zend/bailout.zig delete mode 100644 src/zend/try_catch.zig diff --git a/src/function.zig b/src/function.zig index c164b92..100455d 100644 --- a/src/function.zig +++ b/src/function.zig @@ -113,7 +113,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 (err == error.ZendBailout or err == error.OutOfMemory) zend.bailout.raise(); if (!errors.hasException()) { errors.throwError(null, "%s at %s", .{ @errorName(err).ptr, func_desc.ptr }); } @@ -152,7 +152,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 (err == error.ZendBailout or err == error.OutOfMemory) zend.bailout.raise(); 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 f01b3bd..d02f56b 100644 --- a/src/heap.zig +++ b/src/heap.zig @@ -63,7 +63,7 @@ const php_allocator_impl = struct { .len = len, .return_address = return_address, }; - zend.tryCatchRaw(Context.call, &ctx) catch return null; + zend.bailout.runRaw(Context.call, &ctx) catch return null; return @ptrCast(ctx.result); } @@ -102,7 +102,7 @@ const php_allocator_impl = struct { .new_len = new_len, .return_address = return_address, }; - zend.tryCatchRaw(Context.call, &ctx) catch return null; + zend.bailout.runRaw(Context.call, &ctx) catch return null; return @ptrCast(ctx.result); } diff --git a/src/root.zig b/src/root.zig index f5bddad..af9981a 100644 --- a/src/root.zig +++ b/src/root.zig @@ -51,7 +51,7 @@ test { _ = @import("zend/object.zig"); _ = @import("zend/property_info.zig"); _ = @import("zend/string.zig"); - _ = @import("zend/try_catch.zig"); + _ = @import("zend/bailout.zig"); _ = @import("zval/array.zig"); _ = @import("zval/object.zig"); } diff --git a/src/zend.zig b/src/zend.zig index d50d087..2339adb 100644 --- a/src/zend.zig +++ b/src/zend.zig @@ -1,7 +1,7 @@ const std = @import("std"); -const c = @import("c.zig").c; pub const Array = @import("zend/array.zig").Array; +pub const bailout = @import("zend/bailout.zig"); pub const Callable = @import("zend/callable.zig").Callable; pub const ClassEntry = @import("zend/class_entry.zig").ClassEntry; pub const Function = @import("zend/function.zig").Function; @@ -10,23 +10,6 @@ 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/bailout.zig b/src/zend/bailout.zig new file mode 100644 index 0000000..4ab94c5 --- /dev/null +++ b/src/zend/bailout.zig @@ -0,0 +1,136 @@ +const std = @import("std"); +const c = @import("../c.zig").c; + +pub const Error = error{ZendBailout}; +pub const RawCallback = *const fn (?*anyopaque) callconv(.c) void; + +pub inline fn raise() noreturn { + c.phpz_zend_bailout(); + unreachable; +} + +pub inline fn runRaw(callback: RawCallback, context: ?*anyopaque) Error!void { + if (c.phpz_zend_try_catch(callback, context)) return error.ZendBailout; +} + +pub inline fn runFirstRaw(callback: RawCallback, context: ?*anyopaque) Error!void { + if (c.phpz_zend_first_try_catch(callback, context)) return error.ZendBailout; +} + +pub inline fn run( + comptime Return: type, + comptime Context: type, + context: *Context, + comptime callback: fn (*Context) Return, +) Error!Return { + return runImpl(false, Return, Context, context, callback); +} + +pub inline fn runFirst( + comptime Return: type, + comptime Context: type, + context: *Context, + comptime callback: fn (*Context) Return, +) Error!Return { + return runImpl(true, Return, Context, context, callback); +} + +pub inline fn runSimple(comptime callback: fn () void) Error!void { + return runSimpleImpl(false, callback); +} + +pub inline fn runFirstSimple(comptime callback: fn () void) Error!void { + return runSimpleImpl(true, callback); +} + +inline fn runImpl( + comptime first: bool, + comptime Return: type, + comptime Context: type, + context: *Context, + comptime callback: fn (*Context) Return, +) Error!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 runFirstRaw(Wrapper.call, &frame) + else + try runRaw(Wrapper.call, &frame); + + if (comptime Return == void) return; + return frame.result; +} + +inline fn runSimpleImpl( + comptime first: bool, + comptime callback: fn () void, +) Error!void { + const Wrapper = struct { + fn call(_: ?*anyopaque) callconv(.c) void { + callback(); + } + }; + + if (comptime first) + try runFirstRaw(Wrapper.call, null) + else + try runRaw(Wrapper.call, null); +} + +test { + std.testing.refAllDecls(@This()); +} + +test "run 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(run(void, Context, undefined, Context.callback)) == Error!void); + try std.testing.expect(@TypeOf(runFirst(void, Context, undefined, Context.callback)) == Error!void); + try std.testing.expect(@TypeOf(run(u32, Context, undefined, Context.callbackValue)) == Error!u32); + try std.testing.expect(@TypeOf(runFirst(u32, Context, undefined, Context.callbackValue)) == Error!u32); +} + +test "raw API" { + const Callback = struct { + fn callback(_: ?*anyopaque) callconv(.c) void {} + }; + + try std.testing.expect(@TypeOf(runRaw(Callback.callback, null)) == Error!void); + try std.testing.expect(@TypeOf(runFirstRaw(Callback.callback, null)) == Error!void); +} + +test "simple API" { + const Callback = struct { + fn callback() void {} + }; + + try std.testing.expect(@TypeOf(runSimple(Callback.callback)) == Error!void); + try std.testing.expect(@TypeOf(runFirstSimple(Callback.callback)) == Error!void); +} diff --git a/src/zend/callable.zig b/src/zend/callable.zig index c4c0876..1cec54c 100644 --- a/src/zend/callable.zig +++ b/src/zend/callable.zig @@ -4,7 +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"); +const bailout = @import("bailout.zig"); /// Parsed callable ready for invocation. /// @@ -81,7 +81,7 @@ pub const Callable = extern struct { } pub const CallError = error{ CallFailed, PhpException }; - pub const TryCallError = CallError || try_catch.TryCatchError; + pub const TryCallError = CallError || bailout.Error; /// Invoke the callable with positional arguments (comptime tuple of `c.zval`). /// @@ -162,7 +162,7 @@ pub const Callable = extern struct { 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); + break :blk try bailout.run(CallResult, CallFrame, &frame, CallFrame.call); }, else => blk: { var arr: [n]c.zval = undefined; @@ -170,7 +170,7 @@ pub const Callable = extern struct { 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); + break :blk try bailout.run(CallResult, CallFrame, &frame, CallFrame.call); }, }; diff --git a/src/zend/function.zig b/src/zend/function.zig index c6b457e..66933f3 100644 --- a/src/zend/function.zig +++ b/src/zend/function.zig @@ -4,13 +4,13 @@ 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"); +const bailout = @import("bailout.zig"); pub const Function = opaque { pub const Error = error{ PhpException, }; - pub const TryCallError = Error || try_catch.TryCatchError; + pub const TryCallError = Error || bailout.Error; /// Function type classification. pub const Kind = enum(@TypeOf(c.ZEND_INTERNAL_FUNCTION)) { @@ -172,7 +172,7 @@ pub const Function = opaque { .scope = scope, .retval = retval, }; - try try_catch.tryCatchTyped(void, CallFrame, &frame, CallFrame.call); + try bailout.run(void, CallFrame, &frame, CallFrame.call); }, else => { var arr: [n]c.zval = undefined; @@ -195,7 +195,7 @@ pub const Function = opaque { .retval = retval, .params = &arr, }; - try try_catch.tryCatchTyped(void, CallFrame, &frame, CallFrame.call); + try bailout.run(void, CallFrame, &frame, CallFrame.call); }, } if (errors.hasException()) return error.PhpException; diff --git a/src/zend/object.zig b/src/zend/object.zig index 8f54a7d..2302292 100644 --- a/src/zend/object.zig +++ b/src/zend/object.zig @@ -6,7 +6,7 @@ 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"); +const bailout = @import("bailout.zig"); pub const Object = opaque { pub const InitError = error{InitFailed}; @@ -375,7 +375,7 @@ pub const Object = opaque { .retval = retval, .params = params, }; - const result = try try_catch.tryCatchTyped(CallResult, CallFrame, &frame, CallFrame.call); + const result = try bailout.run(CallResult, CallFrame, &frame, CallFrame.call); if (result == c.FAILURE) return error.MethodCallFailed; if (errors.hasException()) return error.PhpException; } diff --git a/src/zend/try_catch.zig b/src/zend/try_catch.zig deleted file mode 100644 index 8879df2..0000000 --- a/src/zend/try_catch.zig +++ /dev/null @@ -1,129 +0,0 @@ -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 30002b30e409a20a6f5204689f6b056fcd925a66 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Tue, 14 Jul 2026 02:43:37 +0800 Subject: [PATCH 23/36] feat(observer): add Zend observer APIs --- build/phpz.h | 1 + src/Ctx.zig | 8 +++ src/errors.zig | 15 ++++ src/module.zig | 31 +++++++-- src/observer.zig | 175 +++++++++++++++++++++++++++++++++++++++++++++++ src/root.zig | 2 + 6 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 src/observer.zig diff --git a/build/phpz.h b/build/phpz.h index 124ba9f..fe5835a 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 "Zend/zend_observer.h" #include "ext/standard/file.h" #include "ext/standard/info.h" #include "main/SAPI.h" diff --git a/src/Ctx.zig b/src/Ctx.zig index 28c097b..1485c03 100644 --- a/src/Ctx.zig +++ b/src/Ctx.zig @@ -54,6 +54,14 @@ pub const Call = opaque { return @ptrCast(@alignCast(self)); } + /// Get the function executed by this call frame. + /// + /// Ownership: borrowed function pointer owned by Zend. + pub inline fn function(self: *Call) ?*zend.Function { + const fn_ptr = self.ptr().func orelse return null; + return .from(fn_ptr); + } + /// Get the number of arguments passed to the current function/method. /// /// Returns: diff --git a/src/errors.zig b/src/errors.zig index cb0ccfb..50d6c5c 100644 --- a/src/errors.zig +++ b/src/errors.zig @@ -38,6 +38,21 @@ pub const Level = enum(c_int) { /// E_USER_DEPRECATED: User-triggered deprecation notice user_deprecated = c.E_USER_DEPRECATED, _, + + /// Return whether this value contains any bit in `mask`. + pub inline fn matches(self: Level, mask: c_int) bool { + return (@intFromEnum(self) & mask) != 0; + } + + /// Return whether this value contains a core error-level bit. + pub inline fn isCore(self: Level) bool { + return self.matches(c.E_CORE); + } + + /// Return whether this value contains a fatal error-level bit. + pub inline fn isFatal(self: Level) bool { + return self.matches(c.E_FATAL_ERRORS); + } }; pub const ArgumentTypeError = error{PhpArgumentTypeError}; diff --git a/src/module.zig b/src/module.zig index 8959830..e5a3e3c 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"); +const observer = @import("observer.zig"); /// Configuration for creating a PHP extension module. /// @@ -37,6 +38,10 @@ pub const Config = struct { /// In ZTS builds, each TSRM thread receives a separate instance. globals: ?type = null, + /// Observer configuration for profiling, error monitoring, and exception tracking. + /// Each observer type must implement specific methods. + observer: ?observer.Config = 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. @@ -140,15 +145,16 @@ fn makePhpModuleStartupFn( comptime hook_fn: ?PhpHookFn, comptime ini_defs: ?[]const c.zend_ini_entry_def, comptime classes: ?[]const type, + comptime observer_cfg: ?observer.Config, ) ?*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 and classes == null) return null; + const has_observers = comptime if (observer_cfg) |o| o.fcall != null or o.errors != null or o.exception != null else false; + if (!has_symbols and hook_fn == null and ini_defs == null and classes == null and !has_observers) return null; return struct { 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); } if (comptime ini_defs) |defs| { @@ -162,6 +168,9 @@ fn makePhpModuleStartupFn( Class.register(); } } + if (comptime observer_cfg) |cfg| { + observer.startup(cfg); + } if (comptime hook_fn) |f| { f() catch |err| { errors.err(.err, "Module startup function failed: %s", .{@errorName(err).ptr}); @@ -173,11 +182,21 @@ fn makePhpModuleStartupFn( }.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; +fn makePhpModuleShutdownFn( + comptime hook_fn: ?PhpHookFn, + comptime ini_defs: ?[]const c.zend_ini_entry_def, + comptime observer_cfg: ?observer.Config, +) ?*const fn (c_int, c_int) callconv(.c) c.zend_result { + const has_observer_shutdown = comptime if (observer_cfg) |cfg| cfg.exception != null else false; + if (hook_fn == null and ini_defs == null and !has_observer_shutdown) return null; return struct { fn handle(module_type: c_int, module_number: c_int) callconv(.c) c.zend_result { _ = module_type; + defer { + if (comptime observer_cfg) |cfg| { + observer.shutdown(cfg); + } + } if (comptime ini_defs != null) { c.zend_unregister_ini_entries(module_number); } @@ -217,8 +236,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, registered_ini_defs, cfg.classes); - entry.module_shutdown_func = makePhpModuleShutdownFn(cfg.module_shutdown_fn, registered_ini_defs); + entry.module_startup_func = makePhpModuleStartupFn(cfg.name, cfg.module_startup_fn, registered_ini_defs, cfg.classes, cfg.observer); + entry.module_shutdown_func = makePhpModuleShutdownFn(cfg.module_shutdown_fn, registered_ini_defs, cfg.observer); 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; diff --git a/src/observer.zig b/src/observer.zig new file mode 100644 index 0000000..deff801 --- /dev/null +++ b/src/observer.zig @@ -0,0 +1,175 @@ +const std = @import("std"); + +const c = @import("root.zig").c; +const Ctx = @import("Ctx.zig"); +const errors = @import("errors.zig"); +const zend = @import("zend.zig"); +const Zval = @import("zval.zig").Zval; + +/// Function-call observer callbacks. +/// +/// `filter` runs when Zend first installs handlers for a function. Zend caches +/// the result, so it must not depend on per-call or mutable request state. +pub const Fcall = struct { + filter: ?fn (*zend.Function) bool = null, + begin: ?fn (*Ctx.Call) void = null, + end: ?fn (*Ctx.Call, ?*Zval) void = null, +}; + +/// Error observer callbacks. +pub const Error = struct { + filter: ?fn (errors.Level) bool = null, + observe: fn (Info) void, + + /// Borrowed error data passed to an error observer. + /// + /// `filename` and `message` are valid only for the duration of the callback. + pub const Info = struct { + message: [:0]const u8, + file: ?[:0]const u8, + line: u32, + level: errors.Level, + }; +}; + +/// Exception observer callback. +/// +/// The exception object is borrowed and is valid for the duration of the +/// callback. Use `addref()` if it must be retained. +pub const Exception = struct { + observe: fn (*zend.Object) void, +}; + +/// Observer configuration registered during module startup. +pub const Config = struct { + fcall: ?Fcall = null, + errors: ?Error = null, + exception: ?Exception = null, +}; + +fn FcallAdapter(comptime config: Fcall) type { + if (config.begin == null and config.end == null) { + @compileError("observer fcall requires at least one begin or end callback"); + } + + return struct { + fn init(execute_data: ?*c.zend_execute_data) callconv(.c) c.zend_observer_fcall_handlers { + const call = Ctx.Call.from(execute_data.?); + const function = call.function() orelse return .{ .begin = null, .end = null }; + if (config.filter) |filter| { + if (!filter(function)) return .{ .begin = null, .end = null }; + } + return .{ + .begin = if (config.begin != null) &begin else null, + .end = if (config.end != null) &end else null, + }; + } + + fn begin(execute_data: ?*c.zend_execute_data) callconv(.c) void { + if (config.begin) |callback| { + callback(.from(execute_data.?)); + } + } + + fn end(execute_data: ?*c.zend_execute_data, retval: ?*c.zval) callconv(.c) void { + if (config.end) |callback| { + callback( + .from(execute_data.?), + if (retval) |zv| .from(zv) else null, + ); + } + } + + fn register() void { + c.zend_observer_fcall_register(&init); + } + }; +} + +fn ErrorAdapter(comptime config: Error) type { + return struct { + fn stringSlice(raw: ?*c.zend_string) ?[:0]const u8 { + if (raw == null) return null; + return zend.String.from(@ptrCast(raw)).slice(); + } + + fn callback( + error_type: c_int, + error_filename: ?*c.zend_string, + error_line: u32, + message: ?*c.zend_string, + ) callconv(.c) void { + const level: errors.Level = @enumFromInt(error_type); + if (config.filter) |filter| { + if (!filter(level)) return; + } + config.observe(.{ + .level = level, + .file = if (error_filename) |s| zend.String.slice(.from(s)) else null, + .line = error_line, + .message = if (message) |s| zend.String.slice(.from(s)) else "", + }); + } + + fn register() void { + c.zend_observer_error_register(&callback); + } + }; +} + +fn ExceptionAdapter(comptime config: Exception) type { + return struct { + const Hook = *const fn (?*c.zend_object) callconv(.c) void; + + var previous: ?Hook = null; + var installed = false; + + fn callback(exception: ?*c.zend_object) callconv(.c) void { + config.observe(zend.Object.from(exception.?)); + if (previous) |previous_hook| { + previous_hook(exception); + } + } + + fn register() void { + if (installed) return; + previous = c.zend_throw_exception_hook; + c.zend_throw_exception_hook = &callback; + installed = true; + } + + fn unregister() void { + if (installed and c.zend_throw_exception_hook == &callback) { + c.zend_throw_exception_hook = previous; + } + installed = false; + previous = null; + } + }; +} + +/// Register configured observers during MINIT. +pub fn startup(comptime config: Config) void { + if (comptime config.fcall) |fcall| { + FcallAdapter(fcall).register(); + } + if (comptime config.errors) |error_config| { + ErrorAdapter(error_config).register(); + } + if (comptime config.exception) |exception| { + ExceptionAdapter(exception).register(); + } +} + +/// Restore hooks owned by this module during MSHUTDOWN. +/// +/// Zend does not expose unregister functions for fcall or error observers. +pub fn shutdown(comptime config: Config) void { + if (comptime config.exception) |exception| { + ExceptionAdapter(exception).unregister(); + } +} + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/root.zig b/src/root.zig index af9981a..05f4326 100644 --- a/src/root.zig +++ b/src/root.zig @@ -6,6 +6,7 @@ pub const globals = @import("globals.zig"); pub const heap = @import("heap.zig"); pub const ini = @import("ini.zig"); pub const info = @import("info.zig"); +pub const observer = @import("observer.zig"); const mod_helper = @import("module.zig"); pub const module = mod_helper.module; @@ -42,6 +43,7 @@ test { _ = @import("ini.zig"); _ = @import("info.zig"); _ = @import("module.zig"); + _ = @import("observer.zig"); _ = @import("zend.zig"); _ = @import("zval.zig"); _ = @import("zend/array.zig"); From a45998eeadb792f721284cf615c0efffcb6a1b4e Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Tue, 14 Jul 2026 02:43:38 +0800 Subject: [PATCH 24/36] refactor(zval): rename native namespace to raw --- src/Ctx.zig | 35 +++++++++++++++++------------------ src/class.zig | 5 ++--- src/zend/array.zig | 22 +++++++++++----------- src/zend/callable.zig | 16 ++++++++-------- src/zend/class_entry.zig | 2 +- src/zend/object.zig | 2 +- src/zval.zig | 40 ++++++++++++++++++++-------------------- src/zval/array.zig | 6 +++--- src/zval/object.zig | 6 +++--- 9 files changed, 66 insertions(+), 68 deletions(-) diff --git a/src/Ctx.zig b/src/Ctx.zig index 1485c03..bb966fc 100644 --- a/src/Ctx.zig +++ b/src/Ctx.zig @@ -6,7 +6,6 @@ const errors = @import("errors.zig"); const zend = @import("zend.zig"); const ClassEntry = @import("zend/class_entry.zig").ClassEntry; const Zval = @import("zval.zig").Zval; -const native = Zval.native; /// Provides access to the current call frame and return value. /// Passed as `Ctx` to user-defined PHP function/method bindings. @@ -79,7 +78,7 @@ pub const Call = opaque { /// Pointer to the zval at position n pub inline fn arg(self: *Call, n: u32) *c.zval { const base: [*]c.zval = @ptrCast(self.ptr()); - return &base[c.ZEND_CALL_FRAME_SLOT + n - 1]; + return &base[@as(usize, @intCast(c.ZEND_CALL_FRAME_SLOT)) + @as(usize, n) - 1]; } /// Get all arguments as a slice of zvals. @@ -423,18 +422,18 @@ pub const Call = opaque { if (php_union_type.len > 0) php_union_type = php_union_type ++ "|"; php_union_type = php_union_type ++ @tagName(kind); const zk = comptime kind.toZvalKind(); - if (native.is(zv, zk)) { - return @unionInit(Union, @tagName(kind), if (zk == .null) {} else native.asUnchecked(zv, zk)); + if (Zval.raw.is(zv, zk)) { + return @unionInit(Union, @tagName(kind), if (zk == .null) {} else Zval.raw.asUnchecked(zv, zk)); } } - return errors.argumentTypeError(n, "must be of type " ++ php_union_type ++ ", %s given", .{native.kind(zv).cstr()}); + return errors.argumentTypeError(n, "must be of type " ++ php_union_type ++ ", %s given", .{Zval.raw.kind(zv).cstr()}); } else { return zv; } }, .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.nullable) if (Zval.raw.is(zv, .null)) return .null; if (comptime s.resolve) { var err: ?[*:0]u8 = null; runtime.target.parse(zv, false, &err) catch { @@ -450,34 +449,34 @@ pub const Call = opaque { return if (comptime s.nullable) .{ .value = zv } else zv; }, .reference => |s| { - return if (native.is(zv, .reference)) - if (comptime s.zval) zv else native.asUnchecked(zv, .reference) + return if (Zval.raw.is(zv, .reference)) + if (comptime s.zval) zv else Zval.raw.asUnchecked(zv, .reference) else - errors.argumentTypeError(n, "must be of type reference, %s given", .{native.kind(zv).cstr()}); + errors.argumentTypeError(n, "must be of type reference, %s given", .{Zval.raw.kind(zv).cstr()}); }, .object => |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.nullable) if (Zval.raw.is(zv, .null)) return .null; if (comptime s.instanceof) { 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() }); + const obj: *zend.Object = Zval.raw.as(zv, .object) catch return errors.argumentTypeError(n, "must be instance of %s" ++ or_null ++ ", %s given", .{ expected_type.name().ptr, Zval.raw.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)) { - return errors.argumentTypeError(n, "must be of type object" ++ or_null ++ ", %s given", .{native.kind(zv).cstr()}); + } else if (!Zval.raw.is(zv, .object)) { + return errors.argumentTypeError(n, "must be of type object" ++ or_null ++ ", %s given", .{Zval.raw.kind(zv).cstr()}); } - const raw = if (comptime s.zval) zv else native.asUnchecked(zv, .object); + const raw = if (comptime s.zval) zv else Zval.raw.asUnchecked(zv, .object); return if (comptime s.nullable) .{ .value = raw } else raw; }, inline else => |s| { const tag: ExpectArgKind = spec; const or_null = comptime if (s.nullable) " or null" else ""; - if (comptime s.nullable) if (native.is(zv, .null)) return .null; + if (comptime s.nullable) if (Zval.raw.is(zv, .null)) return .null; const zk = comptime tag.toZvalKind(); - if (!native.is(zv, zk)) - return errors.argumentTypeError(n, "must be of type " ++ @tagName(tag) ++ or_null ++ ", %s given", .{native.kind(zv).cstr()}); - const raw = if (comptime s.zval) zv else native.asUnchecked(zv, zk); + if (!Zval.raw.is(zv, zk)) + return errors.argumentTypeError(n, "must be of type " ++ @tagName(tag) ++ or_null ++ ", %s given", .{Zval.raw.kind(zv).cstr()}); + const raw = if (comptime s.zval) zv else Zval.raw.asUnchecked(zv, zk); return if (comptime s.nullable) .{ .value = raw } else raw; }, } diff --git a/src/class.zig b/src/class.zig index 08a9ac1..24ef60a 100644 --- a/src/class.zig +++ b/src/class.zig @@ -16,7 +16,6 @@ const phpz = @import("root.zig"); const c = phpz.c; const zend = @import("zend.zig"); const Zval = @import("zval.zig").Zval; -const native = Zval.native; /// PHP object handler overrides. /// @@ -446,7 +445,7 @@ pub fn Class(comptime class_name: [:0]const u8, comptime T: type) type { .undef, .indirect, .ptr => @compileError("'" ++ @tagName(zk) ++ "' cannot be set as object property"), inline else => { var zv: c.zval = undefined; - native.set(&zv, zk, prop_value); + Zval.raw.set(&zv, zk, prop_value); c.zend_update_property(ce, &self.std, prop_name.ptr, prop_name.len, &zv); }, } @@ -457,7 +456,7 @@ pub fn Class(comptime class_name: [:0]const u8, comptime T: type) type { /// /// Ownership: returned pointer is either borrowed from the object/runtime /// or points at caller-provided scratch. Never dtor the returned pointer - /// directly; use `Zval.native.tryDtor(scratch)` for scratch cleanup. + /// directly; use `Zval.raw.tryDtor(scratch)` for scratch cleanup. /// /// Returns `error.PhpException` if a magic `__get` handler throws. /// diff --git a/src/zend/array.zig b/src/zend/array.zig index f38907b..9283b4e 100644 --- a/src/zend/array.zig +++ b/src/zend/array.zig @@ -1,5 +1,5 @@ const c = @import("../root.zig").c; -const native = @import("../zval.zig").Zval.native; +const Zval = @import("../zval.zig").Zval; pub const Array = opaque { pub const Error = error{ @@ -262,7 +262,7 @@ pub const Array = opaque { /// var sum: i64 = 0; /// arr.eachValue(&sum, struct { /// fn body(zv: *c.zval, s: *i64) void { - /// if (native.is(zv, .int)) s.* += native.asUnchecked(zv, .int); + /// if (Zval.raw.is(zv, .int)) s.* += Zval.raw.asUnchecked(zv, .int); /// } /// }.body); /// ``` @@ -282,7 +282,7 @@ pub const Array = opaque { el += el_size; // IS_UNDEF check in same scope as body → compiler can eliminate redundancy - if (native.is(zv, .undef)) { + if (Zval.raw.is(zv, .undef)) { @branchHint(.unlikely); continue; } @@ -334,7 +334,7 @@ pub const Array = opaque { el += el_size; const key: Key = .{ .int = @as(isize, @intCast(idx)) }; idx += 1; - if (native.is(zv, .undef)) { + if (Zval.raw.is(zv, .undef)) { @branchHint(.unlikely); continue; // IS_UNDEF check } @@ -355,7 +355,7 @@ pub const Array = opaque { .{ .string = k.*.val()[0..k.*.len] } else .{ .int = @bitCast(bucket.h) }; - if (native.is(zv, .undef)) { + if (Zval.raw.is(zv, .undef)) { @branchHint(.unlikely); continue; // IS_UNDEF check } @@ -596,7 +596,7 @@ pub const Array = opaque { const zv: *c.zval = @ptrCast(@alignCast(self.el)); self.el += self.el_size; self.pos += 1; - if (native.is(zv, .undef)) { + if (Zval.raw.is(zv, .undef)) { @branchHint(.unlikely); continue; } @@ -615,7 +615,7 @@ pub const Array = opaque { const zv: *c.zval = @ptrCast(@alignCast(e)); e += self.el_size; p += 1; - if (native.is(zv, .undef)) { + if (Zval.raw.is(zv, .undef)) { @branchHint(.unlikely); continue; } @@ -668,7 +668,7 @@ pub const Array = opaque { self.pos += 1; const key: Key = .{ .int = @as(isize, @intCast(self.idx)) }; self.idx += 1; - if (native.is(zv, .undef)) { + if (Zval.raw.is(zv, .undef)) { @branchHint(.unlikely); continue; } @@ -684,7 +684,7 @@ pub const Array = opaque { .{ .string = k.*.val()[0..k.*.len] } else .{ .int = @bitCast(bucket.h) }; - if (native.is(zv, .undef)) { + if (Zval.raw.is(zv, .undef)) { @branchHint(.unlikely); continue; } @@ -708,7 +708,7 @@ pub const Array = opaque { p += 1; const key: Key = .{ .int = @as(isize, @intCast(i)) }; i += 1; - if (native.is(zv, .undef)) { + if (Zval.raw.is(zv, .undef)) { @branchHint(.unlikely); continue; } @@ -724,7 +724,7 @@ pub const Array = opaque { .{ .string = k.*.val()[0..k.*.len] } else .{ .int = @bitCast(bucket.h) }; - if (native.is(zv, .undef)) { + if (Zval.raw.is(zv, .undef)) { @branchHint(.unlikely); continue; } diff --git a/src/zend/callable.zig b/src/zend/callable.zig index 1cec54c..fd29c46 100644 --- a/src/zend/callable.zig +++ b/src/zend/callable.zig @@ -2,7 +2,7 @@ const std = @import("std"); const errors = @import("../errors.zig"); const c = @import("../root.zig").c; -const native = @import("../zval.zig").Zval.native; +const Zval = @import("../zval.zig").Zval; const Object = @import("object.zig").Object; const bailout = @import("bailout.zig"); @@ -53,7 +53,7 @@ pub const Callable = extern struct { /// (e.g. "function 'xxx' not found"). Pass `null` to discard it. pub fn parse(self: *Callable, zv: *c.zval, comptime nullable: bool, err: ?*?[*:0]u8) ParseError!void { if (err) |e| e.* = null; - if (nullable and native.is(zv, .null)) { + if (nullable and Zval.raw.is(zv, .null)) { self.fci.size = 0; self.fcc.function_handler = null; return; @@ -70,7 +70,7 @@ pub const Callable = extern struct { /// Check whether a zval contains a callable. /// When `nullable` is true, a null zval is also accepted. pub fn isCallable(zv: *c.zval, comptime nullable: bool) bool { - if (comptime nullable) if (native.is(zv, .null)) return true; + if (comptime nullable) if (Zval.raw.is(zv, .null)) return true; return c.zend_is_callable(zv, 0, null); } @@ -95,7 +95,7 @@ pub const Callable = extern struct { var discard: c.zval = undefined; const owns_retval = self.fci.retval == null; if (owns_retval) self.fci.retval = &discard; - defer if (owns_retval) native.dtor(&discard); + defer if (owns_retval) Zval.raw.dtor(&discard); const n = info.@"struct".field_types.len; switch (n) { @@ -135,7 +135,7 @@ pub const Callable = extern struct { const saved_params = self.fci.params; const saved_named_params = self.fci.named_params; - var discard: c.zval = native.undef; + var discard: c.zval = Zval.raw.undef; const owns_retval = saved_retval == null; if (owns_retval) self.fci.retval = &discard; defer { @@ -143,7 +143,7 @@ pub const Callable = extern struct { 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); + if (owns_retval) Zval.raw.tryDtor(&discard); } const n = info.@"struct".field_types.len; @@ -181,13 +181,13 @@ pub const Callable = extern struct { /// Increment refcounts on `function_name` and `fcc.object`. /// Prevents premature destruction when the callable is retained. pub inline fn addref(self: *Callable) void { - native.tryAddref(&self.fci.function_name); + Zval.raw.tryAddref(&self.fci.function_name); if (self.fcc.object) |obj| Object.addref(.from(obj)); } /// Decrement refcounts on `function_name` and `fcc.object`. pub inline fn delref(self: *Callable) void { - native.dtor(&self.fci.function_name); + Zval.raw.dtor(&self.fci.function_name); if (self.fcc.object) |obj| Object.release(.from(obj)); } diff --git a/src/zend/class_entry.zig b/src/zend/class_entry.zig index 81da752..06c4a65 100644 --- a/src/zend/class_entry.zig +++ b/src/zend/class_entry.zig @@ -135,7 +135,7 @@ pub const ClassEntry = opaque { .undef, .indirect, .ptr => @compileError("'" ++ @tagName(zk) ++ "' cannot be set as static property"), inline else => blk: { var zv: c.zval = undefined; - Zval.native.set(&zv, zk, prop_value); + Zval.raw.set(&zv, zk, prop_value); break :blk c.zend_update_static_property(ce, prop_name.ptr, prop_name.len, &zv); }, }; diff --git a/src/zend/object.zig b/src/zend/object.zig index 2302292..40ddefd 100644 --- a/src/zend/object.zig +++ b/src/zend/object.zig @@ -159,7 +159,7 @@ pub const Object = opaque { /// /// Ownership: returned pointer is either borrowed from the object/runtime or /// points at caller-provided scratch. Never dtor the returned pointer - /// directly; use `Zval.native.tryDtor(scratch)` for scratch cleanup. + /// directly; use `Zval.raw.tryDtor(scratch)` for scratch cleanup. /// /// Returns `error.PhpException` if a magic `__get` handler throws. pub fn readProperty( diff --git a/src/zval.zig b/src/zval.zig index 9d91601..bcf5f82 100644 --- a/src/zval.zig +++ b/src/zval.zig @@ -156,7 +156,7 @@ pub const Zval = opaque { /// } /// ``` pub fn kind(self: *Zval) Kind { - return native.kind(self.ptr()); + return raw.kind(self.ptr()); } /// Check if this zval is of a specific type. @@ -182,7 +182,7 @@ pub const Zval = opaque { /// } /// ``` pub fn is(self: *Zval, comptime zk: Kind) bool { - return native.is(self.ptr(), zk); + return raw.is(self.ptr(), zk); } /// Convert this zval to a Zig value with type checking. @@ -208,7 +208,7 @@ pub const Zval = opaque { /// }; /// ``` pub fn as(self: *Zval, comptime zk: Kind) Error!Type(zk) { - return native.as(self.ptr(), zk); + return raw.as(self.ptr(), zk); } /// Convert this zval to a Zig value without type checking. @@ -237,7 +237,7 @@ pub const Zval = opaque { /// const num = zval.asUnchecked(.int); // Undefined behavior if not an int! /// ``` pub fn asUnchecked(self: *Zval, comptime zk: Kind) Type(zk) { - return native.asUnchecked(self.ptr(), zk); + return raw.asUnchecked(self.ptr(), zk); } /// Convert this zval to a Zig value, or return a default value on type mismatch. @@ -303,23 +303,23 @@ pub const Zval = opaque { /// independently owned elsewhere. Destroy any previous zval contents before /// overwriting them. pub fn set(self: *Zval, comptime zk: Kind, val: Type(zk)) void { - native.set(self.ptr(), zk, val); + raw.set(self.ptr(), zk, val); } /// Increment the refcount of this zval's refcounted payload. /// /// Ownership: caller owns the added reference and must `dtor`/delref it. pub inline fn addref(self: *Zval) void { - native.addref(self.ptr()); + raw.addref(self.ptr()); } /// Destroy one owned zval value. pub inline fn dtor(self: *Zval) void { - native.dtor(self.ptr()); + raw.dtor(self.ptr()); } pub inline fn refcount(self: *Zval) u32 { - return native.refcount(self.ptr()); + return raw.refcount(self.ptr()); } /// Optional zval wrapper for handling nullable PHP parameters. @@ -410,12 +410,12 @@ pub const Zval = opaque { /// var raw: *c.zval = undefined; /// try ctx.call.parseArgs("z", .{&raw}); /// - /// if (Zval.native.is(raw, .int)) { - /// const n = Zval.native.asUnchecked(raw, .int); + /// if (Zval.raw.is(raw, .int)) { + /// const n = Zval.raw.asUnchecked(raw, .int); /// } - /// Zval.native.set(raw, .null, {}); + /// Zval.raw.set(raw, .null, {}); /// ``` - pub const native = struct { + pub const raw = struct { pub const undef: c.zval = init(.undef, {}); pub const nil: c.zval = init(.null, {}); @@ -425,7 +425,7 @@ pub const Zval = opaque { /// `dtor()`/`tryDtor()` when the value is refcounted and not transferred. pub fn init(comptime zk: Kind, val: Type(zk)) c.zval { var z: c.zval = undefined; - native.set(&z, zk, val); + raw.set(&z, zk, val); return z; } @@ -458,7 +458,7 @@ pub const Zval = opaque { /// /// Example: /// ```zig - /// const type_name = Zval.native.getTypeName(zv); + /// const type_name = Zval.raw.getTypeName(zv); /// std.debug.print("Got type: {s}\n", .{type_name}); /// ``` pub fn getTypeName(zv: *c.zval) [*:0]const u8 { @@ -509,8 +509,8 @@ pub const Zval = opaque { /// Ownership: scalar values are copied. Returned slices/wrappers/pointers /// are borrowed from `zv`; addref/copy before storing beyond `zv`. pub fn as(zv: *c.zval, comptime zk: Kind) Error!Type(zk) { - if (!native.is(zv, zk)) return Error.TypeMismatch; - return native.asUnchecked(zv, zk); + if (!raw.is(zv, zk)) return Error.TypeMismatch; + return raw.asUnchecked(zv, zk); } /// Convert a raw zval to a Zig value without type checking. @@ -520,7 +520,7 @@ pub const Zval = opaque { pub fn asUnchecked(zv: *c.zval, comptime zk: Kind) Type(zk) { return switch (zk) { .undef, .null => @compileError(std.fmt.comptimePrint( - "'{s}' has no value to convert - use 'Zval.is/Zval.native.is(.{s})' to check the type instead", + "'{s}' has no value to convert - use 'Zval.is/Zval.raw.is(.{s})' to check the type instead", .{ @tagName(zk), @tagName(zk) }, )), .int => zv.value.lval, @@ -587,7 +587,7 @@ pub const Zval = opaque { zv.value.ref = val.ptr(); zv.u1.type_info = c.IS_REFERENCE_EX; }, - .mixed => native.setZval(zv, val, true, false), + .mixed => raw.setZval(zv, val, true, false), .indirect => @compileError("IS_INDIRECT is an internal type, cannot be set directly"), .ptr => @compileError("IS_PTR is an internal type, cannot be set directly"), } @@ -609,7 +609,7 @@ pub const Zval = opaque { /// /// Ownership: caller-provided scratch cleanup helper. pub inline fn tryDtor(z: *c.zval) void { - if (!native.is(z, .undef)) native.dtor(z); + if (!raw.is(z, .undef)) raw.dtor(z); } /// Increment the refcount only when the zval is refcounted. /// @@ -630,5 +630,5 @@ pub const Zval = opaque { test { std.testing.refAllDecls(Zval); std.testing.refAllDecls(Zval.Optional); - std.testing.refAllDecls(Zval.native); + std.testing.refAllDecls(Zval.raw); } diff --git a/src/zval/array.zig b/src/zval/array.zig index aaa2fb6..ce11378 100644 --- a/src/zval/array.zig +++ b/src/zval/array.zig @@ -5,7 +5,7 @@ const Zval = @import("../zval.zig").Zval; pub const Array = opaque { /// Create an empty array in caller-provided zval storage. /// - /// Ownership: caller owns `zv`'s array value; call `Zval.native.dtor(zv)` + /// Ownership: caller owns `zv`'s array value; call `Zval.raw.dtor(zv)` /// unless the zval is returned/transferred to PHP. The returned wrapper is /// borrowed from `zv`. pub fn empty(zv: *c.zval) *Array { @@ -16,7 +16,7 @@ pub const Array = opaque { /// Create an array with initial capacity in caller-provided zval storage. /// - /// Ownership: caller owns `zv`'s array value; call `Zval.native.dtor(zv)` + /// Ownership: caller owns `zv`'s array value; call `Zval.raw.dtor(zv)` /// unless the zval is returned/transferred to PHP. The returned wrapper is /// borrowed from `zv`. pub fn init(zv: *c.zval, capacity: u32) *Array { @@ -31,7 +31,7 @@ pub const Array = opaque { /// /// Ownership: borrowed wrapper; no refcount change. pub fn from(zv: *c.zval) FromError!*Array { - if (Zval.native.getType(zv) != c.IS_ARRAY) return error.TypeMismatch; + if (Zval.raw.getType(zv) != c.IS_ARRAY) return error.TypeMismatch; if (zv.value.arr == null) return error.NullPointer; return @ptrCast(zv); } diff --git a/src/zval/object.zig b/src/zval/object.zig index 6804968..2ef94b0 100644 --- a/src/zval/object.zig +++ b/src/zval/object.zig @@ -5,7 +5,7 @@ const Zval = @import("../zval.zig").Zval; pub const Object = opaque { /// Create a standard object (stdClass) in caller-provided zval storage. /// - /// Ownership: caller owns `zv`'s object value; call `Zval.native.dtor(zv)` + /// Ownership: caller owns `zv`'s object value; call `Zval.raw.dtor(zv)` /// unless the zval is returned/transferred to PHP. The returned wrapper is /// borrowed from `zv`. pub fn init(zv: *c.zval) *Object { @@ -17,7 +17,7 @@ pub const Object = opaque { /// Create an object from a class entry in caller-provided zval storage. /// - /// Ownership: caller owns `zv`'s object value; call `Zval.native.dtor(zv)` + /// Ownership: caller owns `zv`'s object value; call `Zval.raw.dtor(zv)` /// unless the zval is returned/transferred to PHP. The returned wrapper is /// borrowed from `zv`. pub fn initClass(zv: *c.zval, ce: *zend.ClassEntry) InitClassError!*Object { @@ -47,7 +47,7 @@ pub const Object = opaque { /// /// Ownership: borrowed wrapper; no refcount change. pub fn from(zv: *c.zval) FromError!*Object { - if (Zval.native.getType(zv) != c.IS_OBJECT) return error.TypeMismatch; + if (Zval.raw.getType(zv) != c.IS_OBJECT) return error.TypeMismatch; if (zv.value.obj == null) return error.NullPointer; return @ptrCast(zv); } From b80407ee17b2145da8f2b2ae4879ca7f24de0f6c Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Tue, 14 Jul 2026 02:43:39 +0800 Subject: [PATCH 25/36] feat(example): redesign my_php_extension showcase --- examples/my_php_extension/README.md | 89 +--- .../my_php_extension.stub.php | 266 ++++------ .../my_php_extension_arginfo.h | 477 +++++++++--------- examples/my_php_extension/src/allocator.zig | 15 - examples/my_php_extension/src/api.zig | 25 + .../my_php_extension/src/api/collection.zig | 188 +++++++ examples/my_php_extension/src/api/config.zig | 32 ++ examples/my_php_extension/src/api/counter.zig | 54 ++ examples/my_php_extension/src/api/dumper.zig | 180 +++++++ examples/my_php_extension/src/api/entity.zig | 35 ++ .../my_php_extension/src/api/identifiable.zig | 3 + examples/my_php_extension/src/api/metrics.zig | 25 + examples/my_php_extension/src/api/role.zig | 3 + examples/my_php_extension/src/api/status.zig | 3 + examples/my_php_extension/src/api/tag.zig | 28 + examples/my_php_extension/src/api/user.zig | 73 +++ examples/my_php_extension/src/classes.zig | 10 - .../src/classes/abstract_entity.zig | 28 - .../src/classes/array_like.zig | 158 ------ .../my_php_extension/src/classes/counter.zig | 45 -- .../my_php_extension/src/classes/dumper.zig | 149 ------ .../src/classes/example_attribute.zig | 25 - .../src/classes/identifiable.zig | 3 - .../my_php_extension/src/classes/my_error.zig | 10 - .../my_php_extension/src/classes/role.zig | 3 - .../my_php_extension/src/classes/status.zig | 3 - .../my_php_extension/src/classes/user.zig | 93 ---- examples/my_php_extension/src/functions.zig | 344 +------------ examples/my_php_extension/src/ini.zig | 37 +- examples/my_php_extension/src/metrics.zig | 283 +++++++++++ examples/my_php_extension/src/root.zig | 52 +- .../my_php_extension/src/testing/bailout.zig | 46 ++ .../my_php_extension/tests/array_like.phpt | 183 ------- .../my_php_extension/tests/attribute.phpt | 28 - .../my_php_extension/tests/attributes.phpt | 35 ++ ...ap_allocator_bailout.phpt => bailout.phpt} | 10 +- .../my_php_extension/tests/collection.phpt | 66 +++ examples/my_php_extension/tests/config.phpt | 40 ++ .../my_php_extension/tests/constants.phpt | 14 - examples/my_php_extension/tests/counter.phpt | 24 +- examples/my_php_extension/tests/dumper.phpt | 48 ++ .../my_php_extension/tests/dumper_array.phpt | 63 --- .../my_php_extension/tests/dumper_object.phpt | 54 -- .../tests/dumper_reference.phpt | 24 - .../tests/dumper_resource.phpt | 12 - .../tests/dumper_scalars.phpt | 15 - examples/my_php_extension/tests/enum.phpt | 22 - examples/my_php_extension/tests/enums.phpt | 28 + .../my_php_extension/tests/error_class.phpt | 12 - .../my_php_extension/tests/functions.phpt | 49 +- examples/my_php_extension/tests/ini_bool.phpt | 20 - .../my_php_extension/tests/ini_string.phpt | 20 - .../my_php_extension/tests/ini_system.phpt | 16 - .../tests/ini_zig_getters.phpt | 41 -- .../my_php_extension/tests/interface.phpt | 12 - examples/my_php_extension/tests/map.phpt | 92 ---- examples/my_php_extension/tests/metrics.phpt | 183 +++++++ .../tests/module_globals.phpt | 14 - .../tests/new_with_constructor_failure.phpt | 16 - .../tests/property_scratch.phpt | 40 -- .../tests/test_expect_arg_array_object.phpt | 120 ----- .../tests/test_expect_arg_mixed.phpt | 70 --- .../tests/test_expect_arg_scalars.phpt | 158 ------ examples/my_php_extension/tests/user.phpt | 37 ++ .../my_php_extension/tests/user_class.phpt | 24 - src/zval.zig | 12 + 66 files changed, 1871 insertions(+), 2516 deletions(-) delete mode 100644 examples/my_php_extension/src/allocator.zig create mode 100644 examples/my_php_extension/src/api.zig create mode 100644 examples/my_php_extension/src/api/collection.zig create mode 100644 examples/my_php_extension/src/api/config.zig create mode 100644 examples/my_php_extension/src/api/counter.zig create mode 100644 examples/my_php_extension/src/api/dumper.zig create mode 100644 examples/my_php_extension/src/api/entity.zig create mode 100644 examples/my_php_extension/src/api/identifiable.zig create mode 100644 examples/my_php_extension/src/api/metrics.zig create mode 100644 examples/my_php_extension/src/api/role.zig create mode 100644 examples/my_php_extension/src/api/status.zig create mode 100644 examples/my_php_extension/src/api/tag.zig create mode 100644 examples/my_php_extension/src/api/user.zig delete mode 100644 examples/my_php_extension/src/classes.zig delete mode 100644 examples/my_php_extension/src/classes/abstract_entity.zig delete mode 100644 examples/my_php_extension/src/classes/array_like.zig delete mode 100644 examples/my_php_extension/src/classes/counter.zig delete mode 100644 examples/my_php_extension/src/classes/dumper.zig delete mode 100644 examples/my_php_extension/src/classes/example_attribute.zig delete mode 100644 examples/my_php_extension/src/classes/identifiable.zig delete mode 100644 examples/my_php_extension/src/classes/my_error.zig delete mode 100644 examples/my_php_extension/src/classes/role.zig delete mode 100644 examples/my_php_extension/src/classes/status.zig delete mode 100644 examples/my_php_extension/src/classes/user.zig create mode 100644 examples/my_php_extension/src/metrics.zig create mode 100644 examples/my_php_extension/src/testing/bailout.zig delete mode 100644 examples/my_php_extension/tests/array_like.phpt delete mode 100644 examples/my_php_extension/tests/attribute.phpt create mode 100644 examples/my_php_extension/tests/attributes.phpt rename examples/my_php_extension/tests/{heap_allocator_bailout.phpt => bailout.phpt} (60%) create mode 100644 examples/my_php_extension/tests/collection.phpt create mode 100644 examples/my_php_extension/tests/config.phpt delete mode 100644 examples/my_php_extension/tests/constants.phpt create mode 100644 examples/my_php_extension/tests/dumper.phpt delete mode 100644 examples/my_php_extension/tests/dumper_array.phpt delete mode 100644 examples/my_php_extension/tests/dumper_object.phpt delete mode 100644 examples/my_php_extension/tests/dumper_reference.phpt delete mode 100644 examples/my_php_extension/tests/dumper_resource.phpt delete mode 100644 examples/my_php_extension/tests/dumper_scalars.phpt delete mode 100644 examples/my_php_extension/tests/enum.phpt create mode 100644 examples/my_php_extension/tests/enums.phpt delete mode 100644 examples/my_php_extension/tests/error_class.phpt delete mode 100644 examples/my_php_extension/tests/ini_bool.phpt delete mode 100644 examples/my_php_extension/tests/ini_string.phpt delete mode 100644 examples/my_php_extension/tests/ini_system.phpt delete mode 100644 examples/my_php_extension/tests/ini_zig_getters.phpt delete mode 100644 examples/my_php_extension/tests/interface.phpt delete mode 100644 examples/my_php_extension/tests/map.phpt create mode 100644 examples/my_php_extension/tests/metrics.phpt delete mode 100644 examples/my_php_extension/tests/module_globals.phpt delete mode 100644 examples/my_php_extension/tests/new_with_constructor_failure.phpt delete mode 100644 examples/my_php_extension/tests/property_scratch.phpt delete mode 100644 examples/my_php_extension/tests/test_expect_arg_array_object.phpt delete mode 100644 examples/my_php_extension/tests/test_expect_arg_mixed.phpt delete mode 100644 examples/my_php_extension/tests/test_expect_arg_scalars.phpt create mode 100644 examples/my_php_extension/tests/user.phpt delete mode 100644 examples/my_php_extension/tests/user_class.phpt diff --git a/examples/my_php_extension/README.md b/examples/my_php_extension/README.md index db531e3..e898f91 100644 --- a/examples/my_php_extension/README.md +++ b/examples/my_php_extension/README.md @@ -1,77 +1,28 @@ # my_php_extension -A basic PHP extension demonstrating core phpz features: +This extension is the end-to-end feature example for phpz. Its public PHP API is +defined by `my_php_extension.stub.php`; Zig files implement that contract without +duplicating PHP type metadata. -- **Constants**: `MY_EXT_VERSION`, `MyPHPExt\VERSION`, `User::MIN_AGE` -- **Global functions**: `hello()`, `greet(string $name): string` -- **Namespaced functions**: `MyPHPExt\increment(int &$value)`, `MyPHPExt\findById(string|int $id): ?User`, `MyPHPExt\getDefaultUser(): User`, `MyPHPExt\listStatuses(): array`, `MyPHPExt\map(array $arr, callable $cb): array` -- **Interface**: `MyPHPExt\Identifiable` -- **Enums**: `MyPHPExt\Status` (int backed), `MyPHPExt\Role` (string backed) -- **Attributes**: `MyPHPExt\ExampleAttribute` and reflected attributes on `MyPHPExt\User` -- **Classes**: `MyPHPExt\User`, `MyPHPExt\Counter`, `MyPHPExt\Dumper`, `MyPHPExt\MyError`, `MyPHPExt\AbstractEntity` -- **INI directives**: `my_php_extension.greeting` (string, all), `my_php_extension.max_users` (int, system), `my_php_extension.debug` (bool, user) +## API groups -## Notes +- Global functions: `hello()` and `greet()`. +- Namespace functions: `MyPHPExt\increment()` and `MyPHPExt\mapValues()`. +- Object model: `Identifiable`, `Entity`, `User`, `Status`, `Role`, and `Tag`. +- General-purpose classes: `Counter`, `Dumper`, and `Collection`. +- Runtime configuration: static `Config` accessors backed by typed INI entries. +- Request telemetry: static `Metrics::snapshot()` and `Metrics::reset()`. +- Regression-only API: `MyPHPExt\Test\allocatorBailout()`. -Regenerate `ExampleAttribute` arginfo with PHP 8.3+ `php-src/build/gen_stub.php`: +Every exported Zig handler carries a `/// PHP: ...` line matching its stub +signature. Internal helpers and observer callbacks intentionally do not. -```bash -php /path/to/php-src/build/gen_stub.php my_php_extension.stub.php -``` +## Metrics -This uses `php-src/Zend/zend_attributes.stub.php` to resolve -`Attribute::TARGET_*` constants. If you use a standalone copied -`build/gen_stub.php` instead, copy `php-src/Zend/zend_attributes.stub.php` to -`Zend/zend_attributes.stub.php` next to this extension first. PHP 8.2's -generator does not preserve the full `@cvalue` expression for OR'ed -`Attribute::TARGET_*` constants. +`Metrics::snapshot()` returns request-local data grouped under `request`, `calls`, +`returns`, `errors`, and `exceptions`. Exception records contain copied class, +message, code, file, and line values; the observer keeps only the 16 most recent +records and reports overwritten records through `discarded`. -## Run - -```bash -# Build and run tests -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 -``` - -## Property Scratch Values - -Zend property reads may return either a borrowed property zval or a temporary -value written into caller-provided scratch storage, for example when `__get` -materializes a value. Initialize the scratch zval to `undef` and destroy it only -if Zend wrote into it: - -```zig -var scratch = phpz.Zval.native.undef; -const prop = try object.readProperty("name", .read, &scratch); -defer phpz.Zval.native.tryDtor(&scratch); -``` - -The returned `prop` pointer is borrowed unless it points at scratch. Do not dtor -the returned pointer directly. - -## Manually - -```bash -# Build -zig build - -# Run specific PHP code with the extension loaded -php -dextension=./modules/my_php_extension.so -r 'hello();' - -# Test INI directive -php -dextension=./modules/my_php_extension.so -dmy_php_extension.greeting="Hi" -r 'echo ini_get("my_php_extension.greeting");' -``` +The fcall observer uses a fixed-depth timing stack and does not allocate. Calls to +the metrics and test APIs are excluded so observing a snapshot does not alter it. diff --git a/examples/my_php_extension/my_php_extension.stub.php b/examples/my_php_extension/my_php_extension.stub.php index fc9f070..3d78276 100644 --- a/examples/my_php_extension/my_php_extension.stub.php +++ b/examples/my_php_extension/my_php_extension.stub.php @@ -8,245 +8,153 @@ /* ========== global namespace ========== */ namespace { - // gen_stub.php resolves Attribute::TARGET_* constants through the @cvalue - // metadata declared by PHP's own Attribute stub. Prefer running - // php-src/build/gen_stub.php; for a copied standalone gen_stub.php, also - // copy php-src/Zend/zend_attributes.stub.php to ./Zend/zend_attributes.stub.php. require "Zend/zend_attributes.stub.php"; - function hello(): void - { - } + function hello(): void {} - function greet(string $name): string - { - } + function greet(string $name): string {} +} - function moduleGlobalsGet(): int - { - } +namespace MyPHPExt { + /** @var string */ + const VERSION = "0.1.0"; - function moduleGlobalsIncrement(): int - { - } + function increment(int &$value, int $by = 1): void {} + + function mapValues(array $values, callable $mapper): array {} - function iniGetGreeting(): string + interface Identifiable { + public function getId(): int; } - function iniGetMaxUsers(): int + enum Status: int { + case Inactive = 0; + case Active = 1; } - function iniGetDebug(): bool + enum Role: string { + case User = "user"; + case Admin = "admin"; } - function iniGetMode(): string + #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] + final class Tag { + public string $name; + public ?string $description; + + public function __construct( + string $name, + ?string $description = null, + ) {} } - /** @var string */ - const MY_EXT_VERSION = "1.0.0"; -} + #[Tag("entity")] + abstract class Entity implements Identifiable + { + protected int $id; -/* ========== named namespace ========== */ + protected function __construct(int $id) {} -namespace MyPHPExt { - /** @var int */ - const VERSION = 1; + final public function getId(): int {} - function increment(int &$value): void - { + abstract public function label(): string; } - function findById(string|int $id): ?User + #[Tag("model", "Demonstrates properties, inheritance, enums, and attributes")] + final class User extends Entity implements \Stringable { - } + /** @var int */ + public const MIN_AGE = 0; - function getDefaultUser(): User - { - } + public string $name; + public ?int $age; + public Role $role; + public Status $status; - function listStatuses(): array - { - } + public function __construct( + #[Tag("identifier")] + int $id, + string $name, + ?int $age = null, + Role $role = Role::User, + Status $status = Status::Active, + ) {} - function map(array $arr, callable $cb): array - { - } + public function label(): string {} - function testExpectArgScalars(string $str, int $int, float $float, bool $flag = true, ?string $nullable_str = null, int $opt_int = 0): array - { + public function __toString(): string {} } - function testExpectArgArrayObject(array $data, \MyPHPExt\User $user, ?\MyPHPExt\User $nullable_user = null): array - { - } - - function testExpectArgMixed(mixed $value): mixed + final class Counter { - } + public function __construct(int $value = 0) {} - function inspectObjectProperty(object $obj, string $name, bool $silent = false): array - { - } + public function increment(int $by = 1): int {} - function tryCreateInvalidUser(): void - { - } + public function decrement(int $by = 1): int {} - function testHeapAllocatorBailout(): array - { - } + public function value(): int {} - interface Identifiable - { - public function getId(): int; + public function reset(int $value = 0): void {} } - enum Status: int + final class Dumper { - case Active = 1; - case Inactive = 0; + public static function dump(mixed ...$values): void {} } - enum Role: string + final class Collection implements \ArrayAccess, \Countable, \Iterator { - case Admin = 'admin'; - case User = 'user'; - } + public function __construct(array $values = []) {} - class MyError extends \Exception - { - } + public function toArray(): array {} - // This sample uses an OR expression of multiple Attribute::TARGET_* constants. - // Regenerate arginfo with PHP 8.3+ gen_stub.php; PHP 8.2's generator keeps - // only the last @cvalue constant in this expression. - #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] - final class ExampleAttribute - { - public string $name; - public ?string $note = null; + public function offsetExists(mixed $offset): bool {} - public function __construct(string $name, ?string $note = null) - { - } - } + public function offsetGet(mixed $offset): mixed {} - abstract class AbstractEntity implements Identifiable - { - protected function onLoad(): void - { - } + public function offsetSet(mixed $offset, mixed $value): void {} - abstract public function handle(string|int $id): void; - } + public function offsetUnset(mixed $offset): void {} - #[ExampleAttribute("entity", "primary user model")] - #[ExampleAttribute("audited")] - final class User extends AbstractEntity implements \Stringable - { - public string $name; - public int|null $age = null; - /** @var int */ - public const MIN_AGE = 0; + public function count(): int {} - public function __construct(string $name, int|null $age = null) - { - } + public function current(): mixed {} - public function getId(): int - { - } + public function key(): mixed {} - public function handle(#[ExampleAttribute("identifier", "string or int")] string|int $id): void - { - } + public function next(): void {} - public function __toString(): string - { - } - } + public function rewind(): void {} - class Dumper - { - public static function dump(mixed... $value): void - { - } + public function valid(): bool {} } - final class Counter + final class Config { - public function __construct(int $n = 0) - { - } + public static function greeting(): string {} - public function add(int $n): void - { - } + public static function maxUsers(): int {} - public function dec(int $n): void - { - } + public static function debugEnabled(): bool {} - public function value(): int - { - } + public static function mode(): string {} } - class ArrayLike implements \ArrayAccess, \Countable, \Iterator + final class Metrics { - public function __construct(array $data = []) - { - } - - public function toArray(): array - { - } - - // ArrayAccess - public function offsetExists(mixed $offset): bool - { - } - - public function offsetGet(mixed $offset): mixed - { - } - - public function offsetSet(mixed $offset, mixed $value): void - { - } - - public function offsetUnset(mixed $offset): void - { - } - - // Countable - public function count(): int - { - } - - // Iterator - public function current(): mixed - { - } - - public function key(): mixed - { - } - - public function next(): void - { - } - - public function rewind(): void - { - } - - public function valid(): bool - { - } + private function __construct() {} + + public static function snapshot(): array {} + + public static function reset(): void {} } } + +namespace MyPHPExt\Test { + function allocatorBailout(): array {} +} diff --git a/examples/my_php_extension/my_php_extension_arginfo.h b/examples/my_php_extension/my_php_extension_arginfo.h index 0a602eb..40d4fa4 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: 8174e4e61bb73c9c1ec4ce6f84107a80a784c4fe */ + * Stub hash: ba87d13bceef0140a0cc97bbc91d3d4a8b203335 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_hello, 0, 0, IS_VOID, 0) ZEND_END_ARG_INFO() @@ -8,210 +8,165 @@ 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_moduleGlobalsGet, 0, 0, IS_LONG, 0) -ZEND_END_ARG_INFO() - -#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() - -#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_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, by, IS_LONG, 0, "1") ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_MyPHPExt_findById, 0, 1, MyPHPExt\\\125ser, 1) - ZEND_ARG_TYPE_MASK(0, id, MAY_BE_STRING|MAY_BE_LONG, NULL) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_MyPHPExt_mapValues, 0, 2, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, values, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, mapper, IS_CALLABLE, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_MyPHPExt_getDefaultUser, 0, 0, MyPHPExt\\\125ser, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_MyPHPExt_Test_allocatorBailout, 0, 0, IS_ARRAY, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_MyPHPExt_listStatuses, 0, 0, IS_ARRAY, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_Identifiable_getId, 0, 0, IS_LONG, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_MyPHPExt_map, 0, 2, IS_ARRAY, 0) - ZEND_ARG_TYPE_INFO(0, arr, IS_ARRAY, 0) - ZEND_ARG_TYPE_INFO(0, cb, IS_CALLABLE, 0) +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_MyPHPExt_Tag___construct, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, description, IS_STRING, 1, "null") ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_MyPHPExt_testExpectArgScalars, 0, 3, IS_ARRAY, 0) - ZEND_ARG_TYPE_INFO(0, str, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, int, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, float, IS_DOUBLE, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flag, _IS_BOOL, 0, "true") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, nullable_str, IS_STRING, 1, "null") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, opt_int, IS_LONG, 0, "0") +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_MyPHPExt_Entity___construct, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, id, IS_LONG, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_MyPHPExt_testExpectArgArrayObject, 0, 2, IS_ARRAY, 0) - ZEND_ARG_TYPE_INFO(0, data, IS_ARRAY, 0) - ZEND_ARG_OBJ_INFO(0, user, MyPHPExt\\\125ser, 0) - ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, nullable_user, MyPHPExt\\\125ser, 1, "null") -ZEND_END_ARG_INFO() +#define arginfo_class_MyPHPExt_Entity_getId arginfo_class_MyPHPExt_Identifiable_getId -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_MyPHPExt_testExpectArgMixed, 0, 1, IS_MIXED, 0) - ZEND_ARG_TYPE_INFO(0, value, IS_MIXED, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_Entity_label, 0, 0, IS_STRING, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_MyPHPExt_inspectObjectProperty, 0, 2, IS_ARRAY, 0) - ZEND_ARG_TYPE_INFO(0, obj, IS_OBJECT, 0) +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_MyPHPExt_User___construct, 0, 0, 2) + ZEND_ARG_TYPE_INFO(0, id, IS_LONG, 0) ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, silent, _IS_BOOL, 0, "false") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, age, IS_LONG, 1, "null") + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, role, MyPHPExt\\Role, 0, "MyPHPExt\\Role::User") + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, status, MyPHPExt\\Status, 0, "MyPHPExt\\Status::Active") ZEND_END_ARG_INFO() -#define arginfo_MyPHPExt_tryCreateInvalidUser arginfo_hello - -#define arginfo_MyPHPExt_testHeapAllocatorBailout arginfo_MyPHPExt_listStatuses +#define arginfo_class_MyPHPExt_User_label arginfo_class_MyPHPExt_Entity_label -#define arginfo_class_MyPHPExt_Identifiable_getId arginfo_moduleGlobalsGet +#define arginfo_class_MyPHPExt_User___toString arginfo_class_MyPHPExt_Entity_label -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_MyPHPExt_ExampleAttribute___construct, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, note, IS_STRING, 1, "null") -ZEND_END_ARG_INFO() - -#define arginfo_class_MyPHPExt_AbstractEntity_onLoad arginfo_hello - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_AbstractEntity_handle, 0, 1, IS_VOID, 0) - ZEND_ARG_TYPE_MASK(0, id, MAY_BE_STRING|MAY_BE_LONG, NULL) +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_MyPHPExt_Counter___construct, 0, 0, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, value, IS_LONG, 0, "0") ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_MyPHPExt_User___construct, 0, 0, 1) - ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, age, IS_LONG, 1, "null") +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_Counter_increment, 0, 0, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, by, IS_LONG, 0, "1") ZEND_END_ARG_INFO() -#define arginfo_class_MyPHPExt_User_getId arginfo_moduleGlobalsGet +#define arginfo_class_MyPHPExt_Counter_decrement arginfo_class_MyPHPExt_Counter_increment -#define arginfo_class_MyPHPExt_User_handle arginfo_class_MyPHPExt_AbstractEntity_handle +#define arginfo_class_MyPHPExt_Counter_value arginfo_class_MyPHPExt_Identifiable_getId -#define arginfo_class_MyPHPExt_User___toString arginfo_iniGetGreeting - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_Dumper_dump, 0, 0, IS_VOID, 0) - ZEND_ARG_VARIADIC_TYPE_INFO(0, value, IS_MIXED, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_MyPHPExt_Counter___construct, 0, 0, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, n, IS_LONG, 0, "0") +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_Counter_reset, 0, 0, IS_VOID, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, value, IS_LONG, 0, "0") ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_Counter_add, 0, 1, IS_VOID, 0) - ZEND_ARG_TYPE_INFO(0, n, IS_LONG, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_Dumper_dump, 0, 0, IS_VOID, 0) + ZEND_ARG_VARIADIC_TYPE_INFO(0, values, IS_MIXED, 0) ZEND_END_ARG_INFO() -#define arginfo_class_MyPHPExt_Counter_dec arginfo_class_MyPHPExt_Counter_add - -#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, "[]") +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_MyPHPExt_Collection___construct, 0, 0, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, values, IS_ARRAY, 0, "[]") ZEND_END_ARG_INFO() -#define arginfo_class_MyPHPExt_ArrayLike_toArray arginfo_MyPHPExt_listStatuses +#define arginfo_class_MyPHPExt_Collection_toArray arginfo_MyPHPExt_Test_allocatorBailout -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_ArrayLike_offsetExists, 0, 1, _IS_BOOL, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_Collection_offsetExists, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, offset, IS_MIXED, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_ArrayLike_offsetGet, 0, 1, IS_MIXED, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_Collection_offsetGet, 0, 1, IS_MIXED, 0) ZEND_ARG_TYPE_INFO(0, offset, IS_MIXED, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_ArrayLike_offsetSet, 0, 2, IS_VOID, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_Collection_offsetSet, 0, 2, IS_VOID, 0) ZEND_ARG_TYPE_INFO(0, offset, IS_MIXED, 0) ZEND_ARG_TYPE_INFO(0, value, IS_MIXED, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_ArrayLike_offsetUnset, 0, 1, IS_VOID, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_Collection_offsetUnset, 0, 1, IS_VOID, 0) ZEND_ARG_TYPE_INFO(0, offset, IS_MIXED, 0) ZEND_END_ARG_INFO() -#define arginfo_class_MyPHPExt_ArrayLike_count arginfo_moduleGlobalsGet +#define arginfo_class_MyPHPExt_Collection_count arginfo_class_MyPHPExt_Identifiable_getId + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_Collection_current, 0, 0, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_MyPHPExt_Collection_key arginfo_class_MyPHPExt_Collection_current + +#define arginfo_class_MyPHPExt_Collection_next arginfo_hello + +#define arginfo_class_MyPHPExt_Collection_rewind arginfo_hello -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_ArrayLike_current, 0, 0, IS_MIXED, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_Collection_valid, 0, 0, _IS_BOOL, 0) ZEND_END_ARG_INFO() -#define arginfo_class_MyPHPExt_ArrayLike_key arginfo_class_MyPHPExt_ArrayLike_current +#define arginfo_class_MyPHPExt_Config_greeting arginfo_class_MyPHPExt_Entity_label -#define arginfo_class_MyPHPExt_ArrayLike_next arginfo_hello +#define arginfo_class_MyPHPExt_Config_maxUsers arginfo_class_MyPHPExt_Identifiable_getId -#define arginfo_class_MyPHPExt_ArrayLike_rewind arginfo_hello +#define arginfo_class_MyPHPExt_Config_debugEnabled arginfo_class_MyPHPExt_Collection_valid -#define arginfo_class_MyPHPExt_ArrayLike_valid arginfo_iniGetDebug +#define arginfo_class_MyPHPExt_Config_mode arginfo_class_MyPHPExt_Entity_label + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_MyPHPExt_Metrics___construct, 0, 0, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_MyPHPExt_Metrics_snapshot arginfo_MyPHPExt_Test_allocatorBailout + +#define arginfo_class_MyPHPExt_Metrics_reset arginfo_hello ZEND_FUNCTION(hello); ZEND_FUNCTION(greet); -ZEND_FUNCTION(moduleGlobalsGet); -ZEND_FUNCTION(moduleGlobalsIncrement); -ZEND_FUNCTION(iniGetGreeting); -ZEND_FUNCTION(iniGetMaxUsers); -ZEND_FUNCTION(iniGetDebug); -ZEND_FUNCTION(iniGetMode); ZEND_FUNCTION(MyPHPExt_increment); -ZEND_FUNCTION(MyPHPExt_findById); -ZEND_FUNCTION(MyPHPExt_getDefaultUser); -ZEND_FUNCTION(MyPHPExt_listStatuses); -ZEND_FUNCTION(MyPHPExt_map); -ZEND_FUNCTION(MyPHPExt_testExpectArgScalars); -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_FUNCTION(MyPHPExt_mapValues); +ZEND_FUNCTION(MyPHPExt_Test_allocatorBailout); +ZEND_METHOD(MyPHPExt_Tag, __construct); +ZEND_METHOD(MyPHPExt_Entity, __construct); +ZEND_METHOD(MyPHPExt_Entity, getId); ZEND_METHOD(MyPHPExt_User, __construct); -ZEND_METHOD(MyPHPExt_User, getId); -ZEND_METHOD(MyPHPExt_User, handle); +ZEND_METHOD(MyPHPExt_User, label); ZEND_METHOD(MyPHPExt_User, __toString); -ZEND_METHOD(MyPHPExt_Dumper, dump); ZEND_METHOD(MyPHPExt_Counter, __construct); -ZEND_METHOD(MyPHPExt_Counter, add); -ZEND_METHOD(MyPHPExt_Counter, dec); +ZEND_METHOD(MyPHPExt_Counter, increment); +ZEND_METHOD(MyPHPExt_Counter, decrement); ZEND_METHOD(MyPHPExt_Counter, value); -ZEND_METHOD(MyPHPExt_ArrayLike, __construct); -ZEND_METHOD(MyPHPExt_ArrayLike, toArray); -ZEND_METHOD(MyPHPExt_ArrayLike, offsetExists); -ZEND_METHOD(MyPHPExt_ArrayLike, offsetGet); -ZEND_METHOD(MyPHPExt_ArrayLike, offsetSet); -ZEND_METHOD(MyPHPExt_ArrayLike, offsetUnset); -ZEND_METHOD(MyPHPExt_ArrayLike, count); -ZEND_METHOD(MyPHPExt_ArrayLike, current); -ZEND_METHOD(MyPHPExt_ArrayLike, key); -ZEND_METHOD(MyPHPExt_ArrayLike, next); -ZEND_METHOD(MyPHPExt_ArrayLike, rewind); -ZEND_METHOD(MyPHPExt_ArrayLike, valid); +ZEND_METHOD(MyPHPExt_Counter, reset); +ZEND_METHOD(MyPHPExt_Dumper, dump); +ZEND_METHOD(MyPHPExt_Collection, __construct); +ZEND_METHOD(MyPHPExt_Collection, toArray); +ZEND_METHOD(MyPHPExt_Collection, offsetExists); +ZEND_METHOD(MyPHPExt_Collection, offsetGet); +ZEND_METHOD(MyPHPExt_Collection, offsetSet); +ZEND_METHOD(MyPHPExt_Collection, offsetUnset); +ZEND_METHOD(MyPHPExt_Collection, count); +ZEND_METHOD(MyPHPExt_Collection, current); +ZEND_METHOD(MyPHPExt_Collection, key); +ZEND_METHOD(MyPHPExt_Collection, next); +ZEND_METHOD(MyPHPExt_Collection, rewind); +ZEND_METHOD(MyPHPExt_Collection, valid); +ZEND_METHOD(MyPHPExt_Config, greeting); +ZEND_METHOD(MyPHPExt_Config, maxUsers); +ZEND_METHOD(MyPHPExt_Config, debugEnabled); +ZEND_METHOD(MyPHPExt_Config, mode); +ZEND_METHOD(MyPHPExt_Metrics, __construct); +ZEND_METHOD(MyPHPExt_Metrics, snapshot); +ZEND_METHOD(MyPHPExt_Metrics, reset); 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) - 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) - ZEND_NS_FALIAS("MyPHPExt", listStatuses, MyPHPExt_listStatuses, arginfo_MyPHPExt_listStatuses) - ZEND_NS_FALIAS("MyPHPExt", map, MyPHPExt_map, arginfo_MyPHPExt_map) - ZEND_NS_FALIAS("MyPHPExt", testExpectArgScalars, MyPHPExt_testExpectArgScalars, arginfo_MyPHPExt_testExpectArgScalars) - ZEND_NS_FALIAS("MyPHPExt", testExpectArgArrayObject, MyPHPExt_testExpectArgArrayObject, arginfo_MyPHPExt_testExpectArgArrayObject) - 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_NS_FALIAS("MyPHPExt", mapValues, MyPHPExt_mapValues, arginfo_MyPHPExt_mapValues) + ZEND_NS_FALIAS("MyPHPExt\\Test", allocatorBailout, MyPHPExt_Test_allocatorBailout, arginfo_MyPHPExt_Test_allocatorBailout) ZEND_FE_END }; @@ -232,29 +187,34 @@ static const zend_function_entry class_MyPHPExt_Role_methods[] = { }; -static const zend_function_entry class_MyPHPExt_MyError_methods[] = { +static const zend_function_entry class_MyPHPExt_Tag_methods[] = { + ZEND_ME(MyPHPExt_Tag, __construct, arginfo_class_MyPHPExt_Tag___construct, ZEND_ACC_PUBLIC) ZEND_FE_END }; -static const zend_function_entry class_MyPHPExt_ExampleAttribute_methods[] = { - ZEND_ME(MyPHPExt_ExampleAttribute, __construct, arginfo_class_MyPHPExt_ExampleAttribute___construct, ZEND_ACC_PUBLIC) +static const zend_function_entry class_MyPHPExt_Entity_methods[] = { + ZEND_ME(MyPHPExt_Entity, __construct, arginfo_class_MyPHPExt_Entity___construct, ZEND_ACC_PROTECTED) + ZEND_ME(MyPHPExt_Entity, getId, arginfo_class_MyPHPExt_Entity_getId, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL) + ZEND_ABSTRACT_ME_WITH_FLAGS(MyPHPExt_Entity, label, arginfo_class_MyPHPExt_Entity_label, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT) ZEND_FE_END }; -static const zend_function_entry class_MyPHPExt_AbstractEntity_methods[] = { - ZEND_ME(MyPHPExt_AbstractEntity, onLoad, arginfo_class_MyPHPExt_AbstractEntity_onLoad, ZEND_ACC_PROTECTED) - ZEND_ABSTRACT_ME_WITH_FLAGS(MyPHPExt_AbstractEntity, handle, arginfo_class_MyPHPExt_AbstractEntity_handle, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT) +static const zend_function_entry class_MyPHPExt_User_methods[] = { + ZEND_ME(MyPHPExt_User, __construct, arginfo_class_MyPHPExt_User___construct, ZEND_ACC_PUBLIC) + ZEND_ME(MyPHPExt_User, label, arginfo_class_MyPHPExt_User_label, ZEND_ACC_PUBLIC) + ZEND_ME(MyPHPExt_User, __toString, arginfo_class_MyPHPExt_User___toString, ZEND_ACC_PUBLIC) ZEND_FE_END }; -static const zend_function_entry class_MyPHPExt_User_methods[] = { - ZEND_ME(MyPHPExt_User, __construct, arginfo_class_MyPHPExt_User___construct, ZEND_ACC_PUBLIC) - ZEND_ME(MyPHPExt_User, getId, arginfo_class_MyPHPExt_User_getId, ZEND_ACC_PUBLIC) - ZEND_ME(MyPHPExt_User, handle, arginfo_class_MyPHPExt_User_handle, ZEND_ACC_PUBLIC) - ZEND_ME(MyPHPExt_User, __toString, arginfo_class_MyPHPExt_User___toString, ZEND_ACC_PUBLIC) +static const zend_function_entry class_MyPHPExt_Counter_methods[] = { + ZEND_ME(MyPHPExt_Counter, __construct, arginfo_class_MyPHPExt_Counter___construct, ZEND_ACC_PUBLIC) + ZEND_ME(MyPHPExt_Counter, increment, arginfo_class_MyPHPExt_Counter_increment, ZEND_ACC_PUBLIC) + ZEND_ME(MyPHPExt_Counter, decrement, arginfo_class_MyPHPExt_Counter_decrement, ZEND_ACC_PUBLIC) + ZEND_ME(MyPHPExt_Counter, value, arginfo_class_MyPHPExt_Counter_value, ZEND_ACC_PUBLIC) + ZEND_ME(MyPHPExt_Counter, reset, arginfo_class_MyPHPExt_Counter_reset, ZEND_ACC_PUBLIC) ZEND_FE_END }; @@ -265,35 +225,42 @@ static const zend_function_entry class_MyPHPExt_Dumper_methods[] = { }; -static const zend_function_entry class_MyPHPExt_Counter_methods[] = { - ZEND_ME(MyPHPExt_Counter, __construct, arginfo_class_MyPHPExt_Counter___construct, ZEND_ACC_PUBLIC) - ZEND_ME(MyPHPExt_Counter, add, arginfo_class_MyPHPExt_Counter_add, ZEND_ACC_PUBLIC) - ZEND_ME(MyPHPExt_Counter, dec, arginfo_class_MyPHPExt_Counter_dec, ZEND_ACC_PUBLIC) - ZEND_ME(MyPHPExt_Counter, value, arginfo_class_MyPHPExt_Counter_value, ZEND_ACC_PUBLIC) +static const zend_function_entry class_MyPHPExt_Collection_methods[] = { + ZEND_ME(MyPHPExt_Collection, __construct, arginfo_class_MyPHPExt_Collection___construct, ZEND_ACC_PUBLIC) + ZEND_ME(MyPHPExt_Collection, toArray, arginfo_class_MyPHPExt_Collection_toArray, ZEND_ACC_PUBLIC) + ZEND_ME(MyPHPExt_Collection, offsetExists, arginfo_class_MyPHPExt_Collection_offsetExists, ZEND_ACC_PUBLIC) + ZEND_ME(MyPHPExt_Collection, offsetGet, arginfo_class_MyPHPExt_Collection_offsetGet, ZEND_ACC_PUBLIC) + ZEND_ME(MyPHPExt_Collection, offsetSet, arginfo_class_MyPHPExt_Collection_offsetSet, ZEND_ACC_PUBLIC) + ZEND_ME(MyPHPExt_Collection, offsetUnset, arginfo_class_MyPHPExt_Collection_offsetUnset, ZEND_ACC_PUBLIC) + ZEND_ME(MyPHPExt_Collection, count, arginfo_class_MyPHPExt_Collection_count, ZEND_ACC_PUBLIC) + ZEND_ME(MyPHPExt_Collection, current, arginfo_class_MyPHPExt_Collection_current, ZEND_ACC_PUBLIC) + ZEND_ME(MyPHPExt_Collection, key, arginfo_class_MyPHPExt_Collection_key, ZEND_ACC_PUBLIC) + ZEND_ME(MyPHPExt_Collection, next, arginfo_class_MyPHPExt_Collection_next, ZEND_ACC_PUBLIC) + ZEND_ME(MyPHPExt_Collection, rewind, arginfo_class_MyPHPExt_Collection_rewind, ZEND_ACC_PUBLIC) + ZEND_ME(MyPHPExt_Collection, valid, arginfo_class_MyPHPExt_Collection_valid, ZEND_ACC_PUBLIC) ZEND_FE_END }; -static const zend_function_entry class_MyPHPExt_ArrayLike_methods[] = { - ZEND_ME(MyPHPExt_ArrayLike, __construct, arginfo_class_MyPHPExt_ArrayLike___construct, ZEND_ACC_PUBLIC) - ZEND_ME(MyPHPExt_ArrayLike, toArray, arginfo_class_MyPHPExt_ArrayLike_toArray, ZEND_ACC_PUBLIC) - ZEND_ME(MyPHPExt_ArrayLike, offsetExists, arginfo_class_MyPHPExt_ArrayLike_offsetExists, ZEND_ACC_PUBLIC) - ZEND_ME(MyPHPExt_ArrayLike, offsetGet, arginfo_class_MyPHPExt_ArrayLike_offsetGet, ZEND_ACC_PUBLIC) - ZEND_ME(MyPHPExt_ArrayLike, offsetSet, arginfo_class_MyPHPExt_ArrayLike_offsetSet, ZEND_ACC_PUBLIC) - ZEND_ME(MyPHPExt_ArrayLike, offsetUnset, arginfo_class_MyPHPExt_ArrayLike_offsetUnset, ZEND_ACC_PUBLIC) - ZEND_ME(MyPHPExt_ArrayLike, count, arginfo_class_MyPHPExt_ArrayLike_count, ZEND_ACC_PUBLIC) - ZEND_ME(MyPHPExt_ArrayLike, current, arginfo_class_MyPHPExt_ArrayLike_current, ZEND_ACC_PUBLIC) - ZEND_ME(MyPHPExt_ArrayLike, key, arginfo_class_MyPHPExt_ArrayLike_key, ZEND_ACC_PUBLIC) - ZEND_ME(MyPHPExt_ArrayLike, next, arginfo_class_MyPHPExt_ArrayLike_next, ZEND_ACC_PUBLIC) - ZEND_ME(MyPHPExt_ArrayLike, rewind, arginfo_class_MyPHPExt_ArrayLike_rewind, ZEND_ACC_PUBLIC) - ZEND_ME(MyPHPExt_ArrayLike, valid, arginfo_class_MyPHPExt_ArrayLike_valid, ZEND_ACC_PUBLIC) +static const zend_function_entry class_MyPHPExt_Config_methods[] = { + ZEND_ME(MyPHPExt_Config, greeting, arginfo_class_MyPHPExt_Config_greeting, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(MyPHPExt_Config, maxUsers, arginfo_class_MyPHPExt_Config_maxUsers, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(MyPHPExt_Config, debugEnabled, arginfo_class_MyPHPExt_Config_debugEnabled, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(MyPHPExt_Config, mode, arginfo_class_MyPHPExt_Config_mode, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_FE_END +}; + + +static const zend_function_entry class_MyPHPExt_Metrics_methods[] = { + ZEND_ME(MyPHPExt_Metrics, __construct, arginfo_class_MyPHPExt_Metrics___construct, ZEND_ACC_PRIVATE) + ZEND_ME(MyPHPExt_Metrics, snapshot, arginfo_class_MyPHPExt_Metrics_snapshot, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(MyPHPExt_Metrics, reset, arginfo_class_MyPHPExt_Metrics_reset, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) ZEND_FE_END }; static void register_my_php_extension_symbols(int module_number) { - REGISTER_STRING_CONSTANT("MY_EXT_VERSION", "1.0.0", CONST_PERSISTENT); - REGISTER_LONG_CONSTANT("MyPHPExt\\VERSION", 1, CONST_PERSISTENT); + REGISTER_STRING_CONSTANT("MyPHPExt\\VERSION", "0.1.0", CONST_PERSISTENT); } static zend_class_entry *register_class_MyPHPExt_Identifiable(void) @@ -310,14 +277,14 @@ static zend_class_entry *register_class_MyPHPExt_Status(void) { zend_class_entry *class_entry = zend_register_internal_enum("MyPHPExt\\Status", IS_LONG, class_MyPHPExt_Status_methods); - zval enum_case_Active_value; - ZVAL_LONG(&enum_case_Active_value, 1); - zend_enum_add_case_cstr(class_entry, "Active", &enum_case_Active_value); - zval enum_case_Inactive_value; ZVAL_LONG(&enum_case_Inactive_value, 0); zend_enum_add_case_cstr(class_entry, "Inactive", &enum_case_Inactive_value); + zval enum_case_Active_value; + ZVAL_LONG(&enum_case_Active_value, 1); + zend_enum_add_case_cstr(class_entry, "Active", &enum_case_Active_value); + return class_entry; } @@ -325,34 +292,24 @@ static zend_class_entry *register_class_MyPHPExt_Role(void) { zend_class_entry *class_entry = zend_register_internal_enum("MyPHPExt\\Role", IS_STRING, class_MyPHPExt_Role_methods); - zval enum_case_Admin_value; - zend_string *enum_case_Admin_value_str = zend_string_init("admin", strlen("admin"), 1); - ZVAL_STR(&enum_case_Admin_value, enum_case_Admin_value_str); - zend_enum_add_case_cstr(class_entry, "Admin", &enum_case_Admin_value); - zval enum_case_User_value; zend_string *enum_case_User_value_str = zend_string_init("user", strlen("user"), 1); ZVAL_STR(&enum_case_User_value, enum_case_User_value_str); zend_enum_add_case_cstr(class_entry, "User", &enum_case_User_value); - return class_entry; -} - -static zend_class_entry *register_class_MyPHPExt_MyError(zend_class_entry *class_entry_Exception) -{ - zend_class_entry ce, *class_entry; - - INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "MyError", class_MyPHPExt_MyError_methods); - class_entry = zend_register_internal_class_ex(&ce, class_entry_Exception); + zval enum_case_Admin_value; + zend_string *enum_case_Admin_value_str = zend_string_init("admin", strlen("admin"), 1); + ZVAL_STR(&enum_case_Admin_value, enum_case_Admin_value_str); + zend_enum_add_case_cstr(class_entry, "Admin", &enum_case_Admin_value); return class_entry; } -static zend_class_entry *register_class_MyPHPExt_ExampleAttribute(void) +static zend_class_entry *register_class_MyPHPExt_Tag(void) { zend_class_entry ce, *class_entry; - INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "ExampleAttribute", class_MyPHPExt_ExampleAttribute_methods); + INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "Tag", class_MyPHPExt_Tag_methods); class_entry = zend_register_internal_class_ex(&ce, NULL); class_entry->ce_flags |= ZEND_ACC_FINAL; @@ -362,40 +319,54 @@ static zend_class_entry *register_class_MyPHPExt_ExampleAttribute(void) zend_declare_typed_property(class_entry, property_name_name, &property_name_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); zend_string_release(property_name_name); - zval property_note_default_value; - ZVAL_NULL(&property_note_default_value); - zend_string *property_note_name = zend_string_init("note", sizeof("note") - 1, 1); - zend_declare_typed_property(class_entry, property_note_name, &property_note_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING|MAY_BE_NULL)); - zend_string_release(property_note_name); + zval property_description_default_value; + ZVAL_UNDEF(&property_description_default_value); + zend_string *property_description_name = zend_string_init("description", sizeof("description") - 1, 1); + zend_declare_typed_property(class_entry, property_description_name, &property_description_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING|MAY_BE_NULL)); + zend_string_release(property_description_name); - zend_string *attribute_name_Attribute_class_MyPHPExt_ExampleAttribute_0 = zend_string_init_interned("Attribute", sizeof("Attribute") - 1, 1); - zend_attribute *attribute_Attribute_class_MyPHPExt_ExampleAttribute_0 = zend_add_class_attribute(class_entry, attribute_name_Attribute_class_MyPHPExt_ExampleAttribute_0, 1); - zend_string_release(attribute_name_Attribute_class_MyPHPExt_ExampleAttribute_0); - zval attribute_Attribute_class_MyPHPExt_ExampleAttribute_0_arg0; - ZVAL_LONG(&attribute_Attribute_class_MyPHPExt_ExampleAttribute_0_arg0, ZEND_ATTRIBUTE_TARGET_CLASS | ZEND_ATTRIBUTE_TARGET_PARAMETER | ZEND_ATTRIBUTE_IS_REPEATABLE); - ZVAL_COPY_VALUE(&attribute_Attribute_class_MyPHPExt_ExampleAttribute_0->args[0].value, &attribute_Attribute_class_MyPHPExt_ExampleAttribute_0_arg0); + zend_string *attribute_name_Attribute_class_MyPHPExt_Tag_0 = zend_string_init_interned("Attribute", sizeof("Attribute") - 1, 1); + zend_attribute *attribute_Attribute_class_MyPHPExt_Tag_0 = zend_add_class_attribute(class_entry, attribute_name_Attribute_class_MyPHPExt_Tag_0, 1); + zend_string_release(attribute_name_Attribute_class_MyPHPExt_Tag_0); + zval attribute_Attribute_class_MyPHPExt_Tag_0_arg0; + ZVAL_LONG(&attribute_Attribute_class_MyPHPExt_Tag_0_arg0, ZEND_ATTRIBUTE_TARGET_CLASS | ZEND_ATTRIBUTE_TARGET_PARAMETER | ZEND_ATTRIBUTE_IS_REPEATABLE); + ZVAL_COPY_VALUE(&attribute_Attribute_class_MyPHPExt_Tag_0->args[0].value, &attribute_Attribute_class_MyPHPExt_Tag_0_arg0); return class_entry; } -static zend_class_entry *register_class_MyPHPExt_AbstractEntity(zend_class_entry *class_entry_MyPHPExt_Identifiable) +static zend_class_entry *register_class_MyPHPExt_Entity(zend_class_entry *class_entry_MyPHPExt_Identifiable) { zend_class_entry ce, *class_entry; - INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "AbstractEntity", class_MyPHPExt_AbstractEntity_methods); + INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "Entity", class_MyPHPExt_Entity_methods); class_entry = zend_register_internal_class_ex(&ce, NULL); class_entry->ce_flags |= ZEND_ACC_ABSTRACT; zend_class_implements(class_entry, 1, class_entry_MyPHPExt_Identifiable); + zval property_id_default_value; + ZVAL_UNDEF(&property_id_default_value); + zend_string *property_id_name = zend_string_init("id", sizeof("id") - 1, 1); + zend_declare_typed_property(class_entry, property_id_name, &property_id_default_value, ZEND_ACC_PROTECTED, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(property_id_name); + + zend_string *attribute_name_MyPHPExt_Tag_class_MyPHPExt_Entity_0 = zend_string_init_interned("MyPHPExt\\Tag", sizeof("MyPHPExt\\Tag") - 1, 1); + zend_attribute *attribute_MyPHPExt_Tag_class_MyPHPExt_Entity_0 = zend_add_class_attribute(class_entry, attribute_name_MyPHPExt_Tag_class_MyPHPExt_Entity_0, 1); + zend_string_release(attribute_name_MyPHPExt_Tag_class_MyPHPExt_Entity_0); + zval attribute_MyPHPExt_Tag_class_MyPHPExt_Entity_0_arg0; + zend_string *attribute_MyPHPExt_Tag_class_MyPHPExt_Entity_0_arg0_str = zend_string_init("entity", strlen("entity"), 1); + ZVAL_STR(&attribute_MyPHPExt_Tag_class_MyPHPExt_Entity_0_arg0, attribute_MyPHPExt_Tag_class_MyPHPExt_Entity_0_arg0_str); + ZVAL_COPY_VALUE(&attribute_MyPHPExt_Tag_class_MyPHPExt_Entity_0->args[0].value, &attribute_MyPHPExt_Tag_class_MyPHPExt_Entity_0_arg0); + return class_entry; } -static zend_class_entry *register_class_MyPHPExt_User(zend_class_entry *class_entry_MyPHPExt_AbstractEntity, zend_class_entry *class_entry_Stringable) +static zend_class_entry *register_class_MyPHPExt_User(zend_class_entry *class_entry_MyPHPExt_Entity, zend_class_entry *class_entry_Stringable) { zend_class_entry ce, *class_entry; INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "User", class_MyPHPExt_User_methods); - class_entry = zend_register_internal_class_ex(&ce, class_entry_MyPHPExt_AbstractEntity); + class_entry = zend_register_internal_class_ex(&ce, class_entry_MyPHPExt_Entity); class_entry->ce_flags |= ZEND_ACC_FINAL; zend_class_implements(class_entry, 1, class_entry_Stringable); @@ -412,43 +383,56 @@ static zend_class_entry *register_class_MyPHPExt_User(zend_class_entry *class_en zend_string_release(property_name_name); zval property_age_default_value; - ZVAL_NULL(&property_age_default_value); + ZVAL_UNDEF(&property_age_default_value); zend_string *property_age_name = zend_string_init("age", sizeof("age") - 1, 1); zend_declare_typed_property(class_entry, property_age_name, &property_age_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG|MAY_BE_NULL)); zend_string_release(property_age_name); - zend_string *attribute_name_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_0 = zend_string_init_interned("MyPHPExt\\ExampleAttribute", sizeof("MyPHPExt\\ExampleAttribute") - 1, 1); - zend_attribute *attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_0 = zend_add_class_attribute(class_entry, attribute_name_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_0, 2); - zend_string_release(attribute_name_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_0); - zval attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_0_arg0; - zend_string *attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_0_arg0_str = zend_string_init("entity", strlen("entity"), 1); - ZVAL_STR(&attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_0_arg0, attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_0_arg0_str); - ZVAL_COPY_VALUE(&attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_0->args[0].value, &attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_0_arg0); - zval attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_0_arg1; - zend_string *attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_0_arg1_str = zend_string_init("primary user model", strlen("primary user model"), 1); - ZVAL_STR(&attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_0_arg1, attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_0_arg1_str); - ZVAL_COPY_VALUE(&attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_0->args[1].value, &attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_0_arg1); - - zend_string *attribute_name_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_1 = zend_string_init_interned("MyPHPExt\\ExampleAttribute", sizeof("MyPHPExt\\ExampleAttribute") - 1, 1); - zend_attribute *attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_1 = zend_add_class_attribute(class_entry, attribute_name_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_1, 1); - zend_string_release(attribute_name_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_1); - zval attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_1_arg0; - zend_string *attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_1_arg0_str = zend_string_init("audited", strlen("audited"), 1); - ZVAL_STR(&attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_1_arg0, attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_1_arg0_str); - ZVAL_COPY_VALUE(&attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_1->args[0].value, &attribute_MyPHPExt_ExampleAttribute_class_MyPHPExt_User_1_arg0); - - - zend_string *attribute_name_MyPHPExt_ExampleAttribute_func_handle_arg0_0 = zend_string_init_interned("MyPHPExt\\ExampleAttribute", sizeof("MyPHPExt\\ExampleAttribute") - 1, 1); - zend_attribute *attribute_MyPHPExt_ExampleAttribute_func_handle_arg0_0 = zend_add_parameter_attribute(zend_hash_str_find_ptr(&class_entry->function_table, "handle", sizeof("handle") - 1), 0, attribute_name_MyPHPExt_ExampleAttribute_func_handle_arg0_0, 2); - zend_string_release(attribute_name_MyPHPExt_ExampleAttribute_func_handle_arg0_0); - zval attribute_MyPHPExt_ExampleAttribute_func_handle_arg0_0_arg0; - zend_string *attribute_MyPHPExt_ExampleAttribute_func_handle_arg0_0_arg0_str = zend_string_init("identifier", strlen("identifier"), 1); - ZVAL_STR(&attribute_MyPHPExt_ExampleAttribute_func_handle_arg0_0_arg0, attribute_MyPHPExt_ExampleAttribute_func_handle_arg0_0_arg0_str); - ZVAL_COPY_VALUE(&attribute_MyPHPExt_ExampleAttribute_func_handle_arg0_0->args[0].value, &attribute_MyPHPExt_ExampleAttribute_func_handle_arg0_0_arg0); - zval attribute_MyPHPExt_ExampleAttribute_func_handle_arg0_0_arg1; - zend_string *attribute_MyPHPExt_ExampleAttribute_func_handle_arg0_0_arg1_str = zend_string_init("string or int", strlen("string or int"), 1); - ZVAL_STR(&attribute_MyPHPExt_ExampleAttribute_func_handle_arg0_0_arg1, attribute_MyPHPExt_ExampleAttribute_func_handle_arg0_0_arg1_str); - ZVAL_COPY_VALUE(&attribute_MyPHPExt_ExampleAttribute_func_handle_arg0_0->args[1].value, &attribute_MyPHPExt_ExampleAttribute_func_handle_arg0_0_arg1); + zval property_role_default_value; + ZVAL_UNDEF(&property_role_default_value); + zend_string *property_role_name = zend_string_init("role", sizeof("role") - 1, 1); + zend_string *property_role_class_MyPHPExt_Role = zend_string_init("MyPHPExt\\Role", sizeof("MyPHPExt\\Role")-1, 1); + zend_declare_typed_property(class_entry, property_role_name, &property_role_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_role_class_MyPHPExt_Role, 0, 0)); + zend_string_release(property_role_name); + + zval property_status_default_value; + ZVAL_UNDEF(&property_status_default_value); + zend_string *property_status_name = zend_string_init("status", sizeof("status") - 1, 1); + zend_string *property_status_class_MyPHPExt_Status = zend_string_init("MyPHPExt\\Status", sizeof("MyPHPExt\\Status")-1, 1); + zend_declare_typed_property(class_entry, property_status_name, &property_status_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_status_class_MyPHPExt_Status, 0, 0)); + zend_string_release(property_status_name); + + zend_string *attribute_name_MyPHPExt_Tag_class_MyPHPExt_User_0 = zend_string_init_interned("MyPHPExt\\Tag", sizeof("MyPHPExt\\Tag") - 1, 1); + zend_attribute *attribute_MyPHPExt_Tag_class_MyPHPExt_User_0 = zend_add_class_attribute(class_entry, attribute_name_MyPHPExt_Tag_class_MyPHPExt_User_0, 2); + zend_string_release(attribute_name_MyPHPExt_Tag_class_MyPHPExt_User_0); + zval attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg0; + zend_string *attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg0_str = zend_string_init("model", strlen("model"), 1); + ZVAL_STR(&attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg0, attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg0_str); + ZVAL_COPY_VALUE(&attribute_MyPHPExt_Tag_class_MyPHPExt_User_0->args[0].value, &attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg0); + zval attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg1; + zend_string *attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg1_str = zend_string_init("Demonstrates properties, inheritance, enums, and attributes", strlen("Demonstrates properties, inheritance, enums, and attributes"), 1); + ZVAL_STR(&attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg1, attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg1_str); + ZVAL_COPY_VALUE(&attribute_MyPHPExt_Tag_class_MyPHPExt_User_0->args[1].value, &attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg1); + + + zend_string *attribute_name_MyPHPExt_Tag_func___construct_arg0_0 = zend_string_init_interned("MyPHPExt\\Tag", sizeof("MyPHPExt\\Tag") - 1, 1); + zend_attribute *attribute_MyPHPExt_Tag_func___construct_arg0_0 = zend_add_parameter_attribute(zend_hash_str_find_ptr(&class_entry->function_table, "__construct", sizeof("__construct") - 1), 0, attribute_name_MyPHPExt_Tag_func___construct_arg0_0, 1); + zend_string_release(attribute_name_MyPHPExt_Tag_func___construct_arg0_0); + zval attribute_MyPHPExt_Tag_func___construct_arg0_0_arg0; + zend_string *attribute_MyPHPExt_Tag_func___construct_arg0_0_arg0_str = zend_string_init("identifier", strlen("identifier"), 1); + ZVAL_STR(&attribute_MyPHPExt_Tag_func___construct_arg0_0_arg0, attribute_MyPHPExt_Tag_func___construct_arg0_0_arg0_str); + ZVAL_COPY_VALUE(&attribute_MyPHPExt_Tag_func___construct_arg0_0->args[0].value, &attribute_MyPHPExt_Tag_func___construct_arg0_0_arg0); + + return class_entry; +} + +static zend_class_entry *register_class_MyPHPExt_Counter(void) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "Counter", class_MyPHPExt_Counter_methods); + class_entry = zend_register_internal_class_ex(&ce, NULL); + class_entry->ce_flags |= ZEND_ACC_FINAL; return class_entry; } @@ -459,28 +443,41 @@ static zend_class_entry *register_class_MyPHPExt_Dumper(void) INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "Dumper", class_MyPHPExt_Dumper_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_MyPHPExt_Counter(void) +static zend_class_entry *register_class_MyPHPExt_Collection(zend_class_entry *class_entry_ArrayAccess, zend_class_entry *class_entry_Countable, zend_class_entry *class_entry_Iterator) { zend_class_entry ce, *class_entry; - INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "Counter", class_MyPHPExt_Counter_methods); + INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "Collection", class_MyPHPExt_Collection_methods); class_entry = zend_register_internal_class_ex(&ce, NULL); class_entry->ce_flags |= ZEND_ACC_FINAL; + zend_class_implements(class_entry, 3, class_entry_ArrayAccess, class_entry_Countable, class_entry_Iterator); return class_entry; } -static zend_class_entry *register_class_MyPHPExt_ArrayLike(zend_class_entry *class_entry_ArrayAccess, zend_class_entry *class_entry_Countable, zend_class_entry *class_entry_Iterator) +static zend_class_entry *register_class_MyPHPExt_Config(void) { zend_class_entry ce, *class_entry; - INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "ArrayLike", class_MyPHPExt_ArrayLike_methods); + INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "Config", class_MyPHPExt_Config_methods); class_entry = zend_register_internal_class_ex(&ce, NULL); - zend_class_implements(class_entry, 3, class_entry_ArrayAccess, class_entry_Countable, class_entry_Iterator); + class_entry->ce_flags |= ZEND_ACC_FINAL; + + return class_entry; +} + +static zend_class_entry *register_class_MyPHPExt_Metrics(void) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "Metrics", class_MyPHPExt_Metrics_methods); + class_entry = zend_register_internal_class_ex(&ce, NULL); + class_entry->ce_flags |= ZEND_ACC_FINAL; return class_entry; } diff --git a/examples/my_php_extension/src/allocator.zig b/examples/my_php_extension/src/allocator.zig deleted file mode 100644 index 2b9926a..0000000 --- a/examples/my_php_extension/src/allocator.zig +++ /dev/null @@ -1,15 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const phpz = @import("phpz"); - -var debug_allocator = if (builtin.mode == .Debug) std.heap.DebugAllocator(.{}).init else {}; - -pub const gpa = if (builtin.mode == .Debug) debug_allocator.allocator() else phpz.heap.php_allocator; - -pub fn deinit() void { - if (builtin.mode == .Debug) { - if (debug_allocator.deinit() == .leak) { - std.log.err("Memory leak!", .{}); - } - } -} diff --git a/examples/my_php_extension/src/api.zig b/examples/my_php_extension/src/api.zig new file mode 100644 index 0000000..2e3af7e --- /dev/null +++ b/examples/my_php_extension/src/api.zig @@ -0,0 +1,25 @@ +pub const identifiable = @import("api/identifiable.zig"); +pub const status = @import("api/status.zig"); +pub const role = @import("api/role.zig"); +pub const tag = @import("api/tag.zig"); +pub const entity = @import("api/entity.zig"); +pub const user = @import("api/user.zig"); +pub const counter = @import("api/counter.zig"); +pub const dumper = @import("api/dumper.zig"); +pub const collection = @import("api/collection.zig"); +pub const config = @import("api/config.zig"); +pub const metrics = @import("api/metrics.zig"); + +pub const classes = &.{ + identifiable.Class, + status.Class, + role.Class, + tag.Class, + entity.Class, + user.Class, + counter.Class, + dumper.Class, + collection.Class, + config.Class, + metrics.Class, +}; diff --git a/examples/my_php_extension/src/api/collection.zig b/examples/my_php_extension/src/api/collection.zig new file mode 100644 index 0000000..0f09340 --- /dev/null +++ b/examples/my_php_extension/src/api/collection.zig @@ -0,0 +1,188 @@ +const phpz = @import("phpz"); +const Zval = phpz.Zval; + +const Collection = extern struct { + data: *phpz.zend.Array, + iterator: phpz.zend.Array.Iterator, + + pub fn register(impl: anytype) *phpz.ClassEntry { + return .from(impl( + phpz.globals.class.entry("ArrayAccess"), + phpz.globals.class.entry("Countable"), + phpz.globals.class.entry("Iterator"), + )); + } + + pub fn init(self: *Collection) void { + self.data = phpz.zend.Array.empty(); + self.iterator = self.data.iterator(); + } + + pub fn deinit(self: *Collection) void { + self.data.release(); + } + + /// PHP: MyPHPExt\Collection::__construct(array $values = []) + pub fn construct(self: *Collection, ctx: phpz.Ctx) !void { + const args = try ctx.call.expectArgs(&.{ + .{ .array = .{ .optional = true } }, + }, {}); + + if (args[0]) |values| { + self.data.copy(values); + } + } + + /// PHP: MyPHPExt\Collection::toArray(): array + pub fn toArray(self: Collection, ctx: phpz.Ctx) void { + ctx.ret.set(.array, self.data.duplicate()); + } + + /// PHP: MyPHPExt\Collection::offsetExists(mixed $offset): bool + pub fn offsetExists(self: *Collection, ctx: phpz.Ctx) !void { + const args = try ctx.call.expectArgs(&.{ + .{ .mixed = .{} }, + }, {}); + const offset = Zval.from(args[0]); + + ctx.ret.set(.bool, switch (offset.kind()) { + .int => self.data.hasIndex(@intCast(offset.asUnchecked(.int))), + .string => self.data.has(offset.asUnchecked(.string)), + else => false, + }); + } + + /// PHP: MyPHPExt\Collection::offsetGet(mixed $offset): mixed + pub fn offsetGet(self: *Collection, ctx: phpz.Ctx) !void { + const args = try ctx.call.expectArgs(&.{ + .{ .mixed = .{} }, + }, {}); + const offset = Zval.from(args[0]); + + const value = switch (offset.kind()) { + .int => self.data.findIndex(@intCast(offset.asUnchecked(.int))), + .string => self.data.find(offset.asUnchecked(.string)), + else => null, + }; + + if (value) |found| { + Zval.raw.tryAddref(found); + ctx.ret.set(.mixed, found); + } else { + ctx.ret.set(.null, {}); + } + } + + /// PHP: MyPHPExt\Collection::offsetSet(mixed $offset, mixed $value): void + pub fn offsetSet(self: *Collection, ctx: phpz.Ctx) !void { + const args = try ctx.call.expectArgs(&.{ + .{ .mixed = .{} }, + .{ .mixed = .{} }, + }, {}); + const offset = Zval.from(args[0]); + const value = Zval.from(args[1]); + + switch (offset.kind()) { + .int => { + value.tryAddref(); + _ = self.data.updateIndex( + @intCast(offset.asUnchecked(.int)), + value.ptr(), + ); + }, + .string => { + value.tryAddref(); + _ = self.data.update( + offset.asUnchecked(.string), + value.ptr(), + ); + }, + .null => { + value.tryAddref(); + _ = self.data.append(value.ptr()); + }, + else => return, + } + + self.iterator.reset(); + } + + /// PHP: MyPHPExt\Collection::offsetUnset(mixed $offset): void + pub fn offsetUnset(self: *Collection, ctx: phpz.Ctx) !void { + const args = try ctx.call.expectArgs(&.{ + .{ .mixed = .{} }, + }, {}); + const offset = Zval.from(args[0]); + + switch (offset.kind()) { + .int => self.data.deleteIndex( + @intCast(offset.asUnchecked(.int)), + ) catch {}, + .string => self.data.delete( + offset.asUnchecked(.string), + ) catch {}, + else => {}, + } + + self.iterator.reset(); + } + + /// PHP: MyPHPExt\Collection::count(): int + pub fn count(self: Collection, ctx: phpz.Ctx) void { + ctx.ret.set(.int, @intCast(self.data.len())); + } + + /// PHP: MyPHPExt\Collection::current(): mixed + pub fn current(self: *Collection, ctx: phpz.Ctx) void { + if (self.iterator.currentValue()) |value| { + Zval.raw.tryAddref(value); + ctx.ret.set(.mixed, value); + } else { + ctx.ret.set(.null, {}); + } + } + + /// PHP: MyPHPExt\Collection::key(): mixed + pub fn key(self: *Collection, ctx: phpz.Ctx) void { + if (self.iterator.currentKey()) |key_value| { + switch (key_value) { + .int => |index| ctx.ret.set(.int, index), + .string => |key_name| ctx.ret.set(.string, key_name), + } + } else { + ctx.ret.set(.null, {}); + } + } + + /// PHP: MyPHPExt\Collection::next(): void + pub fn next(self: *Collection) void { + self.iterator.advance(); + } + + /// PHP: MyPHPExt\Collection::rewind(): void + pub fn rewind(self: *Collection) void { + self.iterator.reset(); + } + + /// PHP: MyPHPExt\Collection::valid(): bool + pub fn valid(self: *Collection, ctx: phpz.Ctx) void { + ctx.ret.set(.bool, self.iterator.current() != null); + } +}; + +pub const Class = phpz.Class("MyPHPExt\\Collection", Collection); + +comptime { + Class.method("__construct", .construct); + Class.method("toArray", .toArray); + Class.method("offsetExists", .offsetExists); + Class.method("offsetGet", .offsetGet); + Class.method("offsetSet", .offsetSet); + Class.method("offsetUnset", .offsetUnset); + Class.method("count", .count); + Class.method("current", .current); + Class.method("key", .key); + Class.method("next", .next); + Class.method("rewind", .rewind); + Class.method("valid", .valid); +} diff --git a/examples/my_php_extension/src/api/config.zig b/examples/my_php_extension/src/api/config.zig new file mode 100644 index 0000000..18a5251 --- /dev/null +++ b/examples/my_php_extension/src/api/config.zig @@ -0,0 +1,32 @@ +const phpz = @import("phpz"); + +const ini = @import("../ini.zig"); + +/// PHP: MyPHPExt\Config::greeting(): string +fn greeting(ctx: phpz.Ctx) void { + ctx.ret.set(.string, ini.greeting.get()); +} + +/// PHP: MyPHPExt\Config::maxUsers(): int +fn maxUsers(ctx: phpz.Ctx) void { + ctx.ret.set(.int, ini.max_users.get()); +} + +/// PHP: MyPHPExt\Config::debugEnabled(): bool +fn debugEnabled(ctx: phpz.Ctx) void { + ctx.ret.set(.bool, ini.debug.get()); +} + +/// PHP: MyPHPExt\Config::mode(): string +fn mode(ctx: phpz.Ctx) void { + ctx.ret.set(.string, @tagName(ini.mode.get())); +} + +pub const Class = phpz.SimpleClass("MyPHPExt\\Config", void); + +comptime { + Class.method("greeting", greeting); + Class.method("maxUsers", maxUsers); + Class.method("debugEnabled", debugEnabled); + Class.method("mode", mode); +} diff --git a/examples/my_php_extension/src/api/counter.zig b/examples/my_php_extension/src/api/counter.zig new file mode 100644 index 0000000..2e52f6e --- /dev/null +++ b/examples/my_php_extension/src/api/counter.zig @@ -0,0 +1,54 @@ +const Impl = extern struct { + current_value: i64, + + /// PHP: MyPHPExt\Counter::__construct(int $value = 0): void + pub fn construct(self: *Impl, ctx: phpz.Ctx) !void { + const args = try ctx.call.expectArgs(&.{ + .{ .int = .{ .optional = true } }, + }, {}); + self.current_value = args[0] orelse 0; + } + + /// PHP: MyPHPExt\Counter::increment(int $by = 1): int + pub fn increment(self: *Impl, ctx: phpz.Ctx) !void { + const args = try ctx.call.expectArgs(&.{ + .{ .int = .{ .optional = true } }, + }, {}); + self.current_value +|= args[0] orelse 1; + ctx.ret.set(.int, self.current_value); + } + + /// PHP: MyPHPExt\Counter::decrement(int $by = 1): int + pub fn decrement(self: *Impl, ctx: phpz.Ctx) !void { + const args = try ctx.call.expectArgs(&.{ + .{ .int = .{ .optional = true } }, + }, {}); + self.current_value -|= args[0] orelse 1; + ctx.ret.set(.int, self.current_value); + } + + /// PHP: MyPHPExt\Counter::value(): int + pub fn value(self: *const Impl, ctx: phpz.Ctx) void { + ctx.ret.set(.int, self.current_value); + } + + /// PHP: MyPHPExt\Counter::reset(int $value = 0): void + pub fn reset(self: *Impl, ctx: phpz.Ctx) !void { + const args = try ctx.call.expectArgs(&.{ + .{ .int = .{ .optional = true } }, + }, {}); + self.current_value = args[0] orelse 0; + } +}; + +pub const Class = phpz.Class("MyPHPExt\\Counter", Impl); + +comptime { + Class.method("__construct", .construct); + Class.method("increment", .increment); + Class.method("decrement", .decrement); + Class.method("value", .value); + Class.method("reset", .reset); +} + +const phpz = @import("phpz"); diff --git a/examples/my_php_extension/src/api/dumper.zig b/examples/my_php_extension/src/api/dumper.zig new file mode 100644 index 0000000..c287beb --- /dev/null +++ b/examples/my_php_extension/src/api/dumper.zig @@ -0,0 +1,180 @@ +const std = @import("std"); + +const phpz = @import("phpz"); + +/// PHP: MyPHPExt\Dumper::dump(mixed ...$values): void +fn dump(ctx: phpz.Ctx) !void { + for (ctx.call.args()) |*zv| { + dumpValue(.from(zv), 0); + } +} + +fn dumpValue(value: *phpz.Zval, depth: usize) void { + switch (value.kind()) { + .null => { + indent(depth); + _ = phpz.printf("NULL\n", .{}); + }, + .bool => { + indent(depth); + const text: [*:0]const u8 = if (value.asUnchecked(.bool)) "true" else "false"; + _ = phpz.printf("bool(%s)\n", .{text}); + }, + .int => { + indent(depth); + _ = phpz.printf("int(%ld)\n", .{value.asUnchecked(.int)}); + }, + .float => { + indent(depth); + _ = phpz.printf("float(%f)\n", .{value.asUnchecked(.float)}); + }, + .string => { + indent(depth); + const text = value.asUnchecked(.string); + _ = phpz.printf("string(%d) \"%.*s\"\n", .{ + text.len, + text.len, + text.ptr, + }); + }, + .array => { + const array = value.asUnchecked(.array); + indent(depth); + _ = phpz.printf("array(%d) {\n", .{array.len()}); + + var iterator = array.fastIterator(); + while (iterator.next()) |entry| { + indent(depth + 1); + switch (entry.key) { + .int => |index| _ = phpz.printf("[%ld] =>\n", .{index}), + .string => |key| _ = phpz.printf("[\"%.*s\"] =>\n", .{ + key.len, + key.ptr, + }), + } + dumpValue(.from(entry.value), depth + 1); + } + + indent(depth); + _ = phpz.printf("}\n", .{}); + }, + .reference => { + indent(depth); + _ = phpz.printf("&", .{}); + dumpValue(.from(value.asUnchecked(.reference).val()), 0); + }, + .resource => { + indent(depth); + _ = phpz.printf("resource(%d)\n", .{value.asUnchecked(.resource).handle()}); + }, + .object => { + const object = value.asUnchecked(.object); + if (object.isEnum()) { + indent(depth); + dumpEnum(object); + return; + } + + const class_name = object.class().name(); + indent(depth); + _ = phpz.printf("class %.*s#%d (%d) {\n", .{ + class_name.len, + class_name.ptr, + object.handle(), + object.propertyCount(), + }); + + if (object.properties()) |properties| { + var iterator = properties.fastIterator(); + while (iterator.next()) |entry| { + indent(depth + 1); + printPropertyVisibility(entry.key.string); + _ = phpz.printf(" =>\n", .{}); + + const property = phpz.Zval.from(entry.value); + const raw = if (property.is(.indirect)) + property.asUnchecked(.indirect) + else + entry.value; + dumpValue(.from(raw), depth + 1); + } + } + + indent(depth); + _ = phpz.printf("}\n", .{}); + }, + else => { + indent(depth); + _ = phpz.printf("\n", .{@tagName(value.kind()).ptr}); + }, + } +} + +fn dumpEnum(object: *phpz.zend.Object) void { + const class_name = object.class().name(); + const case_name = object.enumCaseName(); + + switch (object.enumBackingType()) { + .int => { + const raw = object.enumCaseValue().?; + const backing = phpz.Zval.raw.asUnchecked(raw, .int); + _ = phpz.printf("enum %.*s { %.*s = %ld }\n", .{ + class_name.len, + class_name.ptr, + case_name.len, + case_name.ptr, + backing, + }); + }, + .string => { + const raw = object.enumCaseValue().?; + const backing = phpz.Zval.raw.asUnchecked(raw, .string); + _ = phpz.printf("enum %.*s { %.*s = \"%.*s\" }\n", .{ + class_name.len, + class_name.ptr, + case_name.len, + case_name.ptr, + backing.len, + backing.ptr, + }); + }, + else => { + _ = phpz.printf("enum %.*s { %.*s }\n", .{ + class_name.len, + class_name.ptr, + case_name.len, + case_name.ptr, + }); + }, + } +} + +fn indent(depth: usize) void { + for (0..depth) |_| { + _ = phpz.printf(" ", .{}); + } +} + +fn printPropertyVisibility(raw: []const u8) void { + if (raw.len > 0 and raw[0] == 0) { + if (raw.len > 2 and raw[1] == '*') { + const name = raw[3..]; + _ = phpz.printf("protected $%.*s", .{ name.len, name.ptr }); + return; + } + + if (std.mem.lastIndexOfScalar(u8, raw, 0)) |last| { + const name = raw[last + 1 ..]; + _ = phpz.printf("private $%.*s", .{ name.len, name.ptr }); + return; + } + } + + _ = phpz.printf("public $%.*s", .{ raw.len, raw.ptr }); +} + +pub const Class = phpz.SimpleClass("MyPHPExt\\Dumper", void); + +comptime { + Class.method("dump", dump); +} diff --git a/examples/my_php_extension/src/api/entity.zig b/examples/my_php_extension/src/api/entity.zig new file mode 100644 index 0000000..2c1c32d --- /dev/null +++ b/examples/my_php_extension/src/api/entity.zig @@ -0,0 +1,35 @@ +const Impl = extern struct { + pub fn register(impl: anytype) *phpz.ClassEntry { + return .from(impl(identifiable.Class.entry.ptr())); + } + + /// PHP: MyPHPExt\Entity::__construct(int $id): void + pub fn construct(self: *Impl, ctx: phpz.Ctx) !void { + const args = try ctx.call.expectArgs(&.{ + .{ .int = .{} }, + }, {}); + + const entity: *Class = .from(.impl, self); + try entity.updateProperty(.int, "id", args[0]); + } + + /// PHP: MyPHPExt\Entity::getId(): int + pub fn getId(self: *const Impl, ctx: phpz.Ctx) !void { + const entity: *Class = .from(.impl, @constCast(self)); + var scratch = phpz.Zval.raw.undef; + defer phpz.Zval.raw.tryDtor(&scratch); + + const id = try entity.property("id", false, &scratch); + ctx.ret.set(.int, phpz.Zval.raw.asUnchecked(id.ptr(), .int)); + } +}; + +pub const Class = phpz.Class("MyPHPExt\\Entity", Impl); + +comptime { + Class.method("__construct", .construct); + Class.method("getId", .getId); +} + +const phpz = @import("phpz"); +const identifiable = @import("identifiable.zig"); diff --git a/examples/my_php_extension/src/api/identifiable.zig b/examples/my_php_extension/src/api/identifiable.zig new file mode 100644 index 0000000..2d0b911 --- /dev/null +++ b/examples/my_php_extension/src/api/identifiable.zig @@ -0,0 +1,3 @@ +const phpz = @import("phpz"); + +pub const Class = phpz.SimpleClass("MyPHPExt\\Identifiable", void); diff --git a/examples/my_php_extension/src/api/metrics.zig b/examples/my_php_extension/src/api/metrics.zig new file mode 100644 index 0000000..e028aee --- /dev/null +++ b/examples/my_php_extension/src/api/metrics.zig @@ -0,0 +1,25 @@ +const phpz = @import("phpz"); +const metrics = @import("../metrics.zig"); + +const methods = struct { + /// PHP: MyPHPExt\Metrics::__construct() + fn construct() void {} + + /// PHP: MyPHPExt\Metrics::snapshot(): array + fn snapshot(ctx: phpz.Ctx) !void { + try metrics.writeSnapshot(ctx); + } + + /// PHP: MyPHPExt\Metrics::reset(): void + fn reset() void { + metrics.reset(); + } +}; + +pub const Class = phpz.SimpleClass("MyPHPExt\\Metrics", methods); + +comptime { + Class.method("__construct", methods.construct); + Class.method("snapshot", methods.snapshot); + Class.method("reset", methods.reset); +} diff --git a/examples/my_php_extension/src/api/role.zig b/examples/my_php_extension/src/api/role.zig new file mode 100644 index 0000000..5f11a1c --- /dev/null +++ b/examples/my_php_extension/src/api/role.zig @@ -0,0 +1,3 @@ +const phpz = @import("phpz"); + +pub const Class = phpz.SimpleClass("MyPHPExt\\Role", void); diff --git a/examples/my_php_extension/src/api/status.zig b/examples/my_php_extension/src/api/status.zig new file mode 100644 index 0000000..3d327c9 --- /dev/null +++ b/examples/my_php_extension/src/api/status.zig @@ -0,0 +1,3 @@ +const phpz = @import("phpz"); + +pub const Class = phpz.SimpleClass("MyPHPExt\\Status", void); diff --git a/examples/my_php_extension/src/api/tag.zig b/examples/my_php_extension/src/api/tag.zig new file mode 100644 index 0000000..05cc564 --- /dev/null +++ b/examples/my_php_extension/src/api/tag.zig @@ -0,0 +1,28 @@ +const Impl = extern struct { + /// PHP: MyPHPExt\Tag::__construct(string $name, ?string $description = null): void + pub fn construct(self: *Impl, ctx: phpz.Ctx) !void { + const name, const description = try ctx.call.expectArgs(&.{ + .{ .string = .{} }, + .{ .string = .{ .optional = true, .nullable = true } }, + }, {}); + + const tag: *Class = .from(.impl, self); + try tag.updateProperty(.string, "name", name); + if (description) |provided| { + switch (provided) { + .null => try tag.updateProperty(.null, "description", {}), + .value => |value| try tag.updateProperty(.string, "description", value), + } + } else { + try tag.updateProperty(.null, "description", {}); + } + } +}; + +pub const Class = phpz.Class("MyPHPExt\\Tag", Impl); + +comptime { + Class.method("__construct", .construct); +} + +const phpz = @import("phpz"); diff --git a/examples/my_php_extension/src/api/user.zig b/examples/my_php_extension/src/api/user.zig new file mode 100644 index 0000000..66d0415 --- /dev/null +++ b/examples/my_php_extension/src/api/user.zig @@ -0,0 +1,73 @@ +const Impl = extern struct { + pub fn register(impl: anytype) *phpz.ClassEntry { + return .from(impl( + entity.Class.entry.ptr(), + phpz.globals.class.entry("Stringable"), + )); + } + + /// PHP: MyPHPExt\User::__construct(int $id, string $name, ?int $age = null, MyPHPExt\Role $role = MyPHPExt\Role::User, MyPHPExt\Status $status = MyPHPExt\Status::Active): void + pub fn construct(self: *Impl, ctx: phpz.Ctx) !void { + const id, const name, const age, const role_arg, const status_arg = try ctx.call.expectArgs( + &.{ + .{ .int = .{} }, + .{ .string = .{} }, + .{ .int = .{ .optional = true, .nullable = true } }, + .{ .object = .{ .optional = true, .instanceof = true } }, + .{ .object = .{ .optional = true, .instanceof = true } }, + }, + .{ {}, {}, {}, .{ .type = role.Class.entry }, .{ .type = status.Class.entry } }, + ); + + const user: *Class = .from(.impl, self); + try user.updateProperty(.int, "id", id); + try user.updateProperty(.string, "name", name); + if (age) |provided| { + switch (provided) { + .null => try user.updateProperty(.null, "age", {}), + .value => |value| try user.updateProperty(.int, "age", value), + } + } else { + try user.updateProperty(.null, "age", {}); + } + + const role_value = role_arg orelse + (role.Class.entry.getEnumCase("User") orelse unreachable); + const status_value = status_arg orelse + (status.Class.entry.getEnumCase("Active") orelse unreachable); + try user.updateProperty(.object, "role", role_value); + try user.updateProperty(.object, "status", status_value); + } + + /// PHP: MyPHPExt\User::label(): string + pub fn label(self: *const Impl, ctx: phpz.Ctx) !void { + try returnName(self, ctx); + } + + /// PHP: MyPHPExt\User::__toString(): string + pub fn toString(self: *const Impl, ctx: phpz.Ctx) !void { + try returnName(self, ctx); + } + + fn returnName(self: *const Impl, ctx: phpz.Ctx) !void { + const user: *Class = .from(.impl, @constCast(self)); + var scratch = phpz.Zval.raw.undef; + defer phpz.Zval.raw.tryDtor(&scratch); + + const name = try user.property("name", false, &scratch); + ctx.ret.set(.string, phpz.Zval.raw.asUnchecked(name.ptr(), .string)); + } +}; + +pub const Class = phpz.Class("MyPHPExt\\User", Impl); + +comptime { + Class.method("__construct", .construct); + Class.method("label", .label); + Class.method("__toString", .toString); +} + +const phpz = @import("phpz"); +const entity = @import("entity.zig"); +const role = @import("role.zig"); +const status = @import("status.zig"); diff --git a/examples/my_php_extension/src/classes.zig b/examples/my_php_extension/src/classes.zig deleted file mode 100644 index e39438f..0000000 --- a/examples/my_php_extension/src/classes.zig +++ /dev/null @@ -1,10 +0,0 @@ -pub const identifiable = @import("classes/identifiable.zig"); -pub const status = @import("classes/status.zig"); -pub const role = @import("classes/role.zig"); -pub const my_error = @import("classes/my_error.zig"); -pub const example_attribute = @import("classes/example_attribute.zig"); -pub const abstract_entity = @import("classes/abstract_entity.zig"); -pub const user = @import("classes/user.zig"); -pub const counter = @import("classes/counter.zig"); -pub const array_like = @import("classes/array_like.zig"); -pub const dumper = @import("classes/dumper.zig"); diff --git a/examples/my_php_extension/src/classes/abstract_entity.zig b/examples/my_php_extension/src/classes/abstract_entity.zig deleted file mode 100644 index ff1e305..0000000 --- a/examples/my_php_extension/src/classes/abstract_entity.zig +++ /dev/null @@ -1,28 +0,0 @@ -const AbstractEntity = extern struct { - pub fn register(impl: anytype) *phpz.ClassEntry { - return .from(impl(Identifiable.entry.ptr())); - } - - /// protected onLoad(): void - pub fn onLoad(ctx: phpz.Ctx) !void { - const this = ctx.call.thisObject().?; - - var ret = phpz.Zval.native.undef; - defer phpz.Zval.native.dtor(&ret); - try this.call("getid", &ret, .{}); - - const id = phpz.Zval.native.asUnchecked(&ret, .int); - _ = phpz.printf("AbstractEntity::onLoad: getId() = %ld\n", .{id}); - } -}; - -pub const Class = phpz.Class("MyPHPExt\\AbstractEntity", AbstractEntity); - -comptime { - Class.method("onLoad", .onLoad); -} - -const phpz = @import("phpz"); -const c = phpz.c; - -const Identifiable = @import("identifiable.zig").Interface; diff --git a/examples/my_php_extension/src/classes/array_like.zig b/examples/my_php_extension/src/classes/array_like.zig deleted file mode 100644 index b459703..0000000 --- a/examples/my_php_extension/src/classes/array_like.zig +++ /dev/null @@ -1,158 +0,0 @@ -const ArrayLike = extern struct { - data: *phpz.zend.Array, - iter: phpz.zend.Array.Iterator, - - pub fn register(impl: anytype) *phpz.ClassEntry { - return .from(impl(phpz.globals.class.entry("ArrayAccess"), phpz.globals.class.entry("Countable"), phpz.globals.class.entry("Iterator"))); - } - - pub fn init(self: *ArrayLike) void { - self.data = phpz.zend.Array.empty(); - self.iter = self.data.iterator(); - } - - pub fn deinit(self: *ArrayLike) void { - self.data.release(); - } - - /// __construct(array $data = []) - pub fn construct(self: *ArrayLike, ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .array = .{ .optional = true } }, - }, {}); - if (args[0]) |arr| { - self.data.copy(arr); - } - } - - /// toArray(): array - pub fn toArray(self: ArrayLike, ctx: phpz.Ctx) void { - ctx.ret.set(.array, self.data.duplicate()); - } - - // ── ArrayAccess ────────────────────────────────────────────── - - /// offsetExists(mixed $offset): bool - pub fn offsetExists(self: *ArrayLike, ctx: phpz.Ctx) !void { - var offset: *c.zval = undefined; - try ctx.call.parseArgs("z", .{&offset}); - ctx.ret.set(.bool, switch (phpz.Zval.native.kind(offset)) { - .int => self.data.hasIndex(@intCast(phpz.Zval.native.asUnchecked(offset, .int))), - .string => self.data.has(phpz.Zval.native.asUnchecked(offset, .string)), - else => false, - }); - } - - /// offsetGet(mixed $offset): mixed - pub fn offsetGet(self: *ArrayLike, ctx: phpz.Ctx) !void { - var offset: *c.zval = undefined; - try ctx.call.parseArgs("z", .{&offset}); - const zv = switch (phpz.Zval.native.kind(offset)) { - .int => self.data.findIndex(@intCast(phpz.Zval.native.asUnchecked(offset, .int))), - .string => self.data.find(phpz.Zval.native.asUnchecked(offset, .string)), - else => null, - }; - if (zv) |v| { - phpz.Zval.native.tryAddref(v); - ctx.ret.set(.mixed, v); - } else { - ctx.ret.set(.null, {}); - } - } - - /// offsetSet(mixed $offset, mixed $value): void - pub fn offsetSet(self: *ArrayLike, ctx: phpz.Ctx) !void { - var offset: ?*c.zval = null; - var value: *c.zval = undefined; - try ctx.call.parseArgs("z!z", .{ &offset, &value }); - phpz.Zval.native.tryAddref(value); - if (offset) |o| { - switch (phpz.Zval.native.kind(o)) { - .int => _ = self.data.updateIndex(@intCast(phpz.Zval.native.asUnchecked(o, .int)), value), - .string => _ = self.data.update(phpz.Zval.native.asUnchecked(o, .string), value), - else => {}, - } - } else { - _ = self.data.append(value); - } - self.iter.reset(); - } - - /// offsetUnset(mixed $offset): void - pub fn offsetUnset(self: *ArrayLike, ctx: phpz.Ctx) !void { - var offset: *c.zval = undefined; - try ctx.call.parseArgs("z", .{&offset}); - switch (phpz.Zval.native.kind(offset)) { - .int => self.data.deleteIndex(@intCast(phpz.Zval.native.asUnchecked(offset, .int))) catch {}, - .string => self.data.delete(phpz.Zval.native.asUnchecked(offset, .string)) catch {}, - else => {}, - } - self.iter.reset(); - } - - // ── Countable ───────────────────────────────────────────────── - - /// count(): int - pub fn count(self: ArrayLike, ctx: phpz.Ctx) void { - ctx.ret.set(.int, @intCast(self.data.len())); - } - - // ── Iterator ────────────────────────────────────────────────── - - /// current(): mixed - pub fn current(self: *ArrayLike, ctx: phpz.Ctx) void { - if (self.iter.currentValue()) |zv| { - phpz.Zval.native.tryAddref(zv); - ctx.ret.set(.mixed, zv); - } else { - ctx.ret.set(.null, {}); - } - } - - /// key(): mixed - pub fn key(self: *ArrayLike, ctx: phpz.Ctx) void { - if (self.iter.currentKey()) |k| { - switch (k) { - .int => |i| ctx.ret.set(.int, i), - .string => |s| ctx.ret.set(.string, s), - } - } else { - ctx.ret.set(.null, {}); - } - } - - /// next(): void - pub fn next(self: *ArrayLike) void { - self.iter.advance(); - } - - /// rewind(): void - pub fn rewind(self: *ArrayLike) void { - self.iter.reset(); - } - - /// valid(): bool - pub fn valid(self: *ArrayLike, ctx: phpz.Ctx) void { - ctx.ret.set(.bool, self.iter.current() != null); - } -}; - -pub const Class = phpz.Class("MyPHPExt\\ArrayLike", ArrayLike); - -comptime { - Class.method("__construct", .construct); - Class.method("toArray", .toArray); - Class.method("offsetExists", .offsetExists); - Class.method("offsetGet", .offsetGet); - Class.method("offsetSet", .offsetSet); - Class.method("offsetUnset", .offsetUnset); - Class.method("count", .count); - Class.method("current", .current); - Class.method("key", .key); - Class.method("next", .next); - Class.method("rewind", .rewind); - Class.method("valid", .valid); -} - -const phpz = @import("phpz"); -const c = phpz.c; diff --git a/examples/my_php_extension/src/classes/counter.zig b/examples/my_php_extension/src/classes/counter.zig deleted file mode 100644 index 3f2dbdd..0000000 --- a/examples/my_php_extension/src/classes/counter.zig +++ /dev/null @@ -1,45 +0,0 @@ -pub const Counter = extern struct { - n: i64, - - /// __construct(int $n = 0) - pub fn construct(self: *Counter, ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .int = .{ .optional = true } }, - }, {}); - self.n = args[0] orelse 0; - } - - /// add(int $n): void - pub fn add(self: *Counter, ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .int = .{} }, - }, {}); - self.n +|= args[0]; - } - - /// dec(int $n): void - pub fn dec(self: *Counter, ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .int = .{} }, - }, {}); - self.n -|= args[0]; - } - - /// value(): int - pub fn value(self: Counter, ctx: phpz.Ctx) void { - ctx.ret.set(.int, self.n); - } -}; - -pub const Class = phpz.Class("MyPHPExt\\Counter", Counter); - -comptime { - Class.method("__construct", .construct); - Class.method("dec", .dec); - Class.method("add", .add); - Class.method("value", .value); -} - -const std = @import("std"); - -const phpz = @import("phpz"); diff --git a/examples/my_php_extension/src/classes/dumper.zig b/examples/my_php_extension/src/classes/dumper.zig deleted file mode 100644 index 8ebf085..0000000 --- a/examples/my_php_extension/src/classes/dumper.zig +++ /dev/null @@ -1,149 +0,0 @@ -const std = @import("std"); - -const phpz = @import("phpz"); - -/// static dump(mixed... $value): void -fn dump(ctx: phpz.Ctx) !void { - for (ctx.call.args()) |*zv| { - dumpValue(.from(zv), 0); - } -} - -fn dumpValue(val: *phpz.Zval, depth: usize) void { - switch (val.kind()) { - .null => { - indent(depth); - _ = phpz.printf("NULL\n", .{}); - }, - .bool => { - indent(depth); - _ = phpz.printf("bool(%s)\n", .{@as([*:0]const u8, if (val.asUnchecked(.bool)) "true" else "false")}); - }, - .int => { - indent(depth); - _ = phpz.printf("int(%ld)\n", .{val.asUnchecked(.int)}); - }, - .float => { - indent(depth); - _ = phpz.printf("float(%f)\n", .{val.asUnchecked(.float)}); - }, - .string => { - indent(depth); - const s = val.asUnchecked(.string); - _ = phpz.printf("string(%d) \"%.*s\"\n", .{ s.len, s.len, s.ptr }); - }, - .array => { - const zarr = val.asUnchecked(.array); - indent(depth); - _ = phpz.printf("array(%d) {\n", .{zarr.len()}); - var it = zarr.fastIterator(); - while (it.next()) |entry| { - indent(depth + 1); - switch (entry.key) { - .int => |n| _ = phpz.printf("[%ld] =>\n", .{n}), - .string => |s| _ = phpz.printf("[\"%.*s\"] =>\n", .{ s.len, s.ptr }), - } - dumpValue(.from(entry.value), depth + 1); - } - indent(depth); - _ = phpz.printf("}\n", .{}); - }, - .reference => { - indent(depth); - _ = phpz.printf("&", .{}); - dumpValue(.from(val.asUnchecked(.reference).val()), 0); - }, - .resource => { - indent(depth); - _ = phpz.printf("resource(%d)\n", .{val.asUnchecked(.resource).handle()}); - }, - .object => { - const obj = val.asUnchecked(.object); - indent(depth); - - // Check if this is an enum - if (obj.isEnum()) { - const case_name = obj.enumCaseName(); - switch (obj.enumBackingType()) { - .int => { - const v = phpz.Zval.native.asUnchecked(obj.enumCaseValue().?, .int); - _ = phpz.printf("enum %.*s { %.*s = %ld }\n", .{ - obj.class().name().len, obj.class().name().ptr, - case_name.len, case_name.ptr, - v, - }); - }, - .string => { - const v = phpz.Zval.native.asUnchecked(obj.enumCaseValue().?, .string); - _ = phpz.printf("enum %.*s { %.*s = \"%.*s\" }\n", .{ - obj.class().name().len, obj.class().name().ptr, - case_name.len, case_name.ptr, - v.len, v.ptr, - }); - }, - else => { - _ = phpz.printf("enum %.*s { %.*s }\n", .{ - obj.class().name().len, obj.class().name().ptr, - case_name.len, case_name.ptr, - }); - }, - } - return; - } - - const class_name = obj.class().name(); - const prop_count = obj.propertyCount(); - indent(depth); - _ = phpz.printf("class %.*s#%d (%d) {\n", .{ class_name.len, class_name.ptr, obj.handle(), prop_count }); - if (obj.properties()) |props| { - var it = props.fastIterator(); - while (it.next()) |entry| { - indent(depth + 1); - printPropertyVisibility(entry.key.string); - _ = phpz.printf(" =>\n", .{}); - const prop = phpz.Zval.from(entry.value); - const real = if (prop.is(.indirect)) prop.asUnchecked(.indirect) else entry.value; - dumpValue(.from(real), depth + 1); - } - } - indent(depth); - _ = phpz.printf("}\n", .{}); - }, - else => { - indent(depth); - _ = phpz.printf("\n", .{@tagName(val.kind()).ptr}); - }, - } -} - -fn indent(depth: usize) void { - var i: usize = 0; - while (i < depth) : (i += 1) { - _ = phpz.printf(" ", .{}); - } -} - -fn printPropertyVisibility(raw: []const u8) void { - if (raw.len > 0 and raw[0] == 0) { - // \0 * \0 name -> protected $name - if (raw.len > 2 and raw[1] == '*') { - const name = raw[3..]; - _ = phpz.printf("protected $%.*s", .{ name.len, name.ptr }); - return; - } - // \0 ClassName \0 name -> private $name - if (std.mem.lastIndexOfScalar(u8, raw, 0)) |last| { - const name = raw[last + 1 ..]; - _ = phpz.printf("private $%.*s", .{ name.len, name.ptr }); - return; - } - } - // public $name - _ = phpz.printf("public $%.*s", .{ raw.len, raw.ptr }); -} - -pub const Class = phpz.SimpleClass("MyPHPExt\\Dumper", void); - -comptime { - Class.method("dump", dump); -} diff --git a/examples/my_php_extension/src/classes/example_attribute.zig b/examples/my_php_extension/src/classes/example_attribute.zig deleted file mode 100644 index f6bcbf3..0000000 --- a/examples/my_php_extension/src/classes/example_attribute.zig +++ /dev/null @@ -1,25 +0,0 @@ -const ExampleAttribute = extern struct { - /// __construct(string $name, ?string $note = null) - pub fn construct(self: *ExampleAttribute, ctx: phpz.Ctx) !void { - const name, const note = try ctx.call.expectArgs(&.{ - .{ .string = .{} }, - .{ .string = .{ .optional = true, .nullable = true } }, - }, {}); - - const attr: *Class = .from(.impl, self); - try attr.updateProperty(.string, "name", name); - if (note) |val| { - if (val.asOptional()) |v| { - try attr.updateProperty(.string, "note", v); - } - } - } -}; - -pub const Class = phpz.Class("MyPHPExt\\ExampleAttribute", ExampleAttribute); - -comptime { - Class.method("__construct", .construct); -} - -const phpz = @import("phpz"); diff --git a/examples/my_php_extension/src/classes/identifiable.zig b/examples/my_php_extension/src/classes/identifiable.zig deleted file mode 100644 index 1595083..0000000 --- a/examples/my_php_extension/src/classes/identifiable.zig +++ /dev/null @@ -1,3 +0,0 @@ -const phpz = @import("phpz"); - -pub const Interface = phpz.SimpleClass("MyPHPExt\\Identifiable", void); diff --git a/examples/my_php_extension/src/classes/my_error.zig b/examples/my_php_extension/src/classes/my_error.zig deleted file mode 100644 index ad91de5..0000000 --- a/examples/my_php_extension/src/classes/my_error.zig +++ /dev/null @@ -1,10 +0,0 @@ -const phpz = @import("phpz"); -const c = phpz.c; - -const MyError = struct { - pub fn register(impl: anytype) *phpz.ClassEntry { - return .from(impl(phpz.globals.class.entry("Exception"))); - } -}; - -pub const Class = phpz.SimpleClass("MyPHPExt\\MyError", MyError); diff --git a/examples/my_php_extension/src/classes/role.zig b/examples/my_php_extension/src/classes/role.zig deleted file mode 100644 index 4deb550..0000000 --- a/examples/my_php_extension/src/classes/role.zig +++ /dev/null @@ -1,3 +0,0 @@ -const phpz = @import("phpz"); - -pub const Enum = phpz.SimpleClass("MyPHPExt\\Role", void); diff --git a/examples/my_php_extension/src/classes/status.zig b/examples/my_php_extension/src/classes/status.zig deleted file mode 100644 index 548becc..0000000 --- a/examples/my_php_extension/src/classes/status.zig +++ /dev/null @@ -1,3 +0,0 @@ -const phpz = @import("phpz"); - -pub const Enum = phpz.SimpleClass("MyPHPExt\\Status", void); diff --git a/examples/my_php_extension/src/classes/user.zig b/examples/my_php_extension/src/classes/user.zig deleted file mode 100644 index 0f36972..0000000 --- a/examples/my_php_extension/src/classes/user.zig +++ /dev/null @@ -1,93 +0,0 @@ -const User = extern struct { - pub fn register(impl: anytype) *phpz.ClassEntry { - return .from(impl(AbstractEntity.entry.ptr(), phpz.globals.class.entry("Stringable"))); - } - - /// __construct(string $name, int|null $age = null) - pub fn construct(self: *User, ctx: phpz.Ctx) !void { - const name, const age = try ctx.call.expectArgs(&.{ - .{ .string = .{} }, - .{ .int = .{ .optional = true, .nullable = true } }, - }, {}); - - const user: *Class = .from(.impl, self); - - try user.updateProperty(.string, "name", name); - if (age) |val| if (val.asOptional()) |v| - try user.updateProperty(.int, "age", v); - - try user.call("onload", null, .{}); - } - - /// handle(string|int $id): void - pub fn handle(self: *User, ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .mixed = .{ .unions = &.{ .int, .string } } }, - }, {}); - const id = args[0]; - - const user: *Class = .from(.impl, self); - const name = blk: { - var scratch = phpz.Zval.native.undef; - const prop = try user.property("name", false, &scratch); - defer phpz.Zval.native.tryDtor(&scratch); - break :blk if (prop.is(.undef)) "" else prop.asUnchecked(.string); - }; - const age: i64 = blk: { - var scratch = phpz.Zval.native.undef; - const prop = try user.property("age", false, &scratch); - defer phpz.Zval.native.tryDtor(&scratch); - break :blk if (prop.is(.undef)) 0 else prop.asUnchecked(.int); - }; - - var buf: [256]u8 = undefined; - switch (id) { - .int => |i| { - const result = try std.fmt.bufPrintSentinel(&buf, "{s}({d}).handle({d})\n", .{ name, age, i }, 0); - _ = phpz.printf(buf[0..result.len :0], .{}); - }, - .string => |s| { - const result = try std.fmt.bufPrintSentinel(&buf, "{s}({d}).handle({s})\n", .{ name, age, s }, 0); - _ = phpz.printf(buf[0..result.len :0], .{}); - }, - } - } - - /// getId(): int - pub fn getId(self: *const User, ctx: phpz.Ctx) void { - _ = self; - ctx.ret.set(.int, 9527); - } - - /// __toString(): string - pub fn toString(self: *const User, ctx: phpz.Ctx) !void { - const user: *Class = .from(.impl, @constCast(self)); - const name = blk: { - var scratch = phpz.Zval.native.undef; - const prop = try user.property("name", false, &scratch); - defer phpz.Zval.native.tryDtor(&scratch); - break :blk if (prop.is(.undef)) "" else prop.asUnchecked(.string); - }; - - const result = try std.fmt.allocPrint(gpa, "User({s})", .{name}); - defer gpa.free(result); - ctx.ret.set(.string, result); - } -}; - -pub const Class = phpz.Class("MyPHPExt\\User", User); - -comptime { - Class.method("__construct", .construct); - Class.method("handle", .handle); - Class.method("getId", .getId); - Class.method("__toString", .toString); -} - -const std = @import("std"); - -const phpz = @import("phpz"); -const c = phpz.c; - -const gpa = @import("../allocator.zig").gpa; -const AbstractEntity = @import("abstract_entity.zig").Class; diff --git a/examples/my_php_extension/src/functions.zig b/examples/my_php_extension/src/functions.zig index 0159ebf..ca1ac60 100644 --- a/examples/my_php_extension/src/functions.zig +++ b/examples/my_php_extension/src/functions.zig @@ -1,368 +1,68 @@ const std = @import("std"); const phpz = @import("phpz"); -const c = phpz.c; const Zval = phpz.Zval; -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 +/// PHP: hello(): void fn hello() void { _ = phpz.printf("Hello from ZIG!\n", .{}); } -/// greet(string $name): string +/// PHP: greet(string $name): string fn greet(ctx: phpz.Ctx) !void { const args = try ctx.call.expectArgs(&.{ .{ .string = .{} }, }, {}); - const name = args[0]; var buffer: [4096]u8 = undefined; - const result: []const u8 = try std.fmt.bufPrint(&buffer, "Hello, {s}!", .{name}); - + const result = try std.fmt.bufPrint(&buffer, "Hello, {s}!", .{args[0]}); 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 +/// PHP: MyPHPExt\increment(int &$value, int $by = 1): void fn increment(ctx: phpz.Ctx) !void { const args = try ctx.call.expectArgs(&.{ .{ .reference = .{} }, + .{ .int = .{ .optional = true } }, }, {}); - const zv = args[0]; - const raw = zv.val(); - const current = Zval.native.asUnchecked(raw, .int); - Zval.native.set(raw, .int, current + 1); -} - -/// findById(string|int $id): ?User -fn findById(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .mixed = .{ .unions = &.{ .int, .string } } }, - }, {}); - switch (args[0]) { - .int => |id| _ = phpz.printf("Found user with ID %d: ", .{id}), - .string => |name| _ = phpz.printf("Found user with name %s: ", .{name.ptr}), - } - ctx.ret.set(.null, {}); -} - -/// getDefaultUser(): User -fn getDefaultUser(ctx: phpz.Ctx) !void { - var name_zv = Zval.native.init(.string, "Default"); - defer Zval.native.dtor(&name_zv); - const age_zv = Zval.native.init(.int, 25); - - const user: *UserClass = try .new(.{ name_zv, age_zv }); - ctx.ret.set(.object, .from(&user.std)); -} - -/// listStatuses(): array -fn listStatuses(ctx: phpz.Ctx) !void { - const cases_fn = StatusEnum.entry.findMethod("cases").?; - - var cases_zv: c.zval = undefined; - try cases_fn.callStatic(StatusEnum.entry, &cases_zv, .{}); - defer phpz.Zval.native.dtor(&cases_zv); - - var ret = phpz.Zval.Array.empty(ctx.ret.ptr()); - var cases = phpz.Zval.native.asUnchecked(&cases_zv, .array); - var it = cases.fastValueIterator(); - while (it.next()) |zv| { - const entry_obj = phpz.Zval.native.asUnchecked(zv, .object); - const name = entry_obj.enumCaseName(); - const value = phpz.Zval.native.asUnchecked(entry_obj.enumCaseValue().?, .int); - ret.set(.int, name, value); - } -} - -/// testExpectArgScalars(string $str, int $int, float $float, bool $flag = true, ?string $nullable_str = null, int $opt_int = 0): array -fn testExpectArgScalars(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .string = .{} }, - .{ .int = .{} }, - .{ .float = .{} }, - .{ .bool = .{ .optional = true } }, - .{ .string = .{ .optional = true, .nullable = true } }, - .{ .int = .{ .optional = true, .zval = true } }, - }, {}); - - const str: []const u8 = args[0]; - const int_val: i64 = args[1]; - const float_val: f64 = args[2]; - const flag: bool = args[3] orelse true; - const nullable_str: ?phpz.Ctx.Call.Nullable([]const u8) = args[4]; - const opt_int_zv: ?*c.zval = args[5]; - - var result = phpz.Zval.Array.empty(ctx.ret.ptr()); - result.set(.string, "str", str); - result.set(.int, "int", int_val); - result.set(.float, "float", float_val); - result.set(.bool, "flag", flag); - - if (nullable_str) |ns| { - switch (ns) { - .null => result.set(.null, "nullable_str", {}), - .value => |s| result.set(.string, "nullable_str", s), - } - } else { - result.set(.null, "nullable_str", {}); - } - if (opt_int_zv) |zv| { - if (Zval.native.is(zv, .int)) { - result.set(.int, "opt_int", Zval.native.asUnchecked(zv, .int)); - } else { - result.set(.null, "opt_int", {}); - } - } else { - result.set(.int, "opt_int", 0); - } + const value = args[0].val(); + const by = args[1] orelse 1; + const current = Zval.raw.asUnchecked(value, .int); + Zval.raw.set(value, .int, current + by); } -/// testExpectArgArrayObject(array $data, \MyPHPExt\User $user, ?\MyPHPExt\User $nullable_user = null): array -fn testExpectArgArrayObject(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .array = .{} }, - .{ .object = .{ .instanceof = true } }, - .{ .object = .{ .instanceof = true, .optional = true, .nullable = true } }, - }, .{ - {}, - .{ .type = UserClass.entry }, - .{ .type = UserClass.entry }, - }); - - const data: *phpz.zend.Array = args[0]; - const user: *phpz.zend.Object = args[1]; - const nullable_user: ?phpz.Ctx.Call.Nullable(*phpz.zend.Object) = args[2]; - - var result = phpz.Zval.Array.empty(ctx.ret.ptr()); - result.set(.int, "data_count", @intCast(data.len())); - - var user_name_scratch = Zval.native.undef; - const user_name_zv = try user.readProperty("name", .read, &user_name_scratch); - defer Zval.native.tryDtor(&user_name_scratch); - if (Zval.native.is(user_name_zv, .string)) { - result.set(.string, "user_name", Zval.native.asUnchecked(user_name_zv, .string)); - } else { - result.set(.string, "user_name", "not_a_string"); - } - - if (nullable_user) |nu| { - switch (nu) { - .null => result.set(.null, "nullable_user", {}), - .value => |obj| { - var nu_name_scratch = Zval.native.undef; - const nu_name_zv = try obj.readProperty("name", .read, &nu_name_scratch); - defer Zval.native.tryDtor(&nu_name_scratch); - if (Zval.native.is(nu_name_zv, .string)) { - result.set(.string, "nullable_user", Zval.native.asUnchecked(nu_name_zv, .string)); - } else { - result.set(.string, "nullable_user", "not_a_string"); - } - }, - } - } else { - result.set(.null, "nullable_user", {}); - } -} - -/// testExpectArgMixed(mixed $value): mixed -fn testExpectArgMixed(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .mixed = .{} }, - }, {}); - - const zv: *c.zval = args[0]; - - // Return the value back to PHP as-is (add refcount to prevent double-free) - ctx.ret.ptr().* = zv.*; - Zval.native.addref(ctx.ret.ptr()); -} - -fn setValueSummary(result: *phpz.Zval.Array, zv: *c.zval) void { - const kind = Zval.native.kind(zv); - - result.set(.string, "kind", @tagName(kind)); - switch (kind) { - .undef => result.set(.string, "value", "undef"), - .null => result.set(.null, "value", {}), - .int => result.set(.int, "value", Zval.native.asUnchecked(zv, .int)), - .float => result.set(.float, "value", Zval.native.asUnchecked(zv, .float)), - .string => result.set(.string, "value", Zval.native.asUnchecked(zv, .string)), - .bool => result.set(.bool, "value", Zval.native.asUnchecked(zv, .bool)), - else => result.set(.string, "value", @tagName(kind)), - } -} - -/// inspectObjectProperty(object $obj, string $name, bool $silent = false): array -fn inspectObjectProperty(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .object = .{} }, - .{ .string = .{} }, - .{ .bool = .{ .optional = true } }, - }, {}); - - const obj: *phpz.zend.Object = args[0]; - const name: []const u8 = args[1]; - const silent: bool = args[2] orelse false; - - var scratch = Zval.native.undef; - const val = if (silent) - try obj.readProperty(name, .isset, &scratch) - else - try obj.readProperty(name, .read, &scratch); - defer Zval.native.tryDtor(&scratch); - - var result = phpz.Zval.Array.empty(ctx.ret.ptr()); - result.set(.bool, "scratch", !Zval.native.is(&scratch, .undef)); - setValueSummary(result, val); -} - -/// tryCreateInvalidUser(): void -fn tryCreateInvalidUser(ctx: phpz.Ctx) !void { - _ = ctx; - - const bad_name = Zval.native.init(.int, 123); - const age = Zval.native.init(.int, 1); - 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; - // TODO: more simple way to parse a callable arg without defining a target struct? - const ht, _ = try ctx.call.expectArgs( +/// PHP: MyPHPExt\mapValues(array $values, callable $mapper): array +fn mapValues(ctx: phpz.Ctx) !void { + var mapper: phpz.zend.Callable = .nil; + const values, _ = try ctx.call.expectArgs( &.{ .{ .array = .{} }, .{ .callable = .{ .resolve = true } }, }, .{ {}, - .{ .target = &cb }, + .{ .target = &mapper }, }, ); - // _ = cb.parse(cb_zv, false, null); // parse twice ! var result = phpz.Zval.Array.empty(ctx.ret.ptr()); - var it = ht.fastIterator(); - while (it.next()) |entry| { - var rv = Zval.native.undef; - try cb.withRetval(&rv).call(.{entry.value.*}); + var iterator = values.fastIterator(); + while (iterator.next()) |entry| { + var mapped = Zval.raw.undef; + defer Zval.raw.tryDtor(&mapped); + try mapper.withRetval(&mapped).call(.{entry.value.*}); switch (entry.key) { - .string => |s| result.set(.mixed, s, &rv), - .int => |i| try result.setAt(.mixed, i, &rv), + .string => |key| result.set(.mixed, key, &mapped), + .int => |key| try result.setAt(.mixed, key, &mapped), } } } -/// iniGetGreeting(): string -fn iniGetGreeting(ctx: phpz.Ctx) !void { - ctx.ret.set(.string, ini_config.greeting.get()); -} - -/// iniGetMaxUsers(): int -fn iniGetMaxUsers(ctx: phpz.Ctx) !void { - ctx.ret.set(.int, ini_config.max_users.get()); -} - -/// iniGetDebug(): bool -fn iniGetDebug(ctx: phpz.Ctx) !void { - 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 { 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); - phpz.function("MyPHPExt\\listStatuses", listStatuses); - phpz.function("MyPHPExt\\map", map); - phpz.function("MyPHPExt\\testExpectArgScalars", testExpectArgScalars); - phpz.function("MyPHPExt\\testExpectArgArrayObject", testExpectArgArrayObject); - 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); + phpz.function("MyPHPExt\\mapValues", mapValues); } diff --git a/examples/my_php_extension/src/ini.zig b/examples/my_php_extension/src/ini.zig index a7f32c9..7614c05 100644 --- a/examples/my_php_extension/src/ini.zig +++ b/examples/my_php_extension/src/ini.zig @@ -1,30 +1,35 @@ -const phpz = @import("phpz"); const std = @import("std"); +const phpz = @import("phpz"); + 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 Mode = enum { safe, fast }; +pub const Mode = enum { + safe, + fast, + 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", + }; + } +}; pub const mode = phpz.ini.Typed(Mode).new(.{ .name = "my_php_extension.mode", .default = .safe, .default_text = "safe", .access = .all, - .parse = parseMode, - .format = formatMode, + .parse = Mode.parse, + .format = Mode.format, }); -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", - }; -} +pub const definitions = &.{ greeting, max_users, debug, mode }; diff --git a/examples/my_php_extension/src/metrics.zig b/examples/my_php_extension/src/metrics.zig new file mode 100644 index 0000000..8337422 --- /dev/null +++ b/examples/my_php_extension/src/metrics.zig @@ -0,0 +1,283 @@ +const std = @import("std"); + +const phpz = @import("phpz"); + +const Call = struct { + const max_depth = 256; + const max_functions = 64; + const max_name_len = 192; + const max_duration_samples = 64; + + total: u64 = 0, + by_name: Metrics = .{}, + depth: usize = 0, + frames: [max_depth]Frame = @splat(.{}), + + const Metric = struct { + name_buffer: [max_name_len]u8 = @splat(0), + name_len: usize = 0, + kind: phpz.zend.Function.Kind = .internal, + role: phpz.zend.Function.Role = .function, + calls: u64 = 0, + duration_ns: [max_duration_samples]u64 = @splat(0), + duration_len: usize = 0, + + fn name(self: *const Metric) []const u8 { + return self.name_buffer[0..self.name_len]; + } + + fn appendDuration(self: *Metric, duration_ns: u64) void { + if (self.duration_len == self.duration_ns.len) return; + self.duration_ns[self.duration_len] = duration_ns; + self.duration_len += 1; + } + }; + + const Metrics = struct { + len: usize = 0, + items: [max_functions]Metric = @splat(.{}), + + fn getOrAdd( + self: *Metrics, + name: []const u8, + kind: phpz.zend.Function.Kind, + role: phpz.zend.Function.Role, + ) ?usize { + for (self.items[0..self.len], 0..) |*item, index| { + if (std.mem.eql(u8, item.name(), name)) return index; + } + if (self.len == self.items.len or name.len > max_name_len) return null; + + const index = self.len; + self.len += 1; + const item = &self.items[index]; + @memcpy(item.name_buffer[0..name.len], name); + item.name_len = name.len; + item.kind = kind; + item.role = role; + return index; + } + }; + + const Frame = struct { + started_at: u64 = 0, + function_index: ?usize = null, + }; + + fn writeName(function: *phpz.zend.Function, buffer: []u8) ?[]const u8 { + const name = function.name(); + if (function.ptr().*.common.scope) |scope| { + const class_name = phpz.zend.ClassEntry.from(scope).name(); + const len = class_name.len + 2 + name.len; + if (len > buffer.len) return null; + @memcpy(buffer[0..class_name.len], class_name); + @memcpy(buffer[class_name.len .. class_name.len + 2], "::"); + @memcpy(buffer[class_name.len + 2 .. len], name); + return buffer[0..len]; + } + if (name.len > buffer.len) return null; + @memcpy(buffer[0..name.len], name); + return buffer[0..name.len]; + } + + fn filter(function: *phpz.zend.Function) bool { + if (function.ptr().*.common.scope) |scope| { + const class_name = phpz.zend.ClassEntry.from(scope).name(); + if (std.mem.eql(u8, class_name, "MyPHPExt\\Metrics")) return false; + } + return !std.mem.eql(u8, function.name(), "MyPHPExt\\Test\\allocatorBailout"); + } + + fn begin(call: *phpz.Ctx.Call) void { + const function = call.function() orelse return; + const function_calls = &Globals.get().function_calls; + function_calls.total += 1; + + var name_buffer: [max_name_len]u8 = undefined; + const function_index = if (writeName(function, &name_buffer)) |name| + function_calls.by_name.getOrAdd(name, function.kind(), function.role()) + else + null; + if (function_index) |index| function_calls.by_name.items[index].calls += 1; + + const current_depth = function_calls.depth; + if (current_depth < function_calls.frames.len) { + function_calls.frames[current_depth] = .{ + .started_at = now(), + .function_index = function_index, + }; + } + function_calls.depth = current_depth + 1; + } + + fn end(call: *phpz.Ctx.Call, retval: ?*phpz.Zval) void { + _ = call; + _ = retval; + const function_calls = &Globals.get().function_calls; + if (function_calls.depth == 0) return; + + function_calls.depth -= 1; + const current_depth = function_calls.depth; + if (current_depth < function_calls.frames.len) { + const frame = function_calls.frames[current_depth]; + if (frame.function_index) |index| { + function_calls.by_name.items[index].appendDuration(now() - frame.started_at); + } + } + } + + fn write(self: *const Call, result: *phpz.Zval.Array) !void { + var by_name_zv = phpz.Zval.raw.undef; + var by_name = phpz.Zval.Array.empty(&by_name_zv); + for (self.by_name.items[0..self.by_name.len]) |*function| { + var function_zv = phpz.Zval.raw.undef; + var item = phpz.Zval.Array.empty(&function_zv); + item.set(.string, "kind", @tagName(function.kind)); + item.set(.string, "role", @tagName(function.role)); + item.set(.int, "calls", phpInt(function.calls)); + + var duration_ns_zv = phpz.Zval.raw.undef; + var duration_ns = phpz.Zval.Array.empty(&duration_ns_zv); + for (function.duration_ns[0..function.duration_len]) |duration| { + try duration_ns.append(.int, phpInt(duration)); + } + item.set(.mixed, "duration_ns", &duration_ns_zv); + by_name.set(.mixed, function.name(), &function_zv); + } + + var function_calls_zv = phpz.Zval.raw.undef; + var function_calls = phpz.Zval.Array.empty(&function_calls_zv); + function_calls.set(.int, "total", phpInt(self.total)); + function_calls.set(.mixed, "by_name", &by_name_zv); + result.set(.mixed, "function_calls", &function_calls_zv); + } +}; + +const Error = struct { + const c = phpz.c; + + total: u64 = 0, + warnings: u64 = 0, + + fn observe(info: phpz.observer.Error.Info) void { + const errors = &Globals.get().errors; + errors.total += 1; + if (info.level.matches(c.E_WARNING | c.E_CORE_WARNING | c.E_COMPILE_WARNING | c.E_USER_WARNING)) { + errors.warnings += 1; + } + } + + fn write(self: *const Error, result: *phpz.Zval.Array) void { + var errors_zv = phpz.Zval.raw.undef; + var errors = phpz.Zval.Array.empty(&errors_zv); + errors.set(.int, "total", phpInt(self.total)); + errors.set(.int, "warnings", phpInt(self.warnings)); + result.set(.mixed, "errors", &errors_zv); + } +}; + +const Exception = struct { + const max_classes = 32; + const max_name_len = 128; + + total: u64 = 0, + by_class: Classes = .{}, + + const Class = struct { + name_buffer: [max_name_len]u8 = @splat(0), + name_len: usize = 0, + count: u64 = 0, + + fn name(self: *const Class) []const u8 { + return self.name_buffer[0..self.name_len]; + } + }; + + const Classes = struct { + len: usize = 0, + items: [max_classes]Class = @splat(.{}), + + fn increment(self: *Classes, name: []const u8) void { + for (self.items[0..self.len]) |*item| { + if (std.mem.eql(u8, item.name(), name)) { + item.count += 1; + return; + } + } + if (self.len == self.items.len or name.len > max_name_len) return; + + const item = &self.items[self.len]; + self.len += 1; + @memcpy(item.name_buffer[0..name.len], name); + item.name_len = name.len; + item.count = 1; + } + }; + + fn observe(exception: *phpz.zend.Object) void { + const exceptions = &Globals.get().exceptions; + exceptions.total += 1; + exceptions.by_class.increment(exception.class().name()); + } + + fn write(self: *const Exception, result: *phpz.Zval.Array) void { + var by_class_zv = phpz.Zval.raw.undef; + var by_class = phpz.Zval.Array.empty(&by_class_zv); + for (self.by_class.items[0..self.by_class.len]) |*class| { + by_class.set(.int, class.name(), phpInt(class.count)); + } + + var exceptions_zv = phpz.Zval.raw.undef; + var exceptions = phpz.Zval.Array.empty(&exceptions_zv); + exceptions.set(.int, "total", phpInt(self.total)); + exceptions.set(.mixed, "by_class", &by_class_zv); + result.set(.mixed, "exceptions", &exceptions_zv); + } +}; + +const Request = struct { + function_calls: Call = .{}, + errors: Error = .{}, + exceptions: Exception = .{}, +}; + +pub const Globals = phpz.ModuleGlobals(Request); + +fn now() u64 { + const io = std.Io.Threaded.global_single_threaded.io(); + return @intCast(std.Io.Clock.awake.now(io).nanoseconds); +} + +fn phpInt(value: anytype) i64 { + return @intCast(@min(value, std.math.maxInt(i64))); +} + +pub const observer: phpz.observer.Config = .{ + .fcall = .{ + .filter = Call.filter, + .begin = Call.begin, + .end = Call.end, + }, + .errors = .{ + .observe = Error.observe, + }, + .exception = .{ + .observe = Exception.observe, + }, +}; + +pub fn reset() void { + Globals.get().* = .{}; +} + +pub fn requestStartup() !void { + reset(); +} + +pub fn writeSnapshot(ctx: phpz.Ctx) !void { + const request = Globals.get(); + const result = phpz.Zval.Array.empty(ctx.ret.ptr()); + try request.function_calls.write(result); + request.errors.write(result); + request.exceptions.write(result); +} diff --git a/examples/my_php_extension/src/root.zig b/examples/my_php_extension/src/root.zig index f9d173b..cfa58bb 100644 --- a/examples/my_php_extension/src/root.zig +++ b/examples/my_php_extension/src/root.zig @@ -1,51 +1,31 @@ const phpz = @import("phpz"); +pub const c = phpz.c; -const allocator = @import("allocator.zig"); -const classes = @import("classes.zig"); +const api = @import("api.zig"); const functions = @import("functions.zig"); -const ini_config = @import("ini.zig"); +const ini = @import("ini.zig"); +const metrics = @import("metrics.zig"); +const bailout = @import("testing/bailout.zig"); -fn shutdown() !void { - allocator.deinit(); -} - -// fn active() !void {} -// -// fn deactivate() !void {} - -fn info(entry: *phpz.ModuleEntry) void { +fn info(_: *c.zend_module_entry) void { phpz.info.table.start(); - phpz.info.table.row(.{ "MyPHPExtension support", "enabled" }); - phpz.info.table.row(.{ "Version", entry.version }); + phpz.info.table.header(.{ "my_php_extension support", "enabled" }); + phpz.info.table.row(.{ "Version", "0.1.0" }); phpz.info.table.end(); } comptime { + _ = functions; + _ = bailout; + phpz.module(.{ .name = "my_php_extension", .version = "0.1.0", - .globals = functions.Globals, - .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, + .classes = api.classes, + .ini = ini.definitions, + .globals = metrics.Globals, + .observer = metrics.observer, + .request_startup_fn = metrics.requestStartup, .info_fn = info, - .ini = &.{ - ini_config.greeting, - ini_config.max_users, - ini_config.debug, - ini_config.mode, - }, }); } diff --git a/examples/my_php_extension/src/testing/bailout.zig b/examples/my_php_extension/src/testing/bailout.zig new file mode 100644 index 0000000..65a34d2 --- /dev/null +++ b/examples/my_php_extension/src/testing/bailout.zig @@ -0,0 +1,46 @@ +const phpz = @import("phpz"); + +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; + } +} + +/// PHP: MyPHPExt\Test\allocatorBailout(): array +fn allocatorBailout(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); +} + +comptime { + phpz.function("MyPHPExt\\Test\\allocatorBailout", allocatorBailout); +} diff --git a/examples/my_php_extension/tests/array_like.phpt b/examples/my_php_extension/tests/array_like.phpt deleted file mode 100644 index 2d00582..0000000 --- a/examples/my_php_extension/tests/array_like.phpt +++ /dev/null @@ -1,183 +0,0 @@ ---TEST-- -ArrayLike implements ArrayAccess, Countable, Iterator ---SKIPIF-- - ---FILE-- -toArray()); - -$data = new MyPHPExt\ArrayLike(['a' => 1, 'b' => 2]); -var_dump($data->toArray()); - -// ── ArrayAccess: offsetExists / offsetGet ─────────────────────── - -var_dump(isset($data['a'])); // true -var_dump(isset($data['x'])); // false -var_dump($data['a']); // 1 -var_dump($data['x']); // null (key not found) - -// ── ArrayAccess: offsetSet ────────────────────────────────────── - -$data['c'] = 3; -$data['d'] = 'hello'; -$data[] = 'append'; -var_dump($data->toArray()); - -// ── ArrayAccess: offsetUnset ──────────────────────────────────── - -unset($data['d']); -var_dump($data->toArray()); -var_dump(isset($data['d'])); // false - -// ── Countable: count ──────────────────────────────────────────── - -var_dump(count($data)); - -// ── int keys ──────────────────────────────────────────────────── - -$intKeys = new MyPHPExt\ArrayLike([10 => 'a', 20 => 'b']); -var_dump($intKeys[10]); // 'a' -var_dump(isset($intKeys[10])); // true -$intKeys[30] = 'c'; -var_dump($intKeys->toArray()); - -// ── Iterator: foreach ─────────────────────────────────────────── - -$it = new MyPHPExt\ArrayLike(['x' => 10, 'y' => 20, 'z' => 30]); -$keys = []; -$values = []; -foreach ($it as $k => $v) { - $keys[] = $k; - $values[] = $v; -} -var_dump($keys); -var_dump($values); - -// ── Iterator: manual iteration ────────────────────────────────── - -$manual = new MyPHPExt\ArrayLike(['one' => 1, 'two' => 2]); -$manual->rewind(); -var_dump($manual->valid()); // true -var_dump($manual->current()); // 1 -var_dump($manual->key()); // 'one' -$manual->next(); -var_dump($manual->valid()); // true -var_dump($manual->current()); // 2 -var_dump($manual->key()); // 'two' -$manual->next(); -var_dump($manual->valid()); // false -var_dump($manual->current()); // null -var_dump($manual->key()); // null - -// ── toArray returns independent copy ───────────────────────────── - -$original = new MyPHPExt\ArrayLike(['x' => 1]); -$copy = $original->toArray(); -$copy['y'] = 2; -var_dump($original->toArray()); // only ['x' => 1], not affected - -// ── empty array foreach ────────────────────────────────────────── - -$emptyArr = new MyPHPExt\ArrayLike(); -$count = 0; -foreach ($emptyArr as $k => $v) { - $count++; -} -var_dump($count); // 0 - -// ── rewind + re-iterate ────────────────────────────────────────── - -$twice = new MyPHPExt\ArrayLike(['a' => 1, 'b' => 2]); -$first = []; -foreach ($twice as $k => $v) { $first[] = $v; } -$second = []; -foreach ($twice as $k => $v) { $second[] = $v; } -var_dump($first === $second); // true - -echo "done\n"; -?> ---EXPECT-- -array(0) { -} -array(2) { - ["a"]=> - int(1) - ["b"]=> - int(2) -} -bool(true) -bool(false) -int(1) -NULL -array(5) { - ["a"]=> - int(1) - ["b"]=> - int(2) - ["c"]=> - int(3) - ["d"]=> - string(5) "hello" - [0]=> - string(6) "append" -} -array(4) { - ["a"]=> - int(1) - ["b"]=> - int(2) - ["c"]=> - int(3) - [0]=> - string(6) "append" -} -bool(false) -int(4) -string(1) "a" -bool(true) -array(3) { - [10]=> - string(1) "a" - [20]=> - string(1) "b" - [30]=> - string(1) "c" -} -array(3) { - [0]=> - string(1) "x" - [1]=> - string(1) "y" - [2]=> - string(1) "z" -} -array(3) { - [0]=> - int(10) - [1]=> - int(20) - [2]=> - int(30) -} -bool(true) -int(1) -string(3) "one" -bool(true) -int(2) -string(3) "two" -bool(false) -NULL -NULL -array(1) { - ["x"]=> - int(1) -} -int(0) -bool(true) -done diff --git a/examples/my_php_extension/tests/attribute.phpt b/examples/my_php_extension/tests/attribute.phpt deleted file mode 100644 index 9a7503e..0000000 --- a/examples/my_php_extension/tests/attribute.phpt +++ /dev/null @@ -1,28 +0,0 @@ ---TEST-- -PHP 8 attributes ---FILE-- -newInstance(); - echo $label, ': ', $instance->name, ': ', $instance->note ?? 'null', PHP_EOL; -} - -$class = new ReflectionClass(User::class); -foreach ($class->getAttributes(ExampleAttribute::class) as $attribute) { - dump_attribute('class', $attribute); -} - -$method = $class->getMethod('handle'); -$parameter = $method->getParameters()[0]; -dump_attribute('parameter id', $parameter->getAttributes(ExampleAttribute::class)[0]); - -?> ---EXPECT-- -class: entity: primary user model -class: audited: null -parameter id: identifier: string or int diff --git a/examples/my_php_extension/tests/attributes.phpt b/examples/my_php_extension/tests/attributes.phpt new file mode 100644 index 0000000..a741a12 --- /dev/null +++ b/examples/my_php_extension/tests/attributes.phpt @@ -0,0 +1,35 @@ +--TEST-- +MyPHPExt Tag supports metadata and repeatable runtime attributes +--FILE-- +getAttributes(Tag::class); +$userTag = $userAttributes[0]->newInstance(); +var_dump($userTag->name); +var_dump($userTag->description); + +#[Tag('first'), Tag('second', 'details')] +class TaggedExample {} + +foreach ((new ReflectionClass(TaggedExample::class))->getAttributes(Tag::class) as $attribute) { + $tag = $attribute->newInstance(); + var_dump([$tag->name, $tag->description]); +} +--EXPECT-- +string(5) "model" +string(59) "Demonstrates properties, inheritance, enums, and attributes" +array(2) { + [0]=> + string(5) "first" + [1]=> + NULL +} +array(2) { + [0]=> + string(6) "second" + [1]=> + string(7) "details" +} diff --git a/examples/my_php_extension/tests/heap_allocator_bailout.phpt b/examples/my_php_extension/tests/bailout.phpt similarity index 60% rename from examples/my_php_extension/tests/heap_allocator_bailout.phpt rename to examples/my_php_extension/tests/bailout.phpt index d499778..840154b 100644 --- a/examples/my_php_extension/tests/heap_allocator_bailout.phpt +++ b/examples/my_php_extension/tests/bailout.phpt @@ -1,14 +1,14 @@ --TEST-- -php allocator converts memory_limit bailout to OutOfMemory and runs defers +my_php_extension allocator bailout preserves Zig cleanup +--EXTENSIONS-- +my_php_extension --INI-- -memory_limit=8M +memory_limit=16M display_errors=0 log_errors=0 --FILE-- --EXPECT-- array(4) { diff --git a/examples/my_php_extension/tests/collection.phpt b/examples/my_php_extension/tests/collection.phpt new file mode 100644 index 0000000..ae691e4 --- /dev/null +++ b/examples/my_php_extension/tests/collection.phpt @@ -0,0 +1,66 @@ +--TEST-- +MyPHPExt\Collection implements ArrayAccess, Countable, and Iterator +--FILE-- + 1, "b" => 2]); +var_dump($collection->toArray()); +var_dump(isset($collection["a"]), isset($collection["missing"])); +var_dump($collection["a"], $collection["missing"]); + +$collection["c"] = 3; +$collection[] = 4; +unset($collection["b"]); +var_dump($collection->toArray()); +var_dump(count($collection)); + +$keys = []; +$values = []; +foreach ($collection as $key => $value) { + $keys[] = $key; + $values[] = $value; +} +var_dump($keys, $values); + +$copy = $collection->toArray(); +$copy["local"] = true; +var_dump(isset($collection["local"])); + +?> +--EXPECT-- +array(2) { + ["a"]=> + int(1) + ["b"]=> + int(2) +} +bool(true) +bool(false) +int(1) +NULL +array(3) { + ["a"]=> + int(1) + ["c"]=> + int(3) + [0]=> + int(4) +} +int(3) +array(3) { + [0]=> + string(1) "a" + [1]=> + string(1) "c" + [2]=> + int(0) +} +array(3) { + [0]=> + int(1) + [1]=> + int(3) + [2]=> + int(4) +} +bool(false) diff --git a/examples/my_php_extension/tests/config.phpt b/examples/my_php_extension/tests/config.phpt new file mode 100644 index 0000000..7721b38 --- /dev/null +++ b/examples/my_php_extension/tests/config.phpt @@ -0,0 +1,40 @@ +--TEST-- +MyPHPExt\Config exposes typed INI values +--INI-- +my_php_extension.greeting=ZigRules +my_php_extension.max_users=42 +my_php_extension.debug=1 +my_php_extension.mode=fast +--FILE-- + +--EXPECT-- +string(8) "ZigRules" +int(42) +bool(true) +string(4) "fast" +bool(false) +string(7) "Runtime" +int(42) +bool(false) +string(4) "safe" diff --git a/examples/my_php_extension/tests/constants.phpt b/examples/my_php_extension/tests/constants.phpt deleted file mode 100644 index fb89d5a..0000000 --- a/examples/my_php_extension/tests/constants.phpt +++ /dev/null @@ -1,14 +0,0 @@ ---TEST-- -Constants ---FILE-- - ---EXPECT-- -MY_EXT_VERSION: 1.0.0 -MyPHPExt\VERSION: 1 -User::MIN_AGE: 0 diff --git a/examples/my_php_extension/tests/counter.phpt b/examples/my_php_extension/tests/counter.phpt index fbd5aee..85ea3f1 100644 --- a/examples/my_php_extension/tests/counter.phpt +++ b/examples/my_php_extension/tests/counter.phpt @@ -1,15 +1,23 @@ --TEST-- -Counter class methods +MyPHPExt Counter updates, reads, and resets its value --FILE-- add(10); -$obj->dec(5); -echo 'counter: ', $obj->value(), PHP_EOL; - -?> +$counter = new Counter(10); +var_dump($counter->value()); +var_dump($counter->increment()); +var_dump($counter->increment(4)); +var_dump($counter->decrement(3)); +$counter->reset(); +var_dump($counter->value()); +$counter->reset(20); +var_dump($counter->value()); --EXPECT-- -counter: 15 +int(10) +int(11) +int(15) +int(12) +int(0) +int(20) diff --git a/examples/my_php_extension/tests/dumper.phpt b/examples/my_php_extension/tests/dumper.phpt new file mode 100644 index 0000000..9a6a8df --- /dev/null +++ b/examples/my_php_extension/tests/dumper.phpt @@ -0,0 +1,48 @@ +--TEST-- +MyPHPExt\Dumper renders representative PHP values +--FILE-- + "phpz", "nested" => [2, false]], + new MyPHPExt\Counter(), + new MyPHPEXT\User(1, 'admin'), + MyPHPExt\Role::Admin, +); + +?> +--EXPECT-- +int(1) +string(5) "hello" +bool(true) +NULL +array(2) { + ["name"] => + string(4) "phpz" + ["nested"] => + array(2) { + [0] => + int(2) + [1] => + bool(false) + } +} +class MyPHPExt\Counter#1 (0) { +} +class MyPHPExt\User#2 (5) { + protected $id => + int(1) + public $name => + string(5) "admin" + public $age => + NULL + public $role => + enum MyPHPExt\Role { User = "user" } + public $status => + enum MyPHPExt\Status { Active = 1 } +} +enum MyPHPExt\Role { Admin = "admin" } diff --git a/examples/my_php_extension/tests/dumper_array.phpt b/examples/my_php_extension/tests/dumper_array.phpt deleted file mode 100644 index dfb3536..0000000 --- a/examples/my_php_extension/tests/dumper_array.phpt +++ /dev/null @@ -1,63 +0,0 @@ ---TEST-- -Dumper::dump with arrays ---FILE-- - 1, "b" => 2]); - -echo "--- nested ---\n"; -\MyPHPExt\Dumper::dump(["a" => 1, "b" => [2, 3, "x" => [4, 5]], "c" => ["nested" => ["deep" => true, -1 => -1, -3 => new \stdClass]]]); - -?> ---EXPECTF-- ---- list --- -array(3) { - [0] => - int(1) - [1] => - int(2) - [2] => - int(3) -} ---- assoc --- -array(2) { - ["a"] => - int(1) - ["b"] => - int(2) -} ---- nested --- -array(3) { - ["a"] => - int(1) - ["b"] => - array(3) { - [0] => - int(2) - [1] => - int(3) - ["x"] => - array(2) { - [0] => - int(4) - [1] => - int(5) - } - } - ["c"] => - array(1) { - ["nested"] => - array(3) { - ["deep"] => - bool(true) - [-1] => - int(-1) - [-3] => - %Aclass stdClass%A - } - } -} diff --git a/examples/my_php_extension/tests/dumper_object.phpt b/examples/my_php_extension/tests/dumper_object.phpt deleted file mode 100644 index 0e76d5b..0000000 --- a/examples/my_php_extension/tests/dumper_object.phpt +++ /dev/null @@ -1,54 +0,0 @@ ---TEST-- -Dumper::dump with objects ---FILE-- - ---EXPECTF-- ---- default user --- -AbstractEntity::onLoad: getId() = 9527 -class MyPHPExt\User%A (2) { - public $name => - string(7) "Default" - public $age => - int(25) -} ---- multiple args --- -int(42) -string(4) "text" -array(3) { - [0] => - int(1) - [1] => - int(2) - [2] => - int(3) -} -class stdClass%A (0) { -} ---- anonymous class --- -class class@anonymous%A (4) { - public $name => - string(5) "Alice" - public $age => - int(30) - protected $role => - string(5) "admin" - private $secret => - string(3) "xyz" -} diff --git a/examples/my_php_extension/tests/dumper_reference.phpt b/examples/my_php_extension/tests/dumper_reference.phpt deleted file mode 100644 index a0008e1..0000000 --- a/examples/my_php_extension/tests/dumper_reference.phpt +++ /dev/null @@ -1,24 +0,0 @@ ---TEST-- -Dumper::dump with reference ---FILE-- - ---EXPECTF-- -int(42) -array(3) { - [0] => - &int(1) - [1] => - int(2) - [2] => - int(3) -} diff --git a/examples/my_php_extension/tests/dumper_resource.phpt b/examples/my_php_extension/tests/dumper_resource.phpt deleted file mode 100644 index 79e7fca..0000000 --- a/examples/my_php_extension/tests/dumper_resource.phpt +++ /dev/null @@ -1,12 +0,0 @@ ---TEST-- -Dumper::dump with resource ---FILE-- - ---EXPECTF-- -resource(%d) diff --git a/examples/my_php_extension/tests/dumper_scalars.phpt b/examples/my_php_extension/tests/dumper_scalars.phpt deleted file mode 100644 index 2fa9ea1..0000000 --- a/examples/my_php_extension/tests/dumper_scalars.phpt +++ /dev/null @@ -1,15 +0,0 @@ ---TEST-- -Dumper::dump with scalars ---FILE-- - ---EXPECTF-- -int(1) -string(5) "hello" -float(%f) -bool(true) -bool(false) -NULL diff --git a/examples/my_php_extension/tests/enum.phpt b/examples/my_php_extension/tests/enum.phpt deleted file mode 100644 index 78c51d3..0000000 --- a/examples/my_php_extension/tests/enum.phpt +++ /dev/null @@ -1,22 +0,0 @@ ---TEST-- -Enum ---FILE-- -name, '=', $case->value, ' '; -} -echo PHP_EOL; - -echo 'Dumper on enum: '; -\MyPHPExt\Dumper::dump(Status::Active, Role::Admin); - -?> ---EXPECT-- -Status cases: Active=1 Inactive=0 -Dumper on enum: enum MyPHPExt\Status { Active = 1 } -enum MyPHPExt\Role { Admin = "admin" } diff --git a/examples/my_php_extension/tests/enums.phpt b/examples/my_php_extension/tests/enums.phpt new file mode 100644 index 0000000..08a7aa0 --- /dev/null +++ b/examples/my_php_extension/tests/enums.phpt @@ -0,0 +1,28 @@ +--TEST-- +MyPHPExt backed enums and Identifiable contract +--FILE-- +value); +var_dump(Status::Active->value); +var_dump(Role::User->value); +var_dump(Role::Admin->value); +var_dump(array_map(static fn (Status $case): string => $case->name, Status::cases())); +var_dump(is_subclass_of(User::class, Identifiable::class)); +--EXPECT-- +int(0) +int(1) +string(4) "user" +string(5) "admin" +array(2) { + [0]=> + string(8) "Inactive" + [1]=> + string(6) "Active" +} +bool(true) diff --git a/examples/my_php_extension/tests/error_class.phpt b/examples/my_php_extension/tests/error_class.phpt deleted file mode 100644 index 654a7b1..0000000 --- a/examples/my_php_extension/tests/error_class.phpt +++ /dev/null @@ -1,12 +0,0 @@ ---TEST-- -Error class ---FILE-- - ---EXPECT-- -MyError is Exception subclass: yes diff --git a/examples/my_php_extension/tests/functions.phpt b/examples/my_php_extension/tests/functions.phpt index 035a471..edb22af 100644 --- a/examples/my_php_extension/tests/functions.phpt +++ b/examples/my_php_extension/tests/functions.phpt @@ -1,39 +1,30 @@ --TEST-- -Functions +my_php_extension functions +--EXTENSIONS-- +my_php_extension --FILE-- 2, 8 => 3], + static fn (int $value): int => $value * 2, +)); +var_dump(MyPHPExt\VERSION); ?> --EXPECT-- Hello from ZIG! -Hello, Alice! -increment(41): 42 -findById: -Found user with ID 1: NULL -Found user with name abc: NULL -getDefaultUser: AbstractEntity::onLoad: getId() = 9527 -User(Default) -listStatuses: array(2) { - ["Active"]=> - int(1) - ["Inactive"]=> - int(0) +string(11) "Hello, Zig!" +int(15) +array(2) { + ["first"]=> + int(4) + [8]=> + int(6) } +string(5) "0.1.0" diff --git a/examples/my_php_extension/tests/ini_bool.phpt b/examples/my_php_extension/tests/ini_bool.phpt deleted file mode 100644 index 71a6fe7..0000000 --- a/examples/my_php_extension/tests/ini_bool.phpt +++ /dev/null @@ -1,20 +0,0 @@ ---TEST-- -INI access=user: ini_set works, default + override + ini_set ---INI-- -my_php_extension.debug=1 ---FILE-- - ---EXPECT-- -default in php.ini: 0 -via -d override: 1 -ini_set result: '1' -after ini_set: 0 diff --git a/examples/my_php_extension/tests/ini_string.phpt b/examples/my_php_extension/tests/ini_string.phpt deleted file mode 100644 index 0c341f6..0000000 --- a/examples/my_php_extension/tests/ini_string.phpt +++ /dev/null @@ -1,20 +0,0 @@ ---TEST-- -INI access=all: ini_set works, default + override + ini_set ---INI-- -my_php_extension.greeting=Override ---FILE-- - ---EXPECT-- -default in php.ini: Hello -via -d override: Override -ini_set result: 'Override' -after ini_set: Runtime diff --git a/examples/my_php_extension/tests/ini_system.phpt b/examples/my_php_extension/tests/ini_system.phpt deleted file mode 100644 index cce3621..0000000 --- a/examples/my_php_extension/tests/ini_system.phpt +++ /dev/null @@ -1,16 +0,0 @@ ---TEST-- -INI access=system: ini_set blocked, only php.ini ---FILE-- - ---EXPECT-- -default (php.ini): 100 -ini_set result: false -after ini_set: 100 diff --git a/examples/my_php_extension/tests/ini_zig_getters.phpt b/examples/my_php_extension/tests/ini_zig_getters.phpt deleted file mode 100644 index c78f74c..0000000 --- a/examples/my_php_extension/tests/ini_zig_getters.phpt +++ /dev/null @@ -1,41 +0,0 @@ ---TEST-- -INI values read via Zig typed getters, with ini_set ---INI-- -my_php_extension.greeting=ZigRules -my_php_extension.max_users=42 -my_php_extension.debug=1 -my_php_extension.mode=fast ---FILE-- - ---EXPECT-- ---- default/override --- -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/interface.phpt b/examples/my_php_extension/tests/interface.phpt deleted file mode 100644 index 2f7d603..0000000 --- a/examples/my_php_extension/tests/interface.phpt +++ /dev/null @@ -1,12 +0,0 @@ ---TEST-- -Interface ---FILE-- - ---EXPECT-- -User implements Identifiable: yes diff --git a/examples/my_php_extension/tests/map.phpt b/examples/my_php_extension/tests/map.phpt deleted file mode 100644 index 84d5680..0000000 --- a/examples/my_php_extension/tests/map.phpt +++ /dev/null @@ -1,92 +0,0 @@ ---TEST-- -map() function ---FILE-- - 1, 'b' => 2, 'c' => 3]; -$with_key = map($assoc, function (int $v): string { - return "val_{$v}"; -}); -var_dump($with_key); - -echo "=== empty array ===", PHP_EOL; -$empty = map([], function ($x) { return $x; }); -var_dump($empty); - -echo "=== mixed key types ===", PHP_EOL; -$mixed = ['foo', 42 => 'bar', 'baz' => 'qux']; -$upper = map($mixed, 'strtoupper'); -var_dump($upper); - -echo "=== closure with external variable ===", PHP_EOL; -$factor = 10; -$scaled = map([2, 5, 7], function (int $n) use ($factor): int { - return $n * $factor; -}); -var_dump($scaled); - -echo "=== object mapping ===", PHP_EOL; -$objs = [new stdClass(), new stdClass()]; -$hashes = map($objs, 'spl_object_hash'); -var_dump($hashes); - -?> ---EXPECTF-- -=== int array: double each item === -array(4) { - [0]=> - int(2) - [1]=> - int(4) - [2]=> - int(6) - [3]=> - int(8) -} -=== string keys preserved === -array(3) { - ["a"]=> - string(5) "val_1" - ["b"]=> - string(5) "val_2" - ["c"]=> - string(5) "val_3" -} -=== empty array === -array(0) { -} -=== mixed key types === -array(3) { - [0]=> - string(3) "FOO" - [42]=> - string(3) "BAR" - ["baz"]=> - string(3) "QUX" -} -=== closure with external variable === -array(3) { - [0]=> - int(20) - [1]=> - int(50) - [2]=> - int(70) -} -=== object mapping === -array(2) { - [0]=> - string(%d) "%x" - [1]=> - string(%d) "%x" -} diff --git a/examples/my_php_extension/tests/metrics.phpt b/examples/my_php_extension/tests/metrics.phpt new file mode 100644 index 0000000..839f90d --- /dev/null +++ b/examples/my_php_extension/tests/metrics.phpt @@ -0,0 +1,183 @@ +--TEST-- +my_php_extension function, error and exception metrics +--EXTENSIONS-- +my_php_extension +--FILE-- + +--EXPECTF-- +array(3) { + ["function_calls"]=> + array(2) { + ["total"]=> + int(11) + ["by_name"]=> + array(6) { + ["metricUserFunction"]=> + array(4) { + ["kind"]=> + string(4) "user" + ["role"]=> + string(8) "function" + ["calls"]=> + int(2) + ["duration_ns"]=> + array(2) { + [0]=> + int(%d) + [1]=> + int(%d) + } + } + ["trim"]=> + array(4) { + ["kind"]=> + string(8) "internal" + ["role"]=> + string(8) "function" + ["calls"]=> + int(2) + ["duration_ns"]=> + array(2) { + [0]=> + int(%d) + [1]=> + int(%d) + } + } + ["strtoupper"]=> + array(4) { + ["kind"]=> + string(8) "internal" + ["role"]=> + string(8) "function" + ["calls"]=> + int(2) + ["duration_ns"]=> + array(2) { + [0]=> + int(%d) + [1]=> + int(%d) + } + } + ["greet"]=> + array(4) { + ["kind"]=> + string(8) "internal" + ["role"]=> + string(8) "function" + ["calls"]=> + int(2) + ["duration_ns"]=> + array(2) { + [0]=> + int(%d) + [1]=> + int(%d) + } + } + ["trigger_error"]=> + array(4) { + ["kind"]=> + string(8) "internal" + ["role"]=> + string(8) "function" + ["calls"]=> + int(1) + ["duration_ns"]=> + array(1) { + [0]=> + int(%d) + } + } + ["Exception::__construct"]=> + array(4) { + ["kind"]=> + string(8) "internal" + ["role"]=> + string(15) "instance_method" + ["calls"]=> + int(2) + ["duration_ns"]=> + array(2) { + [0]=> + int(%d) + [1]=> + int(%d) + } + } + } + } + ["errors"]=> + array(2) { + ["total"]=> + int(1) + ["warnings"]=> + int(1) + } + ["exceptions"]=> + array(2) { + ["total"]=> + int(2) + ["by_class"]=> + array(2) { + ["LogicException"]=> + int(1) + ["RuntimeException"]=> + int(1) + } + } +} +array(3) { + ["function_calls"]=> + array(2) { + ["total"]=> + int(0) + ["by_name"]=> + array(0) { + } + } + ["errors"]=> + array(2) { + ["total"]=> + int(0) + ["warnings"]=> + int(0) + } + ["exceptions"]=> + array(2) { + ["total"]=> + int(0) + ["by_class"]=> + array(0) { + } + } +} diff --git a/examples/my_php_extension/tests/module_globals.phpt b/examples/my_php_extension/tests/module_globals.phpt deleted file mode 100644 index 46dd133..0000000 --- a/examples/my_php_extension/tests/module_globals.phpt +++ /dev/null @@ -1,14 +0,0 @@ ---TEST-- -Module globals ---FILE-- - ---EXPECT-- -int(42) -int(43) -int(43) diff --git a/examples/my_php_extension/tests/new_with_constructor_failure.phpt b/examples/my_php_extension/tests/new_with_constructor_failure.phpt deleted file mode 100644 index b074e5b..0000000 --- a/examples/my_php_extension/tests/new_with_constructor_failure.phpt +++ /dev/null @@ -1,16 +0,0 @@ ---TEST-- -new releases the object when the constructor throws ---FILE-- -getMessage(), 'must be of type string') ? 'message ok' : $e->getMessage(), PHP_EOL; -} - -?> ---EXPECT-- -TypeError -message ok diff --git a/examples/my_php_extension/tests/property_scratch.phpt b/examples/my_php_extension/tests/property_scratch.phpt deleted file mode 100644 index 1b6544d..0000000 --- a/examples/my_php_extension/tests/property_scratch.phpt +++ /dev/null @@ -1,40 +0,0 @@ ---TEST-- -Property reads use caller-provided scratch only when Zend needs it ---FILE-- - 'plain']; - -foreach ([ - inspectObjectProperty($plain, 'name'), - inspectObjectProperty(new MagicBox(), 'name'), - inspectObjectProperty(new TypedBox(), 'name', true), - inspectObjectProperty(new TypedBox(), 'missing', true), -] as $row) { - echo $row['kind'], '|', ($row['scratch'] ? 'scratch' : 'borrowed'), '|'; - var_export($row['value']); - echo PHP_EOL; -} - -?> ---EXPECT-- -string|borrowed|'plain' -string|scratch|'magic:name' -null|borrowed|NULL -null|borrowed|NULL diff --git a/examples/my_php_extension/tests/test_expect_arg_array_object.phpt b/examples/my_php_extension/tests/test_expect_arg_array_object.phpt deleted file mode 100644 index fc486f5..0000000 --- a/examples/my_php_extension/tests/test_expect_arg_array_object.phpt +++ /dev/null @@ -1,120 +0,0 @@ ---TEST-- -testExpectArgArrayObject() — covers .array, .object+type, nullable object+type ---FILE-- - 1, "y" => 2], $u1)); - -// === 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+type with null -> .null) === -echo "=== array + User + null ===", PHP_EOL; -$u3 = new \MyPHPExt\User("Bob", 25); -var_dump(testExpectArgArrayObject(["a"], $u3, null)); - -// === Success: array + User + omitted (covers: optional not passed -> null) === -echo "=== array + User (no 3rd arg) ===", PHP_EOL; -$u4 = new \MyPHPExt\User("Charlie", 40); -var_dump(testExpectArgArrayObject([], $u4)); - -// === Error: too few args (covers: required not passed) === -echo "=== too few: 0 args ===", PHP_EOL; -try { testExpectArgArrayObject(); } catch (ArgumentCountError $e) { echo $e->getMessage(), PHP_EOL; } - -echo "=== too few: 1 arg ===", PHP_EOL; -try { testExpectArgArrayObject([]); } catch (ArgumentCountError $e) { echo $e->getMessage(), PHP_EOL; } - -// === Error: too many args (covers: count > max) === -echo "=== too many: 4 args ===", PHP_EOL; -$u = new \MyPHPExt\User("X", 0); -try { testExpectArgArrayObject([], $u, null, "extra"); } catch (ArgumentCountError $e) { echo $e->getMessage(), PHP_EOL; } - -// === Error: wrong type for array (covers: .array type mismatch) === -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 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; } - -// === Error: wrong class for user (covers: .object instanceof fails) === -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+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+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; } - -?> ---EXPECT-- -=== array + User === -AbstractEntity::onLoad: getId() = 9527 -array(3) { - ["data_count"]=> - int(2) - ["user_name"]=> - string(5) "Alice" - ["nullable_user"]=> - NULL -} -=== array + User + nullable User === -AbstractEntity::onLoad: getId() = 9527 -AbstractEntity::onLoad: getId() = 9527 -array(3) { - ["data_count"]=> - int(3) - ["user_name"]=> - string(5) "First" - ["nullable_user"]=> - string(6) "Second" -} -=== array + User + null === -AbstractEntity::onLoad: getId() = 9527 -array(3) { - ["data_count"]=> - int(1) - ["user_name"]=> - string(3) "Bob" - ["nullable_user"]=> - NULL -} -=== array + User (no 3rd arg) === -AbstractEntity::onLoad: getId() = 9527 -array(3) { - ["data_count"]=> - int(0) - ["user_name"]=> - string(7) "Charlie" - ["nullable_user"]=> - NULL -} -=== too few: 0 args === -MyPHPExt\testExpectArgArrayObject() expects at least 2 arguments, 0 given -=== too few: 1 arg === -MyPHPExt\testExpectArgArrayObject() expects at least 2 arguments, 1 given -=== too many: 4 args === -AbstractEntity::onLoad: getId() = 9527 -MyPHPExt\testExpectArgArrayObject() expects at most 3 arguments, 4 given -=== wrong type: string for array === -MyPHPExt\testExpectArgArrayObject(): Argument #1 ($data) must be of type array, string given -=== not object for user: string === -MyPHPExt\testExpectArgArrayObject(): Argument #2 ($user) must be instance of MyPHPExt\User, string given -=== wrong class for user: stdClass === -MyPHPExt\testExpectArgArrayObject(): Argument #2 ($user) must be instance of MyPHPExt\User, stdClass given -=== not object for nullable_user: int === -MyPHPExt\testExpectArgArrayObject(): Argument #3 ($nullable_user) must be instance of MyPHPExt\User or null, int given -=== wrong class for nullable_user: stdClass === -MyPHPExt\testExpectArgArrayObject(): Argument #3 ($nullable_user) must be instance of MyPHPExt\User or null, stdClass given diff --git a/examples/my_php_extension/tests/test_expect_arg_mixed.phpt b/examples/my_php_extension/tests/test_expect_arg_mixed.phpt deleted file mode 100644 index e5eb982..0000000 --- a/examples/my_php_extension/tests/test_expect_arg_mixed.phpt +++ /dev/null @@ -1,70 +0,0 @@ ---TEST-- -testExpectArgMixed() — covers .mixed (returns raw zval) ---FILE-- -getMessage(), PHP_EOL; } - -// === Error: too many args === -echo "=== too many: 2 args ===", PHP_EOL; -try { testExpectArgMixed(1, 2); } catch (ArgumentCountError $e) { echo $e->getMessage(), PHP_EOL; } - -?> ---EXPECT-- -=== string === -string(11) "hello world" -=== int === -int(42) -=== float === -float(3.14) -=== bool true === -bool(true) -=== bool false === -bool(false) -=== null === -NULL -=== array === -array(3) { - [0]=> - int(1) - [1]=> - string(3) "two" - [2]=> - float(3) -} -=== too few: 0 args === -MyPHPExt\testExpectArgMixed() expects exactly 1 argument, 0 given -=== too many: 2 args === -MyPHPExt\testExpectArgMixed() expects exactly 1 argument, 2 given diff --git a/examples/my_php_extension/tests/test_expect_arg_scalars.phpt b/examples/my_php_extension/tests/test_expect_arg_scalars.phpt deleted file mode 100644 index 69851c0..0000000 --- a/examples/my_php_extension/tests/test_expect_arg_scalars.phpt +++ /dev/null @@ -1,158 +0,0 @@ ---TEST-- -testExpectArgScalars() — covers .string, .int, .float, .bool with optional, nullable, zval ---FILE-- - null, default values) === -echo "=== min 3 args ===", PHP_EOL; -var_dump(testExpectArgScalars("min", 1, 2.5)); - -// === Success: null nullable_str (covers: nullable with null -> .null) === -echo "=== null nullable_str ===", PHP_EOL; -var_dump(testExpectArgScalars("x", 0, 0.0, false, null)); - -// === Success: 4 args (covers: flag false, nullable_str not passed, opt_int not passed) === -echo "=== 4 args: name, int, float, flag=false ===", PHP_EOL; -var_dump(testExpectArgScalars("four", 100, 1.23, false)); - -// === Success: 5 args (covers: flag default=true overriding) === -echo "=== 5 args ===", PHP_EOL; -var_dump(testExpectArgScalars("five", 200, 5.67, false, "five_nullable")); - -// === Error: too few args (covers: required not passed -> ArgumentCountError) === -echo "=== too few: 0 args ===", PHP_EOL; -try { testExpectArgScalars(); } catch (ArgumentCountError $e) { echo $e->getMessage(), PHP_EOL; } - -echo "=== too few: 1 arg ===", PHP_EOL; -try { testExpectArgScalars("a"); } catch (ArgumentCountError $e) { echo $e->getMessage(), PHP_EOL; } - -echo "=== too few: 2 args ===", PHP_EOL; -try { testExpectArgScalars("a", 1); } catch (ArgumentCountError $e) { echo $e->getMessage(), PHP_EOL; } - -// === Error: too many args (covers: count > max -> ArgumentCountError) === -echo "=== too many: 7 args ===", PHP_EOL; -try { testExpectArgScalars("a", 1, 2.0, true, "s", 3, "extra"); } catch (ArgumentCountError $e) { echo $e->getMessage(), PHP_EOL; } - -// === Error: wrong type for each required arg (covers: type mismatch -> TypeError) === -echo "=== wrong type: int for str ===", PHP_EOL; -try { testExpectArgScalars(123, 1, 2.0); } catch (TypeError $e) { echo $e->getMessage(), PHP_EOL; } - -echo "=== wrong type: string for int ===", PHP_EOL; -try { testExpectArgScalars("a", "bad", 2.0); } catch (TypeError $e) { echo $e->getMessage(), PHP_EOL; } - -echo "=== wrong type: string for float ===", PHP_EOL; -try { testExpectArgScalars("a", 1, "bad"); } catch (TypeError $e) { echo $e->getMessage(), PHP_EOL; } - -// === Error: wrong type for optional args (covers: nullable type mismatch) === -echo "=== wrong type: int for bool flag ===", PHP_EOL; -try { testExpectArgScalars("a", 1, 2.0, 123); } catch (TypeError $e) { echo $e->getMessage(), PHP_EOL; } - -echo "=== wrong type: int for nullable_str ===", PHP_EOL; -try { testExpectArgScalars("a", 1, 2.0, true, 123); } catch (TypeError $e) { echo $e->getMessage(), PHP_EOL; } - -echo "=== wrong type: string for opt_int ===", PHP_EOL; -try { testExpectArgScalars("a", 1, 2.0, true, null, "bad"); } catch (TypeError $e) { echo $e->getMessage(), PHP_EOL; } - -?> ---EXPECT-- -=== all 6 args === -array(6) { - ["str"]=> - string(5) "hello" - ["int"]=> - int(42) - ["float"]=> - float(3.14) - ["flag"]=> - bool(true) - ["nullable_str"]=> - string(12) "nullable_val" - ["opt_int"]=> - int(99) -} -=== min 3 args === -array(6) { - ["str"]=> - string(3) "min" - ["int"]=> - int(1) - ["float"]=> - float(2.5) - ["flag"]=> - bool(true) - ["nullable_str"]=> - NULL - ["opt_int"]=> - int(0) -} -=== null nullable_str === -array(6) { - ["str"]=> - string(1) "x" - ["int"]=> - int(0) - ["float"]=> - float(0) - ["flag"]=> - bool(false) - ["nullable_str"]=> - NULL - ["opt_int"]=> - int(0) -} -=== 4 args: name, int, float, flag=false === -array(6) { - ["str"]=> - string(4) "four" - ["int"]=> - int(100) - ["float"]=> - float(1.23) - ["flag"]=> - bool(false) - ["nullable_str"]=> - NULL - ["opt_int"]=> - int(0) -} -=== 5 args === -array(6) { - ["str"]=> - string(4) "five" - ["int"]=> - int(200) - ["float"]=> - float(5.67) - ["flag"]=> - bool(false) - ["nullable_str"]=> - string(13) "five_nullable" - ["opt_int"]=> - int(0) -} -=== too few: 0 args === -MyPHPExt\testExpectArgScalars() expects at least 3 arguments, 0 given -=== too few: 1 arg === -MyPHPExt\testExpectArgScalars() expects at least 3 arguments, 1 given -=== too few: 2 args === -MyPHPExt\testExpectArgScalars() expects at least 3 arguments, 2 given -=== too many: 7 args === -MyPHPExt\testExpectArgScalars() expects at most 6 arguments, 7 given -=== wrong type: int for str === -MyPHPExt\testExpectArgScalars(): Argument #1 ($str) must be of type string, int given -=== wrong type: string for int === -MyPHPExt\testExpectArgScalars(): Argument #2 ($int) must be of type int, string given -=== wrong type: string for float === -MyPHPExt\testExpectArgScalars(): Argument #3 ($float) must be of type float, string given -=== wrong type: int for bool flag === -MyPHPExt\testExpectArgScalars(): Argument #4 ($flag) must be of type bool, int given -=== wrong type: int for nullable_str === -MyPHPExt\testExpectArgScalars(): Argument #5 ($nullable_str) must be of type string or null, int given -=== wrong type: string for opt_int === -MyPHPExt\testExpectArgScalars(): Argument #6 ($opt_int) must be of type int, string given diff --git a/examples/my_php_extension/tests/user.phpt b/examples/my_php_extension/tests/user.phpt new file mode 100644 index 0000000..46cfea6 --- /dev/null +++ b/examples/my_php_extension/tests/user.phpt @@ -0,0 +1,37 @@ +--TEST-- +MyPHPExt User models identity and enum-backed state +--FILE-- +getId()); +var_dump($user->label()); +var_dump((string) $user); +var_dump($user->age); +var_dump($user->role === Role::User); +var_dump($user->status === Status::Active); +var_dump(User::MIN_AGE); + +$admin = new User(9, 'Bob', 32, Role::Admin, Status::Inactive); +var_dump($admin->getId()); +var_dump($admin->name); +var_dump($admin->age); +var_dump($admin->role === Role::Admin); +var_dump($admin->status === Status::Inactive); +--EXPECT-- +int(7) +string(5) "Alice" +string(5) "Alice" +NULL +bool(true) +bool(true) +int(0) +int(9) +string(3) "Bob" +int(32) +bool(true) +bool(true) diff --git a/examples/my_php_extension/tests/user_class.phpt b/examples/my_php_extension/tests/user_class.phpt deleted file mode 100644 index f65e65d..0000000 --- a/examples/my_php_extension/tests/user_class.phpt +++ /dev/null @@ -1,24 +0,0 @@ ---TEST-- -User class methods ---FILE-- -getId(), PHP_EOL; -echo '__toString: ', (string)$user, PHP_EOL; -$user->handle(42); -$user->handle("abc"); -echo 'name: ', $user->name, PHP_EOL; -echo 'age: ', $user->age, PHP_EOL; - -?> ---EXPECT-- -AbstractEntity::onLoad: getId() = 9527 -User::getId: 9527 -__toString: User(Rick) -Rick(18).handle(42) -Rick(18).handle(abc) -name: Rick -age: 18 diff --git a/src/zval.zig b/src/zval.zig index bcf5f82..e820ce1 100644 --- a/src/zval.zig +++ b/src/zval.zig @@ -313,6 +313,18 @@ pub const Zval = opaque { raw.addref(self.ptr()); } + /// Increment the refcount only when this zval contains a refcounted value. + /// + /// Ownership: caller owns the added reference and must `dtor`/delref it. + pub inline fn tryAddref(self: *Zval) void { + raw.tryAddref(self.ptr()); + } + + /// Decrement the refcount only when this zval contains a refcounted value. + pub inline fn tryDelref(self: *Zval) void { + raw.tryDelref(self.ptr()); + } + /// Destroy one owned zval value. pub inline fn dtor(self: *Zval) void { raw.dtor(self.ptr()); From 2545a0bb3e837c2ab7f82027a1ad0894ae314f1e Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Tue, 14 Jul 2026 23:00:26 +0800 Subject: [PATCH 26/36] fix(observer): resolve symbol issues on Windows MSVC --- src/abi.zig | 13 +++++++++++++ src/function.zig | 16 ++++------------ src/globals.zig | 9 ++++----- src/observer.zig | 16 ++++++++++------ 4 files changed, 31 insertions(+), 23 deletions(-) create mode 100644 src/abi.zig diff --git a/src/abi.zig b/src/abi.zig new file mode 100644 index 0000000..deeed2a --- /dev/null +++ b/src/abi.zig @@ -0,0 +1,13 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +pub const fn_cc: std.builtin.CallingConvention = if (builtin.os.tag == .windows and builtin.abi == .msvc) + switch (builtin.cpu.arch) { + .x86_64 => .{ .x86_64_vectorcall = .{} }, + .x86 => .{ .x86_vectorcall = .{} }, + else => .c, + } +else + .c; + +pub const use_dll_import = builtin.os.tag == .windows and builtin.abi == .msvc; diff --git a/src/function.zig b/src/function.zig index 100455d..16781d1 100644 --- a/src/function.zig +++ b/src/function.zig @@ -1,21 +1,13 @@ const std = @import("std"); const builtin = @import("builtin"); +const abi = @import("abi.zig"); const c = @import("root.zig").c; const Ctx = @import("Ctx.zig"); const errors = @import("errors.zig"); const zend = @import("zend.zig"); -const fn_cc: std.builtin.CallingConvention = if (builtin.os.tag == .windows and builtin.abi == .msvc) - switch (builtin.cpu.arch) { - .x86_64 => .{ .x86_64_vectorcall = .{} }, - .x86 => .{ .x86_vectorcall = .{} }, - else => .c, - } -else - .c; - -const Fn = fn (?*c.zend_execute_data, ?*c.zval) callconv(fn_cc) void; +const Fn = fn (?*c.zend_execute_data, ?*c.zval) callconv(abi.fn_cc) void; /// Register a Zig function as a PHP function. /// @@ -102,7 +94,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(fn_cc) void { + fn handle(execute_data: ?*c.zend_execute_data, return_value: ?*c.zval) callconv(abi.fn_cc) void { var ctx: Ctx = .{ .call = .from(execute_data.?), .ret = .from(return_value.?) }; const args = switch (Args) { @Tuple(&.{Ctx}) => .{ctx}, @@ -132,7 +124,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(fn_cc) void { + fn handle(execute_data: ?*c.zend_execute_data, return_value: ?*c.zval) callconv(abi.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 index 33d4a89..7615b05 100644 --- a/src/globals.zig +++ b/src/globals.zig @@ -1,15 +1,14 @@ const std = @import("std"); const builtin = @import("builtin"); +const abi = @import("abi.zig"); 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)), + .ptr => if (abi.use_dll_import) *T else @TypeOf(&@field(c, name)), .value => T, }; } @@ -20,11 +19,11 @@ pub inline fn global( comptime name: []const u8, ) Global(access, T, name) { return switch (access) { - .ptr => if (comptime use_dll_import) + .ptr => if (comptime abi.use_dll_import) @extern(*T, .{ .name = name, .is_dll_import = true }) else &@field(c, name), - .value => if (comptime use_dll_import) + .value => if (comptime abi.use_dll_import) @extern(*T, .{ .name = name, .is_dll_import = true }).* else @field(c, name), diff --git a/src/observer.zig b/src/observer.zig index deff801..1c8c006 100644 --- a/src/observer.zig +++ b/src/observer.zig @@ -1,8 +1,10 @@ const std = @import("std"); +const abi = @import("abi.zig"); const c = @import("root.zig").c; const Ctx = @import("Ctx.zig"); const errors = @import("errors.zig"); +const globals = @import("globals.zig"); const zend = @import("zend.zig"); const Zval = @import("zval.zig").Zval; @@ -119,12 +121,12 @@ fn ErrorAdapter(comptime config: Error) type { fn ExceptionAdapter(comptime config: Exception) type { return struct { - const Hook = *const fn (?*c.zend_object) callconv(.c) void; + const Hook = *const fn (?*c.zend_object) callconv(abi.fn_cc) void; var previous: ?Hook = null; var installed = false; - fn callback(exception: ?*c.zend_object) callconv(.c) void { + fn callback(exception: ?*c.zend_object) callconv(abi.fn_cc) void { config.observe(zend.Object.from(exception.?)); if (previous) |previous_hook| { previous_hook(exception); @@ -133,14 +135,16 @@ fn ExceptionAdapter(comptime config: Exception) type { fn register() void { if (installed) return; - previous = c.zend_throw_exception_hook; - c.zend_throw_exception_hook = &callback; + const hook = globals.global(.ptr, ?Hook, "zend_throw_exception_hook"); + previous = hook.*; + hook.* = &callback; installed = true; } fn unregister() void { - if (installed and c.zend_throw_exception_hook == &callback) { - c.zend_throw_exception_hook = previous; + const hook = globals.global(.ptr, ?Hook, "zend_throw_exception_hook"); + if (installed and hook.* == &callback) { + hook.* = previous; } installed = false; previous = null; From 32728591d019279a49384ce727d95b47b06a04ae Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Tue, 14 Jul 2026 23:00:34 +0800 Subject: [PATCH 27/36] feat(globals): add typed superglobal accessors --- build/phpz_wrapper.h | 4 + src/errors.zig | 5 +- src/globals.zig | 175 +++++++++++++++++++++++++++++++++++---- src/zend/array.zig | 8 ++ src/zend/class_entry.zig | 8 +- src/zend/function.zig | 12 +-- src/zval/array.zig | 12 +++ 7 files changed, 196 insertions(+), 28 deletions(-) diff --git a/build/phpz_wrapper.h b/build/phpz_wrapper.h index 738ca53..c1fd235 100644 --- a/build/phpz_wrapper.h +++ b/build/phpz_wrapper.h @@ -97,6 +97,10 @@ static zend_always_inline void phpz_zval_zval(zval *z, zval *src, bool copy, boo ZVAL_ZVAL(z, src, copy, dtor_src); } +static zend_always_inline void phpz_separate_array(zval *z) { + SEPARATE_ARRAY(z); +} + /* Bridge helpers for anonymous unions. * translate-c numbers unnamed unions (unnamed_0, unnamed_1, ...), * which break when PHP headers change union layout. These inline diff --git a/src/errors.zig b/src/errors.zig index 50d6c5c..d128e51 100644 --- a/src/errors.zig +++ b/src/errors.zig @@ -118,13 +118,12 @@ pub fn throwExceptionEx(ce: *zend.ClassEntry, code: i64, comptime format: [:0]co /// Check whether a PHP exception is pending (EG(exception) != null). pub inline fn hasException() bool { - return globals.executor().exception != null; + return globals.executor().exception() != null; } /// Get the pending PHP exception object (EG(exception)). Returns null if no exception is set. pub inline fn exception() ?*zend.Object { - const obj = globals.executor().exception; - return if (obj) |o| zend.Object.from(o) else null; + return globals.executor().exception(); } /// Clear the pending PHP exception. Does nothing if no exception is set. diff --git a/src/globals.zig b/src/globals.zig index 7615b05..1570cef 100644 --- a/src/globals.zig +++ b/src/globals.zig @@ -3,6 +3,8 @@ const builtin = @import("builtin"); const abi = @import("abi.zig"); const c = @import("c.zig").c; +const Zval = @import("zval.zig").Zval; +const zend = @import("zend.zig"); pub const GlobalAccess = enum { ptr, value }; @@ -77,39 +79,184 @@ pub const class = struct { } }; -pub inline fn executor() *c.zend_executor_globals { - return @ptrCast(c.phpz_executor_globals()); +pub inline fn executor() *ExecutorGlobals { + return .from(c.phpz_executor_globals()); } -pub inline fn compiler() *c.zend_compiler_globals { - return @ptrCast(c.phpz_compiler_globals()); +pub inline fn compiler() *CompilerGlobals { + return .from(c.phpz_compiler_globals()); } -pub inline fn core() *CoreGlobals { +pub inline fn php() *PhpGlobals { return .from(c.phpz_core_globals()); } -pub inline fn sapi() *c.sapi_globals_struct { - return @ptrCast(c.phpz_sapi_globals()); +pub inline fn sapi() *SapiGlobals { + return .from(c.phpz_sapi_globals()); } -pub inline fn file() *c.php_file_globals { - return @ptrCast(c.phpz_file_globals()); +pub inline fn file() *FileGlobals { + return .from(c.phpz_file_globals()); } -pub const CoreGlobals = opaque { - pub inline fn from(core_globals: *c.php_core_globals) *CoreGlobals { +pub const ExecutorGlobals = opaque { + pub const Superglobal = enum { + GET, + POST, + COOKIE, + SERVER, + ENV, + FILES, + REQUEST, + + pub fn name(comptime self: Superglobal) []const u8 { + return "_" ++ @tagName(self); + } + + pub inline fn ensureInitialized(comptime self: Superglobal) void { + const global_name = self.name(); + _ = c.zend_is_auto_global_str(global_name.ptr, global_name.len); + } + }; + + pub inline fn from(globals: *c.zend_executor_globals) *ExecutorGlobals { + return @ptrCast(globals); + } + + pub inline fn ptr(self: *ExecutorGlobals) *c.zend_executor_globals { + return @ptrCast(@alignCast(self)); + } + + pub inline fn symbols(self: *ExecutorGlobals) *zend.Array { + return zend.Array.from(&self.ptr().symbol_table); + } + + /// Return a userland superglobal from `EG(symbol_table)`. + /// + /// Unlike `PhpGlobals.httpGlobal`, this accesses the zval slot that PHP + /// userland reads and writes as `$_GET`, `$_POST`, and so on. The returned + /// wrapper borrows the owner zval stored in the symbol table without + /// changing its copy-on-write state. + pub fn superglobal(self: *ExecutorGlobals, comptime which: Superglobal) ?*Zval.Array { + which.ensureInitialized(); + const name = which.name(); + + const value = self.symbols().find(name) orelse return null; + return Zval.Array.from(value) catch null; + } + + /// Return a userland superglobal prepared for mutation. + /// + /// This calls `separate()` before returning so PHP's array copy-on-write + /// rules are applied to the owner zval stored in `EG(symbol_table)`. + pub fn superglobalMut(self: *ExecutorGlobals, comptime which: Superglobal) ?*Zval.Array { + const value = self.superglobal(which) orelse return null; + value.separate(); + return value; + } + + pub inline fn functions(self: *ExecutorGlobals) *zend.Array { + return zend.Array.from(self.ptr().function_table); + } + + pub inline fn classes(self: *ExecutorGlobals) *zend.Array { + return zend.Array.from(self.ptr().class_table); + } + + pub inline fn exception(self: *ExecutorGlobals) ?*zend.Object { + const object = self.ptr().exception orelse return null; + return zend.Object.from(object); + } +}; + +pub const CompilerGlobals = opaque { + pub inline fn from(globals: *c.zend_compiler_globals) *CompilerGlobals { + return @ptrCast(globals); + } + + pub inline fn ptr(self: *CompilerGlobals) *c.zend_compiler_globals { + return @ptrCast(@alignCast(self)); + } + + // TODO: more impl... +}; + +pub const SapiGlobals = opaque { + pub inline fn from(globals: *c.sapi_globals_struct) *SapiGlobals { + return @ptrCast(globals); + } + + pub inline fn ptr(self: *SapiGlobals) *c.sapi_globals_struct { + return @ptrCast(@alignCast(self)); + } + + // TODO: more impl... +}; + +pub const FileGlobals = opaque { + pub inline fn from(globals: *c.php_file_globals) *FileGlobals { + return @ptrCast(globals); + } + + pub inline fn ptr(self: *FileGlobals) *c.php_file_globals { + return @ptrCast(@alignCast(self)); + } + + // TODO: more impl... +}; + +pub const PhpGlobals = opaque { + pub const HttpSuperglobal = enum(c_int) { + GET = c.TRACK_VARS_GET, + POST = c.TRACK_VARS_POST, + COOKIE = c.TRACK_VARS_COOKIE, + SERVER = c.TRACK_VARS_SERVER, + ENV = c.TRACK_VARS_ENV, + FILES = c.TRACK_VARS_FILES, + + pub fn name(comptime self: HttpSuperglobal) []const u8 { + return "_" ++ @tagName(self); + } + + pub inline fn ensureInitialized(comptime self: HttpSuperglobal) void { + const global_name = self.name(); + _ = c.zend_is_auto_global_str(global_name.ptr, global_name.len); + } + + pub fn index(comptime self: HttpSuperglobal) usize { + return @intCast(@intFromEnum(self)); + } + }; + + pub inline fn from(core_globals: *c.php_core_globals) *PhpGlobals { return @ptrCast(core_globals); } - pub inline fn ptr(self: *CoreGlobals) *c.php_core_globals { + + pub inline fn ptr(self: *PhpGlobals) *c.php_core_globals { return @ptrCast(@alignCast(self)); } - // TODO: $_GET/$_POST/... + /// Return a PHP request superglobal from `PG(http_globals)`. + /// + /// Unlike `ExecutorGlobals.superglobal`, this returns PHP core's request + /// source zval, not the userland variable slot in `EG(symbol_table)`. + /// This is not a writable API; no reference is added, and callers must not + /// mutate, destroy, or retain it across requests or threads. To write to a + /// userland superglobal, use `ExecutorGlobals.superglobalMut()`. + pub fn httpGlobal(self: *PhpGlobals, comptime which: HttpSuperglobal) ?*Zval.Array { + which.ensureInitialized(); + return Zval.Array.from(&self.ptr().http_globals[which.index()]) catch null; + } }; test { std.testing.refAllDecls(@This()); + std.testing.refAllDecls(ExecutorGlobals.Superglobal); std.testing.refAllDecls(@This().class); - std.testing.refAllDecls(CoreGlobals); + std.testing.refAllDecls(ExecutorGlobals); + std.testing.refAllDecls(CompilerGlobals); + std.testing.refAllDecls(PhpGlobals); + std.testing.refAllDecls(PhpGlobals.HttpSuperglobal); + std.testing.refAllDecls(SapiGlobals); + std.testing.refAllDecls(FileGlobals); } diff --git a/src/zend/array.zig b/src/zend/array.zig index 9283b4e..c735cb6 100644 --- a/src/zend/array.zig +++ b/src/zend/array.zig @@ -117,6 +117,14 @@ pub const Array = opaque { return c.zend_hash_str_find(self.ptr(), key.ptr, key.len); } + /// Find a pointer stored under a string key. + /// + /// Ownership: borrowed pointer owned by the array. + pub fn findPtr(self: *Array, comptime T: type, key: []const u8) ?*T { + const value = c.zend_hash_str_find_ptr(self.ptr(), key.ptr, key.len); + return if (value != null) @ptrCast(@alignCast(value)) else null; + } + /// Find a value by index. /// /// Ownership: borrowed zval pointer owned by the array; addref/copy before diff --git a/src/zend/class_entry.zig b/src/zend/class_entry.zig index 06c4a65..87a081a 100644 --- a/src/zend/class_entry.zig +++ b/src/zend/class_entry.zig @@ -7,7 +7,7 @@ const PropertyInfo = @import("property_info.zig").PropertyInfo; pub const ClassEntry = opaque { /// Enum backing type classification. - pub const EnumBackingType = enum(u32) { + pub const EnumBackingType = enum(c_int) { undef = c.IS_UNDEF, int = c.IS_LONG, string = c.IS_STRING, @@ -83,6 +83,12 @@ pub const ClassEntry = opaque { return PropertyInfo.iterator(self); } + /// Get the class method table. + /// Ownership: borrowed array owned by this class entry. + pub inline fn methods(self: *ClassEntry) *Array { + return Array.from(&self.ptr().*.function_table); + } + /// Find a method by name (lowercase) in the class function table pub fn findMethod(self: *ClassEntry, method_name: []const u8) ?*Function { return Function.findMethod(self, method_name); diff --git a/src/zend/function.zig b/src/zend/function.zig index 66933f3..6184d48 100644 --- a/src/zend/function.zig +++ b/src/zend/function.zig @@ -43,20 +43,12 @@ 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( - globals.executor().function_table, - func_name.ptr, - func_name.len, - ))); + return globals.executor().functions().findPtr(Function, func_name); } /// Look up a method directly from a class's function table pub fn findMethod(ce: *ClassEntry, method_name: []const u8) ?*Function { - return @ptrCast(@alignCast(c.zend_hash_str_find_ptr( - &ce.ptr().*.function_table, - method_name.ptr, - method_name.len, - ))); + return ce.methods().findPtr(Function, method_name); } /// Get the function type (internal vs userland). diff --git a/src/zval/array.zig b/src/zval/array.zig index ce11378..7a13f78 100644 --- a/src/zval/array.zig +++ b/src/zval/array.zig @@ -50,6 +50,18 @@ pub const Array = opaque { return .from(self.ptr().value.arr); } + /// Ensure this array zval owns writable storage before in-place mutation. + /// + /// This applies PHP's copy-on-write array separation to the owner zval. If + /// the underlying array is shared, the zval is updated to point at a + /// duplicated array; otherwise this is a no-op. + /// + /// Ownership: the wrapper remains borrowed from the same zval. No caller + /// reference is added or transferred. + pub fn separate(self: *Array) void { + c.phpz_separate_array(self.ptr()); + } + /// Get the number of elements pub fn len(self: *Array) usize { return self.ptr().value.arr.*.nNumOfElements; From 94a67f119da5f100ca6145c27567c6456144a6f4 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Tue, 14 Jul 2026 23:00:35 +0800 Subject: [PATCH 28/36] test(example): add superglobal coverage --- examples/my_php_extension/README.md | 85 ++++++++++++++++--- .../my_php_extension.stub.php | 4 + .../my_php_extension_arginfo.h | 10 ++- examples/my_php_extension/src/root.zig | 2 + .../src/testing/superglobals.zig | 53 ++++++++++++ .../tests/superglobals_borrow.phpt | 60 +++++++++++++ .../tests/superglobals_cgi.phpt | 68 +++++++++++++++ .../tests/superglobals_disabled_sources.phpt | 31 +++++++ .../tests/superglobals_jit.phpt | 32 +++++++ .../superglobals_post_data_disabled.phpt | 49 +++++++++++ .../tests/superglobals_request.phpt | 41 +++++++++ 11 files changed, 424 insertions(+), 11 deletions(-) create mode 100644 examples/my_php_extension/src/testing/superglobals.zig create mode 100644 examples/my_php_extension/tests/superglobals_borrow.phpt create mode 100644 examples/my_php_extension/tests/superglobals_cgi.phpt create mode 100644 examples/my_php_extension/tests/superglobals_disabled_sources.phpt create mode 100644 examples/my_php_extension/tests/superglobals_jit.phpt create mode 100644 examples/my_php_extension/tests/superglobals_post_data_disabled.phpt create mode 100644 examples/my_php_extension/tests/superglobals_request.phpt diff --git a/examples/my_php_extension/README.md b/examples/my_php_extension/README.md index e898f91..96b5f67 100644 --- a/examples/my_php_extension/README.md +++ b/examples/my_php_extension/README.md @@ -1,22 +1,61 @@ # my_php_extension -This extension is the end-to-end feature example for phpz. Its public PHP API is -defined by `my_php_extension.stub.php`; Zig files implement that contract without -duplicating PHP type metadata. +`my_php_extension` is the end-to-end showcase extension for phpz. It is not +intended to be a useful PHP package by itself; it exists to demonstrate how a +real extension is described in PHP stubs, implemented in Zig, registered with +Zend, and exercised through PHPT tests. + +The public PHP API is defined by `my_php_extension.stub.php`; Zig files implement +that contract without duplicating PHP type metadata. The generated +`my_php_extension_arginfo.h` is checked in so the example follows the same stub +to arginfo workflow as a normal PHP extension. ## API groups -- Global functions: `hello()` and `greet()`. -- Namespace functions: `MyPHPExt\increment()` and `MyPHPExt\mapValues()`. -- Object model: `Identifiable`, `Entity`, `User`, `Status`, `Role`, and `Tag`. -- General-purpose classes: `Counter`, `Dumper`, and `Collection`. -- Runtime configuration: static `Config` accessors backed by typed INI entries. -- Request telemetry: static `Metrics::snapshot()` and `Metrics::reset()`. -- Regression-only API: `MyPHPExt\Test\allocatorBailout()`. +- Global functions: `hello()` and `greet()` cover basic exported functions, + argument parsing, and return values. +- Namespace functions: `MyPHPExt\increment()` covers by-reference parameters; + `MyPHPExt\mapValues()` covers arrays and callables. +- Object model: `Identifiable`, `Entity`, `User`, `Status`, `Role`, and `Tag` + cover interfaces, abstract classes, inheritance, final methods, enums, + attributes, typed properties, constants, constructors, and `Stringable`. +- General-purpose classes: `Counter`, `Dumper`, and `Collection` cover normal + classes, static methods, variadic arguments, object state, `ArrayAccess`, + `Countable`, and `Iterator`. +- Runtime configuration: `Config` exposes typed INI entries through static + getters backed by phpz's INI helpers. +- Request telemetry: `Metrics::snapshot()` and `Metrics::reset()` demonstrate + typed module globals plus Zend observer hooks for function calls, returns, + errors, and exceptions. +- Test namespace: `MyPHPExt\Test\allocatorBailout()` exercises bailout-safe + allocator behavior; `superglobalsSnapshot()` and `mutateSuperglobals()` cover + PHP HTTP superglobal access through `PG(http_globals)` and + `EG(symbol_table)`. Every exported Zig handler carries a `/// PHP: ...` line matching its stub signature. Internal helpers and observer callbacks intentionally do not. +## Runtime coverage + +The example is intentionally broad so phpz changes can be validated against PHP +extension patterns that tend to break in real projects: + +- Stub and arginfo generation, including namespaced functions and class entries. +- Module registration, `phpinfo()` output, and extension metadata. +- Function and method argument parsing, including optional, nullable, + by-reference, variadic, array, object, and callable arguments. +- Zend class entries, method tables, constants, properties, interfaces, + inheritance, attributes, and enums. +- Zval and array ownership, including returning arrays, storing mixed values, + adding references, and copy-on-write separation before mutation. +- PHP allocator and Zend bailout paths. +- Typed module globals, request-startup reset hooks, request-local metrics, + observer callbacks, and exception/error recording. +- HTTP superglobals in both PHP core storage and userland symbol-table storage: + `phpz.globals.php().httpGlobal(.GET)` is a read-only request source borrow, + while `phpz.globals.executor().superglobalMut(.GET)` prepares the userland + `$_GET` slot for mutation. + ## Metrics `Metrics::snapshot()` returns request-local data grouped under `request`, `calls`, @@ -24,5 +63,31 @@ signature. Internal helpers and observer callbacks intentionally do not. message, code, file, and line values; the observer keeps only the 16 most recent records and reports overwritten records through `discarded`. +The metrics store is backed by `phpz.ModuleGlobals`, so it exercises phpz's +typed module-global support and per-request reset path in addition to observer +callbacks. + The fcall observer uses a fixed-depth timing stack and does not allocate. Calls to the metrics and test APIs are excluded so observing a snapshot does not alter it. + +## Testing + +Run this extension's PHPT tests from this directory: + +```sh +zig build test +``` + +Pass build options with `-D...` when the PHP installation is not in the default +location: + +```sh +zig build test -Dphp-include-dir=/path/to/php/include/php +zig build test -Dcheck-arginfo=false +``` + +On Windows, use the MSVC target and pass the matching PHP SDK library directory: + +```sh +zig build test -Dtarget=native-windows-msvc -Dphp-include-dir=C:\path\to\php\include -Dphp-lib-dir=C:\path\to\php\lib +``` diff --git a/examples/my_php_extension/my_php_extension.stub.php b/examples/my_php_extension/my_php_extension.stub.php index 3d78276..c25a44c 100644 --- a/examples/my_php_extension/my_php_extension.stub.php +++ b/examples/my_php_extension/my_php_extension.stub.php @@ -157,4 +157,8 @@ public static function reset(): void {} namespace MyPHPExt\Test { function allocatorBailout(): array {} + + function superglobalsSnapshot(): array {} + + function mutateSuperglobals(): void {} } diff --git a/examples/my_php_extension/my_php_extension_arginfo.h b/examples/my_php_extension/my_php_extension_arginfo.h index 40d4fa4..53a3825 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: ba87d13bceef0140a0cc97bbc91d3d4a8b203335 */ + * Stub hash: 59ff8af4c0b6ce18b2d5453d93bc2e4818a4d36c */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_hello, 0, 0, IS_VOID, 0) ZEND_END_ARG_INFO() @@ -21,6 +21,10 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_MyPHPExt_Test_allocatorBailout, 0, 0, IS_ARRAY, 0) ZEND_END_ARG_INFO() +#define arginfo_MyPHPExt_Test_superglobalsSnapshot arginfo_MyPHPExt_Test_allocatorBailout + +#define arginfo_MyPHPExt_Test_mutateSuperglobals arginfo_hello + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MyPHPExt_Identifiable_getId, 0, 0, IS_LONG, 0) ZEND_END_ARG_INFO() @@ -128,6 +132,8 @@ ZEND_FUNCTION(greet); ZEND_FUNCTION(MyPHPExt_increment); ZEND_FUNCTION(MyPHPExt_mapValues); ZEND_FUNCTION(MyPHPExt_Test_allocatorBailout); +ZEND_FUNCTION(MyPHPExt_Test_superglobalsSnapshot); +ZEND_FUNCTION(MyPHPExt_Test_mutateSuperglobals); ZEND_METHOD(MyPHPExt_Tag, __construct); ZEND_METHOD(MyPHPExt_Entity, __construct); ZEND_METHOD(MyPHPExt_Entity, getId); @@ -167,6 +173,8 @@ static const zend_function_entry ext_functions[] = { ZEND_NS_FALIAS("MyPHPExt", increment, MyPHPExt_increment, arginfo_MyPHPExt_increment) ZEND_NS_FALIAS("MyPHPExt", mapValues, MyPHPExt_mapValues, arginfo_MyPHPExt_mapValues) ZEND_NS_FALIAS("MyPHPExt\\Test", allocatorBailout, MyPHPExt_Test_allocatorBailout, arginfo_MyPHPExt_Test_allocatorBailout) + ZEND_NS_FALIAS("MyPHPExt\\Test", superglobalsSnapshot, MyPHPExt_Test_superglobalsSnapshot, arginfo_MyPHPExt_Test_superglobalsSnapshot) + ZEND_NS_FALIAS("MyPHPExt\\Test", mutateSuperglobals, MyPHPExt_Test_mutateSuperglobals, arginfo_MyPHPExt_Test_mutateSuperglobals) ZEND_FE_END }; diff --git a/examples/my_php_extension/src/root.zig b/examples/my_php_extension/src/root.zig index cfa58bb..f337159 100644 --- a/examples/my_php_extension/src/root.zig +++ b/examples/my_php_extension/src/root.zig @@ -6,6 +6,7 @@ const functions = @import("functions.zig"); const ini = @import("ini.zig"); const metrics = @import("metrics.zig"); const bailout = @import("testing/bailout.zig"); +const superglobals = @import("testing/superglobals.zig"); fn info(_: *c.zend_module_entry) void { phpz.info.table.start(); @@ -17,6 +18,7 @@ fn info(_: *c.zend_module_entry) void { comptime { _ = functions; _ = bailout; + _ = superglobals; phpz.module(.{ .name = "my_php_extension", diff --git a/examples/my_php_extension/src/testing/superglobals.zig b/examples/my_php_extension/src/testing/superglobals.zig new file mode 100644 index 0000000..451abe0 --- /dev/null +++ b/examples/my_php_extension/src/testing/superglobals.zig @@ -0,0 +1,53 @@ +const phpz = @import("phpz"); +const Zval = phpz.Zval; + +/// PHP: MyPHPExt\Test\superglobalsSnapshot(): array +fn superglobalsSnapshot(ctx: phpz.Ctx) !void { + _ = try ctx.call.expectArgs(&.{}, {}); + + const php = phpz.globals.php(); + const executor = phpz.globals.executor(); + const get = php.httpGlobal(.GET) orelse return error.SuperglobalUnavailable; + const post = php.httpGlobal(.POST) orelse return error.SuperglobalUnavailable; + const cookie = php.httpGlobal(.COOKIE) orelse return error.SuperglobalUnavailable; + const server = php.httpGlobal(.SERVER) orelse return error.SuperglobalUnavailable; + const env = php.httpGlobal(.ENV) orelse return error.SuperglobalUnavailable; + const files = php.httpGlobal(.FILES) orelse return error.SuperglobalUnavailable; + const request = executor.superglobal(.REQUEST) orelse return error.SuperglobalUnavailable; + + const result = Zval.Array.empty(ctx.ret.ptr()); + result.set(.mixed, "GET", get.ptr()); + result.set(.mixed, "POST", post.ptr()); + result.set(.mixed, "COOKIE", cookie.ptr()); + result.set(.mixed, "SERVER", server.ptr()); + result.set(.mixed, "ENV", env.ptr()); + result.set(.mixed, "FILES", files.ptr()); + result.set(.mixed, "REQUEST", request.ptr()); +} + +/// PHP: MyPHPExt\Test\mutateSuperglobals(): void +fn mutateSuperglobals(ctx: phpz.Ctx) !void { + _ = try ctx.call.expectArgs(&.{}, {}); + + const executor = phpz.globals.executor(); + const get = executor.superglobalMut(.GET) orelse return error.SuperglobalUnavailable; + const post = executor.superglobalMut(.POST) orelse return error.SuperglobalUnavailable; + const cookie = executor.superglobalMut(.COOKIE) orelse return error.SuperglobalUnavailable; + const server = executor.superglobalMut(.SERVER) orelse return error.SuperglobalUnavailable; + const env = executor.superglobalMut(.ENV) orelse return error.SuperglobalUnavailable; + const files = executor.superglobalMut(.FILES) orelse return error.SuperglobalUnavailable; + const request = executor.superglobalMut(.REQUEST) orelse return error.SuperglobalUnavailable; + + get.set(.string, "from_zig", "get"); + post.set(.string, "from_zig", "post"); + cookie.set(.string, "from_zig", "cookie"); + server.set(.string, "from_zig", "server"); + env.set(.string, "from_zig", "env"); + files.set(.string, "from_zig", "files"); + request.set(.string, "from_zig", "request"); +} + +comptime { + phpz.function("MyPHPExt\\Test\\superglobalsSnapshot", superglobalsSnapshot); + phpz.function("MyPHPExt\\Test\\mutateSuperglobals", mutateSuperglobals); +} diff --git a/examples/my_php_extension/tests/superglobals_borrow.phpt b/examples/my_php_extension/tests/superglobals_borrow.phpt new file mode 100644 index 0000000..54fa242 --- /dev/null +++ b/examples/my_php_extension/tests/superglobals_borrow.phpt @@ -0,0 +1,60 @@ +--TEST-- +MyPHPExt superglobal accessors expose read-only PG/EG copy-on-write boundary +--EXTENSIONS-- +my_php_extension +--INI-- +variables_order=EGPCS +request_order=GPC +auto_globals_jit=0 +--FILE-- + 'files']; +$_REQUEST['from_php'] = 'request'; + +$snapshot = MyPHPExt\Test\superglobalsSnapshot(); +var_dump(array_key_exists('from_php', $snapshot['GET'])); +var_dump(array_key_exists('from_php', $snapshot['POST'])); +var_dump(array_key_exists('from_php', $snapshot['COOKIE'])); +var_dump(array_key_exists('from_php', $snapshot['ENV'])); +var_dump(array_key_exists('from_php', $snapshot['FILES'])); +var_dump($snapshot['REQUEST']['from_php']); + +var_dump($_GET['from_php']); +var_dump($_POST['from_php']); +var_dump($_COOKIE['from_php']); +var_dump($_ENV['from_php']); +var_dump($_FILES['from_php']['name']); +?> +--EXPECT-- +string(3) "get" +string(4) "post" +string(6) "cookie" +string(6) "server" +string(3) "env" +string(5) "files" +string(7) "request" +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +string(7) "request" +string(3) "get" +string(4) "post" +string(6) "cookie" +string(3) "env" +string(5) "files" diff --git a/examples/my_php_extension/tests/superglobals_cgi.phpt b/examples/my_php_extension/tests/superglobals_cgi.phpt new file mode 100644 index 0000000..f042497 --- /dev/null +++ b/examples/my_php_extension/tests/superglobals_cgi.phpt @@ -0,0 +1,68 @@ +--TEST-- +MyPHPExt superglobal accessors expose CGI request data and uploads +--EXTENSIONS-- +my_php_extension +--INI-- +variables_order=GPCS +request_order=GPC +auto_globals_jit=1 +file_uploads=1 +--GET-- +get_only=get&shared=get +--COOKIE-- +cookie_only=cookie;shared=cookie +--POST_RAW-- +Content-Type: multipart/form-data; boundary=phpz-boundary +--phpz-boundary +Content-Disposition: form-data; name="post_only" + +post +--phpz-boundary +Content-Disposition: form-data; name="shared" + +post +--phpz-boundary +Content-Disposition: form-data; name="upload"; filename="fixture.txt" +Content-Type: text/plain + +fixture +--phpz-boundary-- +--FILE-- + +--EXPECT-- +string(3) "get" +string(4) "post" +string(6) "cookie" +string(4) "POST" +string(23) "get_only=get&shared=get" +string(11) "fixture.txt" +string(11) "fixture.txt" +string(10) "text/plain" +int(0) +int(7) +bool(true) +string(3) "get" +string(4) "post" +string(6) "cookie" +string(6) "cookie" diff --git a/examples/my_php_extension/tests/superglobals_disabled_sources.phpt b/examples/my_php_extension/tests/superglobals_disabled_sources.phpt new file mode 100644 index 0000000..aebfc41 --- /dev/null +++ b/examples/my_php_extension/tests/superglobals_disabled_sources.phpt @@ -0,0 +1,31 @@ +--TEST-- +MyPHPExt disabled superglobal sources remain empty arrays +--EXTENSIONS-- +my_php_extension +--INI-- +variables_order=S +request_order=GPC +auto_globals_jit=1 +--GET-- +get_only=get +--COOKIE-- +cookie_only=cookie +--POST-- +post_only=post +--FILE-- + +--EXPECT-- +GET=0 +POST=0 +COOKIE=0 +ENV=0 +FILES=0 +REQUEST=0 +string(4) "POST" diff --git a/examples/my_php_extension/tests/superglobals_jit.phpt b/examples/my_php_extension/tests/superglobals_jit.phpt new file mode 100644 index 0000000..314c8fe --- /dev/null +++ b/examples/my_php_extension/tests/superglobals_jit.phpt @@ -0,0 +1,32 @@ +--TEST-- +MyPHPExt superglobal accessors initialize JIT globals +--EXTENSIONS-- +my_php_extension +--INI-- +variables_order=EGPCS +request_order=GPC +auto_globals_jit=1 +register_argc_argv=0 +--ENV-- +PHPZ_SUPERGLOBALS_ENV=environment +--FILE-- + +--EXPECT-- +GET=array +POST=array +COOKIE=array +SERVER=array +ENV=array +FILES=array +REQUEST=array +string(11) "environment" +string(11) "environment" diff --git a/examples/my_php_extension/tests/superglobals_post_data_disabled.phpt b/examples/my_php_extension/tests/superglobals_post_data_disabled.phpt new file mode 100644 index 0000000..49365cd --- /dev/null +++ b/examples/my_php_extension/tests/superglobals_post_data_disabled.phpt @@ -0,0 +1,49 @@ +--TEST-- +MyPHPExt POST and FILES stay empty when POST data reading is disabled +--EXTENSIONS-- +my_php_extension +--INI-- +variables_order=GPCS +request_order=GPC +auto_globals_jit=1 +enable_post_data_reading=0 +--GET-- +get_only=get&shared=get +--COOKIE-- +cookie_only=cookie;shared=cookie +--POST_RAW-- +Content-Type: multipart/form-data; boundary=phpz-boundary +--phpz-boundary +Content-Disposition: form-data; name="post_only" + +post +--phpz-boundary +Content-Disposition: form-data; name="shared" + +post +--phpz-boundary +Content-Disposition: form-data; name="upload"; filename="fixture.txt" +Content-Type: text/plain + +fixture +--phpz-boundary-- +--FILE-- + +--EXPECT-- +string(3) "get" +string(6) "cookie" +int(0) +int(0) +bool(false) +string(6) "cookie" +string(4) "POST" diff --git a/examples/my_php_extension/tests/superglobals_request.phpt b/examples/my_php_extension/tests/superglobals_request.phpt new file mode 100644 index 0000000..3e01787 --- /dev/null +++ b/examples/my_php_extension/tests/superglobals_request.phpt @@ -0,0 +1,41 @@ +--TEST-- +MyPHPExt REQUEST follows request_order and remains a snapshot +--EXTENSIONS-- +my_php_extension +--INI-- +variables_order=GPCS +request_order=GPC +auto_globals_jit=1 +--GET-- +shared=get&get_only=get +--COOKIE-- +shared=cookie;cookie_only=cookie +--POST-- +shared=post&post_only=post +--FILE-- + +--EXPECT-- +string(3) "get" +string(4) "post" +string(6) "cookie" +string(6) "cookie" +string(3) "get" +string(4) "post" +string(6) "cookie" +bool(false) +bool(false) From ff82cf530209e56b997ff77fea1d82d7ad979d13 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Tue, 14 Jul 2026 23:30:21 +0800 Subject: [PATCH 29/36] refactor(zval): clarify wrapper ownership APIs --- src/class.zig | 2 +- src/zend/callable.zig | 6 +++--- src/zend/object.zig | 2 +- src/zval.zig | 32 +++++++++++++++++++++----------- src/zval/array.zig | 22 +++++++++++++++++----- src/zval/object.zig | 22 +++++++++++++++++----- 6 files changed, 60 insertions(+), 26 deletions(-) diff --git a/src/class.zig b/src/class.zig index 24ef60a..0aa27e9 100644 --- a/src/class.zig +++ b/src/class.zig @@ -456,7 +456,7 @@ pub fn Class(comptime class_name: [:0]const u8, comptime T: type) type { /// /// Ownership: returned pointer is either borrowed from the object/runtime /// or points at caller-provided scratch. Never dtor the returned pointer - /// directly; use `Zval.raw.tryDtor(scratch)` for scratch cleanup. + /// directly; use `Zval.raw.tryRelease(scratch)` for scratch cleanup. /// /// Returns `error.PhpException` if a magic `__get` handler throws. /// diff --git a/src/zend/callable.zig b/src/zend/callable.zig index fd29c46..4ecc572 100644 --- a/src/zend/callable.zig +++ b/src/zend/callable.zig @@ -95,7 +95,7 @@ pub const Callable = extern struct { var discard: c.zval = undefined; const owns_retval = self.fci.retval == null; if (owns_retval) self.fci.retval = &discard; - defer if (owns_retval) Zval.raw.dtor(&discard); + defer if (owns_retval) Zval.raw.release(&discard); const n = info.@"struct".field_types.len; switch (n) { @@ -143,7 +143,7 @@ pub const Callable = extern struct { self.fci.param_count = saved_param_count; self.fci.params = saved_params; self.fci.named_params = saved_named_params; - if (owns_retval) Zval.raw.tryDtor(&discard); + if (owns_retval) Zval.raw.tryRelease(&discard); } const n = info.@"struct".field_types.len; @@ -187,7 +187,7 @@ pub const Callable = extern struct { /// Decrement refcounts on `function_name` and `fcc.object`. pub inline fn delref(self: *Callable) void { - Zval.raw.dtor(&self.fci.function_name); + Zval.raw.release(&self.fci.function_name); if (self.fcc.object) |obj| Object.release(.from(obj)); } diff --git a/src/zend/object.zig b/src/zend/object.zig index 40ddefd..9fdf385 100644 --- a/src/zend/object.zig +++ b/src/zend/object.zig @@ -159,7 +159,7 @@ pub const Object = opaque { /// /// Ownership: returned pointer is either borrowed from the object/runtime or /// points at caller-provided scratch. Never dtor the returned pointer - /// directly; use `Zval.raw.tryDtor(scratch)` for scratch cleanup. + /// directly; use `Zval.raw.tryRelease(scratch)` for scratch cleanup. /// /// Returns `error.PhpException` if a magic `__get` handler throws. pub fn readProperty( diff --git a/src/zval.zig b/src/zval.zig index e820ce1..91402b8 100644 --- a/src/zval.zig +++ b/src/zval.zig @@ -143,6 +143,16 @@ pub const Zval = opaque { return @ptrCast(@alignCast(self)); } + /// View this zval as an array zval after checking its type. + pub fn array(self: *Zval) Array.FromError!*Array { + return Array.from(self.ptr()); + } + + /// View this zval as an object zval after checking its type. + pub fn object(self: *Zval) Object.FromError!*Object { + return Object.from(self.ptr()); + } + /// Get the type (Kind) of this zval. /// /// Returns: @@ -308,14 +318,14 @@ pub const Zval = opaque { /// Increment the refcount of this zval's refcounted payload. /// - /// Ownership: caller owns the added reference and must `dtor`/delref it. + /// Ownership: caller owns the added reference and must `release()`/delref it. pub inline fn addref(self: *Zval) void { raw.addref(self.ptr()); } /// Increment the refcount only when this zval contains a refcounted value. /// - /// Ownership: caller owns the added reference and must `dtor`/delref it. + /// Ownership: caller owns the added reference and must `release()`/delref it. pub inline fn tryAddref(self: *Zval) void { raw.tryAddref(self.ptr()); } @@ -325,9 +335,9 @@ pub const Zval = opaque { raw.tryDelref(self.ptr()); } - /// Destroy one owned zval value. - pub inline fn dtor(self: *Zval) void { - raw.dtor(self.ptr()); + /// Release one owned zval value. + pub inline fn release(self: *Zval) void { + raw.release(self.ptr()); } pub inline fn refcount(self: *Zval) u32 { @@ -434,7 +444,7 @@ pub const Zval = opaque { /// Initialize a raw zval value. /// /// Ownership: caller owns the returned zval contents and must call - /// `dtor()`/`tryDtor()` when the value is refcounted and not transferred. + /// `release()`/`tryRelease()` when the value is refcounted and not transferred. pub fn init(comptime zk: Kind, val: Type(zk)) c.zval { var z: c.zval = undefined; raw.set(&z, zk, val); @@ -614,14 +624,14 @@ pub const Zval = opaque { /// /// Ownership: caller owns the added reference and must `dtor`/delref it. pub const addref = c.zval_add_ref; - /// Destroy one owned raw zval value. - pub const dtor = c.zval_ptr_dtor; + /// Release one owned raw zval value using PHP's `zval_ptr_dtor`. + pub const release = c.zval_ptr_dtor; pub const refcount = c.zval_refcount_p; - /// Destroy a scratch/optional zval only if it was initialized. + /// Release a scratch/optional zval only if it was initialized. /// /// Ownership: caller-provided scratch cleanup helper. - pub inline fn tryDtor(z: *c.zval) void { - if (!raw.is(z, .undef)) raw.dtor(z); + pub inline fn tryRelease(z: *c.zval) void { + if (!raw.is(z, .undef)) raw.release(z); } /// Increment the refcount only when the zval is refcounted. /// diff --git a/src/zval/array.zig b/src/zval/array.zig index 7a13f78..ac57c1d 100644 --- a/src/zval/array.zig +++ b/src/zval/array.zig @@ -5,7 +5,7 @@ const Zval = @import("../zval.zig").Zval; pub const Array = opaque { /// Create an empty array in caller-provided zval storage. /// - /// Ownership: caller owns `zv`'s array value; call `Zval.raw.dtor(zv)` + /// Ownership: caller owns `zv`'s array value; call `Zval.raw.release(zv)` /// unless the zval is returned/transferred to PHP. The returned wrapper is /// borrowed from `zv`. pub fn empty(zv: *c.zval) *Array { @@ -16,7 +16,7 @@ pub const Array = opaque { /// Create an array with initial capacity in caller-provided zval storage. /// - /// Ownership: caller owns `zv`'s array value; call `Zval.raw.dtor(zv)` + /// Ownership: caller owns `zv`'s array value; call `Zval.raw.release(zv)` /// unless the zval is returned/transferred to PHP. The returned wrapper is /// borrowed from `zv`. pub fn init(zv: *c.zval, capacity: u32) *Array { @@ -33,7 +33,14 @@ pub const Array = opaque { pub fn from(zv: *c.zval) FromError!*Array { if (Zval.raw.getType(zv) != c.IS_ARRAY) return error.TypeMismatch; if (zv.value.arr == null) return error.NullPointer; - return @ptrCast(zv); + return fromUnchecked(zv); + } + + /// Create from an existing zval pointer without checking its type. + /// + /// Caller must guarantee that `zv` is an array zval. + pub inline fn fromUnchecked(zv: *c.zval) *Array { + return @ptrCast(@alignCast(zv)); } /// Get the underlying zval pointer. @@ -43,10 +50,15 @@ pub const Array = opaque { return @ptrCast(@alignCast(self)); } + /// View this array zval as a generic `Zval`. + pub inline fn zval(self: *Array) *Zval { + return @ptrCast(@alignCast(self)); + } + /// Get the underlying zend.Array pointer. /// /// Ownership: borrowed array pointer owned by this zval. - pub fn array(self: *Array) *zend.Array { + pub fn zendArray(self: *Array) *zend.Array { return .from(self.ptr().value.arr); } @@ -64,7 +76,7 @@ pub const Array = opaque { /// Get the number of elements pub fn len(self: *Array) usize { - return self.ptr().value.arr.*.nNumOfElements; + return self.zendArray().len(); } /// Set a value by string key. diff --git a/src/zval/object.zig b/src/zval/object.zig index 2ef94b0..6961058 100644 --- a/src/zval/object.zig +++ b/src/zval/object.zig @@ -5,7 +5,7 @@ const Zval = @import("../zval.zig").Zval; pub const Object = opaque { /// Create a standard object (stdClass) in caller-provided zval storage. /// - /// Ownership: caller owns `zv`'s object value; call `Zval.raw.dtor(zv)` + /// Ownership: caller owns `zv`'s object value; call `Zval.raw.release(zv)` /// unless the zval is returned/transferred to PHP. The returned wrapper is /// borrowed from `zv`. pub fn init(zv: *c.zval) *Object { @@ -17,7 +17,7 @@ pub const Object = opaque { /// Create an object from a class entry in caller-provided zval storage. /// - /// Ownership: caller owns `zv`'s object value; call `Zval.raw.dtor(zv)` + /// Ownership: caller owns `zv`'s object value; call `Zval.raw.release(zv)` /// unless the zval is returned/transferred to PHP. The returned wrapper is /// borrowed from `zv`. pub fn initClass(zv: *c.zval, ce: *zend.ClassEntry) InitClassError!*Object { @@ -49,7 +49,14 @@ pub const Object = opaque { pub fn from(zv: *c.zval) FromError!*Object { if (Zval.raw.getType(zv) != c.IS_OBJECT) return error.TypeMismatch; if (zv.value.obj == null) return error.NullPointer; - return @ptrCast(zv); + return fromUnchecked(zv); + } + + /// Create from an existing zval pointer without checking its type. + /// + /// Caller must guarantee that `zv` is an object zval. + pub inline fn fromUnchecked(zv: *c.zval) *Object { + return @ptrCast(@alignCast(zv)); } /// Get the underlying zval pointer. @@ -59,10 +66,15 @@ pub const Object = opaque { return @ptrCast(@alignCast(self)); } + /// View this object zval as a generic `Zval`. + pub inline fn zval(self: *Object) *Zval { + return @ptrCast(@alignCast(self)); + } + /// Get the underlying zend.Object pointer. /// /// Ownership: borrowed object pointer owned by this zval. - pub fn object(self: *Object) *zend.Object { + pub fn zendObject(self: *Object) *zend.Object { return .from(self.ptr().value.obj); } @@ -70,7 +82,7 @@ pub const Object = opaque { /// /// Ownership: borrowed class entry pointer owned by PHP. pub fn class(self: *Object) *zend.ClassEntry { - return self.object().class(); + return self.zendObject().class(); } /// Set a property value. From a8929c20f65b9fdcc80848f83ff87609e0b5ec3d Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Tue, 14 Jul 2026 23:30:35 +0800 Subject: [PATCH 30/36] fix(example): retain borrowed superglobal values --- examples/my_php_extension/src/api/entity.zig | 2 +- examples/my_php_extension/src/api/user.zig | 2 +- examples/my_php_extension/src/functions.zig | 2 +- .../src/testing/superglobals.zig | 19 ++++++++++++------- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/examples/my_php_extension/src/api/entity.zig b/examples/my_php_extension/src/api/entity.zig index 2c1c32d..eb90ae3 100644 --- a/examples/my_php_extension/src/api/entity.zig +++ b/examples/my_php_extension/src/api/entity.zig @@ -17,7 +17,7 @@ const Impl = extern struct { pub fn getId(self: *const Impl, ctx: phpz.Ctx) !void { const entity: *Class = .from(.impl, @constCast(self)); var scratch = phpz.Zval.raw.undef; - defer phpz.Zval.raw.tryDtor(&scratch); + defer phpz.Zval.raw.tryRelease(&scratch); const id = try entity.property("id", false, &scratch); ctx.ret.set(.int, phpz.Zval.raw.asUnchecked(id.ptr(), .int)); diff --git a/examples/my_php_extension/src/api/user.zig b/examples/my_php_extension/src/api/user.zig index 66d0415..6f53b11 100644 --- a/examples/my_php_extension/src/api/user.zig +++ b/examples/my_php_extension/src/api/user.zig @@ -52,7 +52,7 @@ const Impl = extern struct { fn returnName(self: *const Impl, ctx: phpz.Ctx) !void { const user: *Class = .from(.impl, @constCast(self)); var scratch = phpz.Zval.raw.undef; - defer phpz.Zval.raw.tryDtor(&scratch); + defer phpz.Zval.raw.tryRelease(&scratch); const name = try user.property("name", false, &scratch); ctx.ret.set(.string, phpz.Zval.raw.asUnchecked(name.ptr(), .string)); diff --git a/examples/my_php_extension/src/functions.zig b/examples/my_php_extension/src/functions.zig index ca1ac60..3704995 100644 --- a/examples/my_php_extension/src/functions.zig +++ b/examples/my_php_extension/src/functions.zig @@ -50,7 +50,7 @@ fn mapValues(ctx: phpz.Ctx) !void { var iterator = values.fastIterator(); while (iterator.next()) |entry| { var mapped = Zval.raw.undef; - defer Zval.raw.tryDtor(&mapped); + defer Zval.raw.tryRelease(&mapped); try mapper.withRetval(&mapped).call(.{entry.value.*}); switch (entry.key) { diff --git a/examples/my_php_extension/src/testing/superglobals.zig b/examples/my_php_extension/src/testing/superglobals.zig index 451abe0..5697165 100644 --- a/examples/my_php_extension/src/testing/superglobals.zig +++ b/examples/my_php_extension/src/testing/superglobals.zig @@ -16,13 +16,18 @@ fn superglobalsSnapshot(ctx: phpz.Ctx) !void { const request = executor.superglobal(.REQUEST) orelse return error.SuperglobalUnavailable; const result = Zval.Array.empty(ctx.ret.ptr()); - result.set(.mixed, "GET", get.ptr()); - result.set(.mixed, "POST", post.ptr()); - result.set(.mixed, "COOKIE", cookie.ptr()); - result.set(.mixed, "SERVER", server.ptr()); - result.set(.mixed, "ENV", env.ptr()); - result.set(.mixed, "FILES", files.ptr()); - result.set(.mixed, "REQUEST", request.ptr()); + setBorrowed(result, "GET", get); + setBorrowed(result, "POST", post); + setBorrowed(result, "COOKIE", cookie); + setBorrowed(result, "SERVER", server); + setBorrowed(result, "ENV", env); + setBorrowed(result, "FILES", files); + setBorrowed(result, "REQUEST", request); +} + +fn setBorrowed(result: *Zval.Array, key: []const u8, value: *Zval.Array) void { + value.zval().addref(); + result.set(.mixed, key, value.ptr()); } /// PHP: MyPHPExt\Test\mutateSuperglobals(): void From c0b7ba65fbd719af6dbe2fbc3cbfc4c1e0c1acf6 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Tue, 14 Jul 2026 23:30:24 +0800 Subject: [PATCH 31/36] chore(examples): regenerate arginfo for PHP 8.2 --- .../my_php_extension.stub.php | 3 +- .../my_php_extension_arginfo.h | 129 +++++++++++------- examples/skeleton/skeleton.stub.php | 1 + examples/skeleton/skeleton_arginfo.h | 9 +- 4 files changed, 84 insertions(+), 58 deletions(-) diff --git a/examples/my_php_extension/my_php_extension.stub.php b/examples/my_php_extension/my_php_extension.stub.php index c25a44c..35af23e 100644 --- a/examples/my_php_extension/my_php_extension.stub.php +++ b/examples/my_php_extension/my_php_extension.stub.php @@ -1,12 +1,11 @@ = 80400) + ZEND_RAW_FENTRY(ZEND_NS_NAME("MyPHPExt", "increment"), zif_MyPHPExt_increment, arginfo_MyPHPExt_increment, 0, NULL, NULL) +#else + ZEND_RAW_FENTRY(ZEND_NS_NAME("MyPHPExt", "increment"), zif_MyPHPExt_increment, arginfo_MyPHPExt_increment, 0) +#endif +#if (PHP_VERSION_ID >= 80400) + ZEND_RAW_FENTRY(ZEND_NS_NAME("MyPHPExt", "mapValues"), zif_MyPHPExt_mapValues, arginfo_MyPHPExt_mapValues, 0, NULL, NULL) +#else + ZEND_RAW_FENTRY(ZEND_NS_NAME("MyPHPExt", "mapValues"), zif_MyPHPExt_mapValues, arginfo_MyPHPExt_mapValues, 0) +#endif +#if (PHP_VERSION_ID >= 80400) + ZEND_RAW_FENTRY(ZEND_NS_NAME("MyPHPExt\\Test", "allocatorBailout"), zif_MyPHPExt_Test_allocatorBailout, arginfo_MyPHPExt_Test_allocatorBailout, 0, NULL, NULL) +#else + ZEND_RAW_FENTRY(ZEND_NS_NAME("MyPHPExt\\Test", "allocatorBailout"), zif_MyPHPExt_Test_allocatorBailout, arginfo_MyPHPExt_Test_allocatorBailout, 0) +#endif +#if (PHP_VERSION_ID >= 80400) + ZEND_RAW_FENTRY(ZEND_NS_NAME("MyPHPExt\\Test", "superglobalsSnapshot"), zif_MyPHPExt_Test_superglobalsSnapshot, arginfo_MyPHPExt_Test_superglobalsSnapshot, 0, NULL, NULL) +#else + ZEND_RAW_FENTRY(ZEND_NS_NAME("MyPHPExt\\Test", "superglobalsSnapshot"), zif_MyPHPExt_Test_superglobalsSnapshot, arginfo_MyPHPExt_Test_superglobalsSnapshot, 0) +#endif +#if (PHP_VERSION_ID >= 80400) + ZEND_RAW_FENTRY(ZEND_NS_NAME("MyPHPExt\\Test", "mutateSuperglobals"), zif_MyPHPExt_Test_mutateSuperglobals, arginfo_MyPHPExt_Test_mutateSuperglobals, 0, NULL, NULL) +#else + ZEND_RAW_FENTRY(ZEND_NS_NAME("MyPHPExt\\Test", "mutateSuperglobals"), zif_MyPHPExt_Test_mutateSuperglobals, arginfo_MyPHPExt_Test_mutateSuperglobals, 0) +#endif ZEND_FE_END }; - static const zend_function_entry class_MyPHPExt_Identifiable_methods[] = { - ZEND_ABSTRACT_ME_WITH_FLAGS(MyPHPExt_Identifiable, getId, arginfo_class_MyPHPExt_Identifiable_getId, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT) - ZEND_FE_END -}; - - -static const zend_function_entry class_MyPHPExt_Status_methods[] = { +#if (PHP_VERSION_ID >= 80400) + ZEND_RAW_FENTRY("getId", NULL, arginfo_class_MyPHPExt_Identifiable_getId, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) +#else + ZEND_RAW_FENTRY("getId", NULL, arginfo_class_MyPHPExt_Identifiable_getId, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT) +#endif ZEND_FE_END }; - -static const zend_function_entry class_MyPHPExt_Role_methods[] = { - ZEND_FE_END -}; - - static const zend_function_entry class_MyPHPExt_Tag_methods[] = { ZEND_ME(MyPHPExt_Tag, __construct, arginfo_class_MyPHPExt_Tag___construct, ZEND_ACC_PUBLIC) ZEND_FE_END }; - static const zend_function_entry class_MyPHPExt_Entity_methods[] = { ZEND_ME(MyPHPExt_Entity, __construct, arginfo_class_MyPHPExt_Entity___construct, ZEND_ACC_PROTECTED) ZEND_ME(MyPHPExt_Entity, getId, arginfo_class_MyPHPExt_Entity_getId, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL) - ZEND_ABSTRACT_ME_WITH_FLAGS(MyPHPExt_Entity, label, arginfo_class_MyPHPExt_Entity_label, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT) +#if (PHP_VERSION_ID >= 80400) + ZEND_RAW_FENTRY("label", NULL, arginfo_class_MyPHPExt_Entity_label, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) +#else + ZEND_RAW_FENTRY("label", NULL, arginfo_class_MyPHPExt_Entity_label, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT) +#endif ZEND_FE_END }; - static const zend_function_entry class_MyPHPExt_User_methods[] = { ZEND_ME(MyPHPExt_User, __construct, arginfo_class_MyPHPExt_User___construct, ZEND_ACC_PUBLIC) ZEND_ME(MyPHPExt_User, label, arginfo_class_MyPHPExt_User_label, ZEND_ACC_PUBLIC) @@ -216,7 +228,6 @@ static const zend_function_entry class_MyPHPExt_User_methods[] = { ZEND_FE_END }; - static const zend_function_entry class_MyPHPExt_Counter_methods[] = { ZEND_ME(MyPHPExt_Counter, __construct, arginfo_class_MyPHPExt_Counter___construct, ZEND_ACC_PUBLIC) ZEND_ME(MyPHPExt_Counter, increment, arginfo_class_MyPHPExt_Counter_increment, ZEND_ACC_PUBLIC) @@ -226,13 +237,11 @@ static const zend_function_entry class_MyPHPExt_Counter_methods[] = { ZEND_FE_END }; - static const zend_function_entry class_MyPHPExt_Dumper_methods[] = { ZEND_ME(MyPHPExt_Dumper, dump, arginfo_class_MyPHPExt_Dumper_dump, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) ZEND_FE_END }; - static const zend_function_entry class_MyPHPExt_Collection_methods[] = { ZEND_ME(MyPHPExt_Collection, __construct, arginfo_class_MyPHPExt_Collection___construct, ZEND_ACC_PUBLIC) ZEND_ME(MyPHPExt_Collection, toArray, arginfo_class_MyPHPExt_Collection_toArray, ZEND_ACC_PUBLIC) @@ -249,7 +258,6 @@ static const zend_function_entry class_MyPHPExt_Collection_methods[] = { ZEND_FE_END }; - static const zend_function_entry class_MyPHPExt_Config_methods[] = { ZEND_ME(MyPHPExt_Config, greeting, arginfo_class_MyPHPExt_Config_greeting, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) ZEND_ME(MyPHPExt_Config, maxUsers, arginfo_class_MyPHPExt_Config_maxUsers, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) @@ -258,7 +266,6 @@ static const zend_function_entry class_MyPHPExt_Config_methods[] = { ZEND_FE_END }; - static const zend_function_entry class_MyPHPExt_Metrics_methods[] = { ZEND_ME(MyPHPExt_Metrics, __construct, arginfo_class_MyPHPExt_Metrics___construct, ZEND_ACC_PRIVATE) ZEND_ME(MyPHPExt_Metrics, snapshot, arginfo_class_MyPHPExt_Metrics_snapshot, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) @@ -283,7 +290,7 @@ static zend_class_entry *register_class_MyPHPExt_Identifiable(void) static zend_class_entry *register_class_MyPHPExt_Status(void) { - zend_class_entry *class_entry = zend_register_internal_enum("MyPHPExt\\Status", IS_LONG, class_MyPHPExt_Status_methods); + zend_class_entry *class_entry = zend_register_internal_enum("MyPHPExt\\Status", IS_LONG, NULL); zval enum_case_Inactive_value; ZVAL_LONG(&enum_case_Inactive_value, 0); @@ -298,7 +305,7 @@ static zend_class_entry *register_class_MyPHPExt_Status(void) static zend_class_entry *register_class_MyPHPExt_Role(void) { - zend_class_entry *class_entry = zend_register_internal_enum("MyPHPExt\\Role", IS_STRING, class_MyPHPExt_Role_methods); + zend_class_entry *class_entry = zend_register_internal_enum("MyPHPExt\\Role", IS_STRING, NULL); zval enum_case_User_value; zend_string *enum_case_User_value_str = zend_string_init("user", strlen("user"), 1); @@ -318,14 +325,16 @@ static zend_class_entry *register_class_MyPHPExt_Tag(void) zend_class_entry ce, *class_entry; INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "Tag", class_MyPHPExt_Tag_methods); +#if (PHP_VERSION_ID >= 80400) + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); +#else class_entry = zend_register_internal_class_ex(&ce, NULL); class_entry->ce_flags |= ZEND_ACC_FINAL; +#endif zval property_name_default_value; ZVAL_UNDEF(&property_name_default_value); - zend_string *property_name_name = zend_string_init("name", sizeof("name") - 1, 1); - zend_declare_typed_property(class_entry, property_name_name, &property_name_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); - zend_string_release(property_name_name); + zend_declare_typed_property(class_entry, ZSTR_KNOWN(ZEND_STR_NAME), &property_name_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); zval property_description_default_value; ZVAL_UNDEF(&property_description_default_value); @@ -336,9 +345,7 @@ static zend_class_entry *register_class_MyPHPExt_Tag(void) zend_string *attribute_name_Attribute_class_MyPHPExt_Tag_0 = zend_string_init_interned("Attribute", sizeof("Attribute") - 1, 1); zend_attribute *attribute_Attribute_class_MyPHPExt_Tag_0 = zend_add_class_attribute(class_entry, attribute_name_Attribute_class_MyPHPExt_Tag_0, 1); zend_string_release(attribute_name_Attribute_class_MyPHPExt_Tag_0); - zval attribute_Attribute_class_MyPHPExt_Tag_0_arg0; - ZVAL_LONG(&attribute_Attribute_class_MyPHPExt_Tag_0_arg0, ZEND_ATTRIBUTE_TARGET_CLASS | ZEND_ATTRIBUTE_TARGET_PARAMETER | ZEND_ATTRIBUTE_IS_REPEATABLE); - ZVAL_COPY_VALUE(&attribute_Attribute_class_MyPHPExt_Tag_0->args[0].value, &attribute_Attribute_class_MyPHPExt_Tag_0_arg0); + ZVAL_LONG(&attribute_Attribute_class_MyPHPExt_Tag_0->args[0].value, ZEND_ATTRIBUTE_TARGET_CLASS | ZEND_ATTRIBUTE_TARGET_PARAMETER | ZEND_ATTRIBUTE_IS_REPEATABLE); return class_entry; } @@ -348,8 +355,12 @@ static zend_class_entry *register_class_MyPHPExt_Entity(zend_class_entry *class_ zend_class_entry ce, *class_entry; INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "Entity", class_MyPHPExt_Entity_methods); +#if (PHP_VERSION_ID >= 80400) + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_ABSTRACT); +#else class_entry = zend_register_internal_class_ex(&ce, NULL); class_entry->ce_flags |= ZEND_ACC_ABSTRACT; +#endif zend_class_implements(class_entry, 1, class_entry_MyPHPExt_Identifiable); zval property_id_default_value; @@ -361,10 +372,8 @@ static zend_class_entry *register_class_MyPHPExt_Entity(zend_class_entry *class_ zend_string *attribute_name_MyPHPExt_Tag_class_MyPHPExt_Entity_0 = zend_string_init_interned("MyPHPExt\\Tag", sizeof("MyPHPExt\\Tag") - 1, 1); zend_attribute *attribute_MyPHPExt_Tag_class_MyPHPExt_Entity_0 = zend_add_class_attribute(class_entry, attribute_name_MyPHPExt_Tag_class_MyPHPExt_Entity_0, 1); zend_string_release(attribute_name_MyPHPExt_Tag_class_MyPHPExt_Entity_0); - zval attribute_MyPHPExt_Tag_class_MyPHPExt_Entity_0_arg0; zend_string *attribute_MyPHPExt_Tag_class_MyPHPExt_Entity_0_arg0_str = zend_string_init("entity", strlen("entity"), 1); - ZVAL_STR(&attribute_MyPHPExt_Tag_class_MyPHPExt_Entity_0_arg0, attribute_MyPHPExt_Tag_class_MyPHPExt_Entity_0_arg0_str); - ZVAL_COPY_VALUE(&attribute_MyPHPExt_Tag_class_MyPHPExt_Entity_0->args[0].value, &attribute_MyPHPExt_Tag_class_MyPHPExt_Entity_0_arg0); + ZVAL_STR(&attribute_MyPHPExt_Tag_class_MyPHPExt_Entity_0->args[0].value, attribute_MyPHPExt_Tag_class_MyPHPExt_Entity_0_arg0_str); return class_entry; } @@ -374,8 +383,12 @@ static zend_class_entry *register_class_MyPHPExt_User(zend_class_entry *class_en zend_class_entry ce, *class_entry; INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "User", class_MyPHPExt_User_methods); +#if (PHP_VERSION_ID >= 80400) + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_MyPHPExt_Entity, ZEND_ACC_FINAL); +#else class_entry = zend_register_internal_class_ex(&ce, class_entry_MyPHPExt_Entity); class_entry->ce_flags |= ZEND_ACC_FINAL; +#endif zend_class_implements(class_entry, 1, class_entry_Stringable); zval const_MIN_AGE_value; @@ -386,9 +399,7 @@ static zend_class_entry *register_class_MyPHPExt_User(zend_class_entry *class_en zval property_name_default_value; ZVAL_UNDEF(&property_name_default_value); - zend_string *property_name_name = zend_string_init("name", sizeof("name") - 1, 1); - zend_declare_typed_property(class_entry, property_name_name, &property_name_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); - zend_string_release(property_name_name); + zend_declare_typed_property(class_entry, ZSTR_KNOWN(ZEND_STR_NAME), &property_name_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); zval property_age_default_value; ZVAL_UNDEF(&property_age_default_value); @@ -413,23 +424,17 @@ static zend_class_entry *register_class_MyPHPExt_User(zend_class_entry *class_en zend_string *attribute_name_MyPHPExt_Tag_class_MyPHPExt_User_0 = zend_string_init_interned("MyPHPExt\\Tag", sizeof("MyPHPExt\\Tag") - 1, 1); zend_attribute *attribute_MyPHPExt_Tag_class_MyPHPExt_User_0 = zend_add_class_attribute(class_entry, attribute_name_MyPHPExt_Tag_class_MyPHPExt_User_0, 2); zend_string_release(attribute_name_MyPHPExt_Tag_class_MyPHPExt_User_0); - zval attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg0; zend_string *attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg0_str = zend_string_init("model", strlen("model"), 1); - ZVAL_STR(&attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg0, attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg0_str); - ZVAL_COPY_VALUE(&attribute_MyPHPExt_Tag_class_MyPHPExt_User_0->args[0].value, &attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg0); - zval attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg1; + ZVAL_STR(&attribute_MyPHPExt_Tag_class_MyPHPExt_User_0->args[0].value, attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg0_str); zend_string *attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg1_str = zend_string_init("Demonstrates properties, inheritance, enums, and attributes", strlen("Demonstrates properties, inheritance, enums, and attributes"), 1); - ZVAL_STR(&attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg1, attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg1_str); - ZVAL_COPY_VALUE(&attribute_MyPHPExt_Tag_class_MyPHPExt_User_0->args[1].value, &attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg1); + ZVAL_STR(&attribute_MyPHPExt_Tag_class_MyPHPExt_User_0->args[1].value, attribute_MyPHPExt_Tag_class_MyPHPExt_User_0_arg1_str); zend_string *attribute_name_MyPHPExt_Tag_func___construct_arg0_0 = zend_string_init_interned("MyPHPExt\\Tag", sizeof("MyPHPExt\\Tag") - 1, 1); zend_attribute *attribute_MyPHPExt_Tag_func___construct_arg0_0 = zend_add_parameter_attribute(zend_hash_str_find_ptr(&class_entry->function_table, "__construct", sizeof("__construct") - 1), 0, attribute_name_MyPHPExt_Tag_func___construct_arg0_0, 1); zend_string_release(attribute_name_MyPHPExt_Tag_func___construct_arg0_0); - zval attribute_MyPHPExt_Tag_func___construct_arg0_0_arg0; zend_string *attribute_MyPHPExt_Tag_func___construct_arg0_0_arg0_str = zend_string_init("identifier", strlen("identifier"), 1); - ZVAL_STR(&attribute_MyPHPExt_Tag_func___construct_arg0_0_arg0, attribute_MyPHPExt_Tag_func___construct_arg0_0_arg0_str); - ZVAL_COPY_VALUE(&attribute_MyPHPExt_Tag_func___construct_arg0_0->args[0].value, &attribute_MyPHPExt_Tag_func___construct_arg0_0_arg0); + ZVAL_STR(&attribute_MyPHPExt_Tag_func___construct_arg0_0->args[0].value, attribute_MyPHPExt_Tag_func___construct_arg0_0_arg0_str); return class_entry; } @@ -439,8 +444,12 @@ static zend_class_entry *register_class_MyPHPExt_Counter(void) zend_class_entry ce, *class_entry; INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "Counter", class_MyPHPExt_Counter_methods); +#if (PHP_VERSION_ID >= 80400) + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); +#else class_entry = zend_register_internal_class_ex(&ce, NULL); class_entry->ce_flags |= ZEND_ACC_FINAL; +#endif return class_entry; } @@ -450,8 +459,12 @@ static zend_class_entry *register_class_MyPHPExt_Dumper(void) zend_class_entry ce, *class_entry; INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "Dumper", class_MyPHPExt_Dumper_methods); +#if (PHP_VERSION_ID >= 80400) + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); +#else class_entry = zend_register_internal_class_ex(&ce, NULL); class_entry->ce_flags |= ZEND_ACC_FINAL; +#endif return class_entry; } @@ -461,8 +474,12 @@ static zend_class_entry *register_class_MyPHPExt_Collection(zend_class_entry *cl zend_class_entry ce, *class_entry; INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "Collection", class_MyPHPExt_Collection_methods); +#if (PHP_VERSION_ID >= 80400) + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); +#else class_entry = zend_register_internal_class_ex(&ce, NULL); class_entry->ce_flags |= ZEND_ACC_FINAL; +#endif zend_class_implements(class_entry, 3, class_entry_ArrayAccess, class_entry_Countable, class_entry_Iterator); return class_entry; @@ -473,8 +490,12 @@ static zend_class_entry *register_class_MyPHPExt_Config(void) zend_class_entry ce, *class_entry; INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "Config", class_MyPHPExt_Config_methods); +#if (PHP_VERSION_ID >= 80400) + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); +#else class_entry = zend_register_internal_class_ex(&ce, NULL); class_entry->ce_flags |= ZEND_ACC_FINAL; +#endif return class_entry; } @@ -484,8 +505,12 @@ static zend_class_entry *register_class_MyPHPExt_Metrics(void) zend_class_entry ce, *class_entry; INIT_NS_CLASS_ENTRY(ce, "MyPHPExt", "Metrics", class_MyPHPExt_Metrics_methods); +#if (PHP_VERSION_ID >= 80400) + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); +#else class_entry = zend_register_internal_class_ex(&ce, NULL); class_entry->ce_flags |= ZEND_ACC_FINAL; +#endif return class_entry; } diff --git a/examples/skeleton/skeleton.stub.php b/examples/skeleton/skeleton.stub.php index 27dd506..fc7c8ce 100644 --- a/examples/skeleton/skeleton.stub.php +++ b/examples/skeleton/skeleton.stub.php @@ -1,6 +1,7 @@ = 80400) + class_entry = zend_register_internal_class_with_flags(&ce, NULL, 0); +#else class_entry = zend_register_internal_class_ex(&ce, NULL); +#endif return class_entry; } From 69ea980b2f11e58e39a1e342d9dd33d0607fc6f9 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Tue, 14 Jul 2026 23:30:36 +0800 Subject: [PATCH 32/36] chore: update zig and translate-c --- README.md | 4 ++-- build.zig.zon | 6 +++--- examples/my_php_extension/build.zig.zon | 2 +- examples/skeleton/build.zig.zon | 2 +- tools/phpz_skel.php | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 60aae1e..7026ecb 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ A Zig framework for building PHP extensions with PHP C API bindings. ## Requirements -- zig: 0.17.0-dev.1282+c0f9b51d8 -- php: 8.2-8.5 (tested on linux/macos) +- zig: 0.17.0-dev.1398+cb5635714 +- php: 8.2-8.5 (NTS/ZTS) on Linux (x86_64/aarch64), macOS (x86_64/aarch64), Windows MSVC (x64) ## Features diff --git a/build.zig.zon b/build.zig.zon index 735401f..8d43158 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,12 +2,12 @@ .name = .phpz, .version = "0.0.0", .fingerprint = 0x935004cc3861700f, - .minimum_zig_version = "0.17.0-dev.1387+01b60634c", + .minimum_zig_version = "0.17.0-dev.1398+cb5635714", .minimum_php_version = "8.2", .dependencies = .{ .translate_c = .{ - .url = "git+https://codeberg.org/happystraw/translate-c?ref=fn-callconv-stuff#e487ee52747cf308af3cc26b34e6e1bbae896a17", - .hash = "translate_c-0.0.0-Q_BUWiQ7BwC08JMURgqiXUf_j-jbG-ALr-e3ZHBsw8uj", + .url = "git+https://codeberg.org/ziglang/translate-c#12c1515c074cfa9c69a799eb33506464a27c6332", + .hash = "translate_c-0.0.0-Q_BUWvo6BwAVl3qDhMeQNrgCAljvDeGUnwqpnGvB0_YM", }, }, .paths = .{ diff --git a/examples/my_php_extension/build.zig.zon b/examples/my_php_extension/build.zig.zon index af0b3c5..6223fa6 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.17.0-dev.1387+01b60634c", + .minimum_zig_version = "0.17.0-dev.1398+cb5635714", .dependencies = .{ .phpz = .{ .path = "../..", diff --git a/examples/skeleton/build.zig.zon b/examples/skeleton/build.zig.zon index b07976d..ae93d3f 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.17.0-dev.1387+01b60634c", + .minimum_zig_version = "0.17.0-dev.1398+cb5635714", .dependencies = .{ .phpz = .{ .path = "../..", diff --git a/tools/phpz_skel.php b/tools/phpz_skel.php index fbbd78c..c0266f5 100755 --- a/tools/phpz_skel.php +++ b/tools/phpz_skel.php @@ -10,7 +10,7 @@ // phpcs:disable Generic.Files.LineLength.TooLong -- Embedded Zig templates use Zig formatting. const MIN_PHP_VERSION = '8.2.0'; -const MIN_ZIG_VERSION = '0.17.0-dev.1282+c0f9b51d8'; +const MIN_ZIG_VERSION = '0.17.0-dev.1398+cb5635714'; const DEFAULT_PHPZ_SPEC = 'git+https://github.com/happystraw/phpz'; const DEFAULT_PHP_INCLUDE_DIR = '/usr/include/php'; const REQUIRED_PHP_EXTENSIONS = ['pcre', 'standard']; From e0843afb1c59b8f98ba61249462eb724cb63241f Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Tue, 14 Jul 2026 23:30:44 +0800 Subject: [PATCH 33/36] feat(tools): refresh skeleton generator and support anyzig --- tools/phpz_skel.php | 289 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 222 insertions(+), 67 deletions(-) diff --git a/tools/phpz_skel.php b/tools/phpz_skel.php index c0266f5..d96bd65 100755 --- a/tools/phpz_skel.php +++ b/tools/phpz_skel.php @@ -570,15 +570,60 @@ private static function writeBufferedOutput(string $output): void } } +readonly class ZigTool +{ + public function __construct( + public string $bin, + public string $version, + public bool $anyzig, + ) { + } + + public function label(): string + { + if ($this->anyzig) { + return 'anyzig (using ' . $this->version . ')'; + } + + return $this->version; + } + + /** + * @return list + */ + public function initCommand(): array + { + if ($this->anyzig) { + return [$this->bin, $this->version, 'init', '--minimal']; + } + + return [$this->bin, 'init', '--minimal']; + } +} + final class Environment { - public static function checkZigVersion(string $zig): string + public static function checkZigTool(string $zig): ZigTool { + if (self::isAnyzig($zig)) { + return new ZigTool($zig, MIN_ZIG_VERSION, true); + } + $version = trim(ProcessRunner::runRequired([$zig, 'version'], PathUtil::cwd())); if (version_compare($version, MIN_ZIG_VERSION, '<')) { Console::fail("Unsupported Zig version $version. Expected >= " . MIN_ZIG_VERSION . '.'); } - return $version; + return new ZigTool($zig, $version, false); + } + + private static function isAnyzig(string $zig): bool + { + [$code, $output] = ProcessRunner::run([$zig, 'any', 'version'], PathUtil::cwd()); + if ($code !== 0) { + return false; + } + + return trim($output) !== ''; } public static function checkPhpVersion(): string @@ -750,7 +795,7 @@ public static function prepareTargetDir(string $targetDir, bool $force): void public static function initZigProject( string $targetDir, - string $zig, + ZigTool $zig, bool $force, bool $keepCommandOutput, ): void { @@ -771,7 +816,7 @@ public static function initZigProject( ProcessRunner::runStep( 'initializing Zig project', - [$zig, 'init', '--minimal'], + $zig->initCommand(), $targetDir, $keepCommandOutput, ); @@ -840,6 +885,7 @@ public static function runGenStub( // Template definitions. const PHPZ_TEST_STEP_TEMPLATE = <<<'ZIG' + // Add PHPT test step. const test_step = b.step("test", "Run PHPT tests"); const test_phpt_cmd = b.addSystemCommand(&[_][]const u8{ "php", @@ -847,37 +893,37 @@ public static function runGenStub( "-q", "--show-diff", "-d", - b.fmt("extension=modules/{s}", .{ext_filename}), + b.fmt("extension=modules/{s}", .{extension_filename}), }); test_phpt_cmd.step.dependOn(b.getInstallStep()); 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 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; ZIG; const PHPZ_WINDOWS_INIT_OPTIONS_TEMPLATE = <<<'ZIG' - .php_lib_dir = if (php_lib_dir) |dir| .{ .cwd_relative = dir } else null, + .php_lib_dir = if (php_lib_dir) |lib_dir| .{ .cwd_relative = lib_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" + const extension_filename = if (target.result.os.tag == .windows) + "php_" ++ extension_name ++ ".dll" else - ext_name ++ ".so"; + extension_name ++ ".so"; ZIG; const PHPZ_UNIX_FILENAME_TEMPLATE = <<<'ZIG' - const ext_filename = ext_name ++ ".so"; + const extension_filename = extension_name ++ ".so"; ZIG; const PHPZ_WINDOWS_FILENAME_TEMPLATE = <<<'ZIG' - const ext_filename = "php_" ++ ext_name ++ ".dll"; + const extension_filename = "php_" ++ extension_name ++ ".dll"; ZIG; const PHPZ_UNIX_TARGET_CHECK_TEMPLATE = <<<'ZIG' @@ -894,16 +940,99 @@ public static function runGenStub( 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");' +php -dextension=./modules/{{EXT_NAME}}.so -r 'echo greet("World"), PHP_EOL;' +php -dextension=./modules/{{EXT_NAME}}.so -r '$c = new Counter(5); $c->add(3); echo $c->value(), PHP_EOL;' 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");' +php -dextension=.\modules\php_{{EXT_NAME}}.dll -r "hello();" +php -dextension=.\modules\php_{{EXT_NAME}}.dll -r 'echo greet("World"), PHP_EOL;' +php -dextension=.\modules\php_{{EXT_NAME}}.dll -r '$c = new Counter(5); $c->add(3); echo $c->value(), PHP_EOL;' +MD; + +const PHPZ_UNIX_RUN_SECTION_TEMPLATE = <<<'MD' +### Unix + +{{PHPZ_BUILD_ACTION}}: + +```bash +{{PHPZ_TEST_COMMAND}} -Dphp-include-dir="$(php-config --include-dir)" +``` + +If `php-config` is not available, pass the PHP include root directly: + +```bash +{{PHPZ_TEST_COMMAND}} -Dphp-include-dir=/usr/include/php +``` + +Try it manually: + +```bash +zig build -Dphp-include-dir="$(php-config --include-dir)" +{{PHPZ_UNIX_MANUAL_COMMANDS}} +``` +MD; + +const PHPZ_WINDOWS_RUN_SECTION_TEMPLATE = <<<'MD' +### Windows + +Windows builds require a PHP development package from php.net matching the runtime PHP version, architecture, thread safety mode, and debug mode. + +{{PHPZ_BUILD_ACTION}}: + +```powershell +{{PHPZ_TEST_COMMAND}} ` + -Dtarget=native-native-msvc ` + -Dphp-include-dir=C:\php-sdk\include ` + -Dphp-lib-dir=C:\php-sdk\lib +``` + +For a thread-safe PHP SDK/runtime, add `-Dwindows-zts=true`: + +```powershell +{{PHPZ_TEST_COMMAND}} ` + -Dtarget=native-native-msvc ` + -Dphp-include-dir=C:\php-sdk\include ` + -Dphp-lib-dir=C:\php-sdk\lib ` + -Dwindows-zts=true +``` + +Try it manually: + +```powershell +zig build ` + -Dtarget=native-native-msvc ` + -Dphp-include-dir=C:\php-sdk\include ` + -Dphp-lib-dir=C:\php-sdk\lib + +{{PHPZ_WINDOWS_MANUAL_COMMANDS}} +``` + +Windows build options: + +| Option | Meaning | +| ----------------------------- | ------------------------------------------------------------------------------------ | +| `-Dtarget=native-native-msvc` | Use the MSVC ABI required by PHP for Windows extensions. | +| `-Dphp-include-dir=...` | PHP SDK include root containing `main`, `Zend`, `TSRM`, and `ext`. | +| `-Dphp-lib-dir=...` | PHP SDK library directory containing the matching `php8*.lib`; required on Windows. | +| `-Dwindows-zts=true` | Link against the thread-safe PHP library, such as `php8ts.lib`. Omit for NTS builds. | +| `-Dwindows-debug=true` | Link against a debug PHP SDK/library. Omit for release PHP builds. | +| `-Dlibc-file=...` | Optional libc paths file, mainly useful for cross-compilation. | +MD; + +const PHPZ_TEST_FILES_STRUCTURE_TEMPLATE = <<<'MD' +├── run-tests.php # PHP PHPT test runner +MD; + +const PHPZ_TEST_DIR_STRUCTURE_TEMPLATE = <<<'MD' +└── tests/ + ├── hello.phpt + ├── greet.phpt + └── counter.phpt MD; const PHPZ_GEN_STUB_REGEN_TEMPLATE = <<<'MD' -After changing `{{EXT_NAME}}.stub.php`, regenerate C arginfo: +After changing `{{EXT_NAME}}.stub.php`, regenerate `{{EXT_NAME}}_arginfo.h`: ```bash php build/gen_stub.php {{EXT_NAME}}.stub.php @@ -911,7 +1040,7 @@ public static function runGenStub( MD; const PHPZ_GEN_STUB_MISSING_TEMPLATE = <<<'MD' -Arginfo generation is not configured. To regenerate after changing `{{EXT_NAME}}.stub.php`, +Arginfo generation is not configured. To regenerate `{{EXT_NAME}}_arginfo.h` after changing `{{EXT_NAME}}.stub.php`, copy gen_stub.php to `build/gen_stub.php`, then run: ```bash @@ -932,15 +1061,21 @@ public static function runGenStub( const Phpz = @import("phpz").Phpz; +const package = @import("build.zig.zon"); +const extension_name = @tagName(package.name); +const extension_version = package.version; + 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}}"; - const libc_file = b.option([]const u8, "libc-file", "Libc paths file for C translation and extension compilation"); + // PHP build options. + const php_include_dir = b.option([]const u8, "php-include-dir", "PHP include directory (main/, Zend/, TSRM/, ext/)") orelse "{{PHP_INCLUDE_DIR}}"; {{PHPZ_WINDOWS_BUILD_OPTIONS}} + const libc_file = b.option([]const u8, "libc-file", "Libc paths file for C translation and extension compilation"); + // Initialize phpz and translate PHP headers. const phpz_dep = b.dependency("phpz", .{}); const phpz = Phpz.init(phpz_dep, .{ .translator = .{ @@ -948,14 +1083,14 @@ public static function runGenStub( .target = target, .optimize = optimize, }, - .libc_file = if (libc_file) |path| .{ .cwd_relative = path } else null, + .libc_file = if (libc_file) |file| .{ .cwd_relative = file } else null, .php_include_dir = .{ .cwd_relative = php_include_dir }, {{PHPZ_WINDOWS_INIT_OPTIONS}} }); - const ext_name = "{{EXT_NAME}}"; - const ext_lib = phpz.addExtension(b, .{ - .name = ext_name, + // Build the PHP extension library. + const extension = phpz.addExtension(b, .{ + .name = extension_name, .root_module = b.createModule(.{ .root_source_file = b.path("src/root.zig"), .target = target, @@ -964,10 +1099,17 @@ public static function runGenStub( .linkage = .dynamic, }); + // Pass extension metadata to Zig source. + const extension_info = b.addOptions(); + extension_info.addOption([:0]const u8, "name", extension_name); + extension_info.addOption([:0]const u8, "version", extension_version); + extension.root_module.addOptions("extension_info", extension_info); + + // Copy the extension to modules/. {{PHPZ_EXT_FILENAME}} const ext_file = std.Build.Step.UpdateSourceFiles.create(b); - ext_file.addCopyFileToSource(ext_lib.getEmittedBin(), b.fmt("modules/{s}", .{ext_filename})); + ext_file.addCopyFileToSource(extension.getEmittedBin(), b.fmt("modules/{s}", .{extension_filename})); b.getInstallStep().dependOn(&ext_file.step); {{PHPZ_TEST_STEP}} } @@ -978,26 +1120,30 @@ public static function runGenStub( A minimal PHP extension built with phpz. -## Run +## What's inside -```bash -{{PHPZ_GEN_STUB_RUN_COMMAND}}{{PHPZ_TEST_COMMAND}} -``` +- **Functions**: `hello()` prints a greeting, `greet(string $name): string` returns a personalized message +- **Class**: `Counter` has an optional initial value, `add(int $n)`, `dec(int $n)`, and `value(): int` -Custom PHP include path: +## Project structure -```bash -{{PHPZ_TEST_COMMAND}} -Dphp-include-dir=/usr/local/include/php +``` +. +├── build.zig # Build config +├── build.zig.zon # Dependencies +├── {{EXT_NAME}}.h # C header: phpz.h + generated arginfo +├── {{EXT_NAME}}.stub.php # PHP API declarations +├── {{EXT_NAME}}_arginfo.h # Generated arginfo (do not edit directly) +{{PHPZ_TEST_FILES_STRUCTURE}}{{PHPZ_SRC_TREE_ENTRY}} +│ └── root.zig # Module setup, functions, and Counter class +{{PHPZ_TEST_DIR_STRUCTURE}} ``` {{PHPZ_GEN_STUB_REGEN_SECTION}} -## Try it manually +## Build and run -```bash -zig build -{{PHPZ_MANUAL_COMMANDS}} -``` +{{PHPZ_BUILD_RUN_SECTIONS}} MD, '{{EXT_NAME}}.h' => <<<'C' #ifndef {{EXT_NAME_UPPER}}_H @@ -1015,6 +1161,7 @@ public static function runGenStub( = 80400) + class_entry = zend_register_internal_class_with_flags(&ce, NULL, 0); +#else class_entry = zend_register_internal_class_ex(&ce, NULL); +#endif return class_entry; } @@ -1097,16 +1245,29 @@ class_entry = zend_register_internal_class_ex(&ce, NULL); 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 { + const extension = @import("extension_info"); + + // 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 = "{{EXT_NAME}}", - .version = "0.1.0", - .classes = &.{counter.Class}, + .name = extension.name, + .version = extension.version, + .classes = &.{CounterClass}, }); } @@ -1129,9 +1290,7 @@ class_entry = zend_register_internal_class_ex(&ce, NULL); ctx.ret.set(.string, result); } -ZIG, - 'src/classes/counter.zig' => <<<'ZIG' -const phpz = @import("phpz"); +// --- Classes --- /// class Counter pub const Counter = extern struct { @@ -1166,16 +1325,6 @@ class_entry = zend_register_internal_class_ex(&ce, NULL); 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); -} - ZIG, 'tests/hello.phpt' => <<<'PHPT' --TEST-- @@ -1270,14 +1419,20 @@ public static function render( $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); + $runSections = []; + if ($unix) { + $runSections[] = PHPZ_UNIX_RUN_SECTION_TEMPLATE; + } + if ($windows) { + $runSections[] = PHPZ_WINDOWS_RUN_SECTION_TEMPLATE; + } + $contents = str_replace('{{PHPZ_BUILD_RUN_SECTIONS}}', implode("\n\n", $runSections), $contents); + $contents = str_replace('{{PHPZ_UNIX_MANUAL_COMMANDS}}', PHPZ_UNIX_MANUAL_COMMANDS_TEMPLATE, $contents); + $contents = str_replace('{{PHPZ_WINDOWS_MANUAL_COMMANDS}}', PHPZ_WINDOWS_MANUAL_COMMANDS_TEMPLATE, $contents); + $contents = str_replace('{{PHPZ_BUILD_ACTION}}', $hasRunTests ? 'Build and run PHPT tests' : 'Build', $contents); + $contents = str_replace('{{PHPZ_TEST_FILES_STRUCTURE}}', $hasRunTests ? PHPZ_TEST_FILES_STRUCTURE_TEMPLATE : '', $contents); + $contents = str_replace('{{PHPZ_TEST_DIR_STRUCTURE}}', $hasRunTests ? PHPZ_TEST_DIR_STRUCTURE_TEMPLATE : '', $contents); + $contents = str_replace('{{PHPZ_SRC_TREE_ENTRY}}', $hasRunTests ? '├── src/' : '└── src/', $contents); $contents = str_replace( "{{PHPZ_TEST_STEP}}\n", $hasRunTests ? PHPZ_TEST_STEP_TEMPLATE . "\n" : '', @@ -1363,8 +1518,8 @@ public static function run(mixed $argv): void $phpVersion = Environment::checkPhpVersion(); Console::check('PHP version', $phpVersion); Console::check('PHP runtime support', 'yes'); - $zigVersion = Environment::checkZigVersion($args->zig); - Console::check('Zig version', $zigVersion); + $zig = Environment::checkZigTool($args->zig); + Console::check('Zig version', $zig->label()); Console::check('phpz package', $args->phpz); if ($args->phpConfig->isExplicit()) { Console::check('php-config', $args->phpConfig->bin); @@ -1375,7 +1530,7 @@ public static function run(mixed $argv): void $parentDir = PathUtil::absolute($args->dir); $targetDir = PathUtil::normalize($parentDir . '/' . $args->ext); ProjectInstaller::prepareTargetDir($targetDir, $args->force); - ProjectInstaller::initZigProject($targetDir, $args->zig, $args->force, $args->verbose); + ProjectInstaller::initZigProject($targetDir, $zig, $args->force, $args->verbose); ProjectInstaller::fetchPhpz($targetDir, $args->zig, $args->phpz, $args->verbose); $genStubPath = PhpToolResolver::resolveOptional( From cc1fa2e0260b6b6feedd58b61abf972aedfb40e6 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Tue, 14 Jul 2026 23:30:46 +0800 Subject: [PATCH 34/36] chore(examples): align extension setup --- README.md | 106 +++++++------------------ examples/my_php_extension/build.zig | 48 ++++++----- examples/my_php_extension/src/root.zig | 11 +-- examples/skeleton/README.md | 103 ++++++++++++++++-------- examples/skeleton/build.zig | 34 +++++--- examples/skeleton/src/root.zig | 6 +- 6 files changed, 156 insertions(+), 152 deletions(-) diff --git a/README.md b/README.md index 7026ecb..ebb9d3f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A Zig framework for building PHP extensions with PHP C API bindings. -> **ℹ️ Note**: This is a personal experimental project — functionality works well for its intended use cases, but expect API changes as it evolves. Pin to a specific commit if you need stability. +> **ℹ️ Note**: phpz already covers most PHP extension use cases, but the API is still evolving. Pin to a specific commit if you need stability. ## Requirements @@ -11,16 +11,16 @@ A Zig framework for building PHP extensions with PHP C API bindings. ## Features -- **Module** — `phpz.module()` w/ `module_startup`, `module_shutdown`, `request_startup`, `request_shutdown`, `info` lifecycle hooks -- **Class & OOP** — `phpz.Class` (Zig `extern struct` ↦ PHP class, `init`/`deinit`), `phpz.SimpleClass` (interfaces, traits, enums, exceptions), methods, properties (static & instance), inheritance via `register()` hook -- **Functions** — `phpz.function()` with type-safe `Ctx` argument for `expectArgs`/`expectArg` parameter extraction (optional, nullable, raw zval) and `parse()` for complex type specs -- **Constants** — global and class constants via `const` in stub files, auto-registered during MINIT by `register_{name}_symbols` -- **Type-safe Zval** — checked conversions (`is`/`as`/`asOrDefault`), `Zval.Array` / `Zval.Object` builders, `Zval.Optional` nullable params, `Zval.native` raw pointer ops -- **Zend APIs** — `zend.Array` (HashTable CRUD, iterators, sort), `zend.String` (concat, hash), `zend.Object` (properties, calls, enum), `zend.ClassEntry`, `zend.Function`, `zend.Callable` (type-safe fci/fcc), `zend.PropertyInfo`, `zend.Reference`, `zend.Resource` -- **INI Settings** — `php.ini` directives with on-update callbacks and runtime getters -- **Error Handling** — PHP error triggers, exception throwing, argument validation errors -- **Memory** — Zig `Allocator` backed by PHP's `emalloc`, with DWARF leak tracing for debug builds -- **Auto-Registration** — stub-file-driven: functions (`ext_functions`) and constants (`const` in stub → `register_{name}_symbols`) are automatically registered at module startup; classes require an explicit `Class.register()` call in `module_startup_fn` +- **Modules** — extension metadata, lifecycle hooks, phpinfo, INI, globals, observers, and exports. +- **Stub integration** — arginfo, function entries, constants, attributes, class metadata, and arginfo checks. +- **Functions and methods** — typed `Ctx` arguments, returns, nullable values, raw zvals, and PHP argument errors. +- **Classes and OOP** — bind Zig `extern struct` lifecycles to PHP objects, with class entries, methods, inheritance, interfaces, enums, exceptions, and handlers. +- **Runtime state** — typed module globals, PHP/Zend globals, and superglobal access for NTS/ZTS builds. +- **Zval and Zend APIs** — zval conversions, ownership helpers, arrays, strings, objects, callables, functions, and resources. +- **INI, phpinfo, and errors** — typed INI values, phpinfo helpers, PHP errors, exceptions, and bailout-safe cleanup. +- **Observers** — function-call, error, and exception hooks for profiling and monitoring. +- **Memory** — PHP-backed Zig allocator with optional debug leak traces. +- **Build integration** — PHP C translation, extension linking, and platform-specific setup. ## Quick Start @@ -38,90 +38,38 @@ zig build test ## Manual Setup -> **💡 Tip**: For complete working examples, see the [`examples/`](./examples/) directory. +> For a complete minimal project, see [`examples/skeleton`](./examples/skeleton). -### 1. Add Phpz - -Add `phpz` to your `build.zig.zon` +### 1. Add phpz ```bash zig fetch --save git+https://github.com/happystraw/phpz ``` -### 2. Generate arginfo.h - -For PHP 8.0+, it's recommended to use stub files to generate arginfo headers: - -Create a stub file (e.g., `my_php_extension.stub.php`): +### 2. Create the extension files -```php -.stub.php` — PHP API declarations +- `_arginfo.h` — generated by PHP's `gen_stub.php` +- `.h` — includes `phpz.h` and the generated arginfo header +- `src/root.zig` — registers functions, classes, and the module -Generate the arginfo header using PHP's gen_stub.php: +### 3. Generate arginfo ```bash -php /path/to/php-src/build/gen_stub.php my_php_extension.stub.php +php /path/to/php-src/build/gen_stub.php .stub.php ``` -This will generate `my_php_extension_arginfo.h` containing all the necessary argument info definitions (functions, classes, constants, etc.). +### 4. Build -### 3. Configure build.zig - -Create a C header file (e.g., `my_php_extension.h`): - -> [phpz.h](./build/phpz.h) provides the core C API needed for building PHP extensions (includes `php.h`, `Zend/zend_API.h` etc.). -> -> Include it in your extension header to access all necessary PHP C APIs. - -```c -#include "phpz.h" -#include "my_php_extension_arginfo.h" -// ... other header files -``` - -Configure your `build.zig`: - -```zig -// Import the Phpz build system module -const Phpz = @import("phpz").Phpz; - -// 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 = .{ - .target = target, - .optimize = optimize, - // C header for translate-c - .c_source_file = b.path("my_php_extension.h"), - }, - // Directory containing PHP header files (main/, Zend/, TSRM/, ext/). - .php_include_dir = .{ .cwd_relative = "/usr/include/php" }, -}); - -// 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, -}); +```bash +zig build -Dphp-include-dir="$(php-config --include-dir)" ``` -Then `zig build` ! +For the complete `build.zig`, `stub.php`, header, Zig module, and PHPT tests, use [`examples/skeleton`](./examples/skeleton) as the reference. ## Examples diff --git a/examples/my_php_extension/build.zig b/examples/my_php_extension/build.zig index 4547845..e3d59c6 100644 --- a/examples/my_php_extension/build.zig +++ b/examples/my_php_extension/build.zig @@ -1,42 +1,40 @@ const std = @import("std"); -// Import the Phpz build system module const Phpz = @import("phpz").Phpz; +const package = @import("build.zig.zon"); +const extension_name = @tagName(package.name); +const extension_version = package.version; + 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/) + // Read PHP build options. 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", "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 + // Initialize phpz and translate PHP headers. 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("my_php_extension.h"), .target = target, .optimize = optimize, }, - .libc_file = libc_file_path, + .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, .windows_debug = windows_debug, }); - // Create the PHP extension as a dynamic library (.so / .dll / .dylib) - const php_ext_name = "my_php_extension"; - const php_ext_lib = phpz.addExtension(b, .{ - .name = php_ext_name, + // Build the PHP extension library. + const ext_lib = phpz.addExtension(b, .{ + .name = extension_name, .root_module = b.createModule(.{ .root_source_file = b.path("src/root.zig"), .target = target, @@ -44,18 +42,24 @@ pub fn build(b: *std.Build) void { }), .linkage = .dynamic, }); - // PHP expects extensions at a specific naming convention - const php_ext_filename = if (target.result.os.tag == .windows) - "php_" ++ php_ext_name ++ ".dll" + + // Pass extension metadata to Zig source. + const extension_info = b.addOptions(); + extension_info.addOption([:0]const u8, "name", extension_name); + extension_info.addOption([:0]const u8, "version", extension_version); + ext_lib.root_module.addOptions("extension_info", extension_info); + + // Copy the extension to modules/. + const extension_filename = if (target.result.os.tag == .windows) + "php_" ++ extension_name ++ ".dll" else - php_ext_name ++ ".so"; + extension_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); + const ext_file = std.Build.Step.UpdateSourceFiles.create(b); + ext_file.addCopyFileToSource(ext_lib.getEmittedBin(), b.fmt("modules/{s}", .{extension_filename})); + b.getInstallStep().dependOn(&ext_file.step); - // Test step: runs PHPT tests + // Add PHPT test step. const test_step = b.step("test", "Run PHPT tests"); const test_phpt_cmd = b.addSystemCommand(&[_][]const u8{ "php", @@ -63,7 +67,7 @@ pub fn build(b: *std.Build) void { "-q", "--show-diff", "-d", - b.fmt("extension=modules/{s}", .{php_ext_filename}), + b.fmt("extension=modules/{s}", .{extension_filename}), }); test_phpt_cmd.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_phpt_cmd.step); diff --git a/examples/my_php_extension/src/root.zig b/examples/my_php_extension/src/root.zig index f337159..44afe87 100644 --- a/examples/my_php_extension/src/root.zig +++ b/examples/my_php_extension/src/root.zig @@ -1,5 +1,4 @@ const phpz = @import("phpz"); -pub const c = phpz.c; const api = @import("api.zig"); const functions = @import("functions.zig"); @@ -8,21 +7,23 @@ const metrics = @import("metrics.zig"); const bailout = @import("testing/bailout.zig"); const superglobals = @import("testing/superglobals.zig"); -fn info(_: *c.zend_module_entry) void { +fn info(m: *phpz.ModuleEntry) void { phpz.info.table.start(); phpz.info.table.header(.{ "my_php_extension support", "enabled" }); - phpz.info.table.row(.{ "Version", "0.1.0" }); + phpz.info.table.row(.{ "version", m.version }); phpz.info.table.end(); } comptime { + const extension = @import("extension_info"); + _ = functions; _ = bailout; _ = superglobals; phpz.module(.{ - .name = "my_php_extension", - .version = "0.1.0", + .name = extension.name, + .version = extension.version, .classes = api.classes, .ini = ini.definitions, .globals = metrics.Globals, diff --git a/examples/skeleton/README.md b/examples/skeleton/README.md index 8074070..5ee2757 100644 --- a/examples/skeleton/README.md +++ b/examples/skeleton/README.md @@ -1,11 +1,11 @@ # Skeleton -A minimal PHP extension built with phpz — the simplest possible starting point for new projects. +A minimal PHP extension built with phpz. ## What's inside - **Functions**: `hello()` prints a greeting, `greet(string $name): string` returns a personalized message -- **Class**: `Counter` — constructor with optional initial value, `add(int $n)`, `dec(int $n)`, `value(): int` +- **Class**: `Counter` has an optional initial value, `add(int $n)`, `dec(int $n)`, and `value(): int` ## Project structure @@ -13,59 +13,94 @@ A minimal PHP extension built with phpz — the simplest possible starting point . ├── build.zig # Build config ├── build.zig.zon # Dependencies -├── skeleton.h # C header (includes phpz.h + arginfo) -├── skeleton.stub.php # Stub file → gen_stub.php → arginfo.h -├── skeleton_arginfo.h # Generated arginfo (do not edit) -├── run-tests.php # PHP test runner +├── skeleton.h # C header: phpz.h + generated arginfo +├── skeleton.stub.php # PHP API declarations +├── skeleton_arginfo.h # Generated arginfo (do not edit directly) +├── run-tests.php # PHP PHPT test runner ├── src/ -│ ├── root.zig # Entry: module setup, hello(), greet() -│ └── classes/ -│ └── counter.zig # Counter class +│ └── root.zig # Module setup, functions, and Counter class └── tests/ ├── hello.phpt ├── greet.phpt └── counter.phpt ``` -## Run +## Regenerate arginfo + +After changing `skeleton.stub.php`, regenerate `skeleton_arginfo.h`: ```bash -# Build and run tests -zig build test +php /path/to/php-src/build/gen_stub.php skeleton.stub.php ``` -Custom PHP include path: +## Build and run + +### Unix + +Build and run PHPT tests: ```bash -zig build test -Dphp-include-dir=/usr/local/include/php +zig build test -Dphp-include-dir="$(php-config --include-dir)" ``` -## Try it manually +If `php-config` is not available, pass the PHP include root directly: ```bash -# Build -zig build +zig build test -Dphp-include-dir=/usr/include/php +``` -# Run functions +Try it manually: + +```bash +zig build -Dphp-include-dir="$(php-config --include-dir)" php -dextension=./modules/skeleton.so -r 'hello();' -php -dextension=./modules/skeleton.so -r 'echo greet("World");' - -# Use the Counter class -php -dextension=./modules/skeleton.so -r ' - $c = new Counter(5); - $c->add(3); - echo $c->value(); -' +php -dextension=./modules/skeleton.so -r 'echo greet("World"), PHP_EOL;' +php -dextension=./modules/skeleton.so -r '$c = new Counter(5); $c->add(3); echo $c->value(), PHP_EOL;' +``` + +### Windows + +Windows builds require a PHP development package from php.net matching the runtime PHP version, architecture, thread safety mode, and debug mode. + +Build and run PHPT tests: + +```powershell +zig build test ` + -Dtarget=native-native-msvc ` + -Dphp-include-dir=C:\php-sdk\include ` + -Dphp-lib-dir=C:\php-sdk\lib +``` + +For a thread-safe PHP SDK/runtime, add `-Dwindows-zts=true`: + +```powershell +zig build test ` + -Dtarget=native-native-msvc ` + -Dphp-include-dir=C:\php-sdk\include ` + -Dphp-lib-dir=C:\php-sdk\lib ` + -Dwindows-zts=true ``` -## Start your own extension +Try it manually: -Copy this directory and rename `skeleton` to your extension name throughout: +```powershell +zig build ` + -Dtarget=native-native-msvc ` + -Dphp-include-dir=C:\php-sdk\include ` + -Dphp-lib-dir=C:\php-sdk\lib + +php -dextension=.\modules\php_skeleton.dll -r "hello();" +php -dextension=.\modules\php_skeleton.dll -r 'echo greet("World"), PHP_EOL;' +php -dextension=.\modules\php_skeleton.dll -r '$c = new Counter(5); $c->add(3); echo $c->value(), PHP_EOL;' +``` -- `build.zig.zon` → `.name` -- `build.zig` → `ext_name` -- `skeleton.h` → rename file and update `#include` guards + `phpext_*_ptr` -- `skeleton.stub.php` → rename file, update functions/classes -- `src/root.zig` → update `module(.name = ...)` +Windows build options: -Then run `gen_stub.php` to regenerate the arginfo header and you're ready to go. +| Option | Meaning | +| ----------------------------- | ------------------------------------------------------------------------------------ | +| `-Dtarget=native-native-msvc` | Use the MSVC ABI required by PHP for Windows extensions. | +| `-Dphp-include-dir=...` | PHP SDK include root containing `main`, `Zend`, `TSRM`, and `ext`. | +| `-Dphp-lib-dir=...` | PHP SDK library directory containing the matching `php8*.lib`; required on Windows. | +| `-Dwindows-zts=true` | Link against the thread-safe PHP library, such as `php8ts.lib`. Omit for NTS builds. | +| `-Dwindows-debug=true` | Link against a debug PHP SDK/library. Omit for release PHP builds. | +| `-Dlibc-file=...` | Optional libc paths file, mainly useful for cross-compilation. | diff --git a/examples/skeleton/build.zig b/examples/skeleton/build.zig index f742981..2e66c60 100644 --- a/examples/skeleton/build.zig +++ b/examples/skeleton/build.zig @@ -2,16 +2,22 @@ const std = @import("std"); const Phpz = @import("phpz").Phpz; +const package = @import("build.zig.zon"); +const extension_name = @tagName(package.name); +const extension_version = package.version; + pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + // PHP build options. 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", "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"); + // Initialize phpz and translate PHP headers. const phpz_dep = b.dependency("phpz", .{}); const phpz = Phpz.init(phpz_dep, .{ .translator = .{ @@ -19,16 +25,16 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }, - .libc_file = if (libc_file) |path| .{ .cwd_relative = path } else null, + .libc_file = if (libc_file) |file| .{ .cwd_relative = file } else null, .php_include_dir = .{ .cwd_relative = php_include_dir }, - .php_lib_dir = if (php_lib_dir) |d| .{ .cwd_relative = d } else null, + .php_lib_dir = if (php_lib_dir) |lib_dir| .{ .cwd_relative = lib_dir } else null, .windows_zts = windows_zts, .windows_debug = windows_debug, }); - const ext_name = "skeleton"; - const ext_lib = phpz.addExtension(b, .{ - .name = ext_name, + // Build the PHP extension library. + const extension = phpz.addExtension(b, .{ + .name = extension_name, .root_module = b.createModule(.{ .root_source_file = b.path("src/root.zig"), .target = target, @@ -37,15 +43,23 @@ pub fn build(b: *std.Build) void { .linkage = .dynamic, }); - const ext_filename = if (target.result.os.tag == .windows) - "php_" ++ ext_name ++ ".dll" + // Pass extension metadata to Zig source. + const extension_info = b.addOptions(); + extension_info.addOption([:0]const u8, "name", extension_name); + extension_info.addOption([:0]const u8, "version", extension_version); + extension.root_module.addOptions("extension_info", extension_info); + + // Copy the extension to modules/. + const extension_filename = if (target.result.os.tag == .windows) + "php_" ++ extension_name ++ ".dll" else - ext_name ++ ".so"; + extension_name ++ ".so"; const ext_file = std.Build.Step.UpdateSourceFiles.create(b); - ext_file.addCopyFileToSource(ext_lib.getEmittedBin(), b.fmt("modules/{s}", .{ext_filename})); + ext_file.addCopyFileToSource(extension.getEmittedBin(), b.fmt("modules/{s}", .{extension_filename})); b.getInstallStep().dependOn(&ext_file.step); + // Add PHPT test step. const test_step = b.step("test", "Run PHPT tests"); const test_phpt_cmd = b.addSystemCommand(&[_][]const u8{ "php", @@ -53,7 +67,7 @@ pub fn build(b: *std.Build) void { "-q", "--show-diff", "-d", - b.fmt("extension=modules/{s}", .{ext_filename}), + b.fmt("extension=modules/{s}", .{extension_filename}), }); test_phpt_cmd.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_phpt_cmd.step); diff --git a/examples/skeleton/src/root.zig b/examples/skeleton/src/root.zig index 423ea50..aeaa3a0 100644 --- a/examples/skeleton/src/root.zig +++ b/examples/skeleton/src/root.zig @@ -14,14 +14,16 @@ comptime { } comptime { + const extension = @import("extension_info"); + // 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", + .name = extension.name, + .version = extension.version, .classes = &.{CounterClass}, }); } From b35df1d0bd683f1b245f2f6459599bb8ebea7536 Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Tue, 14 Jul 2026 23:30:48 +0800 Subject: [PATCH 35/36] ci: expand phpz_skel platform coverage --- .github/scripts/test-phpz-skel.sh | 149 +++++++++++++++++++++------ .github/workflows/test-phpz-skel.yml | 44 +++++++- examples/my_php_extension/build.zig | 6 +- examples/skeleton/build.zig | 6 +- tools/phpz_skel.php | 44 +++++--- 5 files changed, 191 insertions(+), 58 deletions(-) mode change 100644 => 100755 .github/scripts/test-phpz-skel.sh diff --git a/.github/scripts/test-phpz-skel.sh b/.github/scripts/test-phpz-skel.sh old mode 100644 new mode 100755 index 6de27a6..018a106 --- a/.github/scripts/test-phpz-skel.sh +++ b/.github/scripts/test-phpz-skel.sh @@ -5,18 +5,70 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)" PHP_BIN="${PHP_BIN:-$(command -v php)}" SKEL="$ROOT_DIR/tools/phpz_skel.php" PHPZ_REF="${PHPZ_REF:-dev}" +EXPECTED_VERSION="0.0.1" TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/phpz-skel.XXXXXX")" -cleanup() { - rm -rf "$TMP_ROOT" -} -trap cleanup EXIT - fail() { echo "not ok - $*" >&2 exit 1 } +ZIG_BIN="${ZIG:-zig}" +PHP_CONFIG_BIN="${PHP_CONFIG:-$(command -v php-config || true)}" +command -v "$ZIG_BIN" >/dev/null 2>&1 || fail "zig is required" +PHP_INCLUDE_DIR="${PHP_INCLUDE_DIR:-}" +if [ -z "$PHP_INCLUDE_DIR" ]; then + [ -n "$PHP_CONFIG_BIN" ] || fail "php-config or PHP_INCLUDE_DIR is required" + PHP_INCLUDE_DIR="$("$PHP_CONFIG_BIN" --include-dir)" +fi + +ZIG_BUILD_ARGS=() +[ -n "${ZIG_TARGET:-}" ] && ZIG_BUILD_ARGS+=("-Dtarget=$ZIG_TARGET") +[ -n "${LIBC_FILE:-}" ] && ZIG_BUILD_ARGS+=("-Dlibc-file=$LIBC_FILE") +[ -n "${PHP_INCLUDE_DIR:-}" ] && ZIG_BUILD_ARGS+=("-Dphp-include-dir=$PHP_INCLUDE_DIR") +[ -n "${PHP_LIB_DIR:-}" ] && ZIG_BUILD_ARGS+=("-Dphp-lib-dir=$PHP_LIB_DIR") +[ -n "${WINDOWS_ZTS:-}" ] && ZIG_BUILD_ARGS+=("-Dwindows-zts=$WINDOWS_ZTS") + +case "$(uname -s)" in +MINGW* | MSYS* | CYGWIN*) + IS_WINDOWS=1 + DEFAULT_EXTENSION_FILENAME="php_demo_ext.dll" + ;; +*) + IS_WINDOWS=0 + DEFAULT_EXTENSION_FILENAME="demo_ext.so" + ;; +esac +EXTENSION_FILENAME="${PHPZ_EXTENSION_FILENAME:-$DEFAULT_EXTENSION_FILENAME}" + +php_path() { + if [ "$IS_WINDOWS" -eq 1 ] && command -v cygpath >/dev/null 2>&1; then + cygpath -m "$1" + else + printf '%s' "$1" + fi +} + +SKEL_ARGS=() +while [ "$#" -gt 0 ]; do + case "$1" in + --) + shift + SKEL_ARGS=("$@") + break + ;; + *) + echo "unsupported test script argument: $1" + exit 1 + ;; + esac +done + +cleanup() { + rm -rf "$TMP_ROOT" +} +trap cleanup EXIT + note() { echo "==> $*" } @@ -69,15 +121,16 @@ target_dir() { run_skel() { local stdout="$CASE_DIR/stdout" local stderr="$CASE_DIR/stderr" - if ! env ZIG="$ZIG_BIN" "$PHP_BIN" "$SKEL" "$@" > >(tee "$stdout") 2> >(tee "$stderr" >&2); then - fail "command failed unexpectedly: php tools/phpz_skel.php $*" + if ! env ZIG="$ZIG_BIN" "$PHP_BIN" "$SKEL" "${SKEL_ARGS[@]}" "$@" > >(tee "$stdout") 2> >(tee "$stderr" >&2); then + fail "command failed unexpectedly: php tools/phpz_skel.php ${SKEL_ARGS[*]} $*" fi } run_zig() { + local label="$1" shift - if ! (cd "$(target_dir)" && "$ZIG_BIN" "$@"); then - fail "$ZIG_BIN $* failed" + if ! (cd "$(target_dir)" && "$ZIG_BIN" "$@" "${ZIG_BUILD_ARGS[@]}"); then + fail "$ZIG_BIN $* ${ZIG_BUILD_ARGS[*]} failed for $label" fi } @@ -85,19 +138,34 @@ run_php_ri() { local label="$1" local stdout="$CASE_DIR/$label.stdout" local stderr="$CASE_DIR/$label.stderr" - if ! (cd "$(target_dir)" && "$PHP_BIN" -dextension=modules/demo_ext.so --ri demo_ext \ + if ! (cd "$(target_dir)" && "$PHP_BIN" "-dextension=modules/$EXTENSION_FILENAME" --ri demo_ext \ > >(tee "$stdout") 2> >(tee "$stderr" >&2)); then fail "php --ri demo_ext failed" fi assert_contains "$stdout" "demo_ext" - assert_contains "$stdout" "0.1.0" + assert_contains "$stdout" "$EXPECTED_VERSION" } -ZIG_BIN="${ZIG:-zig}" -PHP_CONFIG_BIN="${PHP_CONFIG:-$(command -v php-config || true)}" -command -v "$ZIG_BIN" >/dev/null 2>&1 || fail "zig is required" -[ -n "$PHP_CONFIG_BIN" ] || fail "php-config is required" -PHP_INCLUDE_DIR="$("$PHP_CONFIG_BIN" --include-dir)" +has_skel_arg() { + local expected="$1" + local arg + for arg in "${SKEL_ARGS[@]}"; do + [ "$arg" = "$expected" ] && return 0 + done + return 1 +} + +assert_platform_template() { + local build_file + build_file="$(target_dir)/build.zig" + if has_skel_arg "--onlyunix"; then + assert_contains "$build_file" "This extension only supports Unix targets" + assert_not_contains "$build_file" "php-lib-dir" + elif has_skel_arg "--onlywindows"; then + assert_contains "$build_file" "This extension only supports Windows targets" + assert_contains "$build_file" "php-lib-dir" + fi +} note "lint and help" "$PHP_BIN" -l "$SKEL" >/dev/null @@ -109,40 +177,52 @@ note "default + zig build test" new_case default_build run_skel \ --ext DemoExt \ - --dir "$CASE_DIR/out" \ + --dir "$(php_path "$CASE_DIR/out")" \ --phpz "$PHPZ_REF" assert_file "$(target_dir)/build.zig" assert_dir "$(target_dir)/tests" -assert_executable "$(target_dir)/run-tests.php" -assert_contains "$(target_dir)/build.zig" 'const ext_name = "demo_ext";' -assert_contains "$(target_dir)/src/root.zig" '.name = "demo_ext",' +if [ "$IS_WINDOWS" -eq 1 ]; then + assert_file "$(target_dir)/run-tests.php" +else + assert_executable "$(target_dir)/run-tests.php" +fi +assert_platform_template +assert_contains "$(target_dir)/build.zig" "const extension_name = @tagName(package.name);" +assert_contains "$(target_dir)/build.zig" 'extension_info.addOption([:0]const u8, "version", extension_version);' +assert_contains "$(target_dir)/src/root.zig" ".name = extension.name," assert_contains "$(target_dir)/demo_ext.h" "extern zend_module_entry demo_ext_module_entry;" assert_contains "$CASE_DIR/stdout" "extension: demo_ext" run_zig default-build-test build test run_php_ri default-ri -note "explicit php-config + zig build test" -new_case explicit_php_config -run_skel \ - --ext demo_ext \ - --dir "$CASE_DIR/out" \ - --phpz "$PHPZ_REF" \ - --with-php-config "$PHP_CONFIG_BIN" -assert_contains "$CASE_DIR/stderr" "checking php-config... $PHP_CONFIG_BIN" -assert_contains "$(target_dir)/build.zig" "orelse \"$PHP_INCLUDE_DIR\"" -assert_not_contains "$(target_dir)/build.zig" "php-config" -run_zig explicit-php-config-build-test build test -run_php_ri explicit-php-config-ri +if [ -n "$PHP_CONFIG_BIN" ]; then + note "explicit php-config + zig build test" + new_case explicit_php_config + run_skel \ + --ext demo_ext \ + --dir "$(php_path "$CASE_DIR/out")" \ + --phpz "$PHPZ_REF" \ + --with-php-config "$PHP_CONFIG_BIN" + assert_platform_template + assert_contains "$CASE_DIR/stderr" "checking php-config... $PHP_CONFIG_BIN" + assert_contains "$(target_dir)/build.zig" "orelse \"$PHP_INCLUDE_DIR\"" + assert_not_contains "$(target_dir)/build.zig" "php-config" + run_zig explicit-php-config-build-test build test + run_php_ri explicit-php-config-ri +else + note "explicit php-config + zig build test skipped" +fi note "without run-tests" new_case without_run_tests run_skel \ --ext demo_ext \ - --dir "$CASE_DIR/out" \ + --dir "$(php_path "$CASE_DIR/out")" \ --phpz "$PHPZ_REF" \ --without-run-tests assert_no_path "$(target_dir)/tests" assert_no_path "$(target_dir)/run-tests.php" +assert_platform_template assert_not_contains "$(target_dir)/build.zig" "Run PHPT tests" assert_not_contains "$(target_dir)/build.zig" "run-tests.php" assert_not_contains "$(target_dir)/README.md" "zig build test" @@ -153,12 +233,13 @@ note "without gen-stub + zig build test" new_case without_gen_stub run_skel \ --ext demo_ext \ - --dir "$CASE_DIR/out" \ + --dir "$(php_path "$CASE_DIR/out")" \ --phpz "$PHPZ_REF" \ --without-gen-stub assert_no_path "$(target_dir)/build/gen_stub.php" assert_file "$(target_dir)/demo_ext_arginfo.h" assert_dir "$(target_dir)/tests" +assert_platform_template run_zig without-gen-stub-build-test build test run_php_ri without-gen-stub-ri diff --git a/.github/workflows/test-phpz-skel.yml b/.github/workflows/test-phpz-skel.yml index 6624eae..599d509 100644 --- a/.github/workflows/test-phpz-skel.yml +++ b/.github/workflows/test-phpz-skel.yml @@ -6,18 +6,20 @@ on: paths: - "tools/phpz_skel.php" - ".github/scripts/test-phpz-skel.sh" + - ".github/scripts/setup-php-windows.ps1" - ".github/workflows/test-phpz-skel.yml" pull_request: branches: ["dev"] paths: - "tools/phpz_skel.php" - ".github/scripts/test-phpz-skel.sh" + - ".github/scripts/setup-php-windows.ps1" - ".github/workflows/test-phpz-skel.yml" workflow_dispatch: jobs: - test: - name: test (${{ matrix.os }}, PHP 8.2) + unix: + name: test (${{ matrix.os }}, php 8.2) runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -38,3 +40,41 @@ jobs: env: PHPZ_REF: ${{ github.head_ref || github.ref_name || 'dev' }} run: bash .github/scripts/test-phpz-skel.sh + - name: Run phpz_skel tests (--onlyunix) + shell: bash + env: + PHPZ_REF: ${{ github.head_ref || github.ref_name || 'dev' }} + run: bash .github/scripts/test-phpz-skel.sh -- --onlyunix + + windows: + name: test (windows-latest, php 8.2) + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: mlugg/setup-zig@v2 + - name: Install pkg-config + shell: pwsh + run: | + choco install pkgconfiglite --no-progress -y + where.exe pkg-config + - name: Setup PHP runtime and development files + shell: pwsh + run: | + .\.github\scripts\setup-php-windows.ps1 ` + -PhpVersion "8.2" ` + -Arch "x64" ` + -ThreadSafety "nts" ` + -Destination (Join-Path $env:RUNNER_TEMP "php-sdk") + - name: Run phpz_skel tests + shell: bash + env: + PHPZ_REF: ${{ github.head_ref || github.ref_name || 'dev' }} + ZIG_TARGET: native-native-msvc + run: bash .github/scripts/test-phpz-skel.sh + - name: Run phpz_skel tests (--onlywindows) + shell: bash + env: + PHPZ_REF: ${{ github.head_ref || github.ref_name || 'dev' }} + ZIG_TARGET: native-native-msvc + run: bash .github/scripts/test-phpz-skel.sh -- --onlywindows diff --git a/examples/my_php_extension/build.zig b/examples/my_php_extension/build.zig index e3d59c6..d6e8704 100644 --- a/examples/my_php_extension/build.zig +++ b/examples/my_php_extension/build.zig @@ -55,9 +55,9 @@ pub fn build(b: *std.Build) void { else extension_name ++ ".so"; - const ext_file = std.Build.Step.UpdateSourceFiles.create(b); - ext_file.addCopyFileToSource(ext_lib.getEmittedBin(), b.fmt("modules/{s}", .{extension_filename})); - b.getInstallStep().dependOn(&ext_file.step); + const extension_file = std.Build.Step.UpdateSourceFiles.create(b); + extension_file.addCopyFileToSource(ext_lib.getEmittedBin(), b.fmt("modules/{s}", .{extension_filename})); + b.getInstallStep().dependOn(&extension_file.step); // Add PHPT test step. const test_step = b.step("test", "Run PHPT tests"); diff --git a/examples/skeleton/build.zig b/examples/skeleton/build.zig index 2e66c60..6e784ce 100644 --- a/examples/skeleton/build.zig +++ b/examples/skeleton/build.zig @@ -55,9 +55,9 @@ pub fn build(b: *std.Build) void { else extension_name ++ ".so"; - const ext_file = std.Build.Step.UpdateSourceFiles.create(b); - ext_file.addCopyFileToSource(extension.getEmittedBin(), b.fmt("modules/{s}", .{extension_filename})); - b.getInstallStep().dependOn(&ext_file.step); + const extension_file = std.Build.Step.UpdateSourceFiles.create(b); + extension_file.addCopyFileToSource(extension.getEmittedBin(), b.fmt("modules/{s}", .{extension_filename})); + b.getInstallStep().dependOn(&extension_file.step); // Add PHPT test step. const test_step = b.step("test", "Run PHPT tests"); diff --git a/tools/phpz_skel.php b/tools/phpz_skel.php index d96bd65..9a7e07f 100755 --- a/tools/phpz_skel.php +++ b/tools/phpz_skel.php @@ -260,8 +260,14 @@ public static function cwd(): string public static function normalize(string $path): string { $path = str_replace('\\', '/', $path); + $drive = ''; + if (preg_match('/^[A-Za-z]:\//', $path) === 1) { + $drive = substr($path, 0, 2); + $path = substr($path, 2); + } + $parts = []; - $absolute = str_starts_with($path, '/'); + $absolute = $drive !== '' || str_starts_with($path, '/'); foreach (explode('/', $path) as $part) { if ($part === '' || $part === '.') { continue; @@ -276,12 +282,13 @@ public static function normalize(string $path): string } $parts[] = $part; } - return ($absolute ? '/' : '') . implode('/', $parts); + + return $drive . ($absolute ? '/' : '') . implode('/', $parts); } public static function absolute(string $path): string { - if (str_starts_with($path, '/')) { + if (self::isAbsolute($path)) { return self::normalize($path); } return self::normalize(self::cwd() . '/' . $path); @@ -289,12 +296,17 @@ public static function absolute(string $path): string public static function resolveBin(string $bin): string { - if (str_contains($bin, '/') && !str_starts_with($bin, '/')) { + if (str_contains($bin, '/') && !self::isAbsolute($bin)) { return self::absolute($bin); } return $bin; } + private static function isAbsolute(string $path): bool + { + return str_starts_with($path, '/') || preg_match('/^[A-Za-z]:[\/\\\\]/', $path) === 1; + } + public static function mkdir(string $dir): void { if (is_dir($dir)) { @@ -1108,9 +1120,9 @@ public static function runGenStub( // Copy the extension to modules/. {{PHPZ_EXT_FILENAME}} - const ext_file = std.Build.Step.UpdateSourceFiles.create(b); - ext_file.addCopyFileToSource(extension.getEmittedBin(), b.fmt("modules/{s}", .{extension_filename})); - b.getInstallStep().dependOn(&ext_file.step); + const extension_file = std.Build.Step.UpdateSourceFiles.create(b); + extension_file.addCopyFileToSource(extension.getEmittedBin(), b.fmt("modules/{s}", .{extension_filename})); + b.getInstallStep().dependOn(&extension_file.step); {{PHPZ_TEST_STEP}} } @@ -1404,21 +1416,21 @@ public static function render( } $contents = str_replace( - "{{PHPZ_PLATFORM_TARGET_CHECK}}\n", - $targetCheck === '' ? '' : $targetCheck . "\n", + "{{PHPZ_PLATFORM_TARGET_CHECK}}", + $targetCheck, $contents, ); $contents = str_replace( - "{{PHPZ_WINDOWS_BUILD_OPTIONS}}\n", - $windows ? PHPZ_WINDOWS_BUILD_OPTIONS_TEMPLATE . "\n" : '', + "{{PHPZ_WINDOWS_BUILD_OPTIONS}}", + $windows ? PHPZ_WINDOWS_BUILD_OPTIONS_TEMPLATE : '', $contents, ); $contents = str_replace( - "{{PHPZ_WINDOWS_INIT_OPTIONS}}\n", - $windows ? PHPZ_WINDOWS_INIT_OPTIONS_TEMPLATE . "\n" : '', + "{{PHPZ_WINDOWS_INIT_OPTIONS}}", + $windows ? PHPZ_WINDOWS_INIT_OPTIONS_TEMPLATE : '', $contents, ); - $contents = str_replace("{{PHPZ_EXT_FILENAME}}\n", $extFilename . "\n", $contents); + $contents = str_replace("{{PHPZ_EXT_FILENAME}}", $extFilename, $contents); $runSections = []; if ($unix) { $runSections[] = PHPZ_UNIX_RUN_SECTION_TEMPLATE; @@ -1434,8 +1446,8 @@ public static function render( $contents = str_replace('{{PHPZ_TEST_DIR_STRUCTURE}}', $hasRunTests ? PHPZ_TEST_DIR_STRUCTURE_TEMPLATE : '', $contents); $contents = str_replace('{{PHPZ_SRC_TREE_ENTRY}}', $hasRunTests ? '├── src/' : '└── src/', $contents); $contents = str_replace( - "{{PHPZ_TEST_STEP}}\n", - $hasRunTests ? PHPZ_TEST_STEP_TEMPLATE . "\n" : '', + "{{PHPZ_TEST_STEP}}", + $hasRunTests ? PHPZ_TEST_STEP_TEMPLATE : '', $contents, ); $contents = str_replace('{{PHPZ_TEST_COMMAND}}', $hasRunTests ? 'zig build test' : 'zig build', $contents); From 4273fdd2336e37519c8f441cc884e48a015cf10c Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Tue, 14 Jul 2026 23:43:03 +0800 Subject: [PATCH 36/36] fix(windows): import additional Zend data symbols --- tools/windows_patcher.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/windows_patcher.zig b/tools/windows_patcher.zig index 716bc15..43c81a4 100644 --- a/tools/windows_patcher.zig +++ b/tools/windows_patcher.zig @@ -2,8 +2,12 @@ const std = @import("std"); const default_dllimport_data_symbols = [_][]const u8{ "std_object_handlers", + "zend_empty_array", "zend_empty_string", + "zend_known_strings", + "zend_new_interned_string", "zend_one_char_string", + "zend_string_init_existing_interned", "zend_string_init_interned", // NTS