From 1bc5aea14da6e7cb08fa9d3f2039292c417d559e Mon Sep 17 00:00:00 2001 From: Yutao Fang Date: Sun, 27 Sep 2026 23:12:14 +0800 Subject: [PATCH] feat: infer PHP handler signatures and add Mixed unions Share typed argument parsing and return conversion across functions, closures and class methods while preserving manual context handling and constructor backing results. Expose Nullable and Mixed types, migrate examples and skeleton templates to typed signatures, and cover union values and return ownership. --- .../my_php_extension/src/api/big_integer.zig | 11 +- .../my_php_extension/src/api/collection.zig | 85 ++----- examples/my_php_extension/src/api/config.zig | 17 +- examples/my_php_extension/src/api/counter.zig | 36 +-- examples/my_php_extension/src/api/entity.zig | 12 +- examples/my_php_extension/src/api/tag.zig | 7 +- examples/my_php_extension/src/api/user.zig | 4 +- examples/my_php_extension/src/closures.zig | 24 +- examples/my_php_extension/src/functions.zig | 24 +- .../my_php_extension/src/testing/arrays.zig | 34 ++- .../my_php_extension/src/testing/bailout.zig | 2 +- .../src/testing/boundaries.zig | 13 +- .../src/testing/class_entry.zig | 42 ++-- .../src/testing/class_new.zig | 25 +-- .../my_php_extension/src/testing/closures.zig | 147 ++++++++++-- examples/my_php_extension/src/testing/gc.zig | 33 ++- .../my_php_extension/src/testing/guard.zig | 21 +- .../src/testing/named_arguments.zig | 26 +-- .../my_php_extension/src/testing/objects.zig | 44 ++-- .../src/testing/properties.zig | 23 +- .../src/testing/reference_arguments.zig | 11 +- .../src/testing/serialization.zig | 14 +- .../src/testing/superglobals.zig | 6 +- .../src/testing/typed_arguments.zig | 29 ++- .../my_php_extension/src/testing/zval.zig | 15 +- .../tests/typed_handler_signature.phpt | 104 +++++++++ examples/skeleton/src/root.zig | 34 +-- src/class.zig | 62 ++--- src/closure.zig | 10 +- src/ctx.zig | 68 +++--- src/function.zig | 212 ++++++++++++++++-- src/root.zig | 9 +- tools/phpz_skel.php | 34 +-- 33 files changed, 707 insertions(+), 531 deletions(-) create mode 100644 examples/my_php_extension/tests/typed_handler_signature.phpt diff --git a/examples/my_php_extension/src/api/big_integer.zig b/examples/my_php_extension/src/api/big_integer.zig index 2ddd79e..d587c66 100644 --- a/examples/my_php_extension/src/api/big_integer.zig +++ b/examples/my_php_extension/src/api/big_integer.zig @@ -21,13 +21,10 @@ const BigInteger = struct { } /// PHP: MyPHPExt\BigInteger::__construct(int|string $value = 0): void - pub fn __construct(ctx: phpz.Ctx) !BigInteger { - const args = try ctx.call.expectArgs(&.{ - .{ .mixed = .{ .optional = true, .one_of = &.{ .int, .string } } }, - }, {}); + pub fn __construct(initial: ?phpz.Mixed(&.{ .int, .string })) !BigInteger { var self = try init(); errdefer self.deinit(); - if (args[0]) |input| switch (input) { + if (initial) |input| switch (input) { .int => |n| try self.integer.set(n), .string => |text| { // Managed.setString permits separators and empty input; the @@ -47,10 +44,10 @@ const BigInteger = struct { } /// PHP: MyPHPExt\BigInteger::value(): string - pub fn value(self: *const BigInteger, ctx: phpz.Ctx) !void { + pub fn value(self: *const BigInteger) !*phpz.zend.String { const text = try self.integer.toString(allocator, 10, .lower); defer allocator.free(text); - ctx.retval.set(.string, text); + return phpz.zend.String.init(text, false); } const ops = struct { diff --git a/examples/my_php_extension/src/api/collection.zig b/examples/my_php_extension/src/api/collection.zig index 5b74a4c..b5f216a 100644 --- a/examples/my_php_extension/src/api/collection.zig +++ b/examples/my_php_extension/src/api/collection.zig @@ -32,67 +32,41 @@ const Collection = struct { } /// PHP: MyPHPExt\Collection::__construct(array $values = []) - pub fn __construct(ctx: phpz.Ctx) !Collection { - const args = try ctx.call.expectArgs(&.{ - .{ .array = .{ .optional = true } }, - }, {}); - + pub fn __construct(values: ?*phpz.zend.Array) Collection { var self = init(); - if (args[0]) |values| { - self.data.copy(values); + if (values) |provided| { + self.data.copy(provided); } return self; } /// PHP: MyPHPExt\Collection::toArray(): array - pub fn toArray(self: *const Collection, ctx: phpz.Ctx) void { - ctx.retval.set(.array, self.data.dupe()); + pub fn toArray(self: *const Collection) *phpz.zend.Array { + return self.data.dupe(); } /// 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 = args[0]; - - ctx.retval.set(.bool, switch (offset.kind()) { + pub fn offsetExists(self: *Collection, offset: *Zval) bool { + return 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 = args[0]; - + pub fn offsetGet(self: *Collection, offset: *Zval) ?*Zval { 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.retval.set(.mixed, found); - } else { - ctx.retval.set(.null, {}); - } + return if (value) |found| .from(found) else 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 = args[0]; - const value = args[1]; - + pub fn offsetSet(self: *Collection, offset: *Zval, value: *Zval) void { switch (offset.kind()) { .int => { value.tryAddref(); @@ -119,12 +93,7 @@ const Collection = struct { } /// 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 = args[0]; - + pub fn offsetUnset(self: *Collection, offset: *Zval) void { switch (offset.kind()) { .int => self.data.deleteIndex( @intCast(offset.asUnchecked(.int)), @@ -139,30 +108,24 @@ const Collection = struct { } /// PHP: MyPHPExt\Collection::count(): int - pub fn count(self: *const Collection, ctx: phpz.Ctx) void { - ctx.retval.set(.int, @intCast(self.data.len())); + pub fn count(self: *const Collection) usize { + return 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.retval.set(.mixed, value); - } else { - ctx.retval.set(.null, {}); - } + pub fn current(self: *Collection) ?*Zval { + return if (self.iterator.currentValue()) |value| .from(value) else null; } /// PHP: MyPHPExt\Collection::key(): mixed - pub fn key(self: *Collection, ctx: phpz.Ctx) void { + pub fn key(self: *Collection) phpz.Mixed(&.{ .int, .string, .null }) { if (self.iterator.currentKey()) |key_value| { - switch (key_value) { - .int => |index| ctx.retval.set(.int, index), - .string => |key_name| ctx.retval.set(.string, key_name), - } - } else { - ctx.retval.set(.null, {}); + return switch (key_value) { + .int => |index| .{ .int = index }, + .string => |key_name| .{ .string = key_name }, + }; } + return .null; } /// PHP: MyPHPExt\Collection::next(): void @@ -176,8 +139,8 @@ const Collection = struct { } /// PHP: MyPHPExt\Collection::valid(): bool - pub fn valid(self: *Collection, ctx: phpz.Ctx) void { - ctx.retval.set(.bool, self.iterator.current() != null); + pub fn valid(self: *Collection) bool { + return self.iterator.current() != null; } }; diff --git a/examples/my_php_extension/src/api/config.zig b/examples/my_php_extension/src/api/config.zig index 5db80ab..e566727 100644 --- a/examples/my_php_extension/src/api/config.zig +++ b/examples/my_php_extension/src/api/config.zig @@ -1,25 +1,24 @@ const phpz = @import("phpz"); - const ini = @import("../ini.zig"); /// PHP: MyPHPExt\Config::greeting(): string -pub fn greeting(ctx: phpz.Ctx) !void { - ctx.retval.set(.string, try ini.greeting.get()); +pub fn greeting() ![]const u8 { + return try ini.greeting.get(); } /// PHP: MyPHPExt\Config::maxUsers(): int -pub fn maxUsers(ctx: phpz.Ctx) !void { - ctx.retval.set(.int, try ini.max_users.get()); +pub fn maxUsers() !i64 { + return try ini.max_users.get(); } /// PHP: MyPHPExt\Config::debugEnabled(): bool -pub fn debugEnabled(ctx: phpz.Ctx) !void { - ctx.retval.set(.bool, try ini.debug.get()); +pub fn debugEnabled() !bool { + return try ini.debug.get(); } /// PHP: MyPHPExt\Config::mode(): string -pub fn mode(ctx: phpz.Ctx) !void { - ctx.retval.set(.string, @tagName(try ini.mode.get())); +pub fn mode() ![]const u8 { + return @tagName(try ini.mode.get()); } pub const Class = phpz.Class("MyPHPExt\\Config", @This(), .{}); diff --git a/examples/my_php_extension/src/api/counter.zig b/examples/my_php_extension/src/api/counter.zig index 735a4df..b16efae 100644 --- a/examples/my_php_extension/src/api/counter.zig +++ b/examples/my_php_extension/src/api/counter.zig @@ -2,42 +2,30 @@ const Counter = struct { current_value: i64, /// PHP: MyPHPExt\Counter::__construct(int $value = 0): void - pub fn __construct(ctx: phpz.Ctx) !Counter { - const args = try ctx.call.expectArgs(&.{ - .{ .int = .{ .optional = true } }, - }, {}); - return .{ .current_value = args[0] orelse 0 }; + pub fn __construct(initial: ?i64) Counter { + return .{ .current_value = initial orelse 0 }; } /// PHP: MyPHPExt\Counter::increment(int $by = 1): int - pub fn increment(self: *Counter, ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .int = .{ .optional = true } }, - }, {}); - self.current_value +|= args[0] orelse 1; - ctx.retval.set(.int, self.current_value); + pub fn increment(self: *Counter, by: ?i64) i64 { + self.current_value +|= by orelse 1; + return self.current_value; } /// PHP: MyPHPExt\Counter::decrement(int $by = 1): int - pub fn decrement(self: *Counter, ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .int = .{ .optional = true } }, - }, {}); - self.current_value -|= args[0] orelse 1; - ctx.retval.set(.int, self.current_value); + pub fn decrement(self: *Counter, by: ?i64) i64 { + self.current_value -|= by orelse 1; + return self.current_value; } /// PHP: MyPHPExt\Counter::value(): int - pub fn value(self: *const Counter, ctx: phpz.Ctx) void { - ctx.retval.set(.int, self.current_value); + pub fn value(self: *const Counter) i64 { + return self.current_value; } /// PHP: MyPHPExt\Counter::reset(int $value = 0): void - pub fn reset(self: *Counter, ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .int = .{ .optional = true } }, - }, {}); - self.current_value = args[0] orelse 0; + pub fn reset(self: *Counter, initial: ?i64) void { + self.current_value = initial orelse 0; } }; diff --git a/examples/my_php_extension/src/api/entity.zig b/examples/my_php_extension/src/api/entity.zig index 13b3120..b4d8aad 100644 --- a/examples/my_php_extension/src/api/entity.zig +++ b/examples/my_php_extension/src/api/entity.zig @@ -3,23 +3,19 @@ fn register(register_fn: anytype) *phpz.ClassEntry { } /// PHP: MyPHPExt\Entity::__construct(int $id): void -pub fn __construct(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .int = .{} }, - }, {}); - +pub fn __construct(ctx: phpz.Ctx, id: i64) !void { const entity = ctx.call.this().?; - try entity.setProperty(.int, "id", args[0]); + try entity.setProperty(.int, "id", id); } /// PHP: MyPHPExt\Entity::getId(): int -pub fn getId(ctx: phpz.Ctx) !void { +pub fn getId(ctx: phpz.Ctx) !i64 { const entity = ctx.call.this().?; var scratch = phpz.Zval.raw.undef; defer phpz.Zval.raw.tryRelease(&scratch); const id = try entity.property("id", false, &scratch); - ctx.retval.set(.int, phpz.Zval.raw.asUnchecked(id.ptr(), .int)); + return phpz.Zval.raw.asUnchecked(id.ptr(), .int); } pub const Class = phpz.Class("MyPHPExt\\Entity", @This(), .{ .register = register }); diff --git a/examples/my_php_extension/src/api/tag.zig b/examples/my_php_extension/src/api/tag.zig index 1fb70a3..ec63929 100644 --- a/examples/my_php_extension/src/api/tag.zig +++ b/examples/my_php_extension/src/api/tag.zig @@ -1,10 +1,5 @@ /// PHP: MyPHPExt\Tag::__construct(string $name, ?string $description = null): void -pub fn __construct(ctx: phpz.Ctx) !void { - const name, const description = try ctx.call.expectArgs(&.{ - .{ .string = .{} }, - .{ .string = .{ .optional = true, .nullable = true } }, - }, {}); - +pub fn __construct(ctx: phpz.Ctx, name: []const u8, description: ?phpz.Nullable([]const u8)) !void { const tag = ctx.call.this().?; try tag.setProperty(.string, "name", name); if (description) |provided| { diff --git a/examples/my_php_extension/src/api/user.zig b/examples/my_php_extension/src/api/user.zig index 80f3b1b..85b2020 100644 --- a/examples/my_php_extension/src/api/user.zig +++ b/examples/my_php_extension/src/api/user.zig @@ -40,13 +40,13 @@ pub fn __construct(ctx: phpz.Ctx) !void { /// PHP: MyPHPExt\User::label(): string /// PHP: MyPHPExt\User::__toString(): string -pub fn label(ctx: phpz.Ctx) !void { +pub fn label(ctx: phpz.Ctx) !*phpz.zend.String { const user = ctx.call.this().?; var scratch = phpz.Zval.raw.undef; defer phpz.Zval.raw.tryRelease(&scratch); const name = try user.property("name", false, &scratch); - ctx.retval.set(.string, phpz.Zval.raw.asUnchecked(name.ptr(), .string)); + return name.asUnchecked(.str).copy(); } pub const Class = phpz.Class("MyPHPExt\\User", @This(), .{ .register = register }); diff --git a/examples/my_php_extension/src/closures.zig b/examples/my_php_extension/src/closures.zig index de020d0..e4df8ac 100644 --- a/examples/my_php_extension/src/closures.zig +++ b/examples/my_php_extension/src/closures.zig @@ -2,9 +2,8 @@ const phpz = @import("phpz"); const Zval = phpz.Zval; /// PHP: {closure}(...$args) — accepts two integers and returns their sum. -fn sum(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ .{ .int = .{} }, .{ .int = .{} } }, {}); - ctx.retval.set(.int, args[0] +| args[1]); +fn sum(a: i64, b: i64) i64 { + return a +| b; } /// PHP: MyPHPExt\makeSumClosure(): Closure @@ -23,12 +22,9 @@ const Counter = struct { } /// PHP: MyPHPExt\ClosureCounter::__invoke(int $step = 1): int - pub fn __invoke(self: *Counter, ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .int = .{ .optional = true } }, - }, {}); - self.value +|= args[0] orelse 1; - ctx.retval.set(.int, self.value); + pub fn __invoke(self: *Counter, step: ?i64) i64 { + self.value +|= step orelse 1; + return self.value; } fn deinit(self: *Counter) void { @@ -47,16 +43,12 @@ pub const CounterClass = phpz.Class("MyPHPExt\\ClosureCounter", Counter, .{ }); /// PHP: MyPHPExt\makeCounter(int $start = 0, mixed $held = null): Closure -pub fn makeCounter(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .int = .{ .optional = true } }, - .{ .mixed = .{ .optional = true } }, - }, {}); +pub fn makeCounter(ctx: phpz.Ctx, start: ?i64, held: ?*Zval) !void { const owner = try CounterClass.create(); defer owner.object().release(); const backing = owner.backing().?; - backing.value = args[0] orelse 0; - if (args[1]) |held| Zval.raw.copy(&backing.held, held.ptr()); + backing.value = start orelse 0; + if (held) |value| Zval.raw.copy(&backing.held, value.ptr()); const invoke = phpz.zend.Function.findMethod(CounterClass.entry, "__invoke").?; try invoke.toClosure(ctx.retval, .{ .object = owner.object() }); } diff --git a/examples/my_php_extension/src/functions.zig b/examples/my_php_extension/src/functions.zig index 14e73ce..d5adc55 100644 --- a/examples/my_php_extension/src/functions.zig +++ b/examples/my_php_extension/src/functions.zig @@ -9,14 +9,10 @@ pub fn hello() void { } /// PHP: greet(string $name): string -pub fn greet(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .string = .{} }, - }, {}); - +pub fn greet(name: []const u8) !*phpz.zend.String { var buffer: [4096]u8 = undefined; - const result = try std.fmt.bufPrint(&buffer, "Hello, {s}!", .{args[0]}); - ctx.retval.set(.string, result); + const result = try std.fmt.bufPrint(&buffer, "Hello, {s}!", .{name}); + return phpz.zend.String.init(result, false); } /// PHP: MyPHPExt\increment(int &$value, int $by = 1): void @@ -33,19 +29,7 @@ pub fn increment(ctx: phpz.Ctx) !void { } /// PHP: MyPHPExt\mapValues(array $values, callable $mapper): array -pub fn mapValues(ctx: phpz.Ctx) !void { - var mapper: phpz.zend.Callable = .nil; - const values, _ = try ctx.call.expectArgs( - &.{ - .{ .array = .{} }, - .{ .callable = .{ .resolve = true } }, - }, - .{ - {}, - .{ .out = &mapper }, - }, - ); - +pub fn mapValues(ctx: phpz.Ctx, values: *phpz.zend.Array, mapper: *phpz.zend.Callable) !void { var result = phpz.Zval.Array.empty(ctx.retval.ptr()); var iterator = values.fastIterator(); while (iterator.next()) |entry| { diff --git a/examples/my_php_extension/src/testing/arrays.zig b/examples/my_php_extension/src/testing/arrays.zig index f45e988..dfcc948 100644 --- a/examples/my_php_extension/src/testing/arrays.zig +++ b/examples/my_php_extension/src/testing/arrays.zig @@ -1,38 +1,32 @@ const phpz = @import("phpz"); -pub fn separateArray(ctx: phpz.Ctx) !void { - _ = try ctx.call.expectArgs(&.{.{ .array = .{} }}, {}); - ctx.retval.set(.mixed, ctx.call.arg(1)); - const result = phpz.Zval.Array.fromUnchecked(ctx.retval.ptr()); +pub fn separateArray(ctx: phpz.Ctx, source: *phpz.zend.Array) !void { + if (!source.isImmutable()) source.addref(); + ctx.retval.set(.array, source); + const result = try ctx.retval.array(); result.separate(); result.set(.int, "count", 1); try result.setAt(.int, 0, 2); try result.append(.int, 3); } -pub fn copyArray(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ .{ .array = .{} }, .{ .array = .{} } }, {}); - const result = args[0].dupe(); +pub fn copyArray(ctx: phpz.Ctx, target: *phpz.zend.Array, source: *phpz.zend.Array) void { + const result = target.dupe(); ctx.retval.set(.array, result); - result.copy(args[1]); + result.copy(source); } -pub fn mergeArray(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ .{ .array = .{} }, .{ .array = .{} }, .{ .bool = .{} } }, {}); - const result = args[0].dupe(); +pub fn mergeArray(ctx: phpz.Ctx, target: *phpz.zend.Array, source: *phpz.zend.Array, overwrite: bool) void { + const result = target.dupe(); ctx.retval.set(.array, result); - result.merge(args[1], args[2]); + result.merge(source, overwrite); } -pub fn compareArrays(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .array = .{} }, .{ .array = .{} }, .{ .bool = .{} }, .{ .bool = .{} }, - }, {}); - const result = if (args[3]) - try args[0].compare(args[1], compareIdentical, args[2]) +pub fn compareArrays(left: *phpz.zend.Array, right: *phpz.zend.Array, ordered: bool, identical: bool) !i64 { + return if (identical) + try left.compare(right, compareIdentical, ordered) else - try args[0].compare(args[1], compareValues, args[2]); - ctx.retval.set(.int, result); + try left.compare(right, compareValues, ordered); } fn compareValues(left: *const phpz.c.zval, right: *const phpz.c.zval) c_int { diff --git a/examples/my_php_extension/src/testing/bailout.zig b/examples/my_php_extension/src/testing/bailout.zig index 17bce90..39847fe 100644 --- a/examples/my_php_extension/src/testing/bailout.zig +++ b/examples/my_php_extension/src/testing/bailout.zig @@ -23,7 +23,7 @@ fn exhaustPhpAllocator(scope_defer: *bool, cleanup_defer: *bool, freed_blocks: * /// PHP: MyPHPExt\Test\allocatorBailout(): array pub fn allocatorBailout(ctx: phpz.Ctx) !void { - _ = try ctx.call.expectArgs(&.{}, {}); + try ctx.call.expectNoArgs(); var scope_defer = false; var cleanup_defer = false; diff --git a/examples/my_php_extension/src/testing/boundaries.zig b/examples/my_php_extension/src/testing/boundaries.zig index 0a621b1..0eca529 100644 --- a/examples/my_php_extension/src/testing/boundaries.zig +++ b/examples/my_php_extension/src/testing/boundaries.zig @@ -31,8 +31,7 @@ fn sortWithException(a: *c.Bucket, b: *c.Bucket) phpz.zend.Array.SortOrder { return if (left < right) .less else if (left > right) .greater else .equal; } -pub fn checkArrayCallbacks(ctx: phpz.Ctx) !void { - _ = try ctx.call.expectArgs(&.{}, {}); +pub fn checkArrayCallbacks() !void { const source = phpz.zend.Array.empty(); defer source.release(); for ([_]i64{ 3, 2, 1 }) |number| { @@ -74,9 +73,8 @@ fn throwingNoProperties(obj: ?*c.zend_object) callconv(.c) ?*c.HashTable { return null; } -pub fn checkObjectPropertyHandlers(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{.{ .object = .{ .optional = true, .nullable = true } }}, {}); - if (args[0]) |arg| { +pub fn checkObjectPropertyHandlers(object: ?phpz.Nullable(*phpz.zend.Object)) !void { + if (object) |arg| { if (arg.asOptional()) |lazy| { try expectPhpException(lazy.stdProperties()); try expectPhpException(lazy.properties()); @@ -136,9 +134,8 @@ fn freeObject(obj: ?*c.zend_object) callconv(.c) void { c.zend_object_std_dtor(obj); } -pub fn checkObjectCreation(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{.{ .string = .{} }}, {}); - creation_stage = std.meta.stringToEnum(CreationStage, args[0]) orelse return error.InvalidStage; +pub fn checkObjectCreation(stage: []const u8) !void { + creation_stage = std.meta.stringToEnum(CreationStage, stage) orelse return error.InvalidStage; // Each PHPT defines this dedicated user class; no other class is modified. const entry = (try phpz.ClassEntry.lookup("NativeBoundaryTarget", false)) orelse return error.ClassNotFound; creation_handlers = c.std_object_handlers; diff --git a/examples/my_php_extension/src/testing/class_entry.zig b/examples/my_php_extension/src/testing/class_entry.zig index c2fa121..712b6a6 100644 --- a/examples/my_php_extension/src/testing/class_entry.zig +++ b/examples/my_php_extension/src/testing/class_entry.zig @@ -1,7 +1,7 @@ const std = @import("std"); const phpz = @import("phpz"); -pub fn checkConstantMetadata(_: phpz.Ctx) !void { +pub fn checkConstantMetadata() !void { const text = phpz.zend.Constant.find("readtest\\TEXT") orelse return error.ConstantNotFound; try std.testing.expectEqualStrings("ReadTest\\TEXT", text.name()); try std.testing.expectEqual(@as(u32, phpz.c.PHP_USER_CONSTANT), text.moduleNumber()); @@ -36,41 +36,25 @@ fn expectPhpException(result: phpz.errors.Exception!?*phpz.Zval) !void { try std.testing.expect(pending); } -pub fn readConstant(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ .{ .string = .{} }, .{ .bool = .{} } }, {}); - const value = (try phpz.zend.Constant.get(args[0], null, args[1])) orelse return error.ConstantNotFound; - ctx.retval.set(.mixed, value.ptr()); +pub fn readConstant(name: []const u8, silent: bool) !*phpz.Zval { + return (try phpz.zend.Constant.get(name, null, silent)) orelse error.ConstantNotFound; } -pub fn readClassConstant(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ .{ .string = .{} }, .{ .string = .{} }, .{ .bool = .{} } }, {}); - const entry = (try phpz.ClassEntry.lookup(args[0], true)) orelse return error.ClassNotFound; - const value = (try entry.constant(args[1], args[2])) orelse return error.ConstantNotFound; - ctx.retval.set(.mixed, value.ptr()); +pub fn readClassConstant(class_name: []const u8, name: []const u8, silent: bool) !*phpz.Zval { + const entry = (try phpz.ClassEntry.lookup(class_name, true)) orelse return error.ClassNotFound; + return (try entry.constant(name, silent)) orelse error.ConstantNotFound; } -pub fn lookupClass(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .string = .{} }, - .{ .bool = .{} }, - }, {}); - if (try phpz.ClassEntry.lookup(args[0], args[1])) |entry| { - ctx.retval.set(.string, entry.name()); - } else { - ctx.retval.set(.null, {}); - } +pub fn lookupClass(name: []const u8, autoload: bool) !?[]const u8 { + const entry = (try phpz.ClassEntry.lookup(name, autoload)) orelse return null; + return entry.name(); } /// Return a copy of the property value; the example reports missing and /// uninitialized slots as errors instead of exposing them as PHP values. -pub fn readStaticProperty(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .string = .{} }, - .{ .string = .{} }, - .{ .bool = .{} }, - }, {}); - const entry = (try phpz.ClassEntry.lookup(args[0], true)) orelse return error.ClassNotFound; - const value = (try entry.staticProperty(args[1], args[2])) orelse return error.PropertyNotFound; +pub fn readStaticProperty(class_name: []const u8, name: []const u8, silent: bool) !*phpz.Zval { + const entry = (try phpz.ClassEntry.lookup(class_name, true)) orelse return error.ClassNotFound; + const value = (try entry.staticProperty(name, silent)) orelse return error.PropertyNotFound; if (value.is(.undef)) return error.UninitializedProperty; - ctx.retval.set(.mixed, value.ptr()); + return value; } diff --git a/examples/my_php_extension/src/testing/class_new.zig b/examples/my_php_extension/src/testing/class_new.zig index 2c69506..f91dbf4 100644 --- a/examples/my_php_extension/src/testing/class_new.zig +++ b/examples/my_php_extension/src/testing/class_new.zig @@ -6,19 +6,14 @@ const Value = struct { second: i64 = 0, trace_cleanup: bool = false, - pub fn __construct(ctx: phpz.Ctx) !Value { - const args = try ctx.call.expectArgs(&.{ - .{ .int = .{} }, - .{ .int = .{ .optional = true } }, - .{ .bool = .{ .optional = true } }, - }, {}); - if (args[2] orelse false) { + pub fn __construct(ctx: phpz.Ctx, first: i64, second: ?i64, fail: ?bool) !Value { + if (fail orelse false) { // Trace the already initialized backing when construction fails. const instance = try ValueClass.fromObject(ctx.call.this().?); instance.backing().?.trace_cleanup = true; return phpz.errors.throwError(null, "constructor failed", .{}); } - return .{ .first = args[0], .second = args[1] orelse 7 }; + return .{ .first = first, .second = second orelse 7 }; } pub fn values(self: *const Value, ctx: phpz.Ctx) !void { @@ -42,17 +37,11 @@ const BareClass = phpz.Class("MyPHPExt\\Test\\NewBare", struct { value: i64 = 0 pub const classes = .{ ValueClass, BareClass }; /// Exercise Class.new()/tryNew() with borrowed positional and named arguments. -pub fn newObject(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .bool = .{} }, - .{ .array = .{} }, - .{ .array = .{ .nullable = true } }, - .{ .bool = .{} }, - }, {}); - if (args[0]) { - try create(BareClass, ctx, args[1], args[2].asOptional(), args[3]); +pub fn newObject(ctx: phpz.Ctx, bare: bool, positional: *phpz.zend.Array, named: phpz.Nullable(*phpz.zend.Array), guarded: bool) !void { + if (bare) { + try create(BareClass, ctx, positional, named.asOptional(), guarded); } else { - try create(ValueClass, ctx, args[1], args[2].asOptional(), args[3]); + try create(ValueClass, ctx, positional, named.asOptional(), guarded); } } diff --git a/examples/my_php_extension/src/testing/closures.zig b/examples/my_php_extension/src/testing/closures.zig index 627ff77..fff210c 100644 --- a/examples/my_php_extension/src/testing/closures.zig +++ b/examples/my_php_extension/src/testing/closures.zig @@ -15,9 +15,8 @@ pub fn makeHandlerClosure(ctx: phpz.Ctx) !void { phpz.closure.fromHandler(&sumHandler, ctx.retval); } -fn sum(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ .{ .int = .{} }, .{ .int = .{} } }, {}); - ctx.retval.set(.int, args[0] +| args[1]); +fn sum(left: i64, right: i64) i64 { + return left +| right; } fn empty() void {} @@ -26,14 +25,99 @@ fn fail() !void { return error.ExampleFailure; } -fn value(ctx: phpz.Ctx) void { - ctx.retval.set(.int, 42); +fn value() i64 { + return 42; +} + +fn narrow(number: u8) u8 { + return number; +} + +fn withContext(ctx: phpz.Ctx, number: i64) i64 { + _ = ctx; + return number + 1; +} + +fn nullableString(value_arg: ?phpz.Nullable(*phpz.zend.String)) []const u8 { + if (value_arg) |value_present| return switch (value_present) { + .null => "null", + .value => |str| str.slice(), + }; + return "omitted"; +} + +fn optionalCallable(callback: ?phpz.Nullable(*phpz.zend.Callable)) ![]const u8 { + if (callback) |present| { + switch (present) { + .null => return "null", + .value => |resolved| { + try resolved.tryCall(null, .{}, null); + return "called"; + }, + } + } + return "omitted"; +} + +fn echoBytes(bytes: []const u8) []const u8 { + return bytes; +} + +fn ownedString() *phpz.zend.String { + return phpz.zend.String.init("owned", false); +} + +fn borrowedString(str: *phpz.zend.String) *phpz.zend.String { + return str.copy(); +} + +fn arrayLength(values: *phpz.zend.Array) usize { + return values.len(); +} + +fn newArray() *phpz.zend.Array { + return phpz.zend.Array.empty(); +} + +fn sharedArray(values: *phpz.zend.Array) *phpz.zend.Array { + if (!values.isImmutable()) values.addref(); + return values; +} + +fn copyMixed(value_arg: *phpz.Zval) *phpz.Zval { + return value_arg; +} + +fn maybe(value_arg: bool) ?i64 { + return if (value_arg) 42 else null; +} + +fn narrowFloat(value_arg: f32) f32 { + return value_arg; +} + +fn tooLarge() u64 { + return std.math.maxInt(u64); +} + +const MixedValue = phpz.Mixed(&.{ .null, .int, .float, .bool, .string, .array, .object, .resource }); + +fn mixed(input: MixedValue) MixedValue { + switch (input) { + .array => |array| if (!array.isImmutable()) array.addref(), + inline .object, .resource => |pointer| pointer.addref(), + else => {}, + } + return input; +} + +fn optionalMixed(input: ?phpz.Mixed(&.{ .null, .int, .string })) []const u8 { + return if (input) |present| @tagName(present) else "omitted"; } /// PHP: MyPHPExt\Test\makeFnClosure(string $kind = "sum"): Closure -pub fn makeFnClosure(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{.{ .string = .{ .optional = true } }}, {}); - const kind = args[0] orelse "sum"; +pub fn makeFnClosure(ctx: phpz.Ctx, kind_arg: ?[]const u8) !void { + const kind = kind_arg orelse "sum"; if (std.mem.eql(u8, kind, "sum")) { phpz.closure.fromFn(sum, ctx.retval); } else if (std.mem.eql(u8, kind, "empty")) { @@ -42,6 +126,38 @@ pub fn makeFnClosure(ctx: phpz.Ctx) !void { phpz.closure.fromFn(fail, ctx.retval); } else if (std.mem.eql(u8, kind, "value")) { phpz.closure.fromFn(value, ctx.retval); + } else if (std.mem.eql(u8, kind, "narrow")) { + phpz.closure.fromFn(narrow, ctx.retval); + } else if (std.mem.eql(u8, kind, "with_context")) { + phpz.closure.fromFn(withContext, ctx.retval); + } else if (std.mem.eql(u8, kind, "nullable_string")) { + phpz.closure.fromFn(nullableString, ctx.retval); + } else if (std.mem.eql(u8, kind, "optional_callable")) { + phpz.closure.fromFn(optionalCallable, ctx.retval); + } else if (std.mem.eql(u8, kind, "echo_bytes")) { + phpz.closure.fromFn(echoBytes, ctx.retval); + } else if (std.mem.eql(u8, kind, "owned_string")) { + phpz.closure.fromFn(ownedString, ctx.retval); + } else if (std.mem.eql(u8, kind, "borrowed_string")) { + phpz.closure.fromFn(borrowedString, ctx.retval); + } else if (std.mem.eql(u8, kind, "array_length")) { + phpz.closure.fromFn(arrayLength, ctx.retval); + } else if (std.mem.eql(u8, kind, "new_array")) { + phpz.closure.fromFn(newArray, ctx.retval); + } else if (std.mem.eql(u8, kind, "shared_array")) { + phpz.closure.fromFn(sharedArray, ctx.retval); + } else if (std.mem.eql(u8, kind, "copy_mixed")) { + phpz.closure.fromFn(copyMixed, ctx.retval); + } else if (std.mem.eql(u8, kind, "maybe")) { + phpz.closure.fromFn(maybe, ctx.retval); + } else if (std.mem.eql(u8, kind, "narrow_float")) { + phpz.closure.fromFn(narrowFloat, ctx.retval); + } else if (std.mem.eql(u8, kind, "too_large")) { + phpz.closure.fromFn(tooLarge, ctx.retval); + } else if (std.mem.eql(u8, kind, "mixed")) { + phpz.closure.fromFn(mixed, ctx.retval); + } else if (std.mem.eql(u8, kind, "optional_mixed")) { + phpz.closure.fromFn(optionalMixed, ctx.retval); } else if (std.mem.eql(u8, kind, "guard_value")) { phpz.closure.fromFn(@import("guard.zig").value, ctx.retval); } else if (std.mem.eql(u8, kind, "named")) { @@ -56,14 +172,9 @@ pub fn makeFnClosure(ctx: phpz.Ctx) !void { } /// PHP: MyPHPExt\Test\wrapClosure(string $name, ?object $object = null, ?string $class = null): Closure -pub fn wrapClosure(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .string = .{} }, - .{ .object = .{ .optional = true, .nullable = true } }, - .{ .string = .{ .optional = true, .nullable = true } }, - }, {}); - const object = if (args[1]) |arg| arg.asOptional() else null; - const class_name = if (args[2]) |arg| arg.asOptional() else null; +pub fn wrapClosure(ctx: phpz.Ctx, function_name: []const u8, object_arg: ?phpz.Nullable(*phpz.zend.Object), class_arg: ?phpz.Nullable([]const u8)) !void { + const object = if (object_arg) |arg| arg.asOptional() else null; + const class_name = if (class_arg) |arg| arg.asOptional() else null; const scope = if (class_name) |name| (try phpz.ClassEntry.lookup(name, true)) orelse return error.ClassNotFound else if (object) |obj| @@ -71,8 +182,8 @@ pub fn wrapClosure(ctx: phpz.Ctx) !void { else null; const function = (if (scope) |ce| - phpz.zend.Function.findMethod(ce, args[0]) + phpz.zend.Function.findMethod(ce, function_name) else - phpz.zend.Function.fetch(args[0])) orelse return error.FunctionNotFound; + phpz.zend.Function.fetch(function_name)) orelse return error.FunctionNotFound; try function.toClosure(ctx.retval, .{ .object = object, .called_scope = scope }); } diff --git a/examples/my_php_extension/src/testing/gc.zig b/examples/my_php_extension/src/testing/gc.zig index 23af3d5..31150d2 100644 --- a/examples/my_php_extension/src/testing/gc.zig +++ b/examples/my_php_extension/src/testing/gc.zig @@ -7,30 +7,25 @@ const GcNode = struct { callback: phpz.zend.Callable, /// PHP: MyPHPExt\Test\GcNode::__construct(mixed $first = null, mixed $second = null, ?callable $callback = null): void - pub fn __construct(ctx: phpz.Ctx) !GcNode { - var callback: phpz.zend.Callable = .nil; - const args = try ctx.call.expectArgs(&.{ - .{ .mixed = .{ .optional = true } }, - .{ .mixed = .{ .optional = true } }, - .{ .callable = .{ .optional = true, .nullable = true, .resolve = true } }, - }, .{ {}, {}, .{ .out = &callback } }); - var self: GcNode = .{ .first = Zval.raw.undef, .second = Zval.raw.undef, .callback = callback }; - if (args[0]) |value| Zval.raw.copy(&self.first, value.ptr()); - if (args[1]) |value| Zval.raw.copy(&self.second, value.ptr()); - // Parsing borrows the callable; retain it beyond this constructor call. - if (self.callback.fci.size != 0) self.callback.addref(); + pub fn __construct(first: ?*Zval, second: ?*Zval, callback: ?phpz.Nullable(*phpz.zend.Callable)) GcNode { + var self: GcNode = .{ .first = Zval.raw.undef, .second = Zval.raw.undef, .callback = .nil }; + if (first) |value| Zval.raw.copy(&self.first, value.ptr()); + if (second) |value| Zval.raw.copy(&self.second, value.ptr()); + if (callback) |provided| { + if (provided.asOptional()) |resolved| { + // Copy the borrowed call info and retain its PHP references. + self.callback = resolved.*; + self.callback.addref(); + } + } return self; } /// PHP: MyPHPExt\Test\GcNode::invokeCallback(mixed $value, bool $guarded = false): mixed - pub fn invokeCallback(self: *GcNode, ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .mixed = .{} }, - .{ .bool = .{ .optional = true } }, - }, {}); + pub fn invokeCallback(self: *GcNode, ctx: phpz.Ctx, value: *Zval, guarded: ?bool) !void { if (self.callback.fci.size == 0) return error.MissingCallback; - const params = .{args[0].ptr().*}; - if (args[1] orelse false) { + const params = .{value.ptr().*}; + if (guarded orelse false) { try self.callback.tryCall(ctx.retval.ptr(), params, null); } else { try self.callback.call(ctx.retval.ptr(), params, null); diff --git a/examples/my_php_extension/src/testing/guard.zig b/examples/my_php_extension/src/testing/guard.zig index ef842e7..cb33460 100644 --- a/examples/my_php_extension/src/testing/guard.zig +++ b/examples/my_php_extension/src/testing/guard.zig @@ -19,21 +19,14 @@ fn registerResources(ctx: phpz.GuardCtx, label: []const u8) !void { } } -pub fn guardResources(ctx: phpz.GuardCtx) !void { - var callback: phpz.zend.Callable = .nil; - const args = try ctx.call.expectArgs(&.{ - .{ .string = .{} }, - .{ .callable = .{ .resolve = true } }, - .{ .bool = .{ .optional = true } }, - }, .{ {}, .{ .out = &callback }, {} }); - - try registerResources(ctx, args[0]); - defer std.debug.print("defer {s}\n", .{args[0]}); +pub fn guardResources(ctx: phpz.GuardCtx, label: []const u8, callback: *phpz.zend.Callable, fail: ?bool) !void { + try registerResources(ctx, label); + defer std.debug.print("defer {s}\n", .{label}); try callback.call(null, .{}, null); - if (args[2] orelse false) return error.GuardFailure; + if (fail orelse false) return error.GuardFailure; } -pub fn value(ctx: phpz.GuardCtx) void { - registerResources(ctx, "closure") catch return; - ctx.ret(.int, 42); +pub fn value(ctx: phpz.GuardCtx) !i64 { + try registerResources(ctx, "closure"); + return 42; } diff --git a/examples/my_php_extension/src/testing/named_arguments.zig b/examples/my_php_extension/src/testing/named_arguments.zig index b3ca110..cfa4767 100644 --- a/examples/my_php_extension/src/testing/named_arguments.zig +++ b/examples/my_php_extension/src/testing/named_arguments.zig @@ -48,34 +48,26 @@ fn collect(ctx: phpz.Ctx, result: *Zval.Array, prefix: usize) !void { } } -pub fn parsedSum(ctx: phpz.Ctx) !void { +pub fn parsedSum(ctx: phpz.Ctx) !i64 { var left: phpz.c.zend_long = 0; var right: phpz.c.zend_long = 0; try ctx.call.parseArgs("ll", .{ &left, &right }); - ctx.retval.set(.int, left +| right); + return left +| right; } -pub fn parsedVariadicCount(ctx: phpz.Ctx) !void { +pub fn parsedVariadicCount(ctx: phpz.Ctx) !u32 { var args: ?*phpz.c.zval = null; var count: u32 = 0; try ctx.call.parseArgs("*", .{ &args, &count }); - ctx.retval.set(.int, count); + return count; } /// Exercise named-argument call wrappers with zero or one positional argument. -pub fn invokeArguments(ctx: phpz.Ctx) !void { - var callback: phpz.zend.Callable = .nil; - const args = try ctx.call.expectArgs(&.{ - .{ .string = .{} }, - .{ .callable = .{ .resolve = true } }, - .{ .array = .{} }, - .{ .array = .{ .nullable = true } }, - .{ .bool = .{} }, - }, .{ {}, .{ .out = &callback }, {}, {}, {} }); - const named_params = args[3].asOptional(); - switch (args[2].len()) { - 0 => try invoke(ctx, args[0], &callback, .{}, named_params, args[4]), - 1 => try invoke(ctx, args[0], &callback, .{(args[2].findIndex(0) orelse return error.InvalidPositionalArguments).*}, named_params, args[4]), +pub fn invokeArguments(ctx: phpz.Ctx, mode: []const u8, callback: *phpz.zend.Callable, positional: *phpz.zend.Array, named: phpz.Nullable(*phpz.zend.Array), guarded: bool) !void { + const named_params = named.asOptional(); + switch (positional.len()) { + 0 => try invoke(ctx, mode, callback, .{}, named_params, guarded), + 1 => try invoke(ctx, mode, callback, .{(positional.findIndex(0) orelse return error.InvalidPositionalArguments).*}, named_params, guarded), else => return error.TooManyTestArguments, } } diff --git a/examples/my_php_extension/src/testing/objects.zig b/examples/my_php_extension/src/testing/objects.zig index e9ba213..49705b8 100644 --- a/examples/my_php_extension/src/testing/objects.zig +++ b/examples/my_php_extension/src/testing/objects.zig @@ -1,40 +1,30 @@ const phpz = @import("phpz"); -pub fn objectProperties(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ .{ .object = .{} }, .{ .bool = .{} } }, {}); - const props = if (args[1]) try args[0].stdProperties() else try args[0].properties(); - ctx.retval.set(.array, if (props) |table| table.dupe() else phpz.zend.Array.empty()); +pub fn objectProperties(object: *phpz.zend.Object, standard: bool) !*phpz.zend.Array { + const props = if (standard) try object.stdProperties() else try object.properties(); + return if (props) |table| table.dupe() else phpz.zend.Array.empty(); } -pub fn hasObjectProperty(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ .{ .object = .{} }, .{ .string = .{} }, .{ .bool = .{} } }, {}); - ctx.retval.set(.bool, if (args[2]) - try args[0].hasStdProperty(args[1], .isset) +pub fn hasObjectProperty(object: *phpz.zend.Object, name: []const u8, standard: bool) !bool { + return if (standard) + try object.hasStdProperty(name, .isset) else - try args[0].hasProperty(args[1], .isset)); + try object.hasProperty(name, .isset); } -pub fn initObject(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{.{ .string = .{} }}, {}); - const entry = (try phpz.ClassEntry.lookup(args[0], true)) orelse return error.ClassNotFound; - ctx.retval.set(.object, try phpz.zend.Object.init(entry)); +pub fn initObject(class_name: []const u8) !*phpz.zend.Object { + const entry = (try phpz.ClassEntry.lookup(class_name, true)) orelse return error.ClassNotFound; + return try phpz.zend.Object.init(entry); } -pub fn cloneObject(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{.{ .object = .{} }}, {}); - ctx.retval.set(.object, try args[0].clone()); +pub fn cloneObject(object: *phpz.zend.Object) !*phpz.zend.Object { + return try object.clone(); } -pub fn constructObject(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .string = .{} }, - .{ .array = .{} }, - .{ .bool = .{} }, - }, {}); - const entry = (try phpz.ClassEntry.lookup(args[0], true)) orelse return error.ClassNotFound; - const obj = if (args[2]) - try phpz.zend.Object.tryNew(entry, .{}, args[1]) +pub fn constructObject(class_name: []const u8, named: *phpz.zend.Array, guarded: bool) !*phpz.zend.Object { + const entry = (try phpz.ClassEntry.lookup(class_name, true)) orelse return error.ClassNotFound; + return if (guarded) + try phpz.zend.Object.tryNew(entry, .{}, named) else - try phpz.zend.Object.new(entry, .{}, args[1]); - ctx.retval.set(.object, obj); + try phpz.zend.Object.new(entry, .{}, named); } diff --git a/examples/my_php_extension/src/testing/properties.zig b/examples/my_php_extension/src/testing/properties.zig index 1bc5b10..43c5289 100644 --- a/examples/my_php_extension/src/testing/properties.zig +++ b/examples/my_php_extension/src/testing/properties.zig @@ -2,22 +2,15 @@ const std = @import("std"); const phpz = @import("phpz"); /// Both fixtures pass a borrowed, dereferenced value to the property setters. -pub fn setMixedProperty(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .object = .{} }, - .{ .string = .{} }, - .{ .mixed = .{} }, - .{ .string = .{} }, - }, {}); - const input = args[2]; +pub fn setMixedProperty(ctx: phpz.Ctx, object: *phpz.zend.Object, name: []const u8, input: *phpz.Zval, api: []const u8) !void { const value = if (input.is(.reference)) input.asUnchecked(.reference).val() else input.ptr(); - if (std.mem.eql(u8, args[3], "object")) { - try args[0].setProperty(.mixed, args[1], value); - } else if (std.mem.eql(u8, args[3], "static")) { - try args[0].class().setStaticProperty(.mixed, args[1], value); - } else if (std.mem.eql(u8, args[3], "zval")) { - const object = try phpz.Zval.Object.from(&ctx.call.args()[0]); - try object.set(.mixed, args[1], value); + if (std.mem.eql(u8, api, "object")) { + try object.setProperty(.mixed, name, value); + } else if (std.mem.eql(u8, api, "static")) { + try object.class().setStaticProperty(.mixed, name, value); + } else if (std.mem.eql(u8, api, "zval")) { + const wrapper = try phpz.Zval.Object.from(&ctx.call.args()[0]); + try wrapper.set(.mixed, name, value); } else { return error.UnknownPropertyApi; } diff --git a/examples/my_php_extension/src/testing/reference_arguments.zig b/examples/my_php_extension/src/testing/reference_arguments.zig index 805f6ab..47ddb1e 100644 --- a/examples/my_php_extension/src/testing/reference_arguments.zig +++ b/examples/my_php_extension/src/testing/reference_arguments.zig @@ -3,7 +3,7 @@ const phpz = @import("phpz"); const Kind = phpz.CallFrame.ExpectArgKind; /// PHP: MyPHPExt\Test\referenceArgument(string $mode, string $type, bool $optional, bool $single, mixed &$value = null): string -pub fn referenceArgument(ctx: phpz.Ctx) !void { +pub fn referenceArgument(ctx: phpz.Ctx) ![]const u8 { try ctx.call.expectArgCount(4, 5); const mode = try ctx.call.expectArg(1, .{ .string = .{} }, {}); const type_name = try ctx.call.expectArg(2, .{ .string = .{} }, {}); @@ -32,7 +32,7 @@ pub fn referenceArgument(ctx: phpz.Ctx) !void { return error.InvalidReferenceTestOptions; } -fn inspect(ctx: phpz.Ctx, comptime spec: Kind.Spec, single: bool) !void { +fn inspect(ctx: phpz.Ctx, comptime spec: Kind.Spec, single: bool) ![]const u8 { const parsed = if (single) try ctx.call.expectArg(5, spec, {}) else result: { @@ -54,10 +54,7 @@ fn inspect(ctx: phpz.Ctx, comptime spec: Kind.Spec, single: bool) !void { @compileError("unexpected reference argument result type"); } } - const value = if (comptime spec.reference.optional) parsed orelse { - ctx.retval.set(.string, "omitted"); - return; - } else parsed; + const value = if (comptime spec.reference.optional) parsed orelse return "omitted" else parsed; const outer = ctx.call.arg(5); const reference = phpz.Zval.raw.asUnchecked(outer, .reference); @@ -76,5 +73,5 @@ fn inspect(ctx: phpz.Ctx, comptime spec: Kind.Spec, single: bool) !void { }, }; if (inner.is(.int)) inner.set(.int, inner.asUnchecked(.int) + 1); - ctx.retval.set(.string, @tagName(inner.kind())); + return @tagName(inner.kind()); } diff --git a/examples/my_php_extension/src/testing/serialization.zig b/examples/my_php_extension/src/testing/serialization.zig index 0e58601..b306210 100644 --- a/examples/my_php_extension/src/testing/serialization.zig +++ b/examples/my_php_extension/src/testing/serialization.zig @@ -4,14 +4,13 @@ const SerializableValue = struct { number: i64 = 0, /// PHP: MyPHPExt\Test\SerializableValue::__construct(int $value): void - pub fn __construct(ctx: phpz.Ctx) !SerializableValue { - const args = try ctx.call.expectArgs(&.{.{ .int = .{} }}, {}); - return .{ .number = args[0] }; + pub fn __construct(number: i64) SerializableValue { + return .{ .number = number }; } /// PHP: MyPHPExt\Test\SerializableValue::value(): int - pub fn value(self: *const SerializableValue, ctx: phpz.Ctx) void { - ctx.retval.set(.int, self.number); + pub fn value(self: *const SerializableValue) i64 { + return self.number; } /// PHP: MyPHPExt\Test\SerializableValue::__serialize(): array @@ -21,9 +20,8 @@ const SerializableValue = struct { } /// PHP: MyPHPExt\Test\SerializableValue::__unserialize(array $data): void - pub fn __unserialize(self: *SerializableValue, ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{.{ .array = .{} }}, {}); - const value_zval = args[0].find("value") orelse return error.InvalidSerializedData; + pub fn __unserialize(self: *SerializableValue, data: *phpz.zend.Array) !void { + const value_zval = data.find("value") orelse return error.InvalidSerializedData; const stored = phpz.Zval.from(value_zval); if (!stored.is(.int)) return error.InvalidSerializedData; self.number = stored.asUnchecked(.int); diff --git a/examples/my_php_extension/src/testing/superglobals.zig b/examples/my_php_extension/src/testing/superglobals.zig index d18e2c7..de4ade8 100644 --- a/examples/my_php_extension/src/testing/superglobals.zig +++ b/examples/my_php_extension/src/testing/superglobals.zig @@ -3,7 +3,7 @@ const Zval = phpz.Zval; /// PHP: MyPHPExt\Test\superglobalsSnapshot(): array pub fn superglobalsSnapshot(ctx: phpz.Ctx) !void { - _ = try ctx.call.expectArgs(&.{}, {}); + try ctx.call.expectNoArgs(); const php = phpz.globals.php(); const executor = phpz.globals.executor(); @@ -31,9 +31,7 @@ fn setBorrowed(result: *Zval.Array, key: []const u8, value: *Zval.Array) void { } /// PHP: MyPHPExt\Test\mutateSuperglobals(): void -pub fn mutateSuperglobals(ctx: phpz.Ctx) !void { - _ = try ctx.call.expectArgs(&.{}, {}); - +pub fn mutateSuperglobals() !void { const executor = phpz.globals.executor(); const get = executor.superglobalMut(.GET) orelse return error.SuperglobalUnavailable; const post = executor.superglobalMut(.POST) orelse return error.SuperglobalUnavailable; diff --git a/examples/my_php_extension/src/testing/typed_arguments.zig b/examples/my_php_extension/src/testing/typed_arguments.zig index 25a2009..c82f5ee 100644 --- a/examples/my_php_extension/src/testing/typed_arguments.zig +++ b/examples/my_php_extension/src/testing/typed_arguments.zig @@ -91,7 +91,7 @@ fn writeValue(ctx: phpz.Ctx, value: anytype) !void { } /// Verify borrowing at parse time, before the return path obtains a reference. -pub fn checkStringArguments(ctx: phpz.Ctx) !void { +pub fn checkStringArguments(ctx: phpz.Ctx) !bool { try ctx.call.expectArgCount(1, 1); try ctx.call.expectNoExtraNamedArgs(); const original = try Zval.from(ctx.call.arg(1)).as(.str); @@ -100,13 +100,12 @@ pub fn checkStringArguments(ctx: phpz.Ctx) !void { const tuple = try ctx.call.expectArgs(&.{.{ .string = .{ .as = .str } }}, {}); try std.testing.expect(original == single and original == tuple[0]); try std.testing.expectEqual(count, original.refcount()); - ctx.ret(.bool, true); + return true; } /// The fixture object has a public string `value` and static string `shared`. -pub fn checkZvalStrings(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ .{ .string = .{} }, .{ .object = .{} } }, {}); - const str = phpz.zend.String.init(args[0], false); +pub fn checkZvalStrings(ctx: phpz.Ctx, text: []const u8, object: *phpz.zend.Object) !*phpz.zend.String { + const str = phpz.zend.String.init(text, false); var storage = Zval.raw.init(.str, str); const source = Zval.from(&storage); defer source.release(); @@ -118,7 +117,7 @@ pub fn checkZvalStrings(ctx: phpz.Ctx) !void { try std.testing.expect(source.asUnchecked(.str) == str); try std.testing.expect(source.asUnchecked(.string).ptr == str.slice().ptr); try std.testing.expectEqual(initial, str.refcount()); - try std.testing.expectEqualStrings(args[0], source.asOrDefault(.string, "fallback")); + try std.testing.expectEqualStrings(text, source.asOrDefault(.string, "fallback")); var number = Zval.raw.init(.int, 42); try std.testing.expect(!Zval.raw.is(&number, .string) and !Zval.raw.is(&number, .str)); @@ -172,7 +171,6 @@ pub fn checkZvalStrings(ctx: phpz.Ctx) !void { try checkRefcount(str, initial); } - const object = args[1]; try object.setProperty(.str, "value", str); try checkRefcount(str, initial + 1); try object.setProperty(.string, "value", "reset"); @@ -188,7 +186,7 @@ pub fn checkZvalStrings(ctx: phpz.Ctx) !void { try checkRefcount(str, initial); // Returning a separate owned reference must leave PHP with a live string. - ctx.ret(.str, str.copy()); + return str.copy(); } fn checkRefcount(str: *phpz.zend.String, expected: u32) !void { @@ -196,26 +194,25 @@ fn checkRefcount(str: *phpz.zend.String, expected: u32) !void { } /// Check ownership after a rejected property write, including exception traces. -pub fn checkStringWriteFailure(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ .{ .object = .{} }, .{ .string = .{} }, .{ .string = .{} } }, {}); +pub fn checkStringWriteFailure(ctx: phpz.Ctx, object: *phpz.zend.Object, api: []const u8, exception_name: []const u8) !void { const str = phpz.zend.String.init("owned nonnumeric string for a rejected write", false); defer str.release(); try std.testing.expect(!str.isInterned()); try std.testing.expectEqual(@as(u32, 1), str.refcount()); - const result: anyerror!void = if (std.mem.eql(u8, args[1], "object")) - args[0].setProperty(.str, "value", str) - else if (std.mem.eql(u8, args[1], "zval")) + const result: anyerror!void = if (std.mem.eql(u8, api, "object")) + object.setProperty(.str, "value", str) + else if (std.mem.eql(u8, api, "zval")) // This API consumes the offered reference even when the write fails. (try Zval.Object.from(ctx.call.arg(1))).set(.str, "value", str.copy()) - else if (std.mem.eql(u8, args[1], "static")) - args[0].class().setStaticProperty(.str, "shared", str) + else if (std.mem.eql(u8, api, "static")) + object.class().setStaticProperty(.str, "shared", str) else return error.UnknownPropertyAPI; const pending = phpz.errors.exception(); const expected_exception = if (pending) |exception| - std.mem.eql(u8, args[2], exception.class().name()) + std.mem.eql(u8, exception_name, exception.class().name()) else false; // A __set() exception trace may temporarily retain the string argument. diff --git a/examples/my_php_extension/src/testing/zval.zig b/examples/my_php_extension/src/testing/zval.zig index 901b95c..6e531b2 100644 --- a/examples/my_php_extension/src/testing/zval.zig +++ b/examples/my_php_extension/src/testing/zval.zig @@ -3,19 +3,14 @@ const phpz = @import("phpz"); const Zval = phpz.Zval; /// Shared handler for castValue and castReferenceValue. -pub fn castValue(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .mixed = .{} }, - .{ .string = .{} }, - .{ .bool = .{} }, - }, {}); +pub fn castValue(ctx: phpz.Ctx, value: *Zval, type_name: []const u8, convert: bool) !void { inline for (.{ .int, .float, .bool, .str, .array, .object }) |kind| { - if (std.mem.eql(u8, args[1], if (kind == .str) "string" else @tagName(kind))) { - if (args[2]) { - Zval.raw.copy(ctx.retval.ptr(), args[0].ptr()); + if (std.mem.eql(u8, type_name, if (kind == .str) "string" else @tagName(kind))) { + if (convert) { + Zval.raw.copy(ctx.retval.ptr(), value.ptr()); _ = try ctx.retval.convert(kind); } else { - const result = try args[0].cast(kind); + const result = try value.cast(kind); ctx.retval.set(kind, result); } return; diff --git a/examples/my_php_extension/tests/typed_handler_signature.phpt b/examples/my_php_extension/tests/typed_handler_signature.phpt new file mode 100644 index 0000000..ae8b1c4 --- /dev/null +++ b/examples/my_php_extension/tests/typed_handler_signature.phpt @@ -0,0 +1,104 @@ +--TEST-- +Typed Zig handler parameters and returns use expectArgs and Zval.set semantics +--EXTENSIONS-- +my_php_extension +--FILE-- +get(), $copy); +unset($copy); +checkTyped($weak->get(), null); +$mixedResource = fopen('php://memory', 'w+'); +$copy = $mixed($mixedResource); +unset($mixedResource); +checkTyped(fwrite($copy, 'live'), 4); +rewind($copy); +checkTyped(stream_get_contents($copy), 'live'); +fclose($copy); + +checkTyped(makeFnClosure('owned_string')(), 'owned'); +$source = 'borrowed'; +$copy = makeFnClosure('borrowed_string')($source); +unset($source); +checkTyped($copy, 'borrowed'); +checkTyped(makeFnClosure('array_length')([1, 2]), 2); +checkTyped(makeFnClosure('new_array')(), []); +checkTyped(makeFnClosure('shared_array')([]), []); +$sharedSource = ['answer' => 42]; +$shared = makeFnClosure('shared_array')($sharedSource); +unset($sharedSource); +checkTyped($shared, ['answer' => 42]); +$array = ['answer' => 42]; +$copy = makeFnClosure('copy_mixed')($array); +unset($array); +checkTyped($copy, ['answer' => 42]); +checkTyped(makeFnClosure('maybe')(true), 42); +checkTyped(makeFnClosure('maybe')(false), null); +checkTyped(makeFnClosure('narrow_float')(1.5), 1.5); +try { makeFnClosure('narrow_float')(PHP_FLOAT_MAX); throw new Exception('f32 overflow accepted'); } +catch (ValueError $error) {} +try { makeFnClosure('too_large')(); throw new Exception('u64 overflow accepted'); } +catch (Error $error) { + checkTyped(str_contains($error->getMessage(), 'ReturnValueOutOfRange'), true); +} + +echo "typed handler signatures: passed\n"; +?> +--EXPECT-- +typed handler signatures: passed diff --git a/examples/skeleton/src/root.zig b/examples/skeleton/src/root.zig index 60b49f2..92aaf24 100644 --- a/examples/skeleton/src/root.zig +++ b/examples/skeleton/src/root.zig @@ -28,15 +28,10 @@ const functions = struct { } /// function greet(string $name): string - pub fn greet(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .string = .{} }, - }, {}); - const name = args[0]; - + pub fn greet(name: []const u8) !*phpz.zend.String { var buffer: [256]u8 = undefined; const result = try std.fmt.bufPrint(&buffer, "Hello, {s}!", .{name}); - ctx.ret(.string, result); + return .init(result, false); } }; @@ -47,31 +42,22 @@ const Counter = struct { n: i64 = 0, /// public function __construct(int $n = 0): void - pub fn __construct(ctx: phpz.Ctx) !Counter { - const args = try ctx.call.expectArgs(&.{ - .{ .int = .{ .optional = true } }, - }, {}); - return .{ .n = args[0] orelse 0 }; + pub fn __construct(n: ?i64) Counter { + return .{ .n = n 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]; + pub fn add(self: *Counter, n: i64) void { + self.n +|= n; } /// 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]; + pub fn dec(self: *Counter, n: i64) void { + self.n -|= n; } /// public function value(): int - pub fn value(self: *const Counter, ctx: phpz.Ctx) void { - ctx.ret(.int, self.n); + pub fn value(self: *const Counter) i64 { + return self.n; } }; diff --git a/src/class.zig b/src/class.zig index 6936798..e13a744 100644 --- a/src/class.zig +++ b/src/class.zig @@ -4,10 +4,8 @@ const std = @import("std"); const c = @import("c.zig").c; const CallFrame = @import("ctx.zig").CallFrame; -const Ctx = @import("ctx.zig").Ctx; -const GuardCtx = @import("ctx.zig").GuardCtx; const errors = @import("errors.zig"); -const function_helper = @import("function.zig"); +const functions = @import("function.zig"); const GcBuffer = @import("gc.zig").GcBuffer; const globals = @import("globals.zig"); const stub = @import("stub.zig"); @@ -452,14 +450,17 @@ pub const ObjectHandlers = struct { /// /// ## Method signatures /// -/// - Standard-layout methods and backed static methods use `fn () void` or -/// `fn (Ctx) void`, optionally error-returning. -/// - Backed instance methods receive `*T` or `*const T`, optionally followed by -/// `Ctx`, and return `void` or `!void`. -/// - A backed `__construct` accepts no arguments or one `Ctx` and returns `T` -/// or `!T`. The result replaces the backing while PHP observes `void`; -/// any previous backing is released through `.deinit` when provided. -/// Use `GuardCtx` in place of `Ctx` to enable native resource cleanup on bailout. +/// - Standard-layout methods and backed static methods accept an optional leading +/// `Ctx` or `GuardCtx`, followed by typed PHP parameters, and return values to PHP. +/// - Backed instance methods receive `*T` or `*const T` first, then an optional +/// `Ctx` or `GuardCtx`, followed by typed PHP parameters. +/// - A backed `__construct` accepts an optional leading context and typed PHP +/// parameters, and returns `T` or `!T`. The result replaces the backing while +/// PHP observes `void`; any previous backing is released through `.deinit` when provided. +/// +/// For complex argument specs, take only a context after any receiver and parse manually. +/// Except for backed constructors, non-void results are written to PHP; +/// `void`/`!void` preserves a manually set result. `GuardCtx` enables native cleanup on bailout. /// /// ## Options /// @@ -906,7 +907,7 @@ fn bind( comptime func: anytype, ) void { if (class_layout == .std) { - function_helper.method(class_name, method.name, func); + functions.method(class_name, method.name, func); return; } if (std.mem.eql(u8, method.name, "__construct")) { @@ -914,28 +915,17 @@ fn bind( return; } switch (method.kind) { - .static => function_helper.method(class_name, method.name, func), + .static => functions.method(class_name, method.name, func), .instance => bindInstanceMethod(class_name, T, ClassType, method.name, func), } } fn bindConstructor(comptime class_name: [:0]const u8, comptime T: type, comptime ClassType: type, comptime func: anytype) void { - const params = @typeInfo(@TypeOf(func)).@"fn".param_types; - if (params.len > 1 or (params.len == 1 and params[0] != Ctx and params[0] != GuardCtx)) { - @compileError("unsupported constructor signature for " ++ class_name ++ ": expected fn(Ctx), fn(GuardCtx), or fn()"); - } - const Context = if (params.len == 1) params[0].? else Ctx; - const Args = std.meta.ArgsTuple(@TypeOf(func)); - const handler = function_helper.createHandler(class_name ++ "::__construct()", struct { + const Context = functions.ContextType(func, false); + const handler = functions.createHandler(class_name ++ "::__construct()", struct { fn invoke(ctx: Context) anyerror!void { const self = ClassType.receiver(ctx.call) orelse return; - const args: Args = if (comptime @typeInfo(Args).@"struct".field_types.len == 1) - .{ctx} - else blk: { - try ctx.call.expectNoArgs(); - break :blk .{}; - }; - const value = try @as(anyerror!T, @call(.auto, func, args)); + const value: T = try functions.invoke(func, ctx, {}); self.commitBacking(value); } }.invoke); @@ -948,26 +938,16 @@ fn bindInstanceMethod(comptime class_name: [:0]const u8, comptime T: type, compt if (params.len == 0 or (params[0] != *T and params[0] != *const T)) { @compileError("instance method " ++ func_desc ++ " must take *" ++ @typeName(T) ++ " or *const " ++ @typeName(T) ++ " as its first parameter"); } - if (params.len > 2 or (params.len == 2 and params[1] != Ctx and params[1] != GuardCtx)) { - @compileError("unsupported instance method signature for " ++ func_desc ++ ": expected only an optional Ctx or GuardCtx after the receiver"); - } - - const Context = if (params.len == 2) params[1].? else Ctx; - const Args = std.meta.ArgsTuple(@TypeOf(func)); - const handler = function_helper.createHandler(func_desc, struct { + const Context = functions.ContextType(func, true); + const handler = functions.createHandler(func_desc, struct { fn invoke(ctx: Context) anyerror!void { const self = ClassType.receiver(ctx.call) orelse return; const backing = self.backing() orelse { errors.throwError(null, class_name ++ " object is not initialized; call its constructor first", .{}) catch {}; return; }; - const args: Args = if (comptime params.len == 2) - .{ backing, ctx } - else blk: { - try ctx.call.expectNoArgs(); - break :blk .{backing}; - }; - return @call(.auto, func, args); + const value = try functions.invoke(func, ctx, backing); + try functions.setReturnValue(ctx.retval, value); } }.invoke); @export(&handler, .{ .name = stub.methodSymbolName(class_name, method_name) }); diff --git a/src/closure.zig b/src/closure.zig index a085847..6d977cb 100644 --- a/src/closure.zig +++ b/src/closure.zig @@ -2,8 +2,8 @@ const std = @import("std"); const c = @import("c.zig").c; -const function_helper = @import("function.zig"); -const Handler = function_helper.Handler; +const functions = @import("function.zig"); +const Handler = functions.Handler; const String = @import("zend/string.zig").String; const Zval = @import("zval.zig").Zval; @@ -16,11 +16,13 @@ const handler_arginfo = [_]c.zend_internal_arg_info{ }, }; -/// Create a Closure from fn(Ctx), fn(GuardCtx), or fn(), returning void or !void. +/// Create a Closure from a typed Zig function, optionally with a leading Ctx or +/// GuardCtx. The handler parses PHP parameters and writes non-void Zig results; +/// void/!void preserves a manually set result. /// Uses phpz.function's error and bailout handling. /// See fromHandler for the closure's signature and creation constraints. pub fn fromFn(comptime func: anytype, result: *Zval) void { - const handler = function_helper.createHandler("{closure}()", func); + const handler = functions.createHandler("{closure}()", func); fromHandler(handler, result); } diff --git a/src/ctx.zig b/src/ctx.zig index 7036c52..3e35e47 100644 --- a/src/ctx.zig +++ b/src/ctx.zig @@ -173,7 +173,9 @@ pub const CallFrame = opaque { } } - fn ExpectArgsRuntime(comptime specs: []const ExpectArgKind.Spec) type { + /// Runtime argument type for expectArgs: void when no spec needs runtime data, + /// otherwise a tuple with one Runtime(spec) entry per argument, including void entries. + pub fn ExpectArgsRuntime(comptime specs: []const ExpectArgKind.Spec) type { comptime { var types: [specs.len]type = undefined; var is_all_void = true; @@ -446,35 +448,8 @@ pub const CallFrame = opaque { return switch (spec) { .null => @compileError(".null cannot be used as a standalone type; use another type with the `nullable` flag or use .mixed with unions to allow null as a distinct case"), .mixed => |s| { - if (s.one_of) |u| { - if (u.len <= 1) @compileError("invalid .mixed specification: unions array must contain at least 2 types"); - 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.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"); - if (kind == .reference) @compileError("invalid .mixed specification: unions cannot contain .reference"); - if (kind == .callable) @compileError("invalid .mixed specification: unions cannot contain .callable"); - for (u[0..i]) |prev| if (prev == kind) @compileError("invalid .mixed specification: unions contains duplicate type ." ++ @tagName(kind)); - field_names[i] = @tagName(kind); - field_types[i] = kind.InnerType(); - field_attrs[i] = .{}; - field_values[i] = @backingInt(kind); - } - - const PhpUnionType = @Union( - .auto, - @Enum(TagInt, .exhaustive, &field_names, &field_values), - &field_names, - &field_types, - &field_attrs, - ); - return if (s.optional) ?PhpUnionType else PhpUnionType; - } else { - return if (s.optional) ?*Zval else *Zval; - } + const T = if (s.one_of) |kinds| Mixed(kinds) else *Zval; + return if (s.optional) ?T else T; }, .callable => |s| { const T = if (s.nullable) Nullable(*Zval) else *Zval; @@ -500,8 +475,37 @@ pub const CallFrame = opaque { }; } - /// Returned for nullable typed values (except `.as = .zval`): distinguishes - /// "null was passed" (.null) from "argument omitted" (outer Zig optional). + /// A tagged PHP union, e.g. Mixed(&.{ .int, .string }). + /// Include .null to accept PHP null; an outer optional permits omitted arguments. + pub fn Mixed(comptime kinds: []const ExpectArgKind) type { + if (kinds.len <= 1) @compileError("Mixed requires at least two types"); + const TagInt = @typeInfo(ExpectArgKind).@"enum".tag_type; + var field_names: [kinds.len][]const u8 = undefined; + var field_types: [kinds.len]type = undefined; + var field_attrs: [kinds.len]std.lang.Type.Union.FieldAttributes = undefined; + var field_values: [kinds.len]TagInt = undefined; + inline for (kinds, 0..) |kind, i| { + if (kind == .mixed or kind == .reference or kind == .callable) { + @compileError("Mixed cannot contain ." ++ @tagName(kind)); + } + for (kinds[0..i]) |prev| if (prev == kind) @compileError("Mixed contains duplicate type ." ++ @tagName(kind)); + field_names[i] = @tagName(kind); + field_types[i] = kind.InnerType(); + field_attrs[i] = .{}; + field_values[i] = @backingInt(kind); + } + return @Union( + .auto, + @Enum(TagInt, .exhaustive, &field_names, &field_values), + &field_names, + &field_types, + &field_attrs, + ); + } + + /// Explicit PHP null (.null) or a typed value (.value), for nullable arguments + /// and handler returns. An outer Zig optional distinguishes omitted arguments. + /// expectArgs uses this for nullable typed outputs except `.as = .zval`. pub fn Nullable(comptime T: type) type { return union(enum) { null, diff --git a/src/function.zig b/src/function.zig index 5b0e0c1..3aafbb9 100644 --- a/src/function.zig +++ b/src/function.zig @@ -4,10 +4,12 @@ const abi = @import("abi.zig"); const c = @import("root.zig").c; const Ctx = @import("ctx.zig").Ctx; const GuardCtx = @import("ctx.zig").GuardCtx; +const CallFrame = @import("ctx.zig").CallFrame; const errors = @import("errors.zig"); const guard = @import("guard.zig"); const stub = @import("stub.zig"); const zend = @import("zend.zig"); +const Zval = @import("zval.zig").Zval; pub const Handler = fn (?*c.zend_execute_data, ?*c.zval) callconv(abi.fn_cc) void; @@ -159,12 +161,14 @@ pub fn functions(comptime T: type, comptime options: struct { namespace: []const /// does not report other stub functions that are missing Zig bindings. Prefer /// `functions` when all extension functions can be bound together. /// -/// Supported function signatures: -/// - `fn (Ctx) void|!void` - Access parameters and set return value via ctx -/// - `fn (GuardCtx) void|!void` - Also enable native resource cleanup on bailout -/// - `fn () void|!void` - No parameters or return value +/// The optional Ctx or GuardCtx must be first. Other parameters are extracted +/// through CallFrame.expectArgs. Non-void results are written to PHP's result zval; +/// void/!void leaves it unchanged, allowing the function to set it manually. +/// Use a context-only signature for expectArgs specifications that a Zig type +/// cannot express. /// -/// When the function takes no parameters, PHP will reject calls with extra arguments. +/// When the function takes neither a context nor PHP parameters, extra arguments +/// are rejected. A context-only handler retains control of its own parsing. /// /// Parameters: /// - func_name: The name of the PHP function (null-terminated string) @@ -176,6 +180,10 @@ pub fn functions(comptime T: type, comptime options: struct { namespace: []const /// _ = phpz.printf("Hello from ZIG!\n", .{}); /// } /// +/// fn twice(value: u8) i64 { +/// return @as(i64, value) * 2; +/// } +/// /// fn add(ctx: Ctx) !void { /// const args = try ctx.call.expectArgs(&.{ /// .{ .int = .{} }, @@ -222,31 +230,199 @@ pub fn method(comptime class_name: [:0]const u8, comptime func_name: [:0]const u } } -/// Create a PHP handler from fn(), fn(Ctx), or fn(GuardCtx), returning void or !void. -/// GuardCtx enables bailout capture and resource cleanup. -pub fn createHandler(comptime func_desc: [:0]const u8, comptime invoke: anytype) Handler { - const params = @typeInfo(@TypeOf(invoke)).@"fn".param_types; - if (params.len > 1 or (params.len == 1 and params[0] != Ctx and params[0] != GuardCtx)) { - @compileError("unsupported handler signature for " ++ func_desc ++ ": expected fn(), fn(Ctx), or fn(GuardCtx)"); +fn NullablePayloadType(comptime T: type) ?type { + if (@typeInfo(T) != .@"union" or !@hasField(T, "value")) return null; + const Payload = @FieldType(T, "value"); + return if (T == CallFrame.Nullable(Payload)) Payload else null; +} + +fn mixedKinds(comptime T: type) ?[]const CallFrame.ExpectArgKind { + comptime { + if (@typeInfo(T) != .@"union") return null; + const info = @typeInfo(T).@"union"; + if (info.tag_type == null or info.field_names.len < 2) return null; + var kinds: [info.field_names.len]CallFrame.ExpectArgKind = undefined; + for (info.field_names, 0..) |name, i| { + if (!@hasField(CallFrame.ExpectArgKind, name)) return null; + const kind = @field(CallFrame.ExpectArgKind, name); + if (kind == .mixed or kind == .reference or kind == .callable) return null; + kinds[i] = kind; + } + const result = kinds; + return if (T == CallFrame.Mixed(&result)) &result else null; + } +} + +fn specFor(comptime T: type) CallFrame.ExpectArgKind.Spec { + const optional = @typeInfo(T) == .optional; + const Inner = if (optional) @typeInfo(T).optional.child else T; + const nullable = NullablePayloadType(Inner) != null; + const Base = NullablePayloadType(Inner) orelse Inner; + const Spec = CallFrame.ExpectArgKind.Spec; + + inline for (.{ i8, i16, i32, i64, isize, u8, u16, u32, u64, usize }) |Int| { + if (Base == Int) { + const As = @TypeOf((@as(Spec, .{ .int = .{} })).int.as); + return .{ .int = .{ .as = @field(As, @typeName(Int)), .optional = optional, .nullable = nullable } }; + } + } + if (Base == f32 or Base == f64) { + return .{ .float = .{ .as = if (Base == f32) .f32 else .f64, .optional = optional, .nullable = nullable } }; + } + if (Base == []const u8) return .{ .string = .{ .as = .string, .optional = optional, .nullable = nullable } }; + if (Base == *zend.String) return .{ .string = .{ .as = .str, .optional = optional, .nullable = nullable } }; + if (Base == bool) return .{ .bool = .{ .optional = optional, .nullable = nullable } }; + if (Base == *zend.Array) return .{ .array = .{ .optional = optional, .nullable = nullable } }; + if (Base == *zend.Object) return .{ .object = .{ .optional = optional, .nullable = nullable } }; + if (Base == *zend.Resource) return .{ .resource = .{ .optional = optional, .nullable = nullable } }; + if (Base == *zend.Reference and !nullable) return .{ .reference = .{ .optional = optional } }; + if (Base == *Zval and !nullable) return .{ .mixed = .{ .optional = optional } }; + if (Base == *zend.Callable) return .{ .callable = .{ .optional = optional, .nullable = nullable, .resolve = true } }; + if (!nullable) if (mixedKinds(Base)) |kinds| return .{ .mixed = .{ .optional = optional, .one_of = kinds } }; + @compileError("unsupported PHP handler parameter type " ++ @typeName(T) ++ "; use a leading Ctx or GuardCtx and call expectArgs for complex specifications"); +} + +fn makeCallableArg(comptime T: type, parsed: anytype, resolved: *zend.Callable) T { + if (@typeInfo(T) == .optional) { + return if (parsed) |present| makeCallableArg(@typeInfo(T).optional.child, present, resolved) else null; + } + if (NullablePayloadType(T) != null) { + return switch (parsed) { + .null => .null, + .value => .{ .value = resolved }, + }; + } + return resolved; +} + +pub fn ContextType(comptime func: anytype, comptime has_receiver: bool) type { + const params = @typeInfo(@TypeOf(func)).@"fn".param_types; + const index: usize = if (has_receiver) 1 else 0; + if (params.len > index and (params[index] == Ctx or params[index] == GuardCtx)) return params[index].?; + return Ctx; +} + +fn ResultType(comptime func: anytype) type { + const R = @typeInfo(@TypeOf(func)).@"fn".return_type.?; + return if (@typeInfo(R) == .error_union) @typeInfo(R).error_union.payload else R; +} + +pub fn invoke(comptime func: anytype, ctx: anytype, receiver: anytype) anyerror!ResultType(func) { + const param_types = @typeInfo(@TypeOf(func)).@"fn".param_types; + const has_receiver = @TypeOf(receiver) != void; + const context_idx: usize = if (has_receiver) 1 else 0; + const has_context = param_types.len > context_idx and (param_types[context_idx] == Ctx or param_types[context_idx] == GuardCtx); + const php_params_offset = context_idx + @intFromBool(has_context); + const php_params_count = param_types.len - php_params_offset; + comptime { + if (has_context and param_types[context_idx] != @TypeOf(ctx)) { + @compileError("handler context type does not match its binding"); + } + for (param_types[php_params_offset..]) |ParamType| { + if (ParamType == Ctx or ParamType == GuardCtx) @compileError("Ctx or GuardCtx must precede all PHP parameters"); + } + } + + var params: std.meta.ArgsTuple(@TypeOf(func)) = undefined; + if (comptime has_receiver) params[0] = receiver; + if (comptime has_context) params[context_idx] = ctx; + comptime var callable_count = 0; + const specs = comptime blk: { + var arg_specs: [php_params_count]CallFrame.ExpectArgKind.Spec = undefined; + for (0..php_params_count) |i| { + arg_specs[i] = specFor(param_types[php_params_offset + i].?); + if (arg_specs[i] == .callable) callable_count += 1; + } + break :blk arg_specs; + }; + var resolved_callables: [callable_count]zend.Callable = @splat(.nil); + if (comptime php_params_count == 0) { + if (comptime !has_context) try ctx.call.expectNoArgs(); + } else { + var runtime: CallFrame.ExpectArgsRuntime(&specs) = undefined; + if (comptime @TypeOf(runtime) != void) { + comptime var callable_idx = 0; + inline for (specs, 0..) |spec, i| { + if (comptime spec == .callable) { + runtime[i] = .{ .out = &resolved_callables[callable_idx] }; + callable_idx += 1; + } else runtime[i] = {}; + } + } else runtime = {}; + const values = try ctx.call.expectArgs(&specs, runtime); + inline for (specs, 0..) |spec, i| { + const ParamType = param_types[php_params_offset + i].?; + params[php_params_offset + i] = if (comptime spec == .callable) + makeCallableArg(ParamType, values[i], runtime[i].out) + else + values[i]; + } + } + return @call(.auto, func, params); +} + +pub fn setReturnValue(retval: *Zval, value: anytype) anyerror!void { + const T = @TypeOf(value); + if (T == void) return; + if (@typeInfo(T) == .optional) { + if (value) |present| return setReturnValue(retval, present); + retval.set(.null, {}); + return; } - const Context = if (params.len == 1) params[0].? else Ctx; + if (comptime mixedKinds(T) != null) { + switch (value) { + inline else => |payload| if (@TypeOf(payload) == void) + retval.set(.null, {}) + else + try setReturnValue(retval, payload), + } + return; + } + if (NullablePayloadType(T) != null) { + switch (value) { + .null => retval.set(.null, {}), + .value => |present| try setReturnValue(retval, present), + } + return; + } + if (T == bool) return retval.set(.bool, value); + inline for (.{ i8, i16, i32, i64, isize, u8, u16, u32, u64, usize }) |Int| { + if (T == Int) { + const number = std.math.cast(c.zend_long, value) orelse return error.ReturnValueOutOfRange; + return retval.set(.int, @intCast(number)); + } + } + if (T == f32 or T == f64) return retval.set(.float, @floatCast(value)); + if (T == []u8 or T == []const u8) return retval.set(.string, value); + if (T == *zend.String) return retval.set(.str, value); + if (T == *zend.Array) return retval.set(.array, value); + if (T == *zend.Object) return retval.set(.object, value); + if (T == *zend.Resource) return retval.set(.resource, value); + if (T == *zend.Reference) return retval.set(.reference, value); + if (T == *Zval) return retval.set(.mixed, value.ptr()); + @compileError("unsupported PHP handler return type " ++ @typeName(T)); +} + +pub fn createHandler(comptime func_desc: [:0]const u8, comptime func: anytype) Handler { + const Context = ContextType(func, false); return struct { + fn call(ctx: Context) anyerror!void { + const value = try invoke(func, ctx, {}); + try setReturnValue(ctx.retval, value); + } + fn handle(execute_data: ?*c.zend_execute_data, return_value: ?*c.zval) callconv(abi.fn_cc) void { @as( anyerror!void, if (comptime Context == GuardCtx) blk: { var scope: ?*guard.Scope = null; defer if (scope) |owned| guard.ScopeObject.release(owned); - break :blk zend.bailout.run(invoke, .{GuardCtx{ + break :blk zend.bailout.run(call, .{GuardCtx{ .call = .from(execute_data.?), .retval = .from(return_value.?), .guard_scope = &scope, }}); - } else if (comptime params.len == 0) blk: { - const ctx: Ctx = .{ .call = .from(execute_data.?), .retval = .from(return_value.?) }; - ctx.call.expectNoArgs() catch |err| break :blk err; - break :blk invoke(); - } else invoke(.{ .call = .from(execute_data.?), .retval = .from(return_value.?) }), + } else call(.{ .call = .from(execute_data.?), .retval = .from(return_value.?) }), ) catch |err| { if (err == error.ZendBailout or err == error.OutOfMemory) zend.bailout.raise(); if (!errors.hasException()) { diff --git a/src/root.zig b/src/root.zig index 1453b6f..eba63ca 100644 --- a/src/root.zig +++ b/src/root.zig @@ -26,13 +26,14 @@ pub const Operator = class_helper.Operator; pub const Comparison = class_helper.Comparison; pub const GcBuffer = @import("gc.zig").GcBuffer; -const function_helper = @import("function.zig"); -pub const function = function_helper.function; -pub const functions = function_helper.functions; -pub const namedFunctions = function_helper.namedFunctions; +pub const function = @import("function.zig").function; +pub const functions = @import("function.zig").functions; +pub const namedFunctions = @import("function.zig").namedFunctions; pub const Ctx = @import("ctx.zig").Ctx; pub const CallFrame = @import("ctx.zig").CallFrame; +pub const Nullable = CallFrame.Nullable; +pub const Mixed = CallFrame.Mixed; pub const GuardCtx = @import("ctx.zig").GuardCtx; pub const Zval = @import("zval.zig").Zval; pub const Guard = @import("guard.zig").Guard; diff --git a/tools/phpz_skel.php b/tools/phpz_skel.php index 47077ff..acc43fb 100755 --- a/tools/phpz_skel.php +++ b/tools/phpz_skel.php @@ -1317,15 +1317,10 @@ class_entry = zend_register_internal_class_ex(&ce, NULL); } /// function greet(string $name): string - pub fn greet(ctx: phpz.Ctx) !void { - const args = try ctx.call.expectArgs(&.{ - .{ .string = .{} }, - }, {}); - const name = args[0]; - + pub fn greet(name: []const u8) !*phpz.zend.String { var buffer: [256]u8 = undefined; const result = try std.fmt.bufPrint(&buffer, "Hello, {s}!", .{name}); - ctx.ret(.string, result); + return .init(result, false); } }; @@ -1336,32 +1331,23 @@ class_entry = zend_register_internal_class_ex(&ce, NULL); n: i64 = 0, /// public function __construct(int $n = 0): void - pub fn __construct(ctx: phpz.Ctx) !Counter { - const args = try ctx.call.expectArgs(&.{ - .{ .int = .{ .optional = true } }, - }, {}); - return .{ .n = args[0] orelse 0 }; + pub fn __construct(n: ?i64) Counter { + return .{ .n = n 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]; + pub fn add(self: *Counter, n: i64) void { + self.n +|= n; } /// 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]; + pub fn dec(self: *Counter, n: i64) void { + self.n -|= n; } /// public function value(): int - pub fn value(self: *const Counter, ctx: phpz.Ctx) void { - ctx.ret(.int, self.n); + pub fn value(self: *const Counter) i64 { + return self.n; } }; ZIG,