How to use a pointer from a C API that requires pointer arithmetic to use it?

Hi folks! :wave:

I am learning the JACK API and Zig at the same time, by porting to Zig the example client from here.

I was able to write enough Zig code to connect to JACK, but I’m having trouble to write the callback code (see here the C version). The problem is that to write the audio data in the buffer obtained from JACK C API I’d need to use a pointer with an index, and this isn’t allowed in Zig.

Here is my attempt of writing the callback in Zig (note: I’m using globals for the shared values for simplicity, and float instead of jack_default_audio_sample_t as it’s just an alias to float):

fn process(nframes: c.jack_nframes_t, arg: ?*anyopaque) callconv(.C) c_int {
    _ = arg;
    var out1: *f32 = @ptrCast(@alignCast(c.jack_port_get_buffer(output_port1, nframes)));
    var out2: *f32 = @ptrCast(@alignCast(c.jack_port_get_buffer(output_port1, nframes)));

    for (0..nframes) |i| {
        out1[i] = sine_table[left_phase];
        out2[i] = sine_table[right_phase];
        left_phase += 1;
        right_phase += 3;
        if (left_phase >= TABLE_SIZE) {
            left_phase -= TABLE_SIZE;
        }
        if (right_phase >= TABLE_SIZE) {
            right_phase -= TABLE_SIZE;
        }
    }

    return 0;
}

This doesn’t compile:

simple_client.zig:25:13: error: type '*f32' does not support indexing
        out1[i] = sine_table[left_phase];
        ~~~~^~~
simple_client.zig:25:13: note: operand must be an array, slice, tuple, or vector

I understand Zig doesn’t allow to do pointer arithmetic as C does. So how can I consume a C API in a case like this?

1 Like

You could try casting the opaque pointers to [*]f32 (many-item pointer) instead of *f32 (single-item pointer). Unlike C, Zig has distinct pointer types for single-item and many-item pointers with different properties. [*]f32 not only supports indexing like out1[i] but also pointer arithmetic like out1 += 1.

From https://ziglang.org/documentation/master/#Pointers:

Zig has two kinds of pointers: single-item and many-item.

  • *T - single-item pointer to exactly one item.
    • Supports deref syntax: ptr.*
    • Supports slice syntax: ptr[0..1]
    • Supports pointer subtraction: ptr - ptr
  • [*]T - many-item pointer to unknown number of items.
    • Supports index syntax: ptr[i]
    • Supports slice syntax: ptr[start..end] and ptr[start..]
    • Supports pointer-integer arithmetic: ptr + int, ptr - int
    • Supports pointer subtraction: ptr - ptr

T must have a known size, which means that it cannot be anyopaque or any other opaque type.

You might also want to read up on [*c] pointers which show up in translated C code and function like both single- and many-item pointers at the same time, as a compromise since the C type system does not carry the information necessary to translate to a more specific Zig type.

When interoping with C APIs, it’s important to recognize the difference between single- and many-item pointers and cast/coerce to the corresponding Zig pointer type appropriately.

4 Likes

You might also want to consider using the slice syntax to convert the [*]f32 many-item pointer to a []f32 slice for added type safety. This lets you make use of multi-object for loops and rewrite your function as:

fn process(nframes: c.jack_nframes_t, arg: ?*anyopaque) callconv(.C) c_int {
    _ = arg;
    const out1_ptr: [*]f32 = @ptrCast(@alignCast(c.jack_port_get_buffer(output_port1, nframes)));
    const out1 = out1_ptr[0..nframes];
    const out2_ptr: [*]f32 = @ptrCast(@alignCast(c.jack_port_get_buffer(output_port2, nframes)));
    const out2 = out2_ptr[0..nframes];

    for (out1, out2) |*o1, *o2| {
        o1.* = sine_table[left_phase];
        o2.* = sine_table[right_phase];
        left_phase += 1;
        right_phase += 3;
        if (left_phase >= TABLE_SIZE) {
            left_phase -= TABLE_SIZE;
        }
        if (right_phase >= TABLE_SIZE) {
            right_phase -= TABLE_SIZE;
        }
    }

    return 0;
}
5 Likes

Thank you so much @castholm, for the explanation and the documentation pointers (!), I changed it for a many-item pointers and my code works now !

I feel silly for not having spotted it, I had already went through that section of the docs twice, but for some reason I was assuming I would have to know the size at compile-time, that was silly. :sweat_smile:

Nice touch the conversion to slices, that’s cool indeed, will try that, thank you!
Good day to you!

The trick to convert to slice doesn’t seem to work, though.

If I write it with const as in your answer, I get a compile error:

simple_client.zig:26:9: error: cannot assign to constant
        left = volume * sine_table[left_phase];
        ^~~~

If I change those to var, I get a compile error saying it’s never mutated:

$ zig run simple_client.zig -lc -ljack
simple_client.zig:22:9: error: local variable is never mutated
    var out2 = out2_ptr[0..nframes];
        ^~~~
simple_client.zig:22:9: note: consider using 'const'
simple_client.zig:20:9: error: local variable is never mutated
    var out1 = out1_ptr[0..nframes];
        ^~~~
simple_client.zig:20:9: note: consider using 'const'

It seems that Zig doesn’t “see” that we’re modifying it inside the loop?

What is left here? It seems that it isn’t a pointer into the buffer since you are assigning to it directly. In the example posted above the elements of the buffer were iterated over by reference and assigned with a pointer de-reference:

1 Like

Oh, my mistake, I was missing the .* dereferencing! I really need to review how to use pointers… Thanks, sorry for the bother!

1 Like