Skip to content

Commit

Permalink
Add Python GC support (#244)
Browse files Browse the repository at this point in the history
  • Loading branch information
robert3005 authored Nov 9, 2023
1 parent 3db1d66 commit 50171d7
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 8 deletions.
7 changes: 7 additions & 0 deletions example/classes.zig
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ pub const User = py.class(struct {
e: ?py.PyString = null,

pub fn get(prop: *const Prop) ?py.PyString {
if (prop.e) |e| e.incref();
return prop.e;
}

Expand All @@ -87,6 +88,7 @@ pub const User = py.class(struct {
if (std.mem.indexOfScalar(u8, try value.asSlice(), '@') == null) {
return py.ValueError.raiseFmt("Invalid email address for {s}", .{try self.name.asSlice()});
}
value.incref();
prop.e = value;
}
}),
Expand All @@ -96,6 +98,11 @@ pub const User = py.class(struct {
return py.PyString.createFmt("Hello, {s}!", .{try self.name.asSlice()});
}
}) = .{},

pub fn __del__(self: *Self) void {
self.name.decref();
if (self.email.e) |e| e.decref();
}
});
// --8<-- [end:properties]

Expand Down
202 changes: 199 additions & 3 deletions pydust/src/pytypes.zig
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,17 @@ pub fn Type(comptime name: [:0]const u8, comptime definition: type) type {
};

const bases = Bases(definition);
const attrs = Attributes(definition);
const slots = Slots(definition, name);

const flags = blk: {
var flags_: usize = ffi.Py_TPFLAGS_DEFAULT | ffi.Py_TPFLAGS_BASETYPE;
if (slots.gc.needsGc) {
flags_ |= ffi.Py_TPFLAGS_HAVE_GC;
}

break :blk flags_;
};

pub fn init(module: py.PyModule) PyError!py.PyObject {
var basesPtr: ?*ffi.PyObject = null;
if (bases.bases.len > 0) {
Expand All @@ -61,7 +69,7 @@ pub fn Type(comptime name: [:0]const u8, comptime definition: type) type {
.name = qualifiedName.ptr,
.basicsize = @sizeOf(PyTypeStruct(definition)),
.itemsize = 0,
.flags = ffi.Py_TPFLAGS_DEFAULT | ffi.Py_TPFLAGS_BASETYPE,
.flags = flags,
.slots = @constCast(slots.slots.ptr),
};

Expand Down Expand Up @@ -105,11 +113,22 @@ fn Slots(comptime definition: type, comptime name: [:0]const u8) type {
const properties = Properties(definition);
const doc = Doc(definition, name);
const richcmp = RichCompare(definition);
const gc = GC(definition);

/// Slots populated in the PyType
pub const slots: [:empty]const ffi.PyType_Slot = blk: {
var slots_: [:empty]const ffi.PyType_Slot = &.{};

if (gc.needsGc) {
slots_ = slots_ ++ .{ ffi.PyType_Slot{
.slot = ffi.Py_tp_clear,
.pfunc = @constCast(&gc.tp_clear),
}, ffi.PyType_Slot{
.slot = ffi.Py_tp_traverse,
.pfunc = @constCast(&gc.tp_traverse),
} };
}

if (doc.docLen != 0) {
slots_ = slots_ ++ .{ffi.PyType_Slot{
.slot = ffi.Py_tp_doc,
Expand Down Expand Up @@ -305,7 +324,7 @@ fn Slots(comptime definition: type, comptime name: [:0]const u8) type {
/// Note: tp_del is deprecated in favour of tp_finalize.
///
/// See https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_finalize.
fn tp_finalize(pyself: *ffi.PyObject) void {
fn tp_finalize(pyself: *ffi.PyObject) callconv(.C) void {
// The finalize slot shouldn't alter any exception that is currently set.
// So it's recommended we save the existing one (if any) and restore it afterwards.
// NOTE(ngates): we may want to move this logic to PyErr if it happens more?
Expand Down Expand Up @@ -455,6 +474,183 @@ fn Doc(comptime definition: type, comptime name: [:0]const u8) type {
};
}

fn GC(comptime definition: type) type {
const VisitProc = *const fn (*ffi.PyObject, *anyopaque) callconv(.C) c_int;

return struct {
const needsGc = classNeedsGc(definition);

fn classNeedsGc(comptime CT: type) bool {
inline for (@typeInfo(CT).Struct.fields) |field| {
if (typeNeedsGc(field.type)) {
return true;
}
}
return false;
}

fn typeNeedsGc(comptime FT: type) bool {
return switch (@typeInfo(FT)) {
.Pointer => |p| @typeInfo(p.child) == .Struct and (p.child == ffi.PyObject or typeNeedsGc(p.child)),
.Struct => blk: {
if (State.findDefinition(FT)) |def| {
break :blk switch (def.type) {
.attribute => typeNeedsGc(@typeInfo(FT).Struct.fields[0].type),
.property => classNeedsGc(FT),
.class, .module => false,
};
} else {
break :blk @hasField(FT, "obj") and @hasField(std.meta.fieldInfo(FT, .obj).type, "py") or FT == py.PyObject;
}
},
.Optional => |o| (@typeInfo(o.child) == .Struct or @typeInfo(o.child) == .Pointer) and typeNeedsGc(o.child),
else => return false,
};
}

fn tp_clear(pyself: *ffi.PyObject) callconv(.C) c_int {
var self: *PyTypeStruct(definition) = @ptrCast(pyself);
clearFields(self.state);
return 0;
}

fn clearFields(class: anytype) void {
inline for (@typeInfo(@TypeOf(class)).Struct.fields) |field| {
clear(@field(class, field.name));
}
}

fn clear(obj: anytype) void {
const fieldType = @TypeOf(obj);
switch (@typeInfo(fieldType)) {
.Pointer => |p| if (@typeInfo(p.child) == .Struct) {
if (p.child == ffi.PyObject) {
pyClear(obj);
}
if (State.findDefinition(fieldType)) |def| {
if (def.type == .class) {
pyClear(py.object(obj).py);
}
}
},
.Struct => {
if (State.findDefinition(fieldType)) |def| {
switch (def.type) {
.attribute => clear(@field(obj, @typeInfo(fieldType).Struct.fields[0].name)),
.property => clearFields(obj),
.class, .module => {},
}
} else {
if (@hasField(fieldType, "obj") and @hasField(std.meta.fieldInfo(fieldType, .obj).type, "py")) {
pyClear(obj.obj.py);
}

if (fieldType == py.PyObject) {
pyClear(obj.py);
}
}
},
.Optional => |o| if (@typeInfo(o.child) == .Struct or @typeInfo(o.child) == .Pointer) {
if (obj == null) {
return;
}

clear(obj.?);
},
else => {},
}
}

inline fn pyClear(obj: *ffi.PyObject) void {
var objRef = @constCast(&obj);
const objOld = objRef.*;
objRef.* = undefined;
py.decref(objOld);
}

/// Visit all members of pyself. We visit all PyObjects that this object references
fn tp_traverse(pyself: *ffi.PyObject, visit: VisitProc, arg: *anyopaque) callconv(.C) c_int {
if (pyVisit(py.type_(pyself).obj.py, visit, arg)) |ret| {
return ret;
}

const self: *const PyTypeStruct(definition) = @ptrCast(pyself);
if (traverseFields(self.state, visit, arg)) |ret| {
return ret;
}
return 0;
}

fn traverseFields(class: anytype, visit: VisitProc, arg: *anyopaque) ?c_int {
inline for (@typeInfo(@TypeOf(class)).Struct.fields) |field| {
if (traverse(@field(class, field.name), visit, arg)) |ret| {
return ret;
}
}
return null;
}

fn traverse(obj: anytype, visit: VisitProc, arg: *anyopaque) ?c_int {
const fieldType = @TypeOf(obj);
switch (@typeInfo(@TypeOf(obj))) {
.Pointer => |p| if (@typeInfo(p.child) == .Struct) {
if (p.child == ffi.PyObject) {
if (pyVisit(obj, visit, arg)) |ret| {
return ret;
}
}
if (State.findDefinition(fieldType)) |def| {
if (def.type == .class) {
if (pyVisit(py.object(obj).py, visit, arg)) |ret| {
return ret;
}
}
}
},
.Struct => if (State.findDefinition(fieldType)) |def| {
switch (def.type) {
.attribute => if (traverse(@field(obj, @typeInfo(@TypeOf(obj)).Struct.fields[0].name), visit, arg)) |ret| {
return ret;
},
.property => if (traverseFields(obj, visit, arg)) |ret| {
return ret;
},
.class, .module => {},
}
} else {
if (@hasField(fieldType, "obj") and @hasField(std.meta.fieldInfo(fieldType, .obj).type, "py")) {
if (pyVisit(obj.obj.py, visit, arg)) |ret| {
return ret;
}
}

if (fieldType == py.PyObject) {
if (pyVisit(obj.py, visit, arg)) |ret| {
return ret;
}
}
},
.Optional => |o| if (@typeInfo(o.child) == .Struct or @typeInfo(o.child) == .Pointer) {
if (obj == null) {
return null;
}

if (traverse(obj.?, visit, arg)) |ret| {
return ret;
}
},
else => return null,
}
return null;
}

inline fn pyVisit(obj: *ffi.PyObject, visit: VisitProc, arg: *anyopaque) ?c_int {
const ret = visit(obj, arg);
return if (ret != 0) ret else null;
}
};
}

fn Members(comptime definition: type) type {
return struct {
const count = State.countFieldsWithType(definition, .attribute);
Expand Down
1 change: 1 addition & 0 deletions pydust/src/trampoline.zig
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ pub fn Trampoline(comptime T: type) type {
return obj;
}
},
.Optional => |o| return if (obj) |objP| Trampoline(o.child).asObject(objP) else std.debug.panic("Can't convert null to an object", .{}),
inline else => {},
}
@compileError("Cannot convert into PyObject: " ++ @typeName(T));
Expand Down
18 changes: 13 additions & 5 deletions pydust/src/types/type.zig
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,29 @@ pub const PyType = extern struct {

pub fn name(self: PyType) !py.PyString {
return py.PyString.unchecked(.{
.py = ffi.PyType_GetName(typePtr(self.obj.py)) orelse return PyError.PyRaised,
.py = ffi.PyType_GetName(typePtr(self)) orelse return PyError.PyRaised,
});
}

pub fn qualifiedName(self: PyType) !py.PyString {
return py.PyString.unchecked(.{
.py = ffi.PyType_GetQualName(typePtr(self.obj.py)) orelse return PyError.PyRaised,
.py = ffi.PyType_GetQualName(typePtr(self)) orelse return PyError.PyRaised,
});
}

fn typePtr(obj: *ffi.PyObject) *ffi.PyTypeObject {
return @alignCast(@ptrCast(obj));
pub fn getSlot(self: PyType, slot: c_int) ?*anyopaque {
return ffi.PyType_GetSlot(typePtr(self), slot);
}

pub fn hasFeature(self: PyType, feature: u64) bool {
return ffi.PyType_GetFlags(typePtr(self)) & feature != 0;
}

inline fn typePtr(self: PyType) *ffi.PyTypeObject {
return @alignCast(@ptrCast(self.obj.py));
}

fn objPtr(obj: *ffi.PyTypeObject) *ffi.PyObject {
inline fn objPtr(obj: *ffi.PyTypeObject) *ffi.PyObject {
return @alignCast(@ptrCast(obj));
}
};
Expand Down

0 comments on commit 50171d7

Please sign in to comment.