This directory contains the code used by the Julia loader, implementing the pieces necessary to isolate ourselves from the native dynamic loader enough to reimplement useful features such as RPATH across all platforms.
This loader comprises the julia executable and the libjulia library, which are responsible for setting things up such that libjulia-internal and any other internal dependencies can be reliably loaded.
The code is organized in three pieces:
loader_exe.cgets built into the mainjuliaexecutable. It immediately loadslibjulia.loader_lib.cgets built into the mainlibjuliashared library. This is the main entrypoint for the Julia runtime loading process, which occurs withinjl_load_repl().trampolines/*.S, which contains assembly definitions for symbol forwarding trampolines. These are used to allowlibjuliato re-export symbols such that a C linker can uselibjuliadirectly for embedding usecases.
The main requirements of the loader are as follows:
- Isolation: We need to be able to load our own copy of
libgcc_s.so, etc... On Linux/macOS, proper application ofRPATHcan influence the linker's decisions, however errantLD_LIBRARY_PATHentries or system libraries inserted into the build process can still interfere, not to mention Windows' lack ofRPATH-like capabilities. To address this, the loader is built as a stand-alone binary that does not depend on the large set of dependencies thatlibjulia-internalitself does, and manuallydlopen()'s a list of dependencies using logic similar to that of anRPATH. - Compatibility: We need to support embedding usecases without forcing embedders to care about all of these things.
For linking against the Julia runtime by simply providing
-ljuliaon the link line, we must ensure that all public interfaces, whether function symbols or data symbols, must be exported fromlibjulia. This motivates our usage of function trampolines to re-export functions fromlibjulia-internal, and the reason why all public data symbols are defined withinlibjulia, then imported intolibjulia-internalfor initialization. - Flexibility: We need to be able to make use of system libraries when requested to do so by the user at build time.
Currently, we embed the list of libraries to be
dlopen()'ed withinlibjuliaas a string (See the definition ofDEP_LIBSinMake.incand its usage inloader_lib.c). This is flexible enough as we do not support changing this configuration at runtime, however in the future, we may need to add some simple parsing logic inloader_lib.cto inspect aLocalPreferences.tomland construct the list of libraries to load from that. - Speed: This whole process should be fast, especially function trampolines. To this end, we write everything in low-overhead assembly, borrowing inspiration from the PLT trampolines that the linker already generates when using dynamic libraries.
The public interface exported by libjulia is contained within .inc files stored in src; one for exported data symbols, src/jl_exported_data.inc and one for exported functions, src/jl_exported_funcs.inc.
Adding entries to the data list will cause libjulia to generate a placeholder variable declaration.
Most symbols are declared to be of type void *, however for symbols that are of a different size, they are declared along with their type.
Adding entries to the function list will cause libjulia to generate a trampoline definition (using a trampoline according to the architecture of the target processor) and then at runtime, when libjulia has successfully loaded libjulia-internal, it will dlsym() that symbol from within libjulia-internal and set it as the target of the trampoline.
All initialization will occur automatically upon successful load of libjulia, so there is no need for user code to call an initialization before invoking typical libjulia-internal functions (although initialization of the runtime itself is still necessary, e.g. calling jl_init()).