Creating fully self-contained and pre-compiled library

Hi everyone. We intend to use julia to create a fully self-contained, completely pre-compiled library for integration in a larger software. I did read several posts, watched YouTube, etc. – there’s a lot but also quite scattered information. Also, there’s (for me) still the confusion about PackageCompiler and juliac. Size of the resulting library is not that critical, but smaller is better, of course. So, my questions are:

[Edit: I state the answers to the questions, as found so far, directly here. Will update whenever having found out something more. Also, follow-up questions are added.]

  • What is, as of 2025-08-05, the best way of compiling a standalone library (with C ABI), that does not rely (and thus not include) the julia “runtime”/compiler? (Relying on official releases, no nightly builds etc., and supporting “all” functionality.)
    • If I understood correctly, the “runtime” (libjulia) is always required, and in order not to have the compiler (LLVM) bundled, the only way is juliac.
    • The workflow, for now, is to call julia on <julia-root-folder>/share/julia/juliac/juliac.jl from the command line, providing options and the input .jl file.
  • Does a single library (DLL on Windows) result, or does it rely on other libraries that I need to bundle for distribution?
    • The resulting binary (exe or DLL) is not self-contained, many (to most/all) libraries in <julia-root-folder>/bin are required.
  • [follow-up] Is there a way to identify which ones need to be bundled along the binary to make it work?
  • What about packages that rely on third-party binaries? (Relates to the previous point.)
  • Is that workflow “officially” documented somewhere?
    • It seems not. Correct?
  • What will change with the release of julia 1.12? (buzzwords --trim, juliac)
    • Only with 1.12 (nightly, for now; not rc1), most features of juliac, including --trim become available.
    • In the future, juliac and PackageCompiler.jl should converge (since they “target the same but the former from the command-line, the latter from within julia”).
  • Will the “best way” change in the near future? (So, if possible, would it be better to wait for some weeks, or months, before starting this journey?)
    • There’s only the way with juliac for now, and a nightly build of julia 1.12 is required. Best would be to wait for the official 1.12 release, but these features will retain status “experimental” also at that point.

Thanks for pointing me to the currently right direction; could save me a lot of time searching, reading, and trying. I can also document my “journey” here, or help in updating / extending documentation etc.


Just some of the posts and other resources I found, as reference


Edit: Just wanted to say thanks to everyone working on, or otherwise involved in, julia and especially juliac and related / enabling stuff! :heart_on_fire: Really awesome to have an interactive language / environment to prototype (but running at C-like speed) and then being able to generate a binary (even if it is not as small as if it had been generated using e.g. C or Rust). Folks just have to understand that the former was the initial (and IMO still primary) focus. Still, I do think that the latter (with nice tooling to make binary generation accessible and straightforward) could boost adoption of julia a lot, since it enabled integration of julia-created modules in any other environment, in binary (and thus closed-source) form.

3 Likes

To achieve these two goals:

  • fully self-contained, completely pre-compiled
  • does not rely (and thus not include) the julia “runtime”/compiler

you actually do need --trim — even though you don’t care about file size. And --trim requires the juliac capabilities in the v1.12 RC.

PackageCompiler.jl pre-v1.12 can also create a dll/dylib/so, but it does not have the tooling to guarantee/enforce that everything gets compiled like juliac --trim does. So it bundles the compiler for anything you may have missed.

4 Likes

To add to the reference list: The “state of --trim” talk from JuliaCon’25: https://www.youtube.com/watch?v=3o0lAXCa9Wg&t=24710s

3 Likes

Thanks for the hints. Forgot to note that currently I’m developing on Windows (but will later deploy on linux; but want to have a proof-of-concept on Windows first). So I downloaded the portable nightly binaries (from today, 05:06) and tried to compile the hello-world example. That is, just the following in a .jl file:

function @main(args::Vector{String})::Cint
    println(Core.stdout, "Hello, World!")
    return 0
end

Compilation works fine, and I can see that the generated .exe depends on libjulia.dll and libjulia-internal.dll. So, put these two right next to the .exe such that they are found.

When I run the .exe, it errors out. Could get the following message (when I double-click the .exe instead of calling it from bash/cmd):

Sorry for the German. It says that the entrypoint with that very cryptic name cannot be found in libjulia-internal.dll. Any ideas what is going wrong?


Edit 1: went deeper into analysing dependencies. libjulia-internal depends on ...\mingw64\bin\libstdc++-6. So, I happened to have mingw64 installed / on the path and was not aware that this was required for juliac. For some reasons I had da very old version of it installed (from 2012 or so), but also changing to a recent one (14.2.0) does not solve the problem (but a different entrypoint is not found). For this version, it does not have available:

  • std::__once_functor
  • std::__get_once_mutex()
  • std::__set_once_functor_lock_ptr(std::unique_lock<std::mutex>*)

I then found the sentence “The msys2 libstdc+±6.dll appears to not have the needed symbols while the one distributed with Julia does.” in this thread. So, due to my installation of mingw64 being on the path, these libs are loaded.

