Self-contained juliac demonstration

I was looking for a completely self-contained tutorial or demonstration on how to use the juliac command line from JuliaC.jl and did not find one. I created this short demonstration below.

Steps

Step 1: Install Julia 1.12 or greater
Step 2: Save the script below to “create_multiply_numbers.jl” and then run julia create_multiply_numbers.jl
Step 3: Run build/bin/multiply_numbers.exe 1 2 3 to test your new program itself.

create_multiply_numbers.jl

#!/usr/bin/env -S julia
using Pkg

if !contains(ENV["PATH"], DEPOT_PATH[1]*"/bin")
    @warn "The environment variable PATH does not contain $(DEPOT_PATH[1]*"/bin"). Consider modifying your shell startup."
    @info "Adding $(DEPOT_PATH[1])/bin to \$PATH" ENV["PATH"]
    ENV["PATH"] = "$(DEPOT_PATH[1])/bin:$(ENV["PATH"])"
end

if isnothing(Sys.which("juliac"))
    @info "Installing JuliaC"
    Pkg.Apps.add("JuliaC")
end

if !isdir("multiply_numbers")
    @info "Generating multiply_numbers package"
    Pkg.generate("multiply_numbers")
end

code_file::String = "multiply_numbers/src/multiply_numbers.jl"
if !isfile(code_file) || !contains("@main", read(code_file, String))
    @info "Writing $code_file"
    program = """
    module multiply_numbers

    function (@main)(args::Vector{String})
        numbers = parse.(Int, args)
        println(Core.stdout, prod(numbers))
        return 0
    end

    end # module multiply_numbers
    """
    println(program)
    write("multiply_numbers/src/multiply_numbers.jl", program)
end

@info "Compiling multiply_numbers.exe with juliac multiply_numbers --output-exe multiply_numbers.exe --bundle build --trim"
run(`juliac multiply_numbers --output-exe multiply_numbers.exe --bundle build --trim`)

@info "The size of build/bin/multiply_numbers.exe is $(filesize("build/bin/multiply_numbers.exe")/1024^2) MiB"

@info "Running build/bin/multiply_numbers.exe 5 9 10"
run(`build/bin/multiply_numbers.exe 5 9 10`)

This script does the following:

  1. Checks to see if you have your ~/.julia/bin folder on your environment’s PATH. Adds it temporarily if not.
  2. Checks to see if you have juliac installed. Installs JuliaC.jl as an app if not.
  3. Generates a minimal “multiply_numbers” package if it does not exist.
  4. Writes the “multiply_numbers.jl” program if it does not exist or does contain a @main function.
  5. Compiles “multiply_numbers.jl” into “build/bin/multiply_numbers.exe”
  6. Reports the size of build/bin/multiply_numbers.exe
  7. Executes build/bin/multiply_numbers.exe 5 9 10
32 Likes

Nice! I got this to work on Windows (Julia 1.12.2), and the total bundled directory was 117MB. Looking at the content, it seemed like a lot of unnecessary pieces… fortran and BLAS and PCRE… Copied one at a time to a new directory and ended with just the .exe, libjulia and libjulia-internal, and libopenlibm for it to work (total of 17MB). Isn’t trim supposed to be doing this?

5 Likes

Hmm, I get an error:

melis@blackbox 08:20:/tmp/t$ j --project=. c.jl 
┌ Warning: The environment variable PATH does not contain /home/melis/.julia/bin. Consider modifying your shell startup.
└ @ Main /tmp/t/c.jl:5
┌ Info: Adding /home/melis/.julia/bin to $PATH
└   ENV["PATH"] = "/home/melis/projects/2do-tool:/home/melis/projects/bob:/home/melis/.local/bin:/home/melis/.juliaup/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/opt/cuda/bin:/opt/cuda/nsight_compute:/opt/cuda/nsight_systems/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl"
ERROR: LoadError: MethodError: no method matching occursin(::Vector{UInt8}, ::String)
The function `occursin` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  occursin(::Any)
   @ Base strings/search.jl:807
  occursin(::Regex, ::AbstractString; offset)
   @ Base regex.jl:306
  occursin(::Union{AbstractChar, AbstractString}, ::AbstractString)
   @ Base strings/search.jl:782
  ...

