diff --git a/include/zphp_extension.h b/include/zphp_extension.h index 574910aa..177cfb62 100644 --- a/include/zphp_extension.h +++ b/include/zphp_extension.h @@ -63,6 +63,13 @@ enum { ZPHP_METHOD_STATIC = 1 }; +/* class modifiers for zphp_class_set_flags; final and abstract exclude each other */ +enum { + ZPHP_CLASS_FINAL = 1, + ZPHP_CLASS_ABSTRACT = 2, + ZPHP_CLASS_READONLY = 4 +}; + /* * Lifecycle. module_init runs once per process when the extension loads and * is the only place registration is allowed. worker_init/worker_shutdown run @@ -170,6 +177,9 @@ typedef struct zphp_api { void (*set_request_data)(zphp_ctx *ctx, void *data); void *(*worker_data)(zphp_ctx *ctx); void (*set_worker_data)(zphp_ctx *ctx, void *data); + + /* appended after abi 1 shipped; older runtimes end the table above */ + int (*class_set_flags)(zphp_class *cls, uint32_t flags); } zphp_api; typedef const zphp_extension *(*zphp_entry_fn)(const zphp_api *api); @@ -208,6 +218,7 @@ static inline int zphp_class_add_property(zphp_class *cls, const char *name) { r static inline int zphp_class_add_property_int(zphp_class *cls, const char *name, int64_t value) { return zphp_api_v1->class_add_property_int(cls, name, value); } static inline int zphp_class_add_property_string(zphp_class *cls, const char *name, const char *value) { return zphp_api_v1->class_add_property_string(cls, name, value); } static inline int zphp_class_implements(zphp_class *cls, const char *interface_name) { return zphp_api_v1->class_implements(cls, interface_name); } +static inline int zphp_class_set_flags(zphp_class *cls, uint32_t flags) { return zphp_api_v1->class_set_flags(cls, flags); } static inline int zphp_register_interface(zphp_module *m, const char *name, const char *const *methods, size_t method_count) { return zphp_api_v1->register_interface(m, name, methods, method_count); } static inline int zphp_register_constant_int(zphp_module *m, const char *name, int64_t value) { return zphp_api_v1->register_constant_int(m, name, value); } static inline int zphp_register_constant_float(zphp_module *m, const char *name, double value) { return zphp_api_v1->register_constant_float(m, name, value); } diff --git a/src/extension.zig b/src/extension.zig index 7c18c56e..c89717d8 100644 --- a/src/extension.zig +++ b/src/extension.zig @@ -57,8 +57,13 @@ pub const ClassReg = struct { constants: std.ArrayListUnmanaged(ConstantReg) = .{}, properties: std.ArrayListUnmanaged(PropertyReg) = .{}, resource_type: ?u32 = null, + flags: u32 = 0, }; +const class_flag_final: u32 = 1; +const class_flag_abstract: u32 = 2; +const class_flag_readonly: u32 = 4; + const InterfaceReg = struct { name: []const u8, methods: std.ArrayListUnmanaged([]const u8) = .{} }; // the zphp_module handle: one loaded extension and everything it registered @@ -269,9 +274,12 @@ fn registerClass(vm: *VM, cls: *ClassReg) RuntimeError!void { const a = vm.allocator; if (vm.classes.contains(cls.name) or vm.interfaces.contains(cls.name)) fail("extension '{s}': class '{s}' is already defined", .{ cls.ext.name, cls.name }); var def = ClassDef{ .name = cls.name, .parent = cls.parent }; + def.is_final = cls.flags & class_flag_final != 0; + def.is_abstract = cls.flags & class_flag_abstract != 0; + def.is_readonly = cls.flags & class_flag_readonly != 0; if (cls.resource_type != null) def.native_cleanup = resourceCleanup; for (cls.interfaces.items) |iface| try def.interfaces.append(a, iface); - for (cls.properties.items) |p| try def.properties.append(a, .{ .name = p.name, .default = p.default, .has_default = true }); + for (cls.properties.items) |p| try def.properties.append(a, .{ .name = p.name, .default = p.default, .has_default = true, .is_readonly = def.is_readonly }); for (cls.constants.items) |c| { try def.static_props.put(a, c.name, c.value); try def.constant_names.put(a, c.name, {}); @@ -478,6 +486,15 @@ fn apiClassAddPropertyString(cls: ?*ClassReg, name: CStr, value: CStr) callconv( return classProperty(cls, name, .{ .string = Value.String.borrowed(dupe(v)) }); } +fn apiClassSetFlags(cls: ?*ClassReg, flags: u32) callconv(.c) c_int { + const c = cls orelse return -1; + if (!regOnly(c.ext)) return -1; + if (flags & ~(class_flag_final | class_flag_abstract | class_flag_readonly) != 0) return -1; + if (flags & class_flag_final != 0 and flags & class_flag_abstract != 0) return -1; + c.flags = flags; + return 0; +} + fn apiClassImplements(cls: ?*ClassReg, iface: CStr) callconv(.c) c_int { const c = cls orelse return -1; if (!regOnly(c.ext)) return -1; @@ -922,6 +939,7 @@ pub const Api = extern struct { set_request_data: *const @TypeOf(apiSetRequestData), worker_data: *const @TypeOf(apiWorkerData), set_worker_data: *const @TypeOf(apiSetWorkerData), + class_set_flags: *const @TypeOf(apiClassSetFlags), }; pub const api_v1 = Api{ @@ -987,6 +1005,7 @@ pub const api_v1 = Api{ .set_request_data = apiSetRequestData, .worker_data = apiWorkerData, .set_worker_data = apiSetWorkerData, + .class_set_flags = apiClassSetFlags, }; test "the api table matches the header field by field" { diff --git a/tests/extensions/demo.c b/tests/extensions/demo.c index bcde5fff..98739d6f 100644 --- a/tests/extensions/demo.c +++ b/tests/extensions/demo.c @@ -284,6 +284,11 @@ static int module_init(zphp_module *m) zphp_class_add_method(counter, "make", counter_make, 1, ZPHP_METHOD_STATIC); zphp_register_class(m, "DemoException", "Exception"); + zphp_class_set_flags(zphp_register_class(m, "DemoSealed", NULL), ZPHP_CLASS_FINAL); + zphp_class_set_flags(zphp_register_class(m, "DemoShape", NULL), ZPHP_CLASS_ABSTRACT); + zphp_class *point = zphp_register_class(m, "DemoPoint", NULL); + zphp_class_add_property_int(point, "x", 1); + zphp_class_set_flags(point, ZPHP_CLASS_READONLY); zphp_register_constant_string(m, "DEMO_VERSION", "1.2.3"); zphp_register_constant_int(m, "DEMO_ANSWER", 42); diff --git a/tests/extensions/demo.expected b/tests/extensions/demo.expected index 01bd0918..59cb12c8 100644 --- a/tests/extensions/demo.expected +++ b/tests/extensions/demo.expected @@ -81,5 +81,11 @@ int(4) string(4) "stil" Trying to clone an uncloneable object of class DemoBuffer int(4) +bool(true) +bool(true) +bool(true) +Cannot instantiate abstract class DemoShape +int(1) +Cannot modify readonly property DemoPoint::$x done shutdown sees 2 diff --git a/tests/extensions/demo.php b/tests/extensions/demo.php index fe74cd61..655a8708 100644 --- a/tests/extensions/demo.php +++ b/tests/extensions/demo.php @@ -46,6 +46,10 @@ function throws() { $x = demo_open(4); throw new RuntimeException("mid"); } try { $copy = clone $probe; } catch (Error $e) { echo $e->getMessage(), "\n"; } unset($probe); var_dump(demo_freed()); +$rc = fn(string $c) => new ReflectionClass($c); +var_dump($rc("DemoSealed")->isFinal(), $rc("DemoShape")->isAbstract(), $rc("DemoPoint")->isReadOnly()); +try { new DemoShape(); } catch (Error $e) { echo $e->getMessage(), "\n"; } +try { $pt = new DemoPoint(); var_dump($pt->x); $pt->x = 5; } catch (Error $e) { echo $e->getMessage(), "\n"; } $keep = demo_open(2); register_shutdown_function(function () { echo "shutdown sees ", demo_add(1, 1), "\n"; }); echo "done\n"; diff --git a/tests/extensions/final.php b/tests/extensions/final.php new file mode 100644 index 00000000..437768de --- /dev/null +++ b/tests/extensions/final.php @@ -0,0 +1,2 @@ +&1)" || true check "demo.php output" "$(cat "$SCRIPT_DIR/demo.expected")" "$actual" +contains "final extension class cannot be extended" "Class DemoSub cannot extend final class DemoSealed" "$("$ZPHP" --extension="$DEMO" run "$SCRIPT_DIR/final.php" 2>&1)" actual="$("$ZPHP" --extension "$DEMO" run "$SCRIPT_DIR/demo.php" 2>&1 | head -1)" || true check "--extension PATH form" 'bool(true)' "$actual" actual="$(ZPHP_EXTENSION_DIR="$OUT" "$ZPHP" run "$SCRIPT_DIR/demo.php" 2>&1 | tail -1)" || true