Skip to content

Instantly share code, notes, and snippets.

@charlesastaylor
Last active May 31, 2025 14:30
Show Gist options
  • Save charlesastaylor/1af62766647ee13b9353f04fffe8472c to your computer and use it in GitHub Desktop.
Save charlesastaylor/1af62766647ee13b9353f04fffe8472c to your computer and use it in GitHub Desktop.
Jai metaprogram plugin for linking with RAD Linker
//
// Metaprogram plugin for linking with RAD Linker - https://github.com/EpicGamesExt/raddebugger.
//
// Usage: `jai my_program.jai +RAD_Linker`. The link command is also exposed to be used in custom metaprogram.
//
// By default we expect to find radlink.exe in your path. When using the plugin you can provide a custom path with the
// -path option. When using run_rad_link_command from another metaprogram this can be overriden with the linker_path parameter.
// Or you can always just edit this file to your path!
LINKER_PATH :: "radlink.exe";
// NOTE(Charles): With radlink v0.9.19-alpha I was getting "Error(002): /RAD_LINK_VER: unable to parse minor version" even
// though we are not using that option. I tried looking at code to see whats up, didn't seem like there was a reason for it.
// Just rolling back to v0.9.18-alpha fixed it. I guess I should report this?
// NOTE(Charles): Simps example.jai program causes a crash in radlink, I should maybe also report this. Some other module
// examples do succesfully work though, eg Curls examples.
//
// I have reported the crash issue - https://github.com/EpicGamesExt/raddebugger/issues/514. As pointed about Yasin in the
// comments there the issue seems to be resolved by removing libcmt.lib from link line so I've also added a hack to do that.
run_rad_link_command :: (phase: *Message_Phase, options: Build_Options, extra_args: [] string = .[], linker_path := "") {
target_filename := tprint("%/%.exe", ifx options.output_path else ".", options.output_executable_name);
vc_path := find_visual_studio_in_a_ridiculous_garbage_way();
kit_root := find_windows_kit_root();
linker_path_to_use := ifx linker_path else LINKER_PATH;
// Link arguments have been ordered to match as closely as possible the default way jai compiler outputs them when
// using link.exe.
linker_arguments: [..] string;
array_add(*linker_arguments, linker_path_to_use);
for phase.compiler_generated_object_files array_add(*linker_arguments, it);
for phase.support_object_files array_add(*linker_arguments, it);
array_add(*linker_arguments, tprint("/OUT:%", target_filename));
array_add(*linker_arguments,
"/MACHINE:AMD64",
"/INCREMENTAL:NO",
"/DEBUG", // Output pdbs
tprint("/IMPLIB:%/%.lib", options.intermediate_path, options.output_executable_name),
tprint("/libpath:%", vc_path),
tprint("/libpath:%\\um\\x64", kit_root),
tprint("/libpath:%\\ucrt\\x64", kit_root),
"-nodefaultlib",
// These are used when calling link.exe. If they are enabled here, we succesfully link, but I am getting no
// output when printing strings? I can step through program fine in debugger though?
// "/SUBSYSTEM:windows",
// "/ENTRY:mainCRTStartup",
);
// Jai seems to interleave system and user libaries, we just output system, then user.
for phase.system_libraries {
if it == "libcmt.lib" {
// @Hack this seems to be the cause of a radlink.exe crash when compiling some programs.
log("ATTENTION: libcmt.lib is being excluded from link line as it seems to cause a crash for some programs. Maybe this will cause link issue in other programs though?");
continue;
}
array_add(*linker_arguments, it);
}
for phase.user_libraries array_add(*linker_arguments, it);
// Rad specific arguments
array_add(*linker_arguments, "/RAD_DEBUG"); // Produce .rdi (for rad debugger).
log("Running linker: %\n\n", get_quoted_command_string(linker_arguments));
result := run_command(..linker_arguments);
if result.type == .FAILED_TO_LAUNCH compiler_report(tprint("Failed to launch linker at path '%'", linker_path_to_use));
if result.exit_code compiler_report(tprint("Linker failed with error code: %\n", result.exit_code));
compiler_custom_link_command_is_complete(phase.workspace);
}
#scope_module;
get_plugin :: () -> *Metaprogram_Plugin {
p := New(Metaprogram_Plugin);
p.init = init;
p.before_intercept = before_intercept;
p.message = message;
p.finish = finish;
p.shutdown = shutdown;
p.log_help = log_help;
return p;
}
init :: (p: *Metaprogram_Plugin, options: [] string) -> bool {
cursor := 0;
while cursor < options.count {
s := options[cursor];
if s == {
case "-path";
if (cursor + 1) >= options.count {
log_error("[RAD Linker] -path option requires you to provide a path you bozo!");
return false;
}
linker_path = options[cursor + 1];
if !FU.file_exists(linker_path) {
log_error("[RAD Linker] Trying to override linker path but '%' is not a file", linker_path);
return false;
}
cursor += 2;
// @Incomplete: Provide options for RAD specific linker args, eg /rad_workers. Maybe just pass through anything that starts /rad?
case;
log_error("Android Plugin: Unknown command line argument '%'", s); // This will print only the first unknown argument and exit.
return false;
}
}
return true;
}
before_intercept :: (p: *Metaprogram_Plugin, flags: *Intercept_Flags) {
options := get_build_options(p.workspace);
if options.os_target != .WINDOWS {
compiler_report("[RAD Linker] Only windows is supported!");
}
// @TODO: Check we have a good linker path. If no -path is provided then maybe radlink.exe is not in path, and even if -path
// has been provided, we only check a file exists at that path, not that it is an exe. `radlink /rad_version` can be used
// as a simple test. I did actually try to do this, but run_command wasn't returning the output for some reason?
options.use_custom_link_command = true;
set_build_options(options, p.workspace);
}
message :: (p: *Metaprogram_Plugin, message: *Message) {
options := get_build_options(p.workspace);
if message.kind == {
case .PHASE;
phase_message := cast(*Message_Phase, message);
if phase_message.phase == .READY_FOR_CUSTOM_LINK_COMMAND {
run_rad_link_command(phase_message, options, linker_path = linker_path);
// If compilation fails, finish() is still called but we don't want to do anything. Just set this flag
// on succesfull link (if link fails we compiler_report which does stop us). We could also listen for
// .ERROR, or .COMPLETE messages but no reason to!
compiled_and_linked_succesfully = true;
}
}
}
finish :: (p: *Metaprogram_Plugin) {
if !compiled_and_linked_succesfully return;
// NOTE(Charles): We don't actually need to do anything here, so maybe compiled_and_linked_succesfully could be deleted.
// But just in case we need to in the future, we are setup for success!
}
shutdown :: (p: *Metaprogram_Plugin) {
free(p);
}
log_help :: (p: *Metaprogram_Plugin) {
log(HELP_STRING);
}
HELP_STRING :: #string DONE
On Windows replace using link.exe with the RAD linker. Arguments:
-path Provide a custom path to radlink.exe
DONE
#scope_file
compiled_and_linked_succesfully := false;
linker_path := "";
#import "Basic";
#import "Compiler";
#import "Process";
#import "String";
FU :: #import "File_Utilities";
#if OS == .WINDOWS {
#import "Windows_Resources";
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment