How to wrap a Zig function to be called from C?

Hello all,

Joined this forum yesterday, this is my first post! :slight_smile:

So, here is a question: I have written some Zig that calls the FluidSynth C API. Everything works, now I am refactoring in order to have better abstractions.
I was able to refactor most of it, except the C callback: I am obliged to use a global variable for it and that’s bothering me. :smile:

I would like to know if there is a better way of doing this.

This is what the code looks like:

// this is a Zig port of the C example at:
const std = @import("std");
const fluid = @cImport(@cInclude("fluidsynth.h"));

// this callback will be called every time a sequencer event is received
fn sequencer_callback(
    time: c_uint,
    event: ?*fluid.fluid_event_t,
    seq: ?*fluid.fluid_sequencer_t,
    data: ?*anyopaque,
) callconv(.C) void {
    _ = time;
    _ = data;
    _ = event;
    _ = seq;


const Metronome = struct {
    // metronome parameters
    tempo: u32 = 120,
    pattern_size: u32 = 4,

    // sequencer instance
    seq: ?*fluid.fluid_sequencer_t = null,

    // time marker used to schedule new measures, updated every time a new measure is scheduled
    time_marker: c_uint = 0,

    // sequencer destination ports
    synth_port: fluid.fluid_seq_id_t = 0,
    client_callback_port: fluid.fluid_seq_id_t = 0,

    pub fn init(
        self: *Metronome,
        seq: ?*fluid.fluid_sequencer_t,
        synth: ?*fluid.fluid_synth_t,
    ) void {
        self.seq = seq;

        // we'll send note events to the synth port
        self.synth_port = fluid.fluid_sequencer_register_fluidsynth(seq, synth);

        self.client_callback_port = fluid.fluid_sequencer_register_client(

        // get the current sequencer time
        self.time_marker = fluid.fluid_sequencer_get_tick(seq);

    fn schedule_timer_event(self: *Metronome) void {
        const event = fluid.new_fluid_event();
        defer fluid.delete_fluid_event(event);

        fluid.fluid_event_set_source(event, -1);
        fluid.fluid_event_set_dest(event, self.client_callback_port);
        fluid.fluid_event_timer(event, null);
        _ = fluid.fluid_sequencer_send_at(self.seq, event, self.time_marker, 1);

    fn schedule_note_on(
        self: *Metronome,
        midi_chan: i16,
        time: c_uint,
        note: i16,
        velocity: i16,
    ) void {
        const event = fluid.new_fluid_event();
        defer fluid.delete_fluid_event(event);

        fluid.fluid_event_set_source(event, -1);
        fluid.fluid_event_set_dest(event, self.synth_port);
        fluid.fluid_event_noteon(event, midi_chan, note, velocity);
        _ = fluid.fluid_sequencer_send_at(self.seq, event, time, 1);

    fn schedule_metronome_pattern(self: *Metronome) void {
        const beat_duration_ms: u32 = 60000 / self.tempo;

        const midi_chan = 9; // channel 10 is the drum channel

        const strong_note = 76; // woodblock high
        const weak_note = 77; // woodblock low

        var i: u32 = 0;
        while (i < self.pattern_size) : (i += 1) {
            const note_to_play: i16 = if (i == 0) strong_note else weak_note;
            const velocity: i16 = if (i == 0) 127 else 90;
            const play_at: c_uint = self.time_marker + (i * beat_duration_ms);
            self.schedule_note_on(midi_chan, play_at, note_to_play, velocity);
        self.time_marker += beat_duration_ms * self.pattern_size;

    pub fn retrigger_pattern(self: *Metronome) void {

    pub fn start(self: *Metronome) void {

// needed for the sequencer callback which is called from C
var metronome: Metronome = Metronome{};

pub fn main() void {
    const settings = fluid.new_fluid_settings();
    defer fluid.delete_fluid_settings(settings);
    const synth = fluid.new_fluid_synth(settings);
    defer fluid.delete_fluid_synth(synth);

    // here we load the soundfont file
    _ = fluid.fluid_synth_sfload(synth, "soundfonts/GeneralUser_GS_v1.471.sf2", 1);

    // set up the audio driver to play sounds from the synth
    const audio_driver = fluid.new_fluid_audio_driver(settings, synth);
    defer fluid.delete_fluid_audio_driver(audio_driver);

    // initialize the sequencer
    const seq = fluid.new_fluid_sequencer2(0);
    defer fluid.delete_fluid_sequencer(seq);

    metronome.tempo = 120;
    metronome.pattern_size = 4;
    metronome.init(seq, synth);

    // run the sequencer for 1 minute
    std.time.sleep(std.time.ns_per_min * 1);
    std.debug.print("Practice time is over, stopping now\n", .{});

As you can see, the sequencer_callback is registered inside Metronome.init, but is implemented outside of the struct because its signature needs to be compatible with the C API, which means I cannot have a self argument for it.

So I am stuck to creating a global variable to hold a reference to my Metronome instance.

Is there a better way of doing this?

Thank you in advance!


Hello @eliasdorneles Welcome to ziggit :slight_smile:

Pass @ptrCast(self) as fluid_sequencer_register_client 4th parameter (currently null).
This parameter (data) comes back in sequencer_callback.

    const self: *Metronome = @ptrCast(data.?);

This is a common trick for C callbacks.


Hello @dimdin , thanks =)

Ah, so that’s the trick!

Thank you!
I’ve just tried it, and got it working!

I had to add @ptrCast(@alignCast(data.?)), as it was giving me some errors because of pointer alignment – not sure I understood what’s going on, but it works now. Time to read up on alignment and casting, I guess :smile:

Thanks, have a good day!


Just read through some of Karl Seguins blog posts, here’s one with a nice illustration what @ptrCast does: Zig: Tiptoeing Around @ptrCast

As for pointer alignment, I guess this is situation-dependent, i.e. what types you’re trying to cast into one another, and their respective alignment.

1 Like

For your use case, you can think of it this way: when you convert self to the anyopaque pointer type, the self pointer has whatever alignment it has. But after the conversion, the Zig type system loses that information. When you @ptrCast it back to your self type, it needs to regain that alignment information that was lost, so that’s why you also need @ptrCast. But you can be sure the alignment is correct, because when you converted the pointer originally, it had normal alignment properties (you didn’t explicitly make it under-aligned).

By the way, you need @ptrCast to convert from anyopaque, but the other direction can be done with regular type coercion. So you probably only need one @ptrCast in the callback implementation, but not when you are registering the callback.


@andrewrk oh hi, Andrew! Don’t know if you remember me, but we’ve met! We were in the same RC batch back in 2017, back then you were working in this new programming language… :smile:
So pumped to see how far Zig has come, and also for the future ahead!

Thanks for the explanation – and indeed, I just tried it now without the @ptrCast for the callback registration and the type coercion works fine! :ok_hand:

1 Like

@FObersteiner thanks for that pointer, just read the article – didactic and helpful!

Haha, nice to see you again :grin:

1 Like