# Prototype Julia Launcher via Zig

**URL:** https://discourse.julialang.org/t/prototype-julia-launcher-via-zig/111802
**Category:** General Usage
**Created:** [March 19, 2024, 7:17am UTC](https://discourse.julialang.org/t/prototype-julia-launcher-via-zig/111802 "2024-03-19T07:17:04Z")
**Posts on this page:** 10
**Page:** 1

<div class="post-metadata">

### Author: ![mkitti](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mkitti/32/12459_2.png) [@mkitti](https://discourse.julialang.org/u/mkitti)
#### Post date: [March 19, 2024, 7:17am UTC](https://discourse.julialang.org/t/prototype-julia-launcher-via-zig/111802/1 "2024-03-19T07:17:05Z")

</div>

At times, I have just wanted a small executable to launch Julia with some particular command line options and a few initial commands.

This task could easily be done by a shell script, but I did not want to depend on a particular shell. I also wanted this to work on Windows.

Also this is much less ambitious than PackageCompiler.jl or StaticCompiler.jl in that I am not trying to compile Julia code into an executable.

I built a prototype of what that might look like using [Zig](https://ziglang.org/). I chose to do this in zig because it has a compact toolchain that works well across platforms.

Here’s the prototype script. I’m considering designing a package around this.

```julia
using Libdl
using Pkg
Pkg.activate(; temp=true)
Pkg.add("zig_jll")
using zig_jll

function build_launcher(
    launcher_name::AbstractString,
    cli_options::Vector{String},
    commands::Vector{String}
)
    code = launcher_code(launcher_name, cli_options, commands)
    temp_path = mktempdir()
    code_path = joinpath(temp_path, launcher_name * ".zig")
    write(code_path, code)

    libjulia_path = dirname(dlpath("libjulia"))
    julia_h_path = abspath(joinpath(libjulia_path, "..", "include", "julia"))
    zig_jll.zig() do exe
        run(`$exe build-exe $code_path -lc -ljulia -I $julia_h_path -L $libjulia_path`)
    end
end

function launcher_code(
    launcher_name::AbstractString,
    cli_options::Vector{String},
    commands::Vector{String}
)
    libjulia_path = dirname(dlpath("libjulia"))
    julia_h_path = abspath(joinpath(libjulia_path, "..", "include", "julia"))

    options_buffer = IOBuffer()
    for option in cli_options
        println(options_buffer, " \"$option\",")
    end
    options = String(take!(options_buffer))

    command_buffer = IOBuffer()
    for command in commands
        command = escape_string(command)
        println(command_buffer, " _ = jl_eval_string(\"$command\");")
    end
    commands = String(take!(command_buffer))
"""
// Compile with
// zig build-exe launch.zig -lc -ljulia -I $julia_h_path -L $libjulia_path
const std = @import("std");
const jl = @cImport({
    @cInclude("julia.h");
});

const jl_parse_opts = jl.jl_parse_opts;
const jl_init = jl.jl_init;
const jl_eval_string = jl.jl_eval_string;
const jl_atexit_hook = jl.jl_atexit_hook;

pub fn main() !u8 {
    const allocator = std.heap.page_allocator;

    // Julia command line arguments
    const zig_argv = [_][*:0]const u8{
        "$launcher_name",
$options
    };
    var argc: c_int = zig_argv.len;
    // We need to allocate since jl_parse_opts will return arguments
    var argv = try allocator.alloc([*:0]const u8, zig_argv.len);
    defer allocator.free(argue);
    for (zig_argv, 0..) |_, i| {
        argv[i] = zig_argv[i];
    }

    // Basic embedding
    jl_parse_opts(&argc, @ptrCast(&argv));
    jl_init();
$commands
    jl_atexit_hook(0);

    return 0;
}
"""
end

```

Here’s an example of building and execution.

```julia-repl
julia> build_launcher("julia8", ["--threads=8"], ["""@info "" Threads.nthreads()"""]);

julia> run(`./julia8`)
┌ Info: 
└ Threads.nthreads() = 8
Process(`./julia8`, ProcessExited(0))

julia> filesize("julia8")
598024

```

Edit: Made the example Zig 0.11.0 compatible.

---

<div class="post-metadata">

### Author: ![mkitti](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mkitti/32/12459_2.png) [@mkitti](https://discourse.julialang.org/u/mkitti)
#### Post date: [March 19, 2024, 7:20am UTC](https://discourse.julialang.org/t/prototype-julia-launcher-via-zig/111802/2 "2024-03-19T07:20:54Z")

</div>

Here’s another variant of the Zig code that uses loads libjulia dynamically. If I could figure out a procedure to locate libjulia automatically, this would be more flexible.

```julia
// Compile with
// zig build-exe launch_dlopen.zig -lc -I ~/src/julia/usr/include/julia
const std = @import("std");
const jl = @cImport({
    @cInclude("julia.h");
});

pub fn main() !u8 {
    const allocator = std.heap.page_allocator;

    // TODO: Dynamically locate Julia
    const libjulia_path = "/home/mkitti/src/julia/usr/lib/libjulia.so";
    var ptr = try std.DynLib.open(libjulia_path);
    defer ptr.close();

    // Julia command line arguments
    const zig_argv = [_][*:0]const u8{
        "launch",
        "--project=@pluto",
        "--threads=8"
    };
    var argc: c_int = zig_argv.len;
    // We need to allocate since jl_parse_opts will return arguments
    var argv = try allocator.alloc([*:0]const u8, zig_argv.len);
    defer allocator.free(argv);
    for (zig_argv) |_, i| {
        argv[i] = zig_argv[i];
    }

    //std.debug.print("jl_parse_opts: {s}\n", .{@TypeOf(jl.jl_parse_opts)});
    var jl_parse_opts = ptr.lookup(@TypeOf(jl.jl_parse_opts), "jl_parse_opts") orelse return 1;
    var jl_init = ptr.lookup(@TypeOf(jl.jl_init), "jl_init") orelse return 2;
    var jl_eval_string = ptr.lookup(@TypeOf(jl.jl_eval_string), "jl_eval_string") orelse return 3;
    var jl_atexit_hook = ptr.lookup(@TypeOf(jl.jl_atexit_hook), "jl_atexit_hook") orelse return 4;

    // Basic embedding
    jl_parse_opts(&argc, @ptrCast([*c][*c][*c]u8, &argv));
    jl_init();
    _ = jl_eval_string("println(\"Hello from Julia!\")");
    _ = jl_eval_string("@info \"\" Threads.nthreads() Base.active_project()");
    jl_atexit_hook(0);

    return 0;
}

```

---

<div class="post-metadata">

### Author: ![mnemnion](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mnemnion/32/206596_2.png) [@mnemnion](https://discourse.julialang.org/u/mnemnion)
#### Post date: [March 19, 2024, 8:42pm UTC](https://discourse.julialang.org/t/prototype-julia-launcher-via-zig/111802/3 "2024-03-19T20:42:30Z")

</div>

> [@mkitti](#):
>
> This task could easily be done by a shell script, but I did not want to depend on a particular shell. I also wanted this to work on Windows.

There’s something perverse about using a compiled binary to get around the fact that OSes don’t have a single scripting/shell language which one can count on having around, but it actually makes sense.

It might be possible to get a cross-platform binary, not just a recipe for building the launcher cross platform, using [actually portable executable](https://justine.lol/ape.html). On Unices you could use `which` to find the executable path while building the binary, and on Windows use `where.exe` (I think).

---

<div class="post-metadata">

### Author: ![GunnarFarneback](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gunnarfarneback/32/1827_2.png) [@GunnarFarneback](https://discourse.julialang.org/u/GunnarFarneback)
#### Post date: [March 19, 2024, 9:00pm UTC](https://discourse.julialang.org/t/prototype-julia-launcher-via-zig/111802/4 "2024-03-19T21:00:33Z")

</div>

If you want to find julia at build time it seems more robust to just ask julia.

```julia
$ which julia
/home/gunnar/.juliaup/bin/julia
$ julia -E Sys.BINDIR
"/home/gunnar/.julia/juliaup/julia-1.10.2+0.x64.linux.gnu/bin"

```

---

<div class="post-metadata">

### Author: ![mkitti](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mkitti/32/12459_2.png) [@mkitti](https://discourse.julialang.org/u/mkitti)
#### Post date: [March 20, 2024, 12:27am UTC](https://discourse.julialang.org/t/prototype-julia-launcher-via-zig/111802/5 "2024-03-20T00:27:55Z")

</div>

> [@GunnarFarneback](#):
>
> `julia -E Sys.BINDIR`

`Sys.BINDIR` does not definitely identify the location of `libjulia.{so, dylib, dll}` since the relative location varies by operating system.

I used the code below at build time to obtain the definitve location.

```julia
dirname(dlpath("libjulia"))

```

---

<div class="post-metadata">

### Author: ![jlapeyre](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jlapeyre/32/4514_2.png) [@jlapeyre](https://discourse.julialang.org/u/jlapeyre)
#### Post date: [March 20, 2024, 5:34am UTC](https://discourse.julialang.org/t/prototype-julia-launcher-via-zig/111802/6 "2024-03-20T05:34:05Z")

</div>

Looks like this may have some overlap with [GitHub - Roger-luo/Ion: Ion - a CLI toolbox for Julia developers](https://github.com/Roger-luo/ion)

---

<div class="post-metadata">

### Author: ![mkitti](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mkitti/32/12459_2.png) [@mkitti](https://discourse.julialang.org/u/mkitti)
#### Post date: [March 20, 2024, 8:39am UTC](https://discourse.julialang.org/t/prototype-julia-launcher-via-zig/111802/7 "2024-03-20T08:39:44Z")

</div>

`ion` is meant more for developer tooling and launches julia as a distinct [process](https://github.com/Roger-luo/ion/blob/main/src%2Fbin%2Fion%2Fcommands%2Frun.rs#L120).

This is more similar to PackageCompiler.jl’s [embedding wrapper](https://github.com/JuliaLang/PackageCompiler.jl/blob/master/src%2Fembedding_wrapper.c) and probably should take more inspiration from it. This is the mechanism by which PackageCompiler.jl creates an executable.

Perhaps a main distinguishing factor from `ion` is that I do not launch a separate Julia process. I embed Julia in the executable’s process similar to the executable wrapper above.

I’m using `zig_jll`, which needs updating. This contains the entire zig toolchain and is less than 50 MiB in size on Linux:  
[https://ziglang.org/download/](https://ziglang.org/download/)

---

<div class="post-metadata">

### Author: ![mkitti](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mkitti/32/12459_2.png) [@mkitti](https://discourse.julialang.org/u/mkitti)
#### Post date: [March 20, 2024, 9:21am UTC](https://discourse.julialang.org/t/prototype-julia-launcher-via-zig/111802/8 "2024-03-20T09:21:15Z")

</div>

> [@mnemnion](#):
>
> It might be possible to get a cross-platform binary, not just a recipe for building the launcher cross platform, using [actually portable executable](https://justine.lol/ape.html). On Unices you could use `which` to find the executable path while building the binary, and on Windows use `where.exe` (I think).

One reason I went with `zig` is that it has pretty good cross compilation capability.

A problem with `which` and `where` is that this might not lead to an actual Julia install. Attempting to run `julia` to discover it’s location as Gunnar suggested might be the most robust.

---

<div class="post-metadata">

### Author: ![GunnarFarneback](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gunnarfarneback/32/1827_2.png) [@GunnarFarneback](https://discourse.julialang.org/u/GunnarFarneback)
#### Post date: [March 20, 2024, 9:26am UTC](https://discourse.julialang.org/t/prototype-julia-launcher-via-zig/111802/9 "2024-03-20T09:26:14Z")

</div>

> [@mkitti](#):
>
> One reason I went with `zig` is that it has pretty good cross compilation capability.

Do you get any value from the cross compilation capability when you rely on locating Julia during the build?

---

<div class="post-metadata">

### Author: ![mkitti](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mkitti/32/12459_2.png) [@mkitti](https://discourse.julialang.org/u/mkitti)
#### Post date: [March 20, 2024, 9:44am UTC](https://discourse.julialang.org/t/prototype-julia-launcher-via-zig/111802/10 "2024-03-20T09:44:10Z")

</div>

The [second piece of code](https://discourse.julialang.org/t/prototype-julia-launcher-via-zig/111802/2) I posted uses `dlopen`. I could defer locating Julia to runtime and then memoize that location.

I’m still thinking of applications for this, but I’m mainly thinking of the deployment space.

Essentially, we can take advantage of Zig’s compact cross compiling toolchain to embed a Julia script into a possibly executable. The executable could even assist in retrieving `juliaup` if Julia is not found.
