ziglang/zig
260
6.0k
Fork
You've already forked zig
775

Remove extern/export decls and improve the ergonomics of @extern/@export #30873

Open
opened 2026年01月18日 02:39:54 +01:00 by castholm · 12 comments
Contributor
Copy link

TL;DR: This proposal concerns the overlapping functionality between extern/export decls and the @extern/@export builtins, and the ergonomics of the latter. It is divided up into four smaller sub-proposals which can each be accepted or rejected on an individual basis, but which I believe would synergize well were they all to be accepted.

  1. Remove extern and export decls in favor of the more powerful @extern and @export builtins
  2. Replace the @extern type parameter with the inferred result type
  3. Move the name fields of std.builtin.Extern/ExportOptions to the parameter list of @extern and @export
  4. Rename @extern and @export to @externImport and @externExport

As for the "why", doing these would help eliminate a subtle footgun, while also making the builtins easier and more intuitive for users to use.

These ideas were briefly discussed on Zulip and I was given the go-ahead to file a language proposal.

Status quo example

To help illustrate how the proposed changes would affect Zig code, I'll use the following examples of using extern and export and progressively update them with each sub-proposal:

// Import external functions from the SDL3 library, but "ziggify" the function names.pubconstgetWindowTitle=SDL_GetWindowTitle;extern"SDL3"fnSDL_GetWindowTitle(window:*Window)[*:0]constu8;pubconstsetWindowTitle=SDL_SetWindowTitle;extern"SDL3"fnSDL_SetWindowTitle(window:*Window,title:[*:0]constu8)bool;
// Export functions to external code with an 'acme_' prefix, but use non-prefixed names internally.pubconstadd=acme_add;exportfnacme_add(a:c_int,b:c_int)c_int{returna+%b;}pubconstmul=acme_mul;exportfnacme_mul(a:c_int,b:c_int)c_int{returna*%b;}

1. Remove extern and export decls in favor of the more powerful @extern and @export builtins

Problem:

  • Having both extern/export decls and the @extern/@export builtins means that there are two different ways of doing the same things. extern/export decls are strictly less powerful and flexible than the @extern/@export builtins, only supporting the default linkage and visibility options.

    If you start off by defining a bunch of extern functions and variables, and later realize that you need to change certain options, you will need to rewrite your extern decls into a form that uses the @extern builtin. This isn't difficult, but can be a bit annoying. Had you used @extern from the start, you would only need to trivially adjust the relevant std.builtin.ExternOptions fields.

  • A slightly greater problem is that extern and @extern are not exactly equivalent. extern var a: *T is a special name binding syntax that will make the Zig variable a an alias for the external symbol a, and the expression &a will return the address of the symbol a. const b = @extern(*T, .{ .name = "a" }) on the other hand will make the Zig variable b a pointer to the external symbol a, and the expression &b will return the address of the Zig variable b, not the the address of the symbol a.

    In other words, @extern adds another level of indirection compared to extern. This can be a bit of a footgun. If you already have extern var x: *i32, and you need to rewrite it into the @extern(**i32, .{ .name = "x" }) form, it is very easy to forget to add the second asterisk and end up being one whole level off. Here is a Ziggit thread from a while back where a user was bitten by this. Similar case: #19515.

  • The extern "foo" syntax is easy to get confused with C++'s extern "C". This is obviously a relatively minor issue, but nevertheless it can be a stumbling block for beginners coming to Zig from C/C++.

To make the language surface slightly smaller and to disarm footguns, I propose that the extern/export fn/const/var syntax be removed, leaving the @extern builtin as the only way of declaring a reference to an external symbol and the export builtin as the only way of making a Zig declaration visible to external code. The extern keyword will still retain its use in extern struct/union, but the export keyword no longer has a use and will be deleted from the language.

With these changes, the original examples would need to be rewritten like this:

pubconstgetWindowTitle=@extern(*constfn(window:*Window)callconv(.c)[*:0]constu8,.{.name="SDL_GetWindowTitle",.library_name="SDL3"},);pubconstsetWindowTitle=@extern(*constfn(window:*Window,title:[*:0]constu8)callconv(.c)bool,.{.name="SDL_SetWindowTitle",.library_name="SDL3"},);
pubfnadd(a:c_int,b:c_int)callconv(.c)c_int{returna+%b;}pubfnmul(a:c_int,b:c_int)callconv(.c)c_int{returna*%b;}comptime{@export(&add,.{.name="acme_add"});@export(&mul,.{.name="acme_mul"});}

Some important differences from before:

  • The functions must now explicitly be declared with callconv(.c). I would argue that explicitness is good here. Unless you have read the language spec carefully, it might not obvious that extern and export implicitly cause functions to get declared to use the .c calling convention (when discussing Zig with other users I have had to convince that extern/export work like this on more than one occasion).
  • set/getWindowTitle() are now pointers to external symbols, not aliases for them. You can technically (sometimes?) dereference them at comptime with @extern(...).* and turn them into aliases; the exact behavior is a bit unclear to me (see also: #21027). Regardless, I think having them be pointers reflects their nature as non-Zig-native symbols better, especially if they are defined as weak or interposable and can be overridden by the linker/loader.
  • For each decl, you now need to explicitly specify two names, one for the Zig decl and another for the external symbol. Once again I would argue that explicitness is good; here it helps reinforce to the user that there's a distinction between the Zig-native decl and the external symbol. Compare with other areas of the Zig toolchain that prefer explicitness over implicitness even if it can feel verbose/redundant to the user, such as package names <-> dependency names and module names <-> import names.

However, after looking at the above code you probably think that the const foo = @extern(*const fn () callconv(.c) void, .{ .name = "foo" }) form looks a lot more noisy and awkward compared to the simple extern fn foo() void form. I'm inclined to agree! Which brings us to the next part...

2. Replace the @extern type parameter with the inferred result type

In the recent years, the trend for builtins has been to favor result types over type parameters. Moving the type information from the first parameter to the result type immediately improves the situation by reducing visual clutter:

pubconstgetWindowTitle:*constfn(window:*Window)callconv(.c)[*:0]constu8=@extern(.{.name="SDL_GetWindowTitle",.library_name="SDL3"});pubconstsetWindowTitle:*constfn(window:*Window,title:[*:0]constu8)callconv(.c)bool=@extern(.{.name="SDL_SetWindowTitle",.library_name="SDL3"});

But can we do even better?

3. Move the name fields of std.builtin.Extern/ExportOptions to the parameter list of @extern and @export

If you're referencing external symbols from a library, or if you're exporting your own symbols to external code, it's likely that you will want to use the same options for most/all symbols. However, annoyingly, the name fields of std.builtin.Extern/ExportOptions are used to specify the unique symbol name, which means that you can't define a common options instance and reuse it for all @extern/@export uses. Instead you will need to assign each field explicitly every time.

If we move the symbol name from the name field of the options struct to the @extern/@export parameter list, we can conveniently reuse common options and reduce visual clutter for the reader:

constSDL3:std.builtin.ExternOptions=.{.library_name="SDL3"};pubconstgetWindowTitle:*constfn(window:*Window)callconv(.c)[*:0]constu8=@extern("SDL_GetWindowTitle",SDL3);pubconstsetWindowTitle:*constfn(window:*Window,title:[*:0]constu8)callconv(.c)bool=@extern("SDL_SetWindowTitle",SDL3);
pubfnadd(a:c_int,b:c_int)callconv(.c)c_int{returna+%b;}pubfnmul(a:c_int,b:c_int)callconv(.c)c_int{returna*%b;}comptime{// Setting some options to help illustrate the point.constoptions:std.builtin.ExportOptions=.{.linkage=.weak,.visibility=.protected};@export(&add,"acme_add",options);@export(&mul,"acme_mul",options);}

Both builtins, @extern in particular, now look quite nice and ergonomic to use!

4. Rename @extern and @export to @externImport and @externExport

This is a bit different from the above, but please hear me out.

If you didn't know anything about Zig and saw the names of the builtins @import and @export in isolation, you would probably naturally assume that they are related. After all, they form an antonym pair, and languages like JavaScript use import and export for what you would assume, and in MSVC-flavored C you have __declspec(dllimport) and __declspec(dllexport). However, in actuality, @import and @export are used for two very different kinds of binding/linking: @import only deals with things within the same Zig compilation unit and @export deals with things from the outside world.

Similary, the extern and export keywords don't naturally appear to have anything to do with each other, when in actuality they are sort of the two sides of the same kind of linking. In particular, "this thing is defined elsewhere" is not the only logical conclusion for the meaning of the extern keyword and if you don't have prior familarity with the extern keyword from C it could just as well mean "make this thing available externally". (In fact I still frequently find myself not paying enough attention and typing extern fn instead of export fn.)

Therefore, I would like suggest renaming @extern and @export to @externImport and @externExport respectively, to make it clear and explicit to the user that

  • the builtins deal with external code, not Zig code internal to the compilation unit, and
  • which side (Zig or external code) provides the symbol.

In other contexts in Zig, the term "extern" means "compatible with the C ABI". @externImport for "receive something from the outside world" and @externExport for "provide something to the outside world" feels intuitive to me.

There might be other candidates for names that would work just as well; however, having the terms "import" and "export" in the names makes the builtins map nicely and intuitively to object formats like PE/COFF and Wasm which use separate tables for dynamic imports/exports.

Lastly, in case you're concerned that @externImport appears similar to but doesn't have much in common with how @import works, I'd like to remind you that @import makes use of result types when importing ZON files, e.g. const config: Config = @import("config.zon"), so they are not as dissimilar as they would have been a few versions back.

With all of the above changes, the final revision of the example becomes this:

constSDL3:std.builtin.externs.ImportOptions=.{.library_name="SDL3"};pubconstgetWindowTitle:*constfn(window:*Window)callconv(.c)[*:0]constu8=@externImport("SDL_GetWindowTitle",SDL3);pubconstsetWindowTitle:*constfn(window:*Window,title:[*:0]constu8)callconv(.c)bool=@externImport("SDL_SetWindowTitle",SDL3);
pubfnadd(a:c_int,b:c_int)callconv(.c)c_int{returna+%b;}pubfnmul(a:c_int,b:c_int)callconv(.c)c_int{returna*%b;}comptime{constoptions:std.builtin.externs.ExportOptions=.{.linkage=.weak,.visibility=.protected};@externExport(&add,"acme_add",options);@externExport(&mul,"acme_mul",options);}

(I chose to rename the std.builtin types to std.builtin.externs.Import/ExportOptions per the Avoid Redundant Names in Fully-Qualified Namespaces advice, but this is only a suggestion.)

Other thoughts

This proposal would eliminate the need for the suggested linkname("foo") and linklibrary("bar") proposed in #19999 and #20006 respectively.

It would probably be possible to have zig fmt automatically rewrite most uses of extern fn in the wild into the corresponding @externImport/@externExport variants without causing too much breakage, since function pointers can be (need to be) called without explicitly dereferencing. extern const/var is a bit more prone to breakage because the decls would now be pointers instead of aliases, but external variables are much less common than external functions. export fn/const/var should be fairly simple to automatically rewrite into a comptime block with an @externExport statement.

**TL;DR:** This proposal concerns the overlapping functionality between `extern`/`export` decls and the `@extern`/`@export` builtins, and the ergonomics of the latter. It is divided up into four smaller sub-proposals which can each be accepted or rejected on an individual basis, but which I believe would synergize well were they all to be accepted. 1. Remove `extern` and `export` decls in favor of the more powerful `@extern` and `@export` builtins 2. Replace the `@extern` type parameter with the inferred result type 3. Move the `name` fields of `std.builtin.Extern/ExportOptions` to the parameter list of `@extern` and `@export` 4. Rename `@extern` and `@export` to `@externImport` and `@externExport` As for the "why", doing these would help eliminate a subtle footgun, while also making the builtins easier and more intuitive for users to use. These ideas were briefly discussed on Zulip and I was given the go-ahead to file a language proposal. ### Status quo example To help illustrate how the proposed changes would affect Zig code, I'll use the following examples of using `extern` and `export` and progressively update them with each sub-proposal: ```zig // Import external functions from the SDL3 library, but "ziggify" the function names. pub const getWindowTitle = SDL_GetWindowTitle; extern "SDL3" fn SDL_GetWindowTitle(window: *Window) [*:0]const u8; pub const setWindowTitle = SDL_SetWindowTitle; extern "SDL3" fn SDL_SetWindowTitle(window: *Window, title: [*:0]const u8) bool; ``` ```zig // Export functions to external code with an 'acme_' prefix, but use non-prefixed names internally. pub const add = acme_add; export fn acme_add(a: c_int, b: c_int) c_int { return a +% b; } pub const mul = acme_mul; export fn acme_mul(a: c_int, b: c_int) c_int { return a *% b; } ``` ### 1\. Remove `extern` and `export` decls in favor of the more powerful `@extern` and `@export` builtins Problem: - Having both `extern`/`export` decls and the `@extern`/`@export` builtins means that there are two different ways of doing the same things. `extern`/`export` decls are strictly less powerful and flexible than the `@extern`/`@export` builtins, only supporting the default linkage and visibility options. If you start off by defining a bunch of `extern` functions and variables, and later realize that you need to change certain options, you will need to rewrite your `extern` decls into a form that uses the `@extern` builtin. This isn't difficult, but can be a bit annoying. Had you used `@extern` from the start, you would only need to trivially adjust the relevant `std.builtin.ExternOptions` fields. - A slightly greater problem is that `extern` and `@extern` are not exactly equivalent. `extern var a: *T` is a special name binding syntax that will make the Zig variable `a` *an alias for* the external symbol *a*, and the expression `&a` will return the address of the symbol *a*. `const b = @extern(*T, .{ .name = "a" })` on the other hand will make the Zig variable `b` *a pointer to* the external symbol *a*, and the expression `&b` will return the address of the Zig variable `b`, **not** the the address of the symbol *a*. In other words, `@extern` adds another level of indirection compared to `extern`. This can be a bit of a footgun. If you already have `extern var x: *i32`, and you need to rewrite it into the `@extern(**i32, .{ .name = "x" })` form, it is very easy to forget to add the second asterisk and end up being one whole level off. [Here is a Ziggit thread from a while back where a user was bitten by this.](https://ziggit.dev/t/global-assembly-and-how-to-use-an-extern-to-access-asm-label/11688) Similar case: [#19515](https://github.com/ziglang/zig/issues/19515). - [The `extern "foo"` syntax is easy to get confused with C++'s `extern "C"`.](https://github.com/ziglang/zig/issues/19999#issuecomment-2119247194) This is obviously a relatively minor issue, but nevertheless it can be a stumbling block for beginners coming to Zig from C/C++. To make the language surface slightly smaller and to disarm footguns, I propose that the `extern/export fn/const/var` syntax be removed, leaving the `@extern` builtin as the only way of declaring a reference to an external symbol and the `export` builtin as the only way of making a Zig declaration visible to external code. The `extern` keyword will still retain its use in `extern struct/union`, but the `export` keyword no longer has a use and will be deleted from the language. With these changes, the original examples would need to be rewritten like this: ```zig pub const getWindowTitle = @extern( *const fn (window: *Window) callconv(.c) [*:0]const u8, .{ .name = "SDL_GetWindowTitle", .library_name = "SDL3" }, ); pub const setWindowTitle = @extern( *const fn (window: *Window, title: [*:0]const u8) callconv(.c) bool, .{ .name = "SDL_SetWindowTitle", .library_name = "SDL3" }, ); ``` ```zig pub fn add(a: c_int, b: c_int) callconv(.c) c_int { return a +% b; } pub fn mul(a: c_int, b: c_int) callconv(.c) c_int { return a *% b; } comptime { @export(&add, .{ .name = "acme_add" }); @export(&mul, .{ .name = "acme_mul" }); } ``` Some important differences from before: - The functions must now explicitly be declared with `callconv(.c)`. I would argue that explicitness is good here. Unless you have read the language spec carefully, it might not obvious that `extern` and `export` implicitly cause functions to get declared to use the `.c` calling convention (when discussing Zig with other users I have had to convince that `extern`/`export` work like this on more than one occasion). - `set/getWindowTitle()` are now pointers to external symbols, not aliases for them. You can technically (sometimes?) dereference them at comptime with `@extern(...).*` and turn them into aliases; the exact behavior is a bit unclear to me (see also: [#21027](https://github.com/ziglang/zig/issues/21027)). Regardless, I think having them be pointers reflects their nature as non-Zig-native symbols better, especially if they are defined as weak or interposable and can be overridden by the linker/loader. - For each decl, you now need to explicitly specify two names, one for the Zig decl and another for the external symbol. Once again I would argue that explicitness is good; here it helps reinforce to the user that there's a distinction between the Zig-native decl and the external symbol. Compare with other areas of the Zig toolchain that prefer explicitness over implicitness even if it can feel verbose/redundant to the user, such as package names <-> dependency names and module names <-> import names. However, after looking at the above code you probably think that the `const foo = @extern(*const fn () callconv(.c) void, .{ .name = "foo" })` form looks a lot more noisy and awkward compared to the simple `extern fn foo() void` form. I'm inclined to agree! Which brings us to the next part... ### 2\. Replace the `@extern` type parameter with the inferred result type In the recent years, the trend for builtins has been to favor result types over type parameters. Moving the type information from the first parameter to the result type immediately improves the situation by reducing visual clutter: ```zig pub const getWindowTitle: *const fn (window: *Window) callconv(.c) [*:0]const u8 = @extern(.{ .name = "SDL_GetWindowTitle", .library_name = "SDL3" }); pub const setWindowTitle: *const fn (window: *Window, title: [*:0]const u8) callconv(.c) bool = @extern(.{ .name = "SDL_SetWindowTitle", .library_name = "SDL3" }); ``` But can we do even better? ### 3\. Move the `name` fields of `std.builtin.Extern/ExportOptions` to the parameter list of `@extern` and `@export` If you're referencing external symbols from a library, or if you're exporting your own symbols to external code, it's likely that you will want to use the same options for most/all symbols. However, annoyingly, the `name` fields of `std.builtin.Extern/ExportOptions` are used to specify the unique symbol name, which means that you can't define a common options instance and reuse it for all `@extern`/`@export` uses. Instead you will need to assign each field explicitly every time. If we move the symbol name from the `name` field of the options struct to the `@extern`/`@export` parameter list, we can conveniently reuse common options and reduce visual clutter for the reader: ```zig const SDL3: std.builtin.ExternOptions = .{ .library_name = "SDL3" }; pub const getWindowTitle: *const fn (window: *Window) callconv(.c) [*:0]const u8 = @extern("SDL_GetWindowTitle", SDL3); pub const setWindowTitle: *const fn (window: *Window, title: [*:0]const u8) callconv(.c) bool = @extern("SDL_SetWindowTitle", SDL3); ``` ```zig pub fn add(a: c_int, b: c_int) callconv(.c) c_int { return a +% b; } pub fn mul(a: c_int, b: c_int) callconv(.c) c_int { return a *% b; } comptime { // Setting some options to help illustrate the point. const options: std.builtin.ExportOptions = .{ .linkage = .weak, .visibility = .protected }; @export(&add, "acme_add", options); @export(&mul, "acme_mul", options); } ``` Both builtins, `@extern` in particular, now look quite nice and ergonomic to use! ### 4\. Rename `@extern` and `@export` to `@externImport` and `@externExport` This is a bit different from the above, but please hear me out. If you didn't know anything about Zig and saw the names of the builtins `@import` and `@export` in isolation, you would probably naturally assume that they are related. After all, they form an antonym pair, and languages like JavaScript use `import` and `export` for what you would assume, and in MSVC-flavored C you have `__declspec(dllimport)` and `__declspec(dllexport)`. However, in actuality, `@import` and `@export` are used for two *very* different kinds of binding/linking: `@import` only deals with things within the same Zig compilation unit and `@export` deals with things from the outside world. Similary, the `extern` and `export` keywords don't naturally appear to have anything to do with each other, when in actuality they are sort of the two sides of the same kind of linking. In particular, "this thing is defined elsewhere" is not the only logical conclusion for the meaning of the `extern` keyword and if you don't have prior familarity with the `extern` keyword from C it could just as well mean "make this thing available externally". (In fact I still frequently find myself not paying enough attention and typing `extern fn` instead of `export fn`.) Therefore, I would like suggest renaming `@extern` and `@export` to `@externImport` and `@externExport` respectively, to make it clear and explicit to the user that - the builtins deal with external code, not Zig code internal to the compilation unit, and - which side (Zig or external code) provides the symbol. In other contexts in Zig, the term "extern" means "compatible with the C ABI". `@externImport` for "receive something from the outside world" and `@externExport` for "provide something to the outside world" feels intuitive to me. There might be other candidates for names that would work just as well; however, having the terms "import" and "export" in the names makes the builtins map nicely and intuitively to object formats like PE/COFF and Wasm which use separate tables for dynamic imports/exports. Lastly, in case you're concerned that `@externImport` appears similar to but doesn't have much in common with how `@import` works, I'd like to remind you that `@import` makes use of result types when importing ZON files, e.g. `const config: Config = @import("config.zon")`, so they are not *as* dissimilar as they would have been a few versions back. With all of the above changes, the final revision of the example becomes this: ```zig const SDL3: std.builtin.externs.ImportOptions = .{ .library_name = "SDL3" }; pub const getWindowTitle: *const fn (window: *Window) callconv(.c) [*:0]const u8 = @externImport("SDL_GetWindowTitle", SDL3); pub const setWindowTitle: *const fn (window: *Window, title: [*:0]const u8) callconv(.c) bool = @externImport("SDL_SetWindowTitle", SDL3); ``` ```zig pub fn add(a: c_int, b: c_int) callconv(.c) c_int { return a +% b; } pub fn mul(a: c_int, b: c_int) callconv(.c) c_int { return a *% b; } comptime { const options: std.builtin.externs.ExportOptions = .{ .linkage = .weak, .visibility = .protected }; @externExport(&add, "acme_add", options); @externExport(&mul, "acme_mul", options); } ``` (I chose to rename the `std.builtin` types to `std.builtin.externs.Import/ExportOptions` per the [Avoid Redundant Names in Fully-Qualified Namespaces](https://ziglang.org/documentation/master/#Avoid-Redundant-Names-in-Fully-Qualified-Namespaces) advice, but this is only a suggestion.) ### Other thoughts This proposal would eliminate the need for the suggested `linkname("foo")` and `linklibrary("bar")` proposed in [#19999](https://github.com/ziglang/zig/issues/19999) and [#20006](https://github.com/ziglang/zig/issues/20006) respectively. It would probably be possible to have `zig fmt` automatically rewrite most uses of `extern fn` in the wild into the corresponding `@externImport`/`@externExport` variants without causing too much breakage, since function pointers can be (need to be) called without explicitly dereferencing. `extern const/var` is a bit more prone to breakage because the decls would now be pointers instead of aliases, but external variables are much less common than external functions. `export fn/const/var` should be fairly simple to automatically rewrite into a `comptime` block with an `@externExport` statement.
Contributor
Copy link

+1. Have run into the described footgun wrt pointer types fairly often in osdev where extern var is way more common, and the only place that this makes more annoying for me (as long as items 2 and 3 are included) is one im planning to refactor anyway because the use of export var there results in an incorrect optimization by the compiler that took me a long time to debug (for those curious, the compiler may optimize out accesses to export vars with default initialization in release builds of static executables if the vars are not modified by zig code, resulting in an inability to observe modifications made externally as the export should imo imply is possible)

edited as i just made the same extern/export mixup error whoops

+1. Have run into the described footgun wrt pointer types fairly often in osdev where `extern var` is way more common, and the only place that this makes more annoying for me (as long as items 2 and 3 are included) is one im planning to refactor anyway because the use of export var there results in an incorrect optimization by the compiler that took me a long time to debug (for those curious, the compiler may optimize out accesses to `export var`s with default initialization in release builds of static executables if the vars are not modified by zig code, resulting in an inability to observe modifications made externally as the export should imo imply is possible) edited as i just made the same extern/export mixup error whoops
Contributor
Copy link

I like this, as extern and @extern being non-orthogonal language features has bothered me a for a while. However, there's one part of this proposal I'm concerned about:

  1. Move the name fields of std.builtin.Extern/ExportOptions to the parameter list of @extern and @export

Currently, the SPIR-V target uses the @extern builtin to declare bindings to the outside world (and I think semantically this makes sense). Now, SPIR-V declares bindings not via a name, but via an integer. So, no name for an external symbol even exists, and currently one must pass in a dummy one. I was hoping to in the future make this more ergonomic for SPIR-V by making the name field of that struct optional. This proposal would be counter to that.

(Now there are other problems with std.builtin.Extern of this sort, that certain options don't make sense for certain targets. The status quo, which I'm continuing here, is to just have defaults and ignore fields when they don't make sense for the target. But I'm open to a more principled solution).

Maybe if the name of the field is not set it uses the variable name? Though that might be a little weird.

I like this, as `extern` and `@extern` being non-orthogonal language features has bothered me a for a while. However, there's one part of this proposal I'm concerned about: > 3. Move the `name` fields of `std.builtin.Extern`/`ExportOptions` to the parameter list of `@extern` and `@export` Currently, the SPIR-V target uses the `@extern` builtin to declare bindings to the outside world (and I think semantically this makes sense). Now, SPIR-V declares bindings not via a name, but via an integer. So, no name for an external symbol even exists, and currently one must pass in a dummy one. I was hoping to in the future make this more ergonomic for SPIR-V by making the name field of that struct optional. This proposal would be counter to that. (Now there are other problems with `std.builtin.Extern` of this sort, that certain options don't make sense for certain targets. The status quo, which I'm continuing here, is to just have defaults and ignore fields when they don't make sense for the target. But I'm open to a more principled solution). Maybe if the name of the field is not set it uses the variable name? Though that might be a little weird.
Owner
Copy link

@ashpil that doesn't really seem like a problem to me. I see two paths forward:

  • Have the SPIR-V backend parse the "name" field as an integer; so you do something like @externImport("1", .{}). Admittedly this would be a little awkward if the binding index was generated by comptime logic (you'd have to use something like std.fmt.comptimePrint). But to be honest, I think my second idea is better anyway...
  • Just introduce a different (SPIR-V specific) builtin for this concept. Yes, bindings have a lot in common with extern imports, but there are differences, so why try to force the two into the same box? There's no concrete benefit I'm aware of(?), and it just makes the experience a little clunkier on both sides. A large part of the reason builtin function call syntax is what it is, is that it makes introducing new builtins always non-breaking (for instance we don't need to reserve a new keyword). For pretty much the same reason, having target-specific builtins where appropriate is fine!
@ashpil that doesn't really seem like a problem to me. I see two paths forward: * Have the SPIR-V backend parse the "name" field as an integer; so you do something like `@externImport("1", .{})`. Admittedly this would be a little awkward if the binding index was generated by comptime logic (you'd have to use something like `std.fmt.comptimePrint`). But to be honest, I think my second idea is better anyway... * Just introduce a different (SPIR-V specific) builtin for this concept. Yes, bindings have a lot in common with extern imports, but there *are* differences, so why try to force the two into the same box? There's no concrete benefit I'm aware of(?), and it just makes the experience a little clunkier on *both* sides. A large part of the reason builtin function call syntax is what it is, is that it makes introducing new builtins always non-breaking (for instance we don't need to reserve a new keyword). For pretty much the same reason, having target-specific builtins where appropriate is fine!
Contributor
Copy link

@mlugg regarding the second idea:

That makes a lot of sense to me, and I think that's probably the way forward, as long as this is done consistently. As an example, it's unclear what the is_thread_local field means for e.g., wasm. Does this mean wasm should have its own builtin here? Where is the line drawn?

It is also to nice to see the one specific builtin and always know that it means that the program is getting input from something else, independent of the target. And I can imagine that you might want to e.g., build a program that when compiled to SPIR-V, something acts as a binding, but when compiled to a CPU language, it acts as a regular extern symbol (still possible in your second path forward, yes, but not as directly).

I'm not opposed to any particular solution, as long as there is consistency.

@mlugg regarding the second idea: That makes a lot of sense to me, and I think that's probably the way forward, as long as this is done consistently. As an example, it's unclear what the `is_thread_local` field means for e.g., wasm. Does this mean `wasm` should have its own builtin here? Where is the line drawn? It is also to nice to see the one specific builtin and always know that it means that the program is getting input from something else, independent of the target. And I can imagine that you might want to e.g., build a program that when compiled to SPIR-V, something acts as a binding, but when compiled to a CPU language, it acts as a regular extern symbol (still possible in your second path forward, yes, but not as directly). I'm not opposed to any particular solution, as long as there is consistency.

@ashpil wrote in #30873 (comment):

It is also to nice to see the one specific builtin and always know that it means that the program is getting input from something else, independent of the target. And I can imagine that you might want to e.g., build a program that when compiled to SPIR-V, something acts as a binding, but when compiled to a CPU language, it acts as a regular extern symbol (still possible in your second path forward, yes, but not as directly).

While I agree with you that being able to run the same code on GPU and on the CPU is invaluable, one has to think about how to actually pass the arguments down to the 'shader': in CPU land the concept of extern is linked to global variables, while on the GPU the value of these global variables are in fact stored somewhere in the command buffer for the GPU to execute the shader later. From the point of view of the CPU it has called a function with arguments, while the GPU code sees global variables. When making a function for the CPU you want to use arguments directly, passing them via global will only cause troubles when running multiple threads with the same kernels and different bindings, debugging since anyone could access this global state everywhere, ...

I therefore believe running GPU code on the CPU requires us to write our argument passing logic twice: one with extern globals on the GPU and one with function arguments on the CPU. Then we can call our shader.

My point being: actually you don't want to re-use as is your shader structure: you need globals variables in your shaders and function arguments on the CPU.

@ashpil wrote in https://codeberg.org/ziglang/zig/issues/30873#issuecomment-11120273: > It is also to nice to see the one specific builtin and always know that it means that the program is getting input from something else, independent of the target. And I can imagine that you might want to e.g., build a program that when compiled to SPIR-V, something acts as a binding, but when compiled to a CPU language, it acts as a regular extern symbol (still possible in your second path forward, yes, but not as directly). While I agree with you that being able to run the same code on GPU and on the CPU is invaluable, one has to think about how to actually pass the arguments down to the 'shader': in CPU land the concept of extern is linked to global variables, while on the GPU the value of these global variables are in fact stored somewhere in the command buffer for the GPU to execute the shader later. From the point of view of the CPU it has called a function with arguments, while the GPU code sees global variables. When making a function for the CPU you want to use arguments directly, passing them via global will only cause troubles when running multiple threads with the same kernels and different bindings, debugging since anyone could access this global state everywhere, ... I therefore believe running GPU code on the CPU requires us to write our argument passing logic twice: one with extern globals on the GPU and one with function arguments on the CPU. Then we can call our shader. My point being: actually you don't want to re-use as is your shader structure: you need globals variables in your shaders and function arguments on the CPU.
andrewrk added this to the Urgent milestone 2026年05月02日 00:29:49 +02:00
To be considered along with this proposal: * https://github.com/ziglang/zig/issues/19999 * https://github.com/ziglang/zig/issues/20006
Contributor
Copy link

its been mentioned elsewhere that this proposal could result in the removal of linksection - id like to argue against that as I'm getting a lot of benefit from using linksection in places where export-ing a function would be illegal.

in my specific case, a large chunk of my program is called once during init and never again, so to save memory those parts are placed in a linksection(".init") which can then be unmapped and freed after init is comlpete.
the functions involved cannot be placed in a section using either @export (or the export keyword) as they take normal zig structs and slices and return error unions and thus cannot use an exportable callconv

its been mentioned elsewhere that this proposal could result in the removal of `linksection` - id like to argue against that as I'm getting a lot of benefit from using linksection in places where export-ing a function would be illegal. in my specific case, a large chunk of my program is called once during init and never again, so to save memory those parts are placed in a `linksection(".init")` which can then be unmapped and freed after init is comlpete. the functions involved cannot be placed in a section using either `@export` (or the `export` keyword) as they take normal zig structs and slices and return error unions and thus cannot use an exportable callconv
Author
Contributor
Copy link

When this proposal was first discussed on Zulip a couple of months ago it was pointed out that "extern" might not be the most descriptive name because it implies crossing some kind of ABI/FFI boundary, which isn't true as the feature can be used for symbols only visible within the same compilation unit.

The names @importSymbol/@exportSymbol were proposed as an alternative, which I think I prefer over the originally suggested names.

When this proposal was first discussed on Zulip a couple of months ago it was pointed out that "extern" might not be the most descriptive name because it implies crossing some kind of ABI/FFI boundary, which isn't true as the feature can be used for symbols only visible within the same compilation unit. The names `@importSymbol`/`@exportSymbol` were proposed as an alternative, which I think I prefer over the originally suggested names.

@Khitiara wrote in #30873 (comment):

its been mentioned elsewhere that this proposal could result in the removal of linksection - id like to argue against that as I'm getting a lot of benefit from using linksection in places where export-ing a function would be illegal.

in my specific case, a large chunk of my program is called once during init and never again, so to save memory those parts are placed in a linksection(".init") which can then be unmapped and freed after init is comlpete. the functions involved cannot be placed in a section using either @export (or the export keyword) as they take normal zig structs and slices and return error unions and thus cannot use an exportable callconv

I can just second that.

It's crucial to be able to place functions and data into linksections in embedded/os world.

Without linksection or an equivalent, i would not be able to use Zig for Ashet OS anymore, as i need to be able to place data and functions precisely into SRAM regions to achieve my performance goals. Without linksection, i would not even be able to initialize my external RAM.

@Khitiara wrote in https://codeberg.org/ziglang/zig/issues/30873#issuecomment-14162429: > its been mentioned elsewhere that this proposal could result in the removal of `linksection` - id like to argue against that as I'm getting a lot of benefit from using linksection in places where export-ing a function would be illegal. > > in my specific case, a large chunk of my program is called once during init and never again, so to save memory those parts are placed in a `linksection(".init")` which can then be unmapped and freed after init is comlpete. the functions involved cannot be placed in a section using either `@export` (or the `export` keyword) as they take normal zig structs and slices and return error unions and thus cannot use an exportable callconv I can just second that. It's crucial to be able to place functions and data into linksections in embedded/os world. Without `linksection` or an equivalent, i would not be able to use Zig for Ashet OS anymore, as i need to be able to place data and functions precisely into SRAM regions to achieve my performance goals. Without linksection, i would not even be able to initialize my external RAM.

Noting that accepting this would eliminate this issue: Language inconsistency: aliasing externs

Noting that accepting this would eliminate this issue: [Language inconsistency: aliasing externs](https://github.com/ziglang/zig/issues/21027)
Relevant to this proposal: https://ziggit.dev/t/export-a-thread-local-variable-as-a-weak-symbol/16149

Something I think we should consider is part of this, is that the ergonomics of @extern when used with is_dll_import are awkward. Perhaps this would be a opportunity to improve this as well. This specifically affects Windows use cases.

Consider this example extracted from the new Coff linker tests:

foo.dll:

exportfnfoo1()callconv(.c)u64{return0x1122334411223344;}exportfnfoo2()callconv(.c)u64{return0xaabbccddaabbccdd;}

foo.exe:

externfnfoo1()u64;pubfnmain()!u8{constfoo2=@extern(*constfn()callconv(.c)u64,.{.name="foo2",.is_dll_import=true},);return@intFromBool(0xbbde0021bbde0021!=foo1()+foo2());}

In the case of foo1, because it's not marked is_dll_import, when the linker sees that it's coming from a dll, it generates a thunk containing jmp [<iat_entry_for_foo1>]. The thunk has a comptime known location, but indirects through the IAT at runtime.

Since foo2 is runtime known it can't be in global scope. foo2 is runtime known because this is actually a load via the IAT: ie mov [<iat entry for foo2>]. However, the address of the IAT entry itself (that will eventually contain the address of foo2, pointing into foo.dll at runtime), is comptime known, the linker reserves a slot.

This means there is actually no syntax in the language to achieve the optimization of calling foo2 indirectly through the IAT, ie: call [<iat_entry_for_foo2>]. It it will always load the address from the IAT (result of the @extern call), then use a separate call instruction to call it.

A similar problem exists for data symbols as well, they can't exist in global scope because they are runtime known.

This means that you can't easily write code that works with both dynamic and static linked version of dependencies. For function imports, it will work both ways but be suboptimal in the dll cases (uses thunks), for data symbols you have to use is_dll_import, so the @extern call has to be at runtime

This then forces all the usages of imported data symbols to be at runtime, ie. consider this example from zig-gamedev/zmesh which supports building the C library either as shared or static:

pubfninit(alloc:std.mem.Allocator,io:std.Io)void{std.debug.assert(state==null);state=.{.io=io,.mem_allocator=alloc,.mem_allocations=.init(alloc),};state.?.mem_allocations.ensureTotalCapacity(32)catchunreachable;constzmeshMallocPtr=@extern(*?*constfn(size:usize)callconv(.c)?*anyopaque,.{.name="zmeshMallocPtr",.is_dll_import=options.shared,});constzmeshCallocPtr=@extern(*?*constfn(num:usize,size:usize)callconv(.c)?*anyopaque,.{.name="zmeshCallocPtr",.is_dll_import=options.shared,});constzmeshReallocPtr=@extern(*?*constfn(ptr:?*anyopaque,size:usize)callconv(.c)?*anyopaque,.{.name="zmeshReallocPtr",.is_dll_import=options.shared,});constzmeshFreePtr=@extern(*?*constfn(maybe_ptr:?*anyopaque)callconv(.c)void,.{.name="zmeshFreePtr",.is_dll_import=options.shared,});zmeshMallocPtr.*=zmeshMalloc;zmeshCallocPtr.*=zmeshCalloc;zmeshReallocPtr.*=zmeshRealloc;zmeshFreePtr.*=zmeshFree;meshopt_setAllocator(zmeshMalloc,zmeshFree);}

If we allowed is_dll_import externs to exist in global scope, but cause the result of any operation that takes their address to be runtime known, then I think we could achieve:

  • Codegen optimal indirect calls through the IAT for code symbols
  • Avoid the need to use the full @extern(...) call every time you want to refer to a data symbol. Right now you need to create a function that just returns the result of @extern(...) if you want to share the definition across your codebase.

I don't really have a solid idea on how to actually achieve that, but those are some goals I'd be interested in.

It also almost calls into question the point of is_dll_import being an option at all. For example, if we're asking for a symbol from a specific library, and the frontend knows that library is a dll, then it can convert all the externs from that library to is_dll_import automatically.

Background info: https://learn.microsoft.com/en-us/cpp/build/importing-function-calls-using-declspec-dllimport?view=msvc-170

Something I think we should consider is part of this, is that the ergonomics of `@extern` when used with `is_dll_import` are awkward. Perhaps this would be a opportunity to improve this as well. This specifically affects Windows use cases. Consider this example extracted from the new Coff linker tests: foo.dll: ```zig export fn foo1() callconv(.c) u64 { return 0x1122334411223344; } export fn foo2() callconv(.c) u64 { return 0xaabbccddaabbccdd; } ``` foo.exe: ```zig extern fn foo1() u64; pub fn main() !u8 { const foo2 = @extern( *const fn () callconv(.c) u64, .{ .name = "foo2", .is_dll_import = true }, ); return @intFromBool(0xbbde0021bbde0021 != foo1() + foo2()); } ``` In the case of `foo1`, because it's not marked `is_dll_import`, when the linker sees that it's coming from a dll, it generates a thunk containing `jmp [<iat_entry_for_foo1>]`. The thunk has a comptime known location, but indirects through the IAT at runtime. Since `foo2` is runtime known it can't be in global scope. `foo2` is runtime known because this is actually a load via the IAT: ie `mov [<iat entry for foo2>]`. However, the address of the IAT entry itself (that will eventually contain the address of `foo2`, pointing into `foo.dll` at runtime), is comptime known, the linker reserves a slot. This means there is actually no syntax in the language to achieve the optimization of calling `foo2` indirectly through the IAT, ie: `call [<iat_entry_for_foo2>]`. It it will always load the address from the IAT (result of the `@extern` call), then use a separate `call` instruction to call it. A similar problem exists for data symbols as well, they can't exist in global scope because they are runtime known. This means that you can't easily write code that works with both dynamic and static linked version of dependencies. For function imports, it will work both ways but be suboptimal in the dll cases (uses thunks), for data symbols you have to use `is_dll_import`, so the `@extern` call has to be at runtime This then forces all the usages of imported data symbols to be at runtime, ie. consider this example from `zig-gamedev/zmesh` which supports building the C library either as shared or static: ```zig pub fn init(alloc: std.mem.Allocator, io: std.Io) void { std.debug.assert(state == null); state = .{ .io = io, .mem_allocator = alloc, .mem_allocations = .init(alloc), }; state.?.mem_allocations.ensureTotalCapacity(32) catch unreachable; const zmeshMallocPtr = @extern(*?*const fn (size: usize) callconv(.c) ?*anyopaque, .{ .name = "zmeshMallocPtr", .is_dll_import = options.shared, }); const zmeshCallocPtr = @extern(*?*const fn (num: usize, size: usize) callconv(.c) ?*anyopaque, .{ .name = "zmeshCallocPtr", .is_dll_import = options.shared, }); const zmeshReallocPtr = @extern(*?*const fn (ptr: ?*anyopaque, size: usize) callconv(.c) ?*anyopaque, .{ .name = "zmeshReallocPtr", .is_dll_import = options.shared, }); const zmeshFreePtr = @extern(*?*const fn (maybe_ptr: ?*anyopaque) callconv(.c) void, .{ .name = "zmeshFreePtr", .is_dll_import = options.shared, }); zmeshMallocPtr.* = zmeshMalloc; zmeshCallocPtr.* = zmeshCalloc; zmeshReallocPtr.* = zmeshRealloc; zmeshFreePtr.* = zmeshFree; meshopt_setAllocator(zmeshMalloc, zmeshFree); } ``` If we allowed `is_dll_import` externs to exist in global scope, but cause the result of any operation that takes their address to be runtime known, then I think we could achieve: - Codegen optimal indirect calls through the IAT for code symbols - Avoid the need to use the full `@extern(...)` call every time you want to refer to a data symbol. Right now you need to create a function that just returns the result of `@extern(...)` if you want to share the definition across your codebase. I don't really have a solid idea on how to actually achieve that, but those are some goals I'd be interested in. It also almost calls into question the point of `is_dll_import` being an option at all. For example, if we're asking for a symbol from a specific library, and the frontend knows that library is a dll, then it can convert all the externs from that library to `is_dll_import` automatically. Background info: https://learn.microsoft.com/en-us/cpp/build/importing-function-calls-using-declspec-dllimport?view=msvc-170
Sign in to join this conversation.
No Branch/Tag specified
master
elfv2
spork8
restricted
0.16.x
panic-rewrite
parking-futex-lockfree
windows-Io-cleanup
Io-watch
ParseCommandLineOptions
windows-async-files
poll-ring
debug-file-leaks-differently
debug-file-leaks
ProcessPrng
elfv2-dyn
jobserver
threadtheft
io-threaded-no-queue
0.15.x
Io.net
comptime-allocator
restricted-function-pointers
cli
wasm-linker-writer
wrangle-writer-buffering
sha1-stream
async-await-demo
fixes
0.14.x
ast-node-methods
macos-debug-info
make-vs-configure
fuzz-macos
sans-aro
ArrayList-reserve
incr-bug
llvm-ir-nosanitize-metadata
ci-tarballs
ci-scripts
threadpool
0.12.x
new-pkg-hash
json-diagnostics
more-doctests
rework-comptime-mutation
0.11.x
ci-perf-comment
stage2-async
0.10.x
autofix
0.9.x
aro
hcs
0.8.x
0.7.x
0.16.0
0.15.2
0.15.1
0.15.0
0.14.1
0.14.0
0.12.1
0.13.0
0.12.0
0.11.0
0.10.1
0.10.0
0.9.1
0.9.0
0.8.1
0.8.0
0.7.1
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.0
0.1.1
0.1.0
Labels
Clear labels
abi/f32
abi/ilp32
abi/sf
accepted
This proposal is planned.
arch/21k
arch/6502
arch/aarch64
arch/alpha
arch/amdgcn
arch/arc
arch/arc32
arch/arc64
arch/arm
arch/avr
arch/bfin
arch/bpf
arch/colossus
arch/cris
arch/csky
arch/dlx
arch/epiphany
arch/fr30
arch/frv
arch/hexagon
arch/hppa
arch/hppa64
arch/ia64
arch/kalimba
arch/kvx
arch/lanai
arch/lm32
arch/loongarch32
arch/loongarch64
arch/m32r
arch/m68k
arch/m88k
arch/mcore
arch/microblaze
arch/mips
arch/mips64
arch/mmix
arch/moxie
arch/mrisc32
arch/msp430
arch/nds32
arch/ns32k
arch/nvptx
arch/or1k
arch/powerpc
arch/powerpc64
arch/propeller
arch/riscv32
arch/riscv64
arch/rl78
arch/rx
arch/s390x
arch/sh
arch/sparc
arch/sparc64
arch/spirv
arch/spu
arch/tricore
arch/v850
arch/vax
arch/vc4
arch/ve
arch/wasm
arch/x86
arch/x86_64
arch/xcore
arch/xtensa
autodoc
The web application for interactive documentation and generation of its assets.
backend/c
The C backend outputs C source code.
backend/llvm
The LLVM backend outputs an LLVM bitcode module.
backend/self-hosted
The self-hosted backends produce machine code directly.
binutils
Zig's included binary utilities: zig ar, zig dlltool, zig lib, zig ranlib, zig objcopy, and zig rc.
breaking
Implementing this issue could cause existing code to no longer compile or have different behavior.
build system
The Zig build system - zig build, std.Build, the build runner, and package management.
debug info
An issue related to debug information (e.g. DWARF) produced by the Zig compiler.
docs
An issue with documentation, e.g. the language reference or standard library doc comments.
error message
This issue points out an error message that is unhelpful and should be improved.
frontend
Tokenization, parsing, AstGen, ZonGen, Sema, Legalize, and Liveness.
fuzzing
An issue related to Zig's integrated fuzz testing.
incremental
Reuse of internal compiler state for faster compilation.
lib/c
This issue relates to Zig's libc implementation and/or vendored libcs.
lib/compiler-rt
This issue relates to Zig's compiler-rt library.
lib/cxx
This issue relates to Zig's vendored libc++ and/or libc++abi.
lib/std
This issue relates to Zig's standard library.
lib/tsan
This issue relates to Zig's vendored libtsan.
lib/ubsan-rt
This issue relates to Zig's ubsan-rt library.
lib/unwind
This issue relates to Zig's vendored libunwind.
linking
Zig's integrated object file and incremental linker.
miscompilation
The compiler reports success but produces semantically incorrect code.
os/android
os/contiki
os/dragonfly
os/driverkit
os/emscripten
os/freebsd
os/fuchsia
os/haiku
os/hermit
os/hurd
os/illumos
os/ios
os/linux
os/maccatalyst
os/macos
os/managarm
os/netbsd
os/ohos
os/openbsd
os/plan9
os/redox
os/rtems
os/serenity
os/tvos
os/uefi
os/visionos
os/wasi
os/watchos
os/windows
proposal
This issue suggests language modifications. If it also has the "accepted" label then it is planned.
release notes
This issue or pull request should be mentioned in the release notes.
testing
This issue is related to testing the compiler, standard library, or other parts of Zig.
zig cc
Zig as a drop-in C-family compiler.
zig fmt
The Zig source code formatter.
zig reduce
The Zig source code reduction tool.
bounty
https://ziglang.org/news/announcing-donor-bounties
bug
Observed behavior contradicts documented or intended behavior.
contributor-friendly
This issue is limited in scope and/or knowledge of project internals.
downstream
An issue with a third-party project that uses this project.
enhancement
Solving this issue will likely involve adding new logic or components to the codebase.
infra
An issue related to project infrastructure, e.g. continuous integration.
optimization
A task to improve performance and/or resource usage.
question
No questions on the issue tracker; use a community space instead.
regression
Something that used to work in a previous version stopped working
upstream
An issue with a third-party project that this project uses.
use case
Describes a real use case that is difficult or impossible, but does not propose a solution.
No labels
abi/f32
abi/ilp32
abi/sf
accepted
arch/21k
arch/6502
arch/aarch64
arch/alpha
arch/amdgcn
arch/arc
arch/arc32
arch/arc64
arch/arm
arch/avr
arch/bfin
arch/bpf
arch/colossus
arch/cris
arch/csky
arch/dlx
arch/epiphany
arch/fr30
arch/frv
arch/hexagon
arch/hppa
arch/hppa64
arch/ia64
arch/kalimba
arch/kvx
arch/lanai
arch/lm32
arch/loongarch32
arch/loongarch64
arch/m32r
arch/m68k
arch/m88k
arch/mcore
arch/microblaze
arch/mips
arch/mips64
arch/mmix
arch/moxie
arch/mrisc32
arch/msp430
arch/nds32
arch/ns32k
arch/nvptx
arch/or1k
arch/powerpc
arch/powerpc64
arch/propeller
arch/riscv32
arch/riscv64
arch/rl78
arch/rx
arch/s390x
arch/sh
arch/sparc
arch/sparc64
arch/spirv
arch/spu
arch/tricore
arch/v850
arch/vax
arch/vc4
arch/ve
arch/wasm
arch/x86
arch/x86_64
arch/xcore
arch/xtensa
autodoc
backend/c
backend/llvm
backend/self-hosted
binutils
breaking
build system
debug info
docs
error message
frontend
fuzzing
incremental
lib/c
lib/compiler-rt
lib/cxx
lib/std
lib/tsan
lib/ubsan-rt
lib/unwind
linking
miscompilation
os/android
os/contiki
os/dragonfly
os/driverkit
os/emscripten
os/freebsd
os/fuchsia
os/haiku
os/hermit
os/hurd
os/illumos
os/ios
os/linux
os/maccatalyst
os/macos
os/managarm
os/netbsd
os/ohos
os/openbsd
os/plan9
os/redox
os/rtems
os/serenity
os/tvos
os/uefi
os/visionos
os/wasi
os/watchos
os/windows
proposal
release notes
testing
zig cc
zig fmt
zig reduce
bounty
bug
contributor-friendly
downstream
enhancement
infra
optimization
question
regression
upstream
use case
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
9 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ziglang/zig#30873
Reference in a new issue
ziglang/zig
No description provided.
Delete branch "%!s()"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?