printf() and stdio in the Julia runtime

Libuv wrappers for stdio

julia.h defines libuv wrappers for the stdio.h streams:

uv_stream_t*JL_STDIN;uv_stream_t*JL_STDOUT;uv_stream_t*JL_STDERR;

... and corresponding output functions:

intjl_printf(uv_stream_t*s,constchar*format,...);intjl_vprintf(uv_stream_t*s,constchar*format,va_listargs);

These printf functions are used by julia/{src,ui}/*.c wherever stdio is needed to ensure that output buffering is handled in a unified way.

In special cases, like signal handlers, where the full libuv infrastructure is too heavy, jl_safe_printf() can be used to write(2) directly to STDERR_FILENO:

voidjl_safe_printf(constchar*str,...);

Interface between JL_STD* and Julia code

Base.STDIN, Base.STDOUT and Base.STDERR are bound to the JL_STD*libuv streams defined in the runtime.

Julia’s __init__() function (in base/sysimg.jl) calls reinit_stdio() (in base/stream.jl) to create Julia objects for Base.STDIN, Base.STDOUT and Base.STDERR.

reinit_stdio() uses ccall() to retrieve pointers to JL_STD* and calls jl_uv_handle_type() to inspect the type of each stream. It then creates a Julia Base.IOStream, Base.TTY or Base.PipeEndpoint object to represent each stream, e.g.:

$ julia -e 'println(typeof((STDIN, STDOUT, STDERR)))'
Tuple{Base.TTY,Base.TTY,Base.TTY}

$ julia -e 'println(typeof((STDIN, STDOUT, STDERR)))' < /dev/null 2>/dev/null
Tuple{IOStream,Base.TTY,IOStream}

$ echo hello | julia -e 'println(typeof((STDIN, STDOUT, STDERR)))'| cat
Tuple{Base.PipeEndpoint,Base.PipeEndpoint,Base.TTY}

The Base.read() and Base.write() methods for these streams use ccall() to call libuv wrappers in src/jl_uv.c, e.g.:

stream.jl:functionwrite(s::IO,p::Ptr,nb::Integer)->ccall(:jl_uv_write,...)jl_uv.c:->intjl_uv_write(uv_stream_t*stream,...)->uv_write(uvw,stream,buf,...)

printf() during initialisation

The libuv streams relied upon by jl_printf() etc., are not available until midway through initialisation of the runtime (see init.c, init_stdio()). Error messages or warnings that need to be printed before this are routed to the standard C library fwrite() function by the following mechanism:

In sys.c, the JL_STD* stream pointers are statically initialised to integer constants: STD*_FILENO(0,1and2). In jl_uv.c the jl_uv_puts() function checks its uv_stream_t*stream argument and calls fwrite() if stream is set to STDOUT_FILENO or STDERR_FILENO.

This allows for uniform use of jl_printf() throughout the runtime regardless of whether or not any particular piece of code is reachable before initialisation is complete.

Legacy ios.c library

The julia/src/support/ios.c library is inherited from femtolisp. It provides cross-platform buffered file IO and in-memory temporary buffers.

ios.c is still used by:

  • julia/src/flisp/*.c
  • julia/src/dump.c – for serialisation file IO and for memory buffers.
  • base/iostream.jl – for file IO (see base/fs.jl for libuv equivalent).

Use of ios.c in these modules is mostly self-contained and separated from the libuv I/O system. However, there is one place where femtolisp calls through to jl_printf() with a legacy ios_t stream.

There is a hack in ios.h that makes the ios_t.bm field line up with the uv_stream_t.type and ensures that the values used for ios_t.bm to not overlap with valid UV_HANDLE_TYPE values. This allows uv_stream_t pointers to point to ios_t streams.

This is needed because jl_printf() caller jl_static_show() is passed an ios_t stream by femtolisp’s fl_print() function. Julia’s jl_uv_puts() function has special handling for this:

if(stream->type>UV_HANDLE_TYPE_MAX){returnios_write((ios_t*)stream,str,n);}