Last active
May 31, 2025 14:30
-
-
Save charlesastaylor/1af62766647ee13b9353f04fffe8472c to your computer and use it in GitHub Desktop.
Jai metaprogram plugin for linking with RAD Linker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // 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