ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
33.71k stars 2.47k forks source link

Use case: Ability to create global constructor/destructor functions #20382

Open alexrp opened 2 months ago

alexrp commented 2 months ago

(Note: This is not a proposal at this stage; I know there's little desire for language proposals at this time. I just wanted to indicate that this is a use case where pure Zig is currently unable to replace C. This can evolve into a concrete proposal later.)

I have a C# library that effectively replaces System.Console and instead centers console interaction around a terminal (i.e. VT100+). Among other concerns, this requires some platform-specific code to configure the terminal - termios on Unix, Win32 console on Windows - and to restore its configuration on process exit.

That last part turns out to be quite tricky. It's basically not possible to do reliably from C#. So I ended up writing a dynamically-loaded helper library in C that abstracts away the platform-specific bits and also performs the terminal configuration. Here's how configuration restoration looks, using the GCC/Clang __attribute__((destructor)) feature:

https://github.com/vezel-dev/cathode/blob/6bd76d8836c1988d96c58283de037de63f5e8ab9/src/native/driver-unix.c#L39-L46

I'm compiling that library with zig cc right now, but I'd like to take the extra step and rewrite it in Zig. Unfortunately, it doesn't seem like Zig has the ability to express the constructor/destructor attributes right now.

To my knowledge, there is no other feature (whether it's atexit() or what have you) that is as reliable at running cleanup code as __attribute__((destructor)). And it really is important that this code runs: If the terminal is configured in raw mode and the C# application exits abnormally due to an unhandled exception, the terminal could be left in an unusable state if this destructor doesn't run.

dweiller commented 1 month ago

Disclaimer: I have no idea what happens if you're not making an ELF file.

It is possible to register code to run before/after main (including if std.process.exit() is called, but not if a panic occurs, or it exits due to an unhandled signal) when linking libc by placing an array of function pointers into the .init_array and .fini_array sections. I'm not sure if this is exactly equivalent to the constructor/destructor but it may work for you.

Here's a minimal example:

pub fn main() !void {
    try std.io.getStdOut().writeAll("this is main\n");
    std.process.exit(2);
}

fn constructor1() callconv(.C) void {
    std.io.getStdOut().writeAll("this is constructor1\n") catch @panic("constructor1 failed");
}

fn constructor2() callconv(.C) void {
    std.io.getStdOut().writeAll("this is constructor2\n") catch @panic("constructor2 failed");
}

fn destructor() callconv(.C) void {
    std.io.getStdOut().writeAll("this is destructor\n") catch @panic("destructor failed");
}

export const init_array: [2]*const fn () callconv(.C) void linksection(".init_array") = .{&constructor1, &constructor2};
export const fini_array: [1]*const fn () callconv(.C) void linksection(".fini_array") = .{&destructor};

const std = @import("std");

Which produces the following:

> zig run init_fini.zig -lc
this is constructor1
this is constructor2
this is main
this is destructor

I think this is basically how the constructor/destructor attributes work though someone could correct me.

alexrp commented 1 month ago

I think this is basically how the constructor/destructor attributes work though someone could correct me.

I believe you're right.

The thing is, I compile this library for Windows, macOS, and Linux, so doing this for each OS would get a bit hairy. I think it's also tricky in the more general case because the linker is supposed to have "append semantics" (if memory serves) for these arrays. It's not clear how that interacts with Zig's language semantics.