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,