# IO across the C ABI boundary

**URL:** <https://ziggit.dev/t/io-across-the-c-abi-boundary/16334>\
**Category:** Help\
**Tags:** c, io\
**Created:** [June 25, 2026, 7:24am UTC](https://ziggit.dev/t/io-across-the-c-abi-boundary/16334 "2026-06-25T07:24:07Z")\
**Posts on this page:** 10\
**Page:** 1

<div class="post-metadata">

**Author:** ![Jan200101](https://ziggit.dev/user_avatar/ziggit.dev/jan200101/32/8236_2.png) [@Jan200101](https://ziggit.dev/u/Jan200101)\
**Post date:** [June 25, 2026, 7:24am UTC](https://ziggit.dev/t/io-across-the-c-abi-boundary/16334/1 "2026-06-25T07:24:07Z")

</div>

I’m in the inital stages of rewriting a Database system I wrote in C in Zig with the intention of providing a C API to replace the old system but I’m not sure of the best way to handle Io in this.

Pretty much all IO operations now go through the Io layer and for the Zig module its trivial to just allow passing an Io object explicitly, for the C API this isn’t possible because it doesn’t have that Io layer, what would be the best way to do Io in this case?

If I were to simply target mainstream Zig targets like Linux then a global `Io.Threaded` instance would be good enough but I also target devices that require a custom `Io` implementation so thats sadly not an option.

I could just add a build option which then makes it pick between different `Io` implementations but if someone else needs their custom then they would be required to edit the code, is there any good way to deal with this?

One idea I have had is a Callback function implemented Zig that takes a opaque pointer and modifies it as if it were an `Io` pointer, that way someone could add their own zig file that implements only 1 function and get the functionality needed.

---

<div class="post-metadata">

**Author:** ![Cloudef](https://ziggit.dev/user_avatar/ziggit.dev/cloudef/32/1370_2.png) [@Cloudef](https://ziggit.dev/u/Cloudef)\
**Post date:** [June 25, 2026, 7:28am UTC](https://ziggit.dev/t/io-across-the-c-abi-boundary/16334/2 "2026-06-25T07:28:57Z")

</div>

Could you not have a C function that lets you set the global io? (You can pass it as pointer and deref)

Other option is to let the user of the zig module / artifact to addImport to it that contains the Io instance.

I dont think `std_options` kind of approach works for you here since I assume you are compiling a static library.

---

<div class="post-metadata">

**Author:** ![Jan200101](https://ziggit.dev/user_avatar/ziggit.dev/jan200101/32/8236_2.png) [@Jan200101](https://ziggit.dev/u/Jan200101)\
**Post date:** [June 25, 2026, 8:19am UTC](https://ziggit.dev/t/io-across-the-c-abi-boundary/16334/3 "2026-06-25T08:19:19Z")

</div>

> [@Cloudef](#):
>
> Could you not have a C function that lets you set the global io? (You can pass it as pointer and deref)

My first thought as well but the fact that the Io struct isn’t extern makes it difficult to pass around without ugly hacky workarounds.

> [@Cloudef](#):
>
> Other option is to let the user of the zig module / artifact to addImport to it that contains the Io instance.
> 
> I dont think `std_options` kind of approach works for you here since I assume you are compiling a static library.

Maybe the best option is to make it importable and then simply check if root has a global io implementation by name i.e. `@hasField(root, "default_io")`  
That way I can default to `Io.Threaded` and replace it in a minimal Zig project, still kind of ugly though.

---

<div class="post-metadata">

**Author:** ![Cloudef](https://ziggit.dev/user_avatar/ziggit.dev/cloudef/32/1370_2.png) [@Cloudef](https://ziggit.dev/u/Cloudef)\
**Post date:** [June 25, 2026, 8:54am UTC](https://ziggit.dev/t/io-across-the-c-abi-boundary/16334/4 "2026-06-25T08:54:20Z")

</div>

Considering the Io struct is 2 pointers you could also make extern version of it and convert between.

---

<div class="post-metadata">

**Author:** ![vulpesx](https://ziggit.dev/user_avatar/ziggit.dev/vulpesx/32/3989_2.png) [@vulpesx](https://ziggit.dev/u/vulpesx)\
**Post date:** [June 25, 2026, 9:04am UTC](https://ziggit.dev/t/io-across-the-c-abi-boundary/16334/5 "2026-06-25T09:04:27Z")

</div>

The vtable has pointers to functions _without_ the c calling convention.

You would have to make a zig wrapper to convert arguments to and call the c ABI. That is another vtable, which may or may not be acceptable.

---

<div class="post-metadata">

**Author:** ![Cloudef](https://ziggit.dev/user_avatar/ziggit.dev/cloudef/32/1370_2.png) [@Cloudef](https://ziggit.dev/u/Cloudef)\
**Post date:** [June 25, 2026, 9:16am UTC](https://ziggit.dev/t/io-across-the-c-abi-boundary/16334/6 "2026-06-25T09:16:40Z")

</div>

You can have pointers to auto layout structs in extern structs. You only have to make extern version of the `std.Io`. In this case C callconv does not matter since its being used by zig code anyways. C ABI is used as the ABI boundary.

---

<div class="post-metadata">

**Author:** ![vulpesx](https://ziggit.dev/user_avatar/ziggit.dev/vulpesx/32/3989_2.png) [@vulpesx](https://ziggit.dev/u/vulpesx)\
**Post date:** [June 25, 2026, 9:50am UTC](https://ziggit.dev/t/io-across-the-c-abi-boundary/16334/7 "2026-06-25T09:50:52Z")

</div>

Yes, I didn’t realise you were only talking about 1 of problems.

---

<div class="post-metadata">

**Author:** ![KilianHanich](https://ziggit.dev/user_avatar/ziggit.dev/kilianhanich/32/8254_2.png) [@KilianHanich](https://ziggit.dev/u/KilianHanich)\
**Post date:** [June 25, 2026, 11:19am UTC](https://ziggit.dev/t/io-across-the-c-abi-boundary/16334/8 "2026-06-25T11:19:39Z")

</div>

I would probably do something like this:

In the C API, have an init and a deinit function.  
The function implementations internally can `.init()`/`.deinit()` on a global io implementation.

So something like this:

```zig
pub export fn db_init() Result {
    // ...
    global_io.init() catch |err| switch (err) {
        // error handling
    };
    // ...
    return .success;
}

pub export fn db_deinit() void {
    // ...
    global_io.deinit();
    // ...
}

```

The type of the Io implementation would be chosen via a buildsystem config.

So a user would either do this:

```Zig
const default_io_db = b.dependency("db", .{
    .target = target,
    .optimize = optimize,
});
const db = default_io_db.module("db");

```

or this:

```Zig
const custom_io_db = b.dependency("db", .{
    .target = target,
    .optimize = optimize,
    .use_custom_io = true,
});
const db = custom_io_db.module("db");
const my_io = b.createModule(...);
db.addImport("custom_io", my_io);

```

And inside of the buildsystem description, I would just pass the “use\_custom\_io” option down to your code with `false` as a default.  
My code would then look something like this:

```Zig
const config = @import("config");

var global_io: if (config.use_custom_io) @import("custom_io") else std.Io.Threaded = undefined;

```

And of course I would document that the user can chose the io implementation by giving the `"db"` module an import called `"custom_io"` which follows the necessary interface.

---

<div class="post-metadata">

**Author:** ![Jan200101](https://ziggit.dev/user_avatar/ziggit.dev/jan200101/32/8236_2.png) [@Jan200101](https://ziggit.dev/u/Jan200101)\
**Post date:** [June 25, 2026, 12:37pm UTC](https://ziggit.dev/t/io-across-the-c-abi-boundary/16334/9 "2026-06-25T12:37:18Z")

</div>

This was more or less my current plan, minus some details.

I don’t think just doing `global_io.init()` would work because this isn’t a reliable pattern, `Io.Threaded` for example takes extra arguments for it.

Having a generic function that just returns io might be better and allows for more complex setups:

```zig
var global_io: std.Io = undefined;
const init_io: fn () std.Io = if (config.use_custom_io) @import("custom_io").init_io else init_default_io;
const deinit_io: fn () std.Io = if (config.use_custom_io) @import("custom_io").deinit_io else deinit_default_io;

var default_io_impl: std.Io.Threaded = undefined;
fn init_default_io() std.Io {
    default_io_impl = .init(std.heap.c_allocator, {});
    return default_io_impl.io();
}

pub export fn db_init() Result {
    global_io = init_io();
    return .success;
}

// pretend I also wrote deinit functions here

```

(essentialy pseudocode, but I hope you get what I mean)

---

<div class="post-metadata">

**Author:** ![typedlambda](https://ziggit.dev/user_avatar/ziggit.dev/typedlambda/32/3613_2.png) [@typedlambda](https://ziggit.dev/u/typedlambda)\
**Post date:** [July 4, 2026, 8:16pm UTC](https://ziggit.dev/t/io-across-the-c-abi-boundary/16334/10 "2026-07-04T20:16:05Z")

</div>

If it’s enough to have a FILE I would probably start with a libc `funopen()` handle as a first step.