Stacktrace:
 [1] contains(haystack::String, needle::Vector{UInt8})
   @ Base ./strings/util.jl:140
 [2] top-level scope
   @ /tmp/t/c.jl:21
 [3] include(mod::Module, _path::String)
   @ Base ./Base.jl:306
 [4] exec_options(opts::Base.JLOptions)
   @ Base ./client.jl:317
 [5] _start()
   @ Base ./client.jl:550
in expression starting at /tmp/t/c.jl:21

Seems the read(code_file) file returns Vector{UInt8} while contains() is used to check for string "@main". Curious why the code works for you, but not for me?

julia> versioninfo()
Julia Version 1.12.2
Commit ca9b6662be4 (2025-11-20 16:25 UTC)
Build Info:
  Official https://julialang.org release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 8 × Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz
  WORD_SIZE: 64
  LLVM: libLLVM-18.1.7 (ORCJIT, skylake)
  GC: Built with stock GC
Threads: 8 default, 1 interactive, 8 GC (on 8 virtual cores)
Environment:
  JULIA_PKG_USE_CLI_GIT = true
  JULIA_NUM_THREADS = auto
2 Likes

Good catch. It should be read(code_file, String). I revised it.

2 Likes

I think trim tries to reduce the size of the executable itself. These extra shared libraries are part of the --bundle portion which I think copies of all Julia’s dependencies.

2 Likes

Sounds like something else that could be --trimmed

Thanks @mkitti
Do you know if JuliaC will change the requirements of embedding Julia in other languages? Certain aspects (libuv, signals, etc) appear to be hard roadblocks for the integration of Julia based shared libraries.

2 Likes

There’s a simple, complete working example in the Appendix to Julia 1.12 brings progress on standalone binaries and more [LWN.net]

2 Likes

Thank you for this.

Reading your script, am I correct in understanding that the whole process is:

A) Have juliac installed

B) Have a module with an @main

C) Call juliac modulename --output-exe exename.exe --bundle build --trim

Basically. You may also need to add ~/.julia/bin to your path in order to invoke juliac later. You may also want to consider the apps options in Project.toml.

The intention here is to develop a minimum working example in executable script form.

3 Likes

Don’t know about signals, but libuv is still present with libraries compiled by JuliaC, which is non-ideal for my use cases. On the other hand, a problem JuliaC did solve is that the startup memory usage is now tiny, rather than ~200 MB with the normal Julia installation.

2 Likes

That’s the mark of a great MWE: you took something that felt like it would take forever for me to figure out, and showed that it’s actually not that complicated. Thank you.

4 Likes

Thanks for posting this example. I have a question on the outputs of these compiled binaries- as fare as I can tell, they only return Ints to indicate whether or not the code successfully ran. If I wanted to call one of these binaries using another language (i.e. Matlab, Python) how exactly should I get usable outputs from it? Whenever I’ve tried to return anything but an Int (like a float) I end up with an error.

I will first point out that the issue you highlighted is not unique to Julia. C and C++ main programs also only “return” integers. These integers actually are to indicate if an error occurred. If the program ran successfully, you should return 0.

For executables, the main way they might offer complex outputs is either by printing a value to stdout or writing content to a file. The Unix tool chain consists of a whole series of tools which communciate this way.

While you could try to print a floating point value for MATLAB to read there is probably a better way. What you probably want is not an executable but a shared library with functions that other software can run. A shared library is sometimes called a DLL on Windows. MATLAB has an interface that creates a special shared library called a MEX file.

In the case of a shared library, the Julia function runs as part of the program that calls it rather than as a separate program. The Julia function can then provide MATLAB or Python with a pointer in memory containing numbers. Sometimes this is done by returning a pointer, but more often this is done by passing a pointer argument by reference. Part of the issue there is that we need to convey both the memory location and its size. Unfortunately, there is no univesal cross-language way to communicate this for arbitrarily large numeric arrays. One attempt to do is Apache Arrow.

With JuliaC, there is a way to create a shared library. Instead of --output-exe you would want to use --output-lib. Furthermore, you might want to use @ccallable and also add --compile-ccallable. Since there is interest, I will consider posting another thread to do a self contained example for creating library. I will post it here when I complete it.

7 Likes

Ah, I didn’t even realize JuliaC had an --output-lib option. I’ll have to fiddle around with that - thanks!