How to access the return value of a C function that can return null to return an error?

Hi, simple question here, I’m trying to find how to call a C function that can return null and catch the null case to return an error relative to the function’s meaning of null;

uint8_t	*string_bsearch(const uint8_t *s, int32_t c)
{
	if (c == 0x00)
		return ((uint8_t *)&s[string_length(s)]);
	else
		return (memory_search(s, c, string_length(s))); //return 0 if not found
}

This function can return 0 if it finds nothing so far I’ve made that binding for that function :

pub fn stringBsearch(str: []const u8, c: i32) ![]const u8 {
    const result: ?[*:0]const u8 = clib.string_bsearch(@ptrCast(str[0..]), c);

    if (result != null) {
        return (std.mem.span(result.?));
    } else {
        return (CResult.NotFound);
    }
}

and for my test I have this two functions so far :

test "string_bsearch : test1" {
    const result = try bind.stringBsearch("This is a string", 'g');
    try expectTrue(result[0] == 'g');
}

test "string_bsearch : test2" {
    if (bind.stringBsearch("This is a string", 'z')) |some| {
        std.debug.print("found :{s}", .{some});
    } else |err| {
        try expectError(Cresult.NotFound, err);
    }
}

The first test seems to work as expected but the second one doesn’t compile, I get this error message and I’m a bit confuse, the if + capture seems like the correct way to unpack an error right ?