If I remove my mingw64 installation from the PATH, four mingw64-libs are not found (atomic-1, gcc_s_seh-1, winpthread-1, stdc++-6). If I use those from the /bin folder of julia, I get a step further, namely calling the .exe providing console output about what else is missing (funnily I cannot find that these are missing using a dependency analyser on my .exe).

In the end, I get the .exe for the hello-world example to work (:slight_smile:) if these libs from <julia-root>/bin are available (besides the hello.exe):

  • hello.exe (1.648 MB)
  • libatomic-1.dll (0.256 MB)
  • libgcc_s_seh-1.dll (0.932 MB)
  • libjulia-internal.dll (14.556 MB)
  • libjulia.dll (0.214 MB)
  • libopenlibm.dll (0.523 MB)
  • libpcre2-8.dll (0.802 MB)
  • libstdc+±6.dll (25.018 MB)
  • libwinpthread-1.dll (0.323 MB)

For all binary-size guys out there, that’s 42.6 MB of libraries (almost exclusively made up by the two highlighted-in-bold-font ones).


Edit 2: I was calling the .exe from bash (on mingw64) before. Then the above libs are sufficient. If called from cmd/powershell, it complains about two other libs not being available, but still prints the “Hello world!” to the console. These libs are:

  • libgmp-10.dll (1.054 MB)
  • libmpfr-6.dll (2.505 MB)
3 Likes

First, a question: is there any way of finding out what libraries (from <julia-root>/bin) are needed for a given juliac-compiled binary? Either by juliac-related tooling, or then afterwards by analysing the binary?


So, next step: move from an .exe to a library (.dll on Windows). Changed the file to now just be a Base.@ccallable function say_hello()::Cint, everything else is the same. Compilation worked (just using --output-lib), but when I want to dlopen the library (from julia, of course :wink:), I get a popup-error-message again stating that entrypoint ijl_gc_small_alloc is not available. Any hints on what happened now and where to start to make the library work?

Edit: Forgot the additional --compile-ccallable argument. But still, when trying to load the library in julia, I now get the message about a new missing entrypoint named ijl_autoinit_and_adopt_thread.

However, when calling the library from Go, for example, it works. :thinking:

1 Like

Yeah, it’s still quite hard and on the bleeding edge right now. Julia dynamically links to lots of libraries — both at startup and dynamically loaded. A few quick notes:

  • PackageCompiler has the smarts to get the rpaths right and gather all the artifacts and other libraries. The current juliac.jl script doesn’t incorporate all of that, but the --relative-rpath flag can get you a little closer. This is a very good reason to incorporate juliac.jl into PackageCompiler.
  • Using --trim on nightly is currently broken for non-trivial use due to trimming: no way to `ccall` into a dynamically-computed library name · Issue #57707 · JuliaLang/julia · GitHub. The v1.12 RC (juliaup’s +beta channel currently) will probably get you farther.
1 Like

If your functions are simple enough you may find StaticCompiler.jl as an alternative.

I am also very interested in this and actively trying to get a library to trim and distribute. I am currently able to generate the “.so” file and have filtered out the required libraries “by hand” (going through one-by-one in the RPATH and checking if renaming them breaks the executable). Now when I try to run a binary compiled with the library on another machine I am still missing the artifacts, so I’m trying to understand how PackageCompiler.jl includes them so that it’s portable… working on that right now. It seems to me that we can just copy over the required artifacts to a local dir (just like the RPATH libraries), but I’m not sure if I have to tell julia still where to look.

I was able to strip out libstdc+±6.dll by the way, which seems to take up a majority of your memory. I still have BLAS bundled though (35MB), and getting rid of that is turning out to be tricky, for instance because it is challenging to track from where BLAS is actually called.

I’m similarly trying to figure out (by reverse engineering PackageCompiler, among other things) what the minimum set of DLLs is for a given program. I’d be interested in knowing what the minimum set of these is for the Julia language to fully work – I don’t believe it is the full distro you get on a basic juliaup install. The test case would be a minimal shell application that links to libjulia and provides a REPL-like experience that would include adding packages, eval, and AOT/JIT compilation. And flag those that you could take away if you ran strictly on the interpreter.

I have not been able to find an authoritative list or tool (other than PackageCompiler, which is doing something a little different) with this information.

Depending on your use case, this might work: put your julia code inside an AppBundler.jl app and have your library call that. No trim functionality, unless you delete files manually.

I have now managed to also compile it and run everything on a clean podman VM. For the artifacts, as a crutch I also compile with PackageCompiler.jl and then just copy over the artifacts that are bundled there. Probably I could go in and figure out how PackageCompiler.jl constructs the artifacts bundle and reuse that code.

I think in my many follow-ups and edits, my current pain point was lost.

So I can compile the “hello world” example, but when trying to load the library in julia, I get an error about a missing entrypoint named ijl_autoinit_and_adopt_thread.

However, when calling the library from Go, for example, it works. What happens when Libdl.dlopen-ing (not even ccall-ing) a shared library in julia? Why is this entrypoint called (and/or by whom)?

(“But why calling it from julia?!”, you might ask. Because tests and “validation scenarios” will be available for the julia-code version, and I will want to run them again against the juliac-generated library to ensure that it works exactly the same.)