A mean to attach methods to C structs

I’m currently in middle of refactoring the API code that I used for my PHP extension. The initial version was very low effort. For text strings I have this in php.zig:

pub const String = c.zend_string;

And associated functions:

pub fn getStringContent(str: *const String) [:0]const u8 {
    const s: [*]const u8 = @ptrCast(&str.*.val[0]);
    const len = str.*.len;
    return @ptrCast(s[0..len]);
}

pub fn compareStrings(s1: *const String, s2: *const String) bool {
    const sc1 = getStringContent(s1);
    const sc2 = getStringContent(s2);
    return std.mem.eql(u8, sc1, sc2);
}

pub fn parseBool(s: *String) bool {
    return c.zend_ini_parse_bool(s);
}

pub fn parseLong(s: *String) Long {
    return c.zend_atol(&s.val[0], s.len);
}

Just a straight mapping to the C API basically. Extremely low-effort.

When the refactoring, I want to make the API more idiomatic Zig. More OOPish. To that end, I decided to wrap the PHP/Zend string type in a struct and attach various helper methods to it:

pub const String = struct {
    impl: c.zend_string,    

    pub fn slice(self: *const @This()) [:0]const u8 {
        const s: [*]const u8 = @ptrCast(&self.impl.val[0]);
        const len = self.impl.len;
        return @ptrCast(s[0..len]);
    }

    pub fn length(self: *const @This()) usize {
        return self.impl.len;
    }
    
    // etc...
}

At the binary level, String contains exactly the data as c.zend_string. That allows me to freely between *c.zend_string and *String. Every times where this occurs though, it requires the use of @ptrCast(). That includes situations involving pointers to functions accepting strings as arguments.

It would be nice if the compiler would recognize that String and c.zend_string are the same thing and convert pointers automatically.

If the ABI of c.zend_string is part of the public API of the library you are porting, you could work as follows:

pub const ZendString = extern struct { // extern is required
    // fields here
    pub fn length(str: *ZendString) {
        return zend_length(str);
    }
    extern "c" fn zend_length(str: ?*ZendString); //optional bc C
};

This will work just fine, but it does require you to take some ownership of the translate-c output.

If the ABI of c.zend_tring is not part of the public API of the library, then usually the library itself has functions in charge of creating and destroying the struct and users should work only with handles to it. In this case, you could still work similarly to the above, but instead you’d write something like

pub const ZendString = opaque { // opaque types must be passed by pointer
    // fields here
    pub fn length(str: *ZendString) {
        return zend_length(str);
    }
    extern "c" fn zend_length(str: ?*ZendString); //optional bc C
};
2 Likes

The number of functions is quite large. And as we know, it’s not possible to attach new decls to a comptime-defined struct. In theory though, I could use comptime fields. It really shouldn’t matter whether I’m performing .[function name] on a namespace or a 0-byte struct.

An ability to attach methods to structs generator by translate-c would be a cleaner solution.