/Users/plgol/zig/0.12.0-dev.2341+92211135f/files/lib/std/testing.zig:38:9: error: expected error union type, found '@typeInfo(@typeInfo(@TypeOf(bindings.stringBsearch)).Fn.return_type.?).ErrorUnion.error_set'
    if (actual_error_union) |actual_payload| {

I’m not sure what that means ? can someone explain to me what I’m doing wrong ?

expectError second argument must be an error union (the return type of bind.stringBearch) and not an error set (Cresult).
e.g.

    try expectError(Cresult.NotFound, bind.stringBsearch("This is a string", 'z'));
2 Likes

Oh ok thanks this works perfectly, now I have another question how do you convert an array of strings to a safe Zig type ?
I have this function :

uint8_t	**string_split(struct s_allocator *allocator, const uint8_t *string,
		const uint8_t *delim)

and this binding :

pub fn stringSplit(allocator: *Callocator, str: []const u8, delim: []const u8) ![][]const u8 {
    const result: ?[*:0][*:0]const u8 = @ptrCast(clib.string_split(allocator, @ptrCast(str), @ptrCast(delim[0..])));
    if (result != null) {
        return std.mem.span(result.?[0..]);
    } else {
        return CResult.Null;
    }
}

but it doesn’t work do you have any suggestions on how I might convert that type ?

I can think two ways to do it:

  1. allocate the outer array and fill it with slices of the inner array.
  2. create an iterator of the outer array elements that its next returns a slice of the inner array.
2 Likes

First you need to get your type correct, note 0 and null are different values.
I think in your case, your type is basically this, but without the optional:

So we end up with:

const result: [*:null][*:0]const u8 = @ptrCast(clib.string_split(allocator, str.ptr, delim.ptr));

A null result is equal to a null-sentinel-terminated slice of length zero, so optional would make us handle the same thing in two different ways.

Now we can convert the outer sentinel-terminated slice to a normal slice:

const results = std.mem.span(result);
if(results.len > 0) {
  return results;
} else {
  return CResult.Null;
}

Also your input types should be zero terminated, so you end up with:

pub fn stringSplit(allocator: *Callocator, str: [:0]const u8, delim: [:0]const u8) ![][:0]const u8 {
    const result: [*:null][*:0]const u8 = @ptrCast(clib.string_split(allocator, str.ptr, delim.ptr));
    const results = std.mem.span(result);
    if(results.len > 0) {
        return results;
    } else {
        return CResult.Null;
    }
}

Personally I would stop here, because I find this is an easy middle ground where the types basically meet in the middle. I don’t think there is a big reason to go all the way to eliminate the inner sentinel terminated slices, instead I would just work with the inner sentinel terminated slices directly.

If you want to convert to all non-sentinel slices anyway you can use one of the approaches @dimdin pointed out.

1 Like

Thanks both of you for your time, just a last check, I’m hopping to make the function fail it’s memory allocation in one of my test case, so I would need the null case to be available for the outer array, how can I do that ? Also I agree I can convert the inner arrays on the spot for testing. (sorry If I’m bothering but the testing for the split is really important to me a lot of what I do depends on that split in my library).

Edit : Ok I think I’ve found it now at least it compiles without complaining:

pub fn stringSplit(allocator: *Callocator, str: []const u8, delim: []const u8) ![*:null]const [*c]const u8 {
    const result: ?[*:null]const [*c]const u8 = clib.string_split(allocator, str.ptr, delim.ptr);
    if (result) |r| {
        const ret = std.mem.span(r);
        return (ret);
    } else {
        return CResult.Null;
    }
}

If your split function is both allowed to return zero elements as a valid result AND at the same time is allowed to return null to indicate error, then that means that your c function does return a value that can’t be interpreted correctly in the case of zero elements / error. They would both use the same c value (a null pointer) to denote two different outcomes.

Are you sure your split function is ever allowed/able to return zero values?
I would think it returns always at least 1 value (in case there is no split), even if that value itself may be a string of length 0 (in case the input had length 0).

1 Like

I see where you are coming from, my bad for the explanation, the function is expected to always return at least one null terminated string if the inputs are invalid or too short. But in the case of an out of memory during the allocation of the outer array, or if any of the inner string allocation fails, than there is some cleanup, and the functions just returns null.

Edit : Ok everything work as expected Thanks for all the answers :ok_hand:

test "string_split : test1" {
    const heap = bind.clib.heap_init();
    defer _ = bind.clib.heap_deinit(heap);

    const result = try bind.stringSplit(heap, "This is a test", " ");
    var itterator = std.mem.splitAny(u8, "This is a test", " ");
    var i: usize = 0;
    while (itterator.next()) |split| : (i += 1) {
        try expectEqualString(split, std.mem.span(result[i]));
    }
    defer _ = bind.clib.string_split_destroy(heap, @constCast(@ptrCast(result)));
}

The optional is needed because else the many item pointer has an element type that isn’t allowed to be null, which also results in null not being a valid sentinel.

So we actually need the optional, thus the type is identical to what was provided by @castholm in that answer originally: [*:null]?[*:0]const u8

So here is my revised function:

pub fn stringSplit(allocator: *Callocator, str: [:0]const u8, delim: [:0]const u8) ![][:0]const u8 {
    const result: [*:null]?[*:0]const u8 = @ptrCast(clib.string_split(allocator, str.ptr, delim.ptr));
    const results = std.mem.span(result);
    if(results.len > 0) {
        return @ptrCast(results);
    } else {
        return CResult.Null;
    }
}

Also here is another example:

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const OptionalString = ?[*:0]const u8;
    const data = try allocator.allocSentinel(OptionalString, 3, null);
    defer allocator.free(data);
    data[0] = "";
    data[1] = "another";
    data[2] = "thing";

    const ManyStrings = [*:null]?[*:0]const u8;
    const many_ptr: ManyStrings = data.ptr;

    const span = std.mem.span(many_ptr);
    const span2: [][*:0]const u8 = @ptrCast(span);

    std.debug.print("data:         type: {: <25}    {any}\n", .{ @TypeOf(data), data });
    std.debug.print("many_ptr:     type: {: <25}    {*}\n", .{ @TypeOf(many_ptr), many_ptr });
    std.debug.print("span:         type: {: <25}    {any}\n", .{ @TypeOf(span), span });
    std.debug.print("span2:        type: {: <25}    {any}\n", .{ @TypeOf(span2), span2 });
}

Output:

data:         type: [:null]?[*:0]const u8        { u8@10d9008, u8@10d75e7, u8@10d75ef }
many_ptr:     type: [*:null]?[*:0]const u8       ?[*:0]const u8@7f60f5d9a000
span:         type: [:null]?[*:0]const u8        { u8@10d9008, u8@10d75e7, u8@10d75ef }
span2:        type: [][*:0]const u8              { u8@10d9008, u8@10d75e7, u8@10d75ef }
2 Likes

Thanks for the great example, that’s brilliant. :slight_smile:

1 Like