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().