Producing relocatable ELF with Zig build system

Hi there! I’m trying to use the Zig build system and toolchain to replace my embedded/GameCube C++ project’s build tooling entirely. I’m nearly there, but I need to ultimately build a relocatable ELF library, so that it may be converted to GameCube’s native object format by another tool.

Basically I’d like to run:

zig ld.lld -T ogc.ld -r -e _prolog -u _prolog -u _epilog -u _unresolved -g --gc-sections zig-out/lib/libmod.a -o zig-out/lib/libmod.elf

Note the -r - I can’t seem to find a way to link a relocatable ELF via the build system APIs, for one. I wouldn’t mind needing to shell out to zig ld.lld in a run step explicitly if required, but I’m also not sure how to determine the path of the zig executable to use e.g. the one used to invoke zig build.

Any suggestions would be appreciated, thanks!

I don’t know too much about the linker command you use, but you can use b.addSystemCommand with b.graph.zig_exe.

It sounds like you’re looking for a Compile step which emits an object file (i.e. a relocatable) rather than an executable or library. Take a look at std.Build.addObject, it should do what you need.

(It’s recommended to avoid using zig ld.lld directly fwiw—that subcommand is an implementation detail and could disappear at any time!)

1 Like

Thanks for this, I still get objects/libraries/shared libraries mixed up more than I should.

std.Build.addObject() seems right, can/should I pass the remaining -e, -u, and --gc-sections flags?

obj.link_gc_sections = true;
obj.entry = .{ .symbol_name = "_prolog" };

Might need a compiler enhancement for the -u args. Just so I understand what’s the problem if you leave those off?

Also may I see ogc.ld?

Thanks for bringing this up. AFAIK you’re the first person to try to use Zig for GameCube and actually bothering to share the problems you ran into.

2 Likes

Might need a compiler enhancement for the -u args.

Nah, we already expose that functionality too. Here’s the std.Build snippet:

obj.forceUndefinedSymbol("_prolog");
obj.forceUndefinedSymbol("_epilog");
obj.forceUndefinedSymbol("_unresolved");
1 Like
  --force_undefined [name]       Specify the symbol must be defined for the link to succeed

Bit of an odd name and description there, isn’t it?

  -u SYMBOL, --undefined SYMBOL
                              Start with undefined reference to SYMBOL

ld --help not much better…

It’s an accurate description (it marks the symbol as initially undefined which means the linker is required to find a definition for the link to succeed—this is important because it means the linker will include objects from static libraries if the objects define those symbols), but I agree a confusing one. I think linker CLI is something Zig can easily improve on compared to traditional linker implementations. A better name for this flag would probably be something like --need-definition for instance.

1 Like

Sure! Here’s a buildable subset of the full project if you’re curious: https://codeberg.org/ComplexPlane/wsmod-sample

https://codeberg.org/ComplexPlane/wsmod-sample/src/branch/addLibrary/ogc.ld

ogc.ld is originally from the devkitpro-ppc toolchain, I appended the constructor/destructor symbols to the end.

Anyways, this config does seem to build+link correctly, but I did notice it bloats the final binary by ~10% compared to addLibrary() + zig ld.lld:

mod.link_gc_sections = true;
mod.link_function_sections = true;
mod.link_data_sections = true;
mod.entry = .{ .symbol_name = "_prolog" };
mod.forceUndefinedSymbol("_prolog");
mod.forceUndefinedSymbol("_epilog");
mod.forceUndefinedSymbol("_unresolved");
mod.setLinkerScript(b.path("ogc.ld"));

You can verify this yourself by comparing zig build && ./todo.sh on the main branch vs. addLibrary branch.