Skip to content

Commit 4e42f71

Browse files
committed
Add Python GC support
1 parent 79d5341 commit 4e42f71

File tree

5 files changed

+315
-9
lines changed

5 files changed

+315
-9
lines changed

example/classes.zig

+4
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ pub const User = py.class(struct {
7979
e: ?py.PyString = null,
8080

8181
pub fn get(prop: *const Prop) ?py.PyString {
82+
if (prop.e) |e| {
83+
e.incref();
84+
}
8285
return prop.e;
8386
}
8487

@@ -87,6 +90,7 @@ pub const User = py.class(struct {
8790
if (std.mem.indexOfScalar(u8, try value.asSlice(), '@') == null) {
8891
return py.ValueError.raiseFmt("Invalid email address for {s}", .{try self.name.asSlice()});
8992
}
93+
value.incref();
9094
prop.e = value;
9195
}
9296
}),

pydust/src/pytypes.zig

+220-4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const funcs = @import("functions.zig");
2222
const PyError = @import("errors.zig").PyError;
2323
const PyMemAllocator = @import("mem.zig").PyMemAllocator;
2424
const tramp = @import("trampoline.zig");
25+
const unlimited = @import("unlimited_api.zig");
2526

2627
/// For a given Pydust class definition, return the encapsulating PyType struct.
2728
pub fn PyTypeStruct(comptime definition: type) type {
@@ -42,9 +43,17 @@ pub fn Type(comptime name: [:0]const u8, comptime definition: type) type {
4243
};
4344

4445
const bases = Bases(definition);
45-
const attrs = Attributes(definition);
4646
const slots = Slots(definition, name);
4747

48+
const flags = blk: {
49+
var flags_: usize = ffi.Py_TPFLAGS_DEFAULT | ffi.Py_TPFLAGS_BASETYPE;
50+
if (slots.gc.needsGc) {
51+
flags_ |= ffi.Py_TPFLAGS_HAVE_GC;
52+
}
53+
54+
break :blk flags_;
55+
};
56+
4857
pub fn init(module: py.PyModule) PyError!py.PyObject {
4958
var basesPtr: ?*ffi.PyObject = null;
5059
if (bases.bases.len > 0) {
@@ -61,7 +70,7 @@ pub fn Type(comptime name: [:0]const u8, comptime definition: type) type {
6170
.name = qualifiedName.ptr,
6271
.basicsize = @sizeOf(PyTypeStruct(definition)),
6372
.itemsize = 0,
64-
.flags = ffi.Py_TPFLAGS_DEFAULT | ffi.Py_TPFLAGS_BASETYPE,
73+
.flags = flags,
6574
.slots = @constCast(slots.slots.ptr),
6675
};
6776

@@ -105,10 +114,24 @@ fn Slots(comptime definition: type, comptime name: [:0]const u8) type {
105114
const properties = Properties(definition);
106115
const doc = Doc(definition, name);
107116
const richcmp = RichCompare(definition);
117+
const gc = GC(definition);
108118

109119
/// Slots populated in the PyType
110120
pub const slots: [:empty]const ffi.PyType_Slot = blk: {
111-
var slots_: [:empty]const ffi.PyType_Slot = &.{};
121+
var slots_: [:empty]const ffi.PyType_Slot = &.{ffi.PyType_Slot{
122+
.slot = ffi.Py_tp_dealloc,
123+
.pfunc = @constCast(&tp_dealloc),
124+
}};
125+
126+
if (gc.needsGc) {
127+
slots_ = slots_ ++ .{ ffi.PyType_Slot{
128+
.slot = ffi.Py_tp_clear,
129+
.pfunc = @constCast(&gc.tp_clear),
130+
}, ffi.PyType_Slot{
131+
.slot = ffi.Py_tp_traverse,
132+
.pfunc = @constCast(&gc.tp_traverse),
133+
} };
134+
}
112135

113136
if (doc.docLen != 0) {
114137
slots_ = slots_ ++ .{ffi.PyType_Slot{
@@ -305,7 +328,7 @@ fn Slots(comptime definition: type, comptime name: [:0]const u8) type {
305328
/// Note: tp_del is deprecated in favour of tp_finalize.
306329
///
307330
/// See https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_finalize.
308-
fn tp_finalize(pyself: *ffi.PyObject) void {
331+
fn tp_finalize(pyself: *ffi.PyObject) callconv(.C) void {
309332
// The finalize slot shouldn't alter any exception that is currently set.
310333
// So it's recommended we save the existing one (if any) and restore it afterwards.
311334
// NOTE(ngates): we may want to move this logic to PyErr if it happens more?
@@ -320,6 +343,22 @@ fn Slots(comptime definition: type, comptime name: [:0]const u8) type {
320343
ffi.PyErr_Restore(error_type, error_value, error_tb);
321344
}
322345

346+
/// Deallocte the type. Carefully handling cases where class implements `__del__`
347+
fn tp_dealloc(pyself: *ffi.PyObject) callconv(.C) void {
348+
const typeObj = py.type_(pyself);
349+
unlimited.finalizeFromDealloc(pyself);
350+
351+
if (gc.needsGc) {
352+
ffi.PyObject_GC_UnTrack(pyself);
353+
}
354+
355+
_ = gc.tp_clear(pyself);
356+
357+
const freeFn: *const fn (*anyopaque) void = @alignCast(@ptrCast(typeObj.getSlot(ffi.Py_tp_free).?));
358+
freeFn(pyself);
359+
typeObj.decref();
360+
}
361+
323362
fn bf_getbuffer(pyself: *ffi.PyObject, view: *ffi.Py_buffer, flags: c_int) callconv(.C) c_int {
324363
// In case of any error, the view.obj field must be set to NULL.
325364
view.obj = null;
@@ -455,6 +494,183 @@ fn Doc(comptime definition: type, comptime name: [:0]const u8) type {
455494
};
456495
}
457496

497+
fn GC(comptime definition: type) type {
498+
const VisitProc = *const fn (*ffi.PyObject, *anyopaque) callconv(.C) c_int;
499+
500+
return struct {
501+
const needsGc = classNeedsGc(definition);
502+
503+
fn classNeedsGc(comptime CT: type) bool {
504+
inline for (@typeInfo(CT).Struct.fields) |field| {
505+
if (typeNeedsGc(field.type)) {
506+
return true;
507+
}
508+
}
509+
return false;
510+
}
511+
512+
fn typeNeedsGc(comptime FT: type) bool {
513+
return switch (@typeInfo(FT)) {
514+
.Pointer => |p| @typeInfo(p.child) == .Struct and (p.child == ffi.PyObject or typeNeedsGc(p.child)),
515+
.Struct => blk: {
516+
if (State.findDefinition(FT)) |def| {
517+
break :blk switch (def.type) {
518+
.attribute => typeNeedsGc(@typeInfo(FT).Struct.fields[0].type),
519+
.property => classNeedsGc(FT),
520+
.class, .module => false,
521+
};
522+
} else {
523+
break :blk @hasField(FT, "obj") and @hasField(std.meta.fieldInfo(FT, .obj).type, "py") or FT == py.PyObject;
524+
}
525+
},
526+
.Optional => |o| (@typeInfo(o.child) == .Struct or @typeInfo(o.child) == .Pointer) and typeNeedsGc(o.child),
527+
else => return false,
528+
};
529+
}
530+
531+
fn tp_clear(pyself: *ffi.PyObject) callconv(.C) c_int {
532+
var self: *PyTypeStruct(definition) = @ptrCast(pyself);
533+
clearFields(self.state);
534+
return 0;
535+
}
536+
537+
fn clearFields(class: anytype) void {
538+
inline for (@typeInfo(@TypeOf(class)).Struct.fields) |field| {
539+
clear(@field(class, field.name));
540+
}
541+
}
542+
543+
fn clear(obj: anytype) void {
544+
const fieldType = @TypeOf(obj);
545+
switch (@typeInfo(fieldType)) {
546+
.Pointer => |p| if (@typeInfo(p.child) == .Struct) {
547+
if (p.child == ffi.PyObject) {
548+
pyClear(obj);
549+
}
550+
if (State.findDefinition(fieldType)) |def| {
551+
if (def.type == .class) {
552+
pyClear(py.object(obj).py);
553+
}
554+
}
555+
},
556+
.Struct => {
557+
if (State.findDefinition(fieldType)) |def| {
558+
switch (def.type) {
559+
.attribute => clear(@field(obj, @typeInfo(fieldType).Struct.fields[0].name)),
560+
.property => clearFields(obj),
561+
.class, .module => {},
562+
}
563+
} else {
564+
if (@hasField(fieldType, "obj") and @hasField(std.meta.fieldInfo(fieldType, .obj).type, "py")) {
565+
pyClear(obj.obj.py);
566+
}
567+
568+
if (fieldType == py.PyObject) {
569+
pyClear(obj.py);
570+
}
571+
}
572+
},
573+
.Optional => |o| if (@typeInfo(o.child) == .Struct or @typeInfo(o.child) == .Pointer) {
574+
if (obj == null) {
575+
return;
576+
}
577+
578+
clear(obj.?);
579+
},
580+
else => {},
581+
}
582+
}
583+
584+
inline fn pyClear(obj: *ffi.PyObject) void {
585+
var objRef = @constCast(&obj);
586+
const objOld = objRef.*;
587+
objRef.* = undefined;
588+
py.decref(objOld);
589+
}
590+
591+
/// Visit all members of pyself. We visit all PyObjects that this object references
592+
fn tp_traverse(pyself: *ffi.PyObject, visit: VisitProc, arg: *anyopaque) callconv(.C) c_int {
593+
if (pyVisit(py.type_(pyself).obj.py, visit, arg)) |ret| {
594+
return ret;
595+
}
596+
597+
const self: *const PyTypeStruct(definition) = @ptrCast(pyself);
598+
if (traverseFields(self.state, visit, arg)) |ret| {
599+
return ret;
600+
}
601+
return 0;
602+
}
603+
604+
fn traverseFields(class: anytype, visit: VisitProc, arg: *anyopaque) ?c_int {
605+
inline for (@typeInfo(@TypeOf(class)).Struct.fields) |field| {
606+
if (traverse(@field(class, field.name), visit, arg)) |ret| {
607+
return ret;
608+
}
609+
}
610+
return null;
611+
}
612+
613+
fn traverse(obj: anytype, visit: VisitProc, arg: *anyopaque) ?c_int {
614+
const fieldType = @TypeOf(obj);
615+
switch (@typeInfo(@TypeOf(obj))) {
616+
.Pointer => |p| if (@typeInfo(p.child) == .Struct) {
617+
if (p.child == ffi.PyObject) {
618+
if (pyVisit(obj, visit, arg)) |ret| {
619+
return ret;
620+
}
621+
}
622+
if (State.findDefinition(fieldType)) |def| {
623+
if (def.type == .class) {
624+
if (pyVisit(py.object(obj).py, visit, arg)) |ret| {
625+
return ret;
626+
}
627+
}
628+
}
629+
},
630+
.Struct => if (State.findDefinition(fieldType)) |def| {
631+
switch (def.type) {
632+
.attribute => if (traverse(@field(obj, @typeInfo(@TypeOf(obj)).Struct.fields[0].name), visit, arg)) |ret| {
633+
return ret;
634+
},
635+
.property => if (traverseFields(obj, visit, arg)) |ret| {
636+
return ret;
637+
},
638+
.class, .module => {},
639+
}
640+
} else {
641+
if (@hasField(fieldType, "obj") and @hasField(std.meta.fieldInfo(fieldType, .obj).type, "py")) {
642+
if (pyVisit(obj.obj.py, visit, arg)) |ret| {
643+
return ret;
644+
}
645+
}
646+
647+
if (fieldType == py.PyObject) {
648+
if (pyVisit(obj.py, visit, arg)) |ret| {
649+
return ret;
650+
}
651+
}
652+
},
653+
.Optional => |o| if (@typeInfo(o.child) == .Struct or @typeInfo(o.child) == .Pointer) {
654+
if (obj == null) {
655+
return null;
656+
}
657+
658+
if (traverse(obj.?, visit, arg)) |ret| {
659+
return ret;
660+
}
661+
},
662+
else => return null,
663+
}
664+
return null;
665+
}
666+
667+
inline fn pyVisit(obj: *ffi.PyObject, visit: VisitProc, arg: *anyopaque) ?c_int {
668+
const ret = visit(obj, arg);
669+
return if (ret != 0) ret else null;
670+
}
671+
};
672+
}
673+
458674
fn Members(comptime definition: type) type {
459675
return struct {
460676
const count = State.countFieldsWithType(definition, .attribute);

pydust/src/trampoline.zig

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ pub fn Trampoline(comptime T: type) type {
9090
return obj;
9191
}
9292
},
93+
.Optional => |o| return if (obj) |objP| Trampoline(o.child).asObject(objP) else std.debug.panic("Can't convert null to an object", .{}),
9394
inline else => {},
9495
}
9596
@compileError("Cannot convert into PyObject: " ++ @typeName(T));

pydust/src/types/type.zig

+13-5
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,29 @@ pub const PyType = extern struct {
2626

2727
pub fn name(self: PyType) !py.PyString {
2828
return py.PyString.unchecked(.{
29-
.py = ffi.PyType_GetName(typePtr(self.obj.py)) orelse return PyError.PyRaised,
29+
.py = ffi.PyType_GetName(typePtr(self)) orelse return PyError.PyRaised,
3030
});
3131
}
3232

3333
pub fn qualifiedName(self: PyType) !py.PyString {
3434
return py.PyString.unchecked(.{
35-
.py = ffi.PyType_GetQualName(typePtr(self.obj.py)) orelse return PyError.PyRaised,
35+
.py = ffi.PyType_GetQualName(typePtr(self)) orelse return PyError.PyRaised,
3636
});
3737
}
3838

39-
fn typePtr(obj: *ffi.PyObject) *ffi.PyTypeObject {
40-
return @alignCast(@ptrCast(obj));
39+
pub fn getSlot(self: PyType, slot: c_int) ?*anyopaque {
40+
return ffi.PyType_GetSlot(typePtr(self), slot);
41+
}
42+
43+
pub fn hasFeature(self: PyType, feature: u64) bool {
44+
return ffi.PyType_GetFlags(typePtr(self)) & feature != 0;
45+
}
46+
47+
inline fn typePtr(self: PyType) *ffi.PyTypeObject {
48+
return @alignCast(@ptrCast(self.obj.py));
4149
}
4250

43-
fn objPtr(obj: *ffi.PyTypeObject) *ffi.PyObject {
51+
inline fn objPtr(obj: *ffi.PyTypeObject) *ffi.PyObject {
4452
return @alignCast(@ptrCast(obj));
4553
}
4654
};

0 commit comments

Comments
 (0)