Skip to content

Commit 50171d7

Browse files
authored
Add Python GC support (#244)
1 parent 3db1d66 commit 50171d7

File tree

4 files changed

+220
-8
lines changed

4 files changed

+220
-8
lines changed

example/classes.zig

+7
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ 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| e.incref();
8283
return prop.e;
8384
}
8485

@@ -87,6 +88,7 @@ pub const User = py.class(struct {
8788
if (std.mem.indexOfScalar(u8, try value.asSlice(), '@') == null) {
8889
return py.ValueError.raiseFmt("Invalid email address for {s}", .{try self.name.asSlice()});
8990
}
91+
value.incref();
9092
prop.e = value;
9193
}
9294
}),
@@ -96,6 +98,11 @@ pub const User = py.class(struct {
9698
return py.PyString.createFmt("Hello, {s}!", .{try self.name.asSlice()});
9799
}
98100
}) = .{},
101+
102+
pub fn __del__(self: *Self) void {
103+
self.name.decref();
104+
if (self.email.e) |e| e.decref();
105+
}
99106
});
100107
// --8<-- [end:properties]
101108

pydust/src/pytypes.zig

+199-3
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,17 @@ pub fn Type(comptime name: [:0]const u8, comptime definition: type) type {
4242
};
4343

4444
const bases = Bases(definition);
45-
const attrs = Attributes(definition);
4645
const slots = Slots(definition, name);
4746

47+
const flags = blk: {
48+
var flags_: usize = ffi.Py_TPFLAGS_DEFAULT | ffi.Py_TPFLAGS_BASETYPE;
49+
if (slots.gc.needsGc) {
50+
flags_ |= ffi.Py_TPFLAGS_HAVE_GC;
51+
}
52+
53+
break :blk flags_;
54+
};
55+
4856
pub fn init(module: py.PyModule) PyError!py.PyObject {
4957
var basesPtr: ?*ffi.PyObject = null;
5058
if (bases.bases.len > 0) {
@@ -61,7 +69,7 @@ pub fn Type(comptime name: [:0]const u8, comptime definition: type) type {
6169
.name = qualifiedName.ptr,
6270
.basicsize = @sizeOf(PyTypeStruct(definition)),
6371
.itemsize = 0,
64-
.flags = ffi.Py_TPFLAGS_DEFAULT | ffi.Py_TPFLAGS_BASETYPE,
72+
.flags = flags,
6573
.slots = @constCast(slots.slots.ptr),
6674
};
6775

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

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

122+
if (gc.needsGc) {
123+
slots_ = slots_ ++ .{ ffi.PyType_Slot{
124+
.slot = ffi.Py_tp_clear,
125+
.pfunc = @constCast(&gc.tp_clear),
126+
}, ffi.PyType_Slot{
127+
.slot = ffi.Py_tp_traverse,
128+
.pfunc = @constCast(&gc.tp_traverse),
129+
} };
130+
}
131+
113132
if (doc.docLen != 0) {
114133
slots_ = slots_ ++ .{ffi.PyType_Slot{
115134
.slot = ffi.Py_tp_doc,
@@ -305,7 +324,7 @@ fn Slots(comptime definition: type, comptime name: [:0]const u8) type {
305324
/// Note: tp_del is deprecated in favour of tp_finalize.
306325
///
307326
/// See https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_finalize.
308-
fn tp_finalize(pyself: *ffi.PyObject) void {
327+
fn tp_finalize(pyself: *ffi.PyObject) callconv(.C) void {
309328
// The finalize slot shouldn't alter any exception that is currently set.
310329
// So it's recommended we save the existing one (if any) and restore it afterwards.
311330
// NOTE(ngates): we may want to move this logic to PyErr if it happens more?
@@ -455,6 +474,183 @@ fn Doc(comptime definition: type, comptime name: [:0]const u8) type {
455474
};
456475
}
457476

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