Using Zig to Call C Code: Strings

I’m a Zig newcomer, and I had trouble finding detailed documentation about how to handle strings when calling C code from Zig.

I wrote this explanation of how strings differ between Zig and C and how to use Zig to write safer wrappers for C functions that take strings as input or output:

I’m still a Zig beginner, so I appreciate any feedback about the post, especially regarding the last example, whose behavior I don’t fully understand.

7 Likes

Cool write up.
Regarding the last example, I don’t get why you’re calling the C code to duplicate the string, and then doing the same thing with zig functions. Just allocate a buffer and copy the contents of the string, no need for the C detour.

Thanks for reading!

It’s admittedly a contrived example because there’s no reason to call strdup from Zig since Zig has better mechanisms for copying a string.

I’m using strdup just because it’s a simple example of how to manage a string in Zig when it’s the output of a C function.

Great article. Some notes:

From the language reference, the first line in the C Pointers section:

This type is to be avoided whenever possible. The only valid reason for using a C pointer is in auto-generated code from translating C code.

The reason they exist is because auto-generated code can’t tell if a pointer translated from C should be a single item pointer or a many item pointer. C pointers coerce to single or many pointers, so for code where you know the intention of the pointer you should use the respective single or many pointer. By using C pointers you miss out on enforced null checking and automatic dereferencing using the . operator. You should change this

    const cCopy: [*c]u8 = cString.strdup(str);
    if (cCopy == null) {
        // Maybe we can return a better error by calling std.os.errno(), but for
        // now, return a generic error.
        return error.StrdupFailure;
    }

To this:

    const cCopy: [*:0]u8 = cString.strdup(str) orelse return error.OutOfMemory;

strdup can only fail with ENOMEM so I changed the error. What’s nice about Zig is that control flow statements are expressions, so we can put a return on the right hand side of the orelse here.
And just to note that the function Allocator.dupeZ exists which will copy a slice and append a null terminator if necessary - though using it kinda defeats the point of the last example.

The downside of this revision is that it’s slower than the previous version. I’m allocating and copying memory twice: once in C, and another time in Zig. If this were performance-critical code, perhaps I’d take the speedup by making the caller deal with the C array, but in general, I prefer to let Zig manage my memory.

If you want the performance and to let the allocator manage the memory, you wouldn’t call strdup at all and just copy using the passed in allocator - the call to strdup is completely unnecessary in the last example.

1 Like

Thanks, @efjimm! I’ve updated the post to integrate your suggestions.

By using C pointers you miss out on enforced null checking and automatic dereferencing using the . operator. You should change this

Ah, cool! I didn’t understand that about C pointers. I thought they were automatically what you get back from C code, but now I understand that I have some control in a more specific type.

And just to note that the function Allocator.dupeZ exists which will copy a slice and append a null terminator if necessary - though using it kinda defeats the point of the last example.

Oh, cool! I wasn’t aware of the dupeZ function. That’s simpler than what I was doing.

If you want the performance and to let the allocator manage the memory, you wouldn’t call strdup at all and just copy using the passed in allocator - the call to strdup is completely unnecessary in the last example.

Right, I wish there was a better libc function to call to demonstrate the concept.

What the post is meant to demonstrate is “here’s what you would do with a C function that returns a C-allocated array.” I’m ignoring the particulars of what strdup achieves and focusing more on the fact that it allocates and returns a string from C.

Because, similarly, there’s no reason to call C’s strlen function on a Zig string because I can just check the .len property in O(1) time. But the idea is just to show how to pass a Zig string to a C function that takes a string input.

2 Likes

It’s hard to find non-trivial examples that also display nicely in a blog post. I think it’s great idea to have more material and examples about the C-interoperability aspect of Zig.

4 Likes