Initialization of the Julia runtime

How does the Julia runtime execute julia-e'println("HelloWorld!")' ?

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 setsjl_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_tx){returnboxed_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("HelloWorld!")'), 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("HelloWorld!").

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.:

HelloWorld!
Stack frameSource codeNotes
jl_uv_write()jl_uv.ccalled though Base.ccall()
julia_write_282942stream.jlfunction write!{T}(s::AsyncStream, a::Array{T})
julia_print_284639ascii.jlprint(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.cBase.print(Base.TTY, ASCIIString)
jl_apply()julia.h 
jl_trampoline()builtins.c 
jl_apply()julia.h 
jl_apply_generic()gf.cBase.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.cBase.println(Base.TTY, ASCIIString, ASCIIString...)
jl_apply()julia.h 
jl_trampoline()builtins.c 
jl_apply()julia.h 
jl_apply_generic()gf.cBase.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().