Crash on python bindings when using pointers as handlers

I am trying to manipulate zig code from python using pointers as opaque handlers. It doesn’t go well:

I have this zig piece of code:

const Quux = extern struct {
  field: u32,
};

const Foo = struct {
  quuxes: []const Quux,

  pub fn init() Foo {
    const quuxes = &[_]Quux{ Quux{ .field = 42 } };
    // also tried
    // const quuxes = allocator.alloc(Quux, 1) catch unreachable;
    return Foo{
      .quuxes = quuxes,
    };
  }
};

export fn foo() *Foo {
  var f = Foo.init();
  std.log.debug("returning {*}", .{ &f });
  std.log.debug("f.quuxes {*}", .{ &f.quuxes });
  std.log.debug("f.quuxes {any}", .{ f.quuxes[0] });
  return &f;
}

export fn bar(f: *Foo) void {
  std.log.debug("bar {*}", .{ &f });
  std.log.debug("f.quuxes {*}", .{ &f.quuxes });
  std.log.debug("f.quuxes {any}", .{ f.quuxes[0] }); // This crashes
}

Note that Foo is not extern. This is by design. So I can’t return it to python. I just return a pointer to it and expect the pointer to be passed in order to do things with the structure on zig side.

I compile with:

zig build-lib python.zig -dynamic

In python I do:

import ctypes

lib = ctypes.CDLL("./libpython.so")
f = lib.foo()
print(f'0x{f:02X}')
lib.bar(f)

And I get a crash on the last line of bar. Note that my pointers looks correct:

debug: returning python.Foo@7fff42cabc80
debug: f.quuxes []const python.Quux@7fff42cabc80
debug: f.quuxes python.Quux{ .field = 42 }
0x42CABC80
debug: bar *python.Foo@7fff42cabc78
debug: f.quuxes []const python.Quux@42cabc80
Segmentation fault (core dumped)

So maybe Quux is being allocated on the stack and get deallocated during the two calls? I tried allocating the field dynamically but it still crashes.

Any help is appreciated.

Variable f is a function local variable. When foo function terminates, f lifetime also ends. By returning the address of f you actually return an address to a temporary storage that is reused when f returns.
So bar get this address, that points to something random; then it tries to dereference the random contents, it crashes.

But this crashes too though:

var globalf: Foo = undefined;

export fn foo() *Foo {
  globalf = Foo.init();
  std.log.debug("returning {*}", .{ &globalf });
  std.log.debug("globalf.quuxes {*}", .{ &globalf.quuxes });
  std.log.debug("globalf.quuxes {any}", .{ globalf.quuxes[0] });
  return &globalf;
}

export fn bar(f: *Foo) void {
  std.log.debug("bar {*}", .{ &f });
  std.log.debug("f.quuxes {*}", .{ &f.quuxes });
  std.log.debug("f.quuxes {any}", .{ f.quuxes[0] });
}

EDIT: I missed the global variable initialization.

This works:

const std = @import("std");

const Quux = extern struct {
    field: u32,
};

const Foo = struct {
    quuxes: []const Quux,

    pub fn init() Foo {
        const quuxes = &[_]Quux{Quux{ .field = 42 }};
        return Foo{
            .quuxes = quuxes,
        };
    }
};

var globalf: Foo = undefined;

export fn foo() *Foo {
    globalf = Foo.init();
    return &globalf;
}

pub fn main() void {
    const f: *Foo = foo();
    std.log.debug("f.quuxes {any}", .{f.quuxes[0]});
}

A guess: add extern in const Foo = struct { => const Foo = extern struct {

Foo can’t be extern for various reason (one being that it contains slices).

Even allocating on the stack does not work:

export fn foo() *Foo {
  var f: *Foo = allocator.create(Foo) catch unreachable;
  f.quuxes = allocator.alloc(Quux, 1) catch unreachable;
  f.quuxes[0].field = 42;
  std.log.debug("foo f {*}", .{ f });
  std.log.debug("f.quuxes {*}", .{ &f.quuxes });
  std.log.debug("f.quuxes {any}", .{ f.quuxes[0] });
  return f;
}

export fn bar(f: *Foo) void {
  std.log.debug("bar f {*}", .{ f });
  std.log.debug("f.quuxes {*}", .{ &f.quuxes });
  std.log.debug("f.quuxes {any}", .{ f.quuxes[0] });
}

Looking at the pointer values, it seems that the pointer get sliced to 32bits when going through python.

Looking on google, it seems many people are struggling passing pointers back and forth with ctypes. So I think this is a ctypes issue and not a zig issue.

So in addition to the aforementioned local variable issue, there was a ctypes problem, so posting it here for posterity:

import ctypes

lib = ctypes.CDLL("./libpython.so")
# You need to add this following line or your pointer will be truncated
lib.foo.restype = ctypes.c_void_p
f = lib.foo()
print(f'0x{f:02X}')
# You need to add this following line or your pointer will be truncated
lib.bar.argtypes = (ctypes.c_void_p,)
lib.bar(f)

You need to use restype and argtypes to specify that your function deal with pointers. By default, ctypes will consider everything a i32.

1 Like