Initialization of the Julia runtime
How does the Julia runtime execute julia -e 'println("Hello World!")' ?
main()
Execution starts at main() in cli/loader_exe.c,
which calls jl_load_repl() in cli/loader_lib.c
which loads a few libraries, eventually calling jl_repl_entrypoint() in src/jlapi.c.
The final thing main() does is to use the return value of jl_load_repl() as exit status.
jl_repl_entrypoint()
jl_repl_entrypoint() calls libsupport_init()
to set the C library locale and to initialize the "ios" library (see ios_init_stdstreams()
and Legacy ios.c library).
Next jl_parse_opts() is called to process
command line options. Note that jl_parse_opts() only deals with options that affect code generation
or early initialization. Other options are handled later by exec_options() in base/client.jl.
jl_parse_opts() stores command line options in the global jl_options struct.
jl_resolve_sysimg_location() searches configured paths for the base system image.
See Building the Julia system image.
If a sysimage is found, it is pre-loaded via jl_preload_sysimg() from staticdata.c.
Next it calls jl_init_() in init.c
(which is closely related to jl_init() used for Embedding Julia), more on that in the
next section.
The penultimate step is to call true_main() and keep its return value.
Then it calls jl_atexit_hook().
This calls Base._atexit, then calls jl_gc_run_all_finalizers()
and cleans up libuv handles, plus a bunch of other things.
Finally the return value of true_main() from above is returned by jl_repl_entrypoint()
which eventually reaches the top-level main() function which uses it as its exit status.
jl_init_()
This function initializes a bunch of things, and also calls libsupport_init()
again (it does nothing the second time).
restore_signals() is called
to zero the signal handler mask.
jl_init_tasks() creates the jl_datatype_t* jl_task_type
object; initializes the global jl_root_task struct; and sets jl_current_task to the root task.
jl_install_default_signal_handlers() initializes platform specific signal handlers
for SIGSEGV (OSX, Linux), and SIGFPE (Windows).
Other signals (SIGINFO, SIGBUS, SIGILL, SIGTERM, SIGABRT, SIGQUIT, SIGSYS and SIGPIPE) are
hooked up to sigdie_handler()
which prints a backtrace.
jl_gc_init() sets up allocation pools
and lists for weak refs, preserved values and finalization.
jl_init_box_caches() pre-allocates
global boxed integer value objects for values up to 1024. This speeds up allocation of boxed ints
later on. e.g.:
jl_value_t *jl_box_uint8(uint32_t x)
{
return boxed_uint8_cache[(uint8_t)x];
}jl_init_() ends by calling _finish_jl_init_(), also defined in src/init.c.
_finish_jl_init_()
jl_init_codegen() initializes
the LLVM library.
If a sysimage has been specified it is now loaded via
jl_restore_system_image()
Otherwise it calls
jl_init_types() creates jl_datatype_t
type description objects for the built-in types defined in julia.h.
e.g.
jl_any_type = jl_new_abstracttype(jl_symbol("Any"), core, NULL, jl_emptysvec);
jl_any_type->super = jl_any_type;
jl_type_type = jl_new_abstracttype(jl_symbol("Type"), core, jl_any_type, jl_emptysvec);
jl_int32_type = jl_new_primitivetype(jl_symbol("Int32"), core,
jl_any_type, jl_emptysvec, 32);Next up is jl_init_flisp()
which loads and initializes a pre-compiled femtolisp image containing the scanner/parser.
jl_init_serializer() initializes
8-bit serialization tags for builtin jl_value_t values.
If there is no sysimg file (sysimage.kind == JL_IMAGE_KIND_NONE) then the Core and Main modules are
created and boot.jl is evaluated:
creates a new Julia module Intrinsics containing constant jl_intrinsic_type symbols. These define
an integer code for each intrinsic function.
emit_intrinsic() translates
these symbols into LLVM instructions during code generation.
jl_init_primitives()hooks C
functions up to Julia function symbols. e.g. the symbol Core.:(===)() is bound to C function pointer
jl_f_is() by calling add_builtin_func("===", jl_f_is).
jl_init_main_module()initializes
the global "Main" module.
calls jl_load_() in the same file which then calls
jl_parse_eval_all. That then repeatedly
calls jl_toplevel_eval_flex()
to execute boot.jl.
post_boot_hooks()initializes
global C pointers to Julia globals defined in boot.jl.
From this point on, the code is again uniform, regardless of whether there is a sysimage or not.
Now it runs a loops to call
jl_module_run_initializer() for
each deserialized module to run the __init__() function.
Finally jl_install_sigint_handler()
is called
true_main_()
If a .jl "program" file was supplied on the command line, then exec_program()
calls jl_load(program,len) which
calls jl_parse_eval_all which repeatedly
calls jl_toplevel_eval_flex()
to execute the program.
However, in our example (julia -e 'println("Hello World!")'), jl_get_global(jl_base_module, jl_symbol("_start"))
looks up Base._start and jl_apply()
executes it.
Base._start
Base._start calls Base.exec_options
which calls jl_parse_input_line("println("Hello World!")")
to create an expression object and Core.eval(Main, ex) to execute the parsed expression ex in the module context of Main.
Core.eval
Core.eval(Main, ex) calls jl_toplevel_eval_in(m, ex),
which calls jl_toplevel_eval_flex.
jl_toplevel_eval_flex implements a simple heuristic to decide whether to compile a given code thunk or run it by interpreter.
When given println("Hello World!"), it would usually decide to run the code by interpreter, in which case it calls
jl_interpret_toplevel_thunk, which then calls
eval_body.
The stack dump below shows how the interpreter works its way through various methods of Base.println()
and Base.print() before arriving at write(s::IO, a::Array{T}) where T
which does ccall(jl_uv_write()).
jl_uv_write() calls uv_write()
to write "Hello World!" to JL_STDOUT. See Libuv wrappers for stdio.:
Hello World!| Stack frame | Source code | Notes |
|---|---|---|
jl_uv_write() | jl_uv.c | called though ccall |
julia_write_282942 | stream.jl | function write!(s::IO, a::Array{T}) where T |
julia_print_284639 | ascii.jl | print(io::IO, s::String) = (write(io, s); nothing) |
jlcall_print_284639 | ||
jl_apply() | julia.h | |
jl_trampoline() | builtins.c | |
jl_apply() | julia.h | |
jl_apply_generic() | gf.c | Base.print(Base.TTY, String) |
jl_apply() | julia.h | |
jl_trampoline() | builtins.c | |
jl_apply() | julia.h | |
jl_apply_generic() | gf.c | Base.print(Base.TTY, String, Char, Char...) |
jl_apply() | julia.h | |
jl_f_apply() | builtins.c | |
jl_apply() | julia.h | |
jl_trampoline() | builtins.c | |
jl_apply() | julia.h | |
jl_apply_generic() | gf.c | Base.println(Base.TTY, String, String...) |
jl_apply() | julia.h | |
jl_trampoline() | builtins.c | |
jl_apply() | julia.h | |
jl_apply_generic() | gf.c | Base.println(String,) |
jl_apply() | julia.h | |
do_call() | interpreter.c | |
eval_body() | interpreter.c | |
jl_interpret_toplevel_thunk | interpreter.c | |
jl_toplevel_eval_flex | toplevel.c | |
jl_toplevel_eval_in | toplevel.c | |
Core.eval | boot.jl |
Since our example has just one function call, which has done its job of printing "Hello World!",
the stack now rapidly unwinds back to main().