An edge case for Io.Dir.realpath*() functions

Hi everyone,

This was the topic that finally motivated my first post here. Not to try and litigate it, but try and understand it, and perhaps ask what to do about an edge case if the long considered removal were to be accepted one day.

I’m pretty new to Zig, but feel I’ve gotten pretty far. I’ve written a couple personal projects and my own C library bindings, including fun with comptime, and have gotten very good at grepping my way through the stdlib.

It is clear the I/O module’s filesystem handling is moving in a specific direction, with common StackOverflow suggestions like getFdPath being removed in 0.16. And I can see the issues in both efficiency and compatibility with the realpath family. So as I learn Zig, I’m taking its discouragement to heart. Especially because when I’ve previously gotten frustrated something was missing, there was a better way.

(For instance, by digging into how realpath is implemented, I learned that nearly every OS in the posix module has a magic file descriptor number for the working directory. Neat!)

Zig’s general philosophy seems to suggest: if you really need to do something with paths a manner more specific than Dir offers, you should be using OS-specific APIs from the start. Dir is an interface for ease of use and broad compatibility, but it has a “narrow contract” in return.

I actually did use OS specific APIs when I had to integrate a C library into a project, which only accepts paths. Treating them as opaque tokens passed by the user works well.

But what about writing unit tests for said C library integration?

Then it becomes a lot trickier. Because all of the std.testing utility functions use the Dir API, requiring things like this:

test "open" {
    // verify that calling open() creates a new file from the user's path input
    const io = std.testing.io;
    const dir = std.testing.tmpDir(.{});
    // hack: because there is no way to get an fd path anymore, move around cwd to make everything relative
    const cwd = std.Io.Dir.cwd();
    defer std.process.setCurrentDir(io, cwd) catch {};
    try std.process.setCurrentDir(io, dir.dir);

    var handle = try henrylib.open("test"); // use a relative path to keep it in the tmpDir
    defer handle.close() catch {};

    try dir.dir.access(io, "test", .{}); // verify that the file was created
}

It works, but as someone who’s written a lot of C, Python, and Rust in the past, it feels very strange and clunky. (The latter language making Path a first-class opaque object with OS-specific behaviors.)

Is this really what the Dir API intends to encourage in users of the language?

Would it be possible to have other types of path utilities for std.testing at least?

Or is this less strange and clunky in your opinions than in mine?