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.c
gets built into the mainjulia
executable. It immediately loadslibjulia
.loader_lib.c
gets built into the mainlibjulia
shared 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 allowlibjulia
to re-export symbols such that a C linker can uselibjulia
directly 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 ofRPATH
can influence the linker's decisions, however errantLD_LIBRARY_PATH
entries 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-internal
itself 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
-ljulia
on 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-internal
for 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 withinlibjulia
as a string (See the definition ofDEP_LIBS
inMake.inc
and 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.c
to inspect aLocalPreferences.toml
and 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()
).