[ANN] JuliaLibWrapping.jl: bringing Julia libraries to C and Python users

I’m pleased to formally announce JuliaLibWrapping.jl (JLW), a tool for turning Julia packages into shared libraries with C and Python interfaces. This project has been a close collaboration with @Jorge_Vieyra, with important contributions from the JuliaC.jl developers and Frederick Ekre.

The overall goal is to extend our community’s impact, by making Julia packages available to users of other programming languages. Currently JLW supports Python and C, but R and Matlab are likely to follow soon.

JLW lets package authors expose a curated API that can be installed and used like a native component of the target ecosystem. Python users get an ordinary package with NumPy integration and Python exceptions; C users get a shared library and header. The package can bundle its Julia runtime dependencies, so users need neither a separate Julia installation nor a Julia project to manage.

This makes JLW an alternative to JuliaCall. JuliaCall offers Python rich, dynamic access to Julia itself, but requires that users install Julia. JLW instead exposes a bounded, compiled library interface: less open-ended, but encapsulated behind a stable ABI and usable from languages other than Python.

JLW builds on JuliaC.jl and includes JLWInterop, which provides common ABI conventions for arrays, strings, ownership, and error handling. The aim is to make these concerns shared infrastructure rather than something every package must design independently.

JuliaLibWrapping v0.2 and JLWInterop v0.2 are available now and require Julia 1.13 or later.

We expect further improvements in the coming months, but JLW should already be fairly usable now. We’d love feedback from package authors and users of mixed-language systems, so don’t hesitate to post here or file an issue.

Great news!

Not clear to me: must the Julia libraries and package dependencies be compilable with JuliaC? Is there a list of supported packages somewhere?

Yes, they have to be compatible with JuliaC, and no, there’s no list. My limited experience suggests that most packages will need a little help. For example, Build a compiled .so and create a Python binding - Pull Request #43 - HolyLab/MatrixCovers.jl - GitHub needed:

For reference, the package as a whole is >3800LOC excluding docstrings and comments. Overall these were pretty modest changes, and in my experience agents tend to figure them out quite easily.

I should say, though, that YMMV depending on the package. Some packages have dynamic dispatch nearly everywhere, and those will be much more difficult to wrap.

Nice work!

What are your thoughts on the planned R package and how to make that work for package developers? CRAN requirements for packages are they should be at most 10MB https://cran.r-project.org/, which seems hard to satisfy with this approach. Is it just meant for non-CRAN packages?

No real idea yet. I agree that for some packages, that’s going to be a hard goal to meet. (For that MatrixCovers package, the .so was 5MB.) One current obstacle is that Julia’s runtime needs to be “privatized,” to allow users to combine two or more Julia packages in a single Python/R/whatever session. The better long-term solution will be to link to a single shared runtime, and that will allow the sizes of each of the libraries to shrink.

Cool! This will be ideal for SolarPosition.jl as well, since it’s mostly base julia. Biggest challenge is that most packages I tried aren’t compatible with trimming, fixing that will take a lot of effort.

Maybe trivial now, but I would also be interested to see how you are going to actually ship the python bindings to pypi. I guess you want to do this automatically via a github action, to keep the Python package in sync with the Julia releases?

Maybe of interest for @droodman

I ran into an old problem of Julia’s runtime interfering with stdio in a C program. Here’s a MWE of the problem when embedding a Julia library in a C program. The Julia code exports a trivial function for computing an integer plus one. The C program runs a loop that keeps reading an integer from stdin, adding one to it (by calling the Julia function), and printing it to stdout, until reaching the end of stdin. The problem symptoms show up when I use pipes in a shell in Linux:

# { echo 1; echo 2; } | ./add-one-test
2
3

# { echo 1; sleep 1; echo 2; } | ./add-one-test
2

The second command should also print “3”. Instead, the program stops after the first value while the pipe is temporarily empty.

The Julia code is:

# mylib.jl
module mylib

using JLWInterop

addone(x) = x + 1

@api addone(x::Int64)::Int64

end

The C program is:

/* add-one-test.c */

#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

#include "mylib.h"

int main(void)
{
    int64_t input;

    while (scanf("%" SCNd64, &input) == 1) {
        JLWResult_Int64 result = mylib_addone(input);
        if (result.status.code != 0) {
            fprintf(stderr, "mylib_addone failed with status %" PRId32 "\n",
                    result.status.code);
            return 1;
        }

        printf("%" PRId64 "\n", result.value);
    }

    return 0;
}

The exact same problem also shows up when I use Python instead of C:

from mylib_py import addone

while True:
    try:
        print(addone(int(input())), flush=True)
    except EOFError:
        break

I’m looking this over. It seems well documented, uses OffsetArrays, so I suppose makes 0-based for C and Python users, everything as expected (only types compatible with C supported mapping to float and double, but both to float in Python, and BigInt or machine integers there?) except maybe:

Array{T,N} arguments must be dense, writable when the Julia function mutates them, and use the carrier’s column-major layout. The generated Python helper accepts contiguous one-dimensional arrays. For N ≥ 2, it requires a Fortran-contiguous NumPy array; use np.asfortranarray(a) when appropriate.

I.e. Julia’s column-major is retained (same as Fortran and R, unlike C/C++ and Python’s row-major), as sort of expected. Non-issue for 1D, works for any other only C and Python users need to be aware of. Do you always make a C API, then optionally Python API, and it is then on top of the C API? Just like is often a thing for Python packages, but that C API is not your concern. R often wraps C++ code, while R is 1-based like Julia, so when that is supported, would R API be also built on the C API, or not because of OffsetArray? Do you think you could generate something callable from R in addition to C and Python? Ideally with one command and getting one .so out? And DLL etc. for Windows, that seemed supported.

Is the setup for the producer, rather than the consumer, still also rather simple? And what about when your code changes, do you need to regenerate and edit some files? JuliaCall will be simpler for the “producer”, basically no work, not much if anything produced, but is it a bit harder for the consumer?

Declared Julia type Argument carrier Return carrier Python value
scalar same type, by value same type, by value int, float, or bool
String CString{:borrowed} CString{:owned} str
Vector{String} CStrArray{:borrowed} CStrArray{:owned} list[str]
Dict{String,V} for scalar V CDict{:borrowed,V} CDict{:owned,V} dict[str, V]
..

[C doesn’t have a Dict, so I’m not sure supported in the C API, CDict implies it might, or only in Python API, i.e. implementation detail for it?]

Python users may expect dict to be ordered (since the new default in all currently supported Python versions), so it may be surprising to them that it isn’t. At least could be documented. If you use OrderedDict, is that also supported and mapped to (or from) Python’s dict? Are only Vectors (and Dicts) of Strings supported, or such just an example?

Do you support same or more types as JuliaCall? As an example, I’m not sure if this is only for JuliaCall or in both direction:

0.9.32 (2026-05-14)
..
JuliaCall now launches Julia with 1 thread by default.
..
Initial experimental support for free-threaded Python 3.14.

I suppose you also support 1 thread, not more or non-default?

I’m not super familiar with how the Python and R packaging ecosystems work, but do we know what would be needed to get these distributed there? I have a couple of potential packages I’d like to do this for :smiley:

I assume we’d have to lock the versions of libraries Julia needs, like openssl, openblas, etc. That could be a bit of a sticking point, though if we get native linking that all goes away.