Initialization of the Julia runtime¶
How does the Julia runtime execute julia -e 'println("Hello World!")'
?
main()¶
Execution starts at main() in julia/ui/repl.c.
main() calls libsupport_init() to set the C library locale and to initialise the “ios” library (see ios_init_stdstreams() and Legacy ios.c library).
Next parse_opts()
is called to process command line options. Note that parse_opts()
only deals with options that affect code generation or early initialisation. Other
options are handled later by process_options() in base/client.jl.
parse_opts()
stores command line options in the global jl_options
struct.
julia_init()¶
julia_init() in task.c is called by main() and calls _julia_init() in init.c.
_julia_init()
begins by calling libsupport_init()
again (it does
nothing the second time).
restore_signals() is called to zero the signal handler mask.
jl_resolve_sysimg_location() searches configured paths for the base system image. See Building the Julia system image.
jl_gc_init() sets up allocation pools and lists for: weak refs, preserved values and finalization.
jl_init_frontend() loads and initialises a pre-compiled femtolisp image containing the scanner/parser;
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"), NULL, jl_null);
jl_any_type->super = jl_any_type;
jl_type_type = jl_new_abstracttype(jl_symbol("Type"), jl_any_type, jl_null);
jl_int32_type = jl_new_bitstype(jl_symbol("Int32"),
jl_any_type, jl_null, 32);
jl_init_tasks() creates
the jl_datatype_t* jl_task_type
object; initialises the global
jl_root_task
struct; and
sets jl_current_task
to the root task.
jl_init_codegen() initialises the LLVM library.
jl_init_serializer()
initialises 8-bit serialisation tags for 256 frequently used
jl_value_t
values. The serialisation mechanism uses these tags as
shorthand (in lieu of storing whole objects) to save storage space.
If there is no sysimg file (!jl_options.image_file
) then
then Core
and Main
modules are created and boot.jl
is evaluated:
jl_core_module = jl_new_module(jl_symbol("Core"))
creates
the Julia Core
module.
jl_init_intrinsic_functions() 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
Base.is()
is bound to C function pointer jl_f_is()
by calling add_builtin_func("eval", jl_f_top_eval)
, which does:
jl_set_const(jl_core_module,
jl_symbol("is"),
jl_new_closure(jl_f_top_eval, jl_symbol("eval"), NULL));
jl_new_main_module()
creates the global “Main” module and sets
jl_current_task->current_module = jl_main_module
.
Note: _julia_init() then sets jl_root_task->current_module = jl_core_module
. jl_root_task
is an alias of jl_current_task
at this point, so the current_module set by jl_new_main_module()
above is overwritten.
jl_load(“boot.jl”, sizeof(“boot.jl”)) calls jl_parse_eval_all(“boot.jl”) which repeatedly calls jl_parse_next() and jl_toplevel_eval_flex() to parse and execute boot.jl. TODO – drill down into eval?
jl_get_builtin_hooks() initialises global C pointers to Julia globals defined in boot.jl
.
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];
}
_julia_init() iterates over the jl_core_module->bindings.table
looking for jl_datatype_t
values and sets the type name’s module prefix to jl_core_module
.
jl_add_standard_imports(jl_main_module) does “using Base” in the “Main” module.
Note: _julia_init()
now reverts to jl_root_task->current_module = jl_main_module
as it was before being set to jl_core_module
above.
Platform specific signal handlers are initialised 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_init_restored_modules() calls jl_module_run_initializer() for each deserialised module to run the __init__()
function.
Finally sigint_handler() is hooked up to SIGINT
and calls jl_throw(jl_interrupt_exception)
.
_julia_init()
then returns back to main() in julia/ui/repl.c and main() calls true_main(argc, (char**)argv)
.
true_main()¶
true_main() loads the contents of argv[]
into Base.ARGS
.
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_parse_next() and jl_toplevel_eval_flex() to parse and 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.process_options which calls jl_parse_input_line(“println(“Hello World!”)”) to create an expression object and Base.eval()
to execute it.
Base.eval¶
Base.eval()
was mapped to jl_f_top_eval by jl_init_primitives()
.
jl_f_top_eval() calls jl_toplevel_eval_in(jl_main_module, ex), where “ex” is the parsed expression println("Hello World!")
.
jl_toplevel_eval_in() calls jl_toplevel_eval_flex() which calls eval() in interpreter.c.
The stack dump below shows how the interpreter works its way through various methods of Base.println()
and Base.print()
before arriving at write{T}(s::AsyncStream, a::Array{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 Base.ccall() |
julia_write_282942 | stream.jl | function write!{T}(s::AsyncStream, a::Array{T}) |
julia_print_284639 | ascii.jl | print(io::IO, s::ASCIIString) = (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, ASCIIString) |
jl_apply() | julia.h | |
jl_trampoline() | builtins.c | |
jl_apply() | julia.h | |
jl_apply_generic() | gf.c | Base.print(Base.TTY, ASCIIString, 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, ASCIIString, ASCIIString...) |
jl_apply() | julia.h | |
jl_trampoline() | builtins.c | |
jl_apply() | julia.h | |
jl_apply_generic() | gf.c | Base.println(ASCIIString,) |
jl_apply() | julia.h | |
do_call() | interpreter.c | |
eval() | interpreter.c | |
jl_interpret_toplevel_expr() | interpreter.c | |
jl_toplevel_eval_flex() | toplevel.c | |
jl_toplevel_eval() | toplevel.c | |
jl_toplevel_eval_in() | builtins.c | |
jl_f_top_eval() | builtins.c |
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()
.
jl_atexit_hook()¶
main()
calls jl_atexit_hook(). This
calls _atexit for each module, then calls jl_gc_run_all_finalizers()
and cleans up libuv handles.
julia_save()¶
Finally, main()
calls julia_save(), which if requested on the command line, saves the runtime state to a new system image. See jl_compile_all() and jl_save_system_image().