Hi all,
I’m reaching out to see if anyone in the Julia community has experience or advice on calling C code from Julia in a way that still supports automatic differentiation (AD).
My motivation
I’m working on a project that relies on an extensively developed C library. Rewriting this codebase in Julia is unfortunately not feasible, but I would like to call into it from Julia and still benefit from automatic differentiation to speed up downstream computations.
What I’ve tried
I’ve experimented a bit with Julia’s native @ccall
interface (docs), which works well for straightforward interop. However, as expected, it doesn’t seem to play nicely with AD out of the box.
I tried this simple example:
double square(double x)
{
return x * x;
}
with
using ForwardDiff
const lib = "./mylib.so"
square(x) = ccall((:square, lib), Cdouble, (Cdouble,), x)
println(square(3.0)) # 9.0
println(ForwardDiff.derivative(square, 3.0)) # LoadError
Since I’m relatively new to automatic differentiation, I’d love to hear if:
- There are Julia packages or tools that can enable AD through external C calls
- There are recommended patterns or workarounds for integrating AD with foreign function calls
Any pointers, examples or guidance would be very much appreciated! Thanks in advance!
2 Likes
If you can compile the external library with clang and include llvm bytecode in the object file, Enzyme.jl is able to differentiate through that. Hopefully @vchuravy @wsmoses can provide concrete examples for that.
2 Likes
My naive attempt
julia> using Enzyme
julia> file = """
double square(double x) {
return x * x;
}
""";
julia> run(pipeline(`clang -x c - -fPIC -fembed-bitcode -shared -o libsquare.dylib`; stdin=IOBuffer(file)))
Process(`clang -x c - -fPIC -fembed-bitcode -shared -o libsquare.dylib`, ProcessExited(0))
julia> csquare(x::Cdouble) = @ccall "./libsquare.dylib".square(x::Cdouble)::Cdouble
csquare (generic function with 1 method)
julia> first(first(autodiff(Reverse, csquare, Active, Active(3.14))))
ERROR:
No reverse pass found for ejlstr$square$./libsquare.dylib
at context: %3 = call double @"ejlstr$square$./libsquare.dylib"(double %0) #5, !dbg !14
Stacktrace:
[1] csquare
@ ./REPL[16]:1
failed, so yeah, I hope they can provide a working example 
Edit: I found this thread Is it possible to use autodiff on an external program? - #2 by wsmoses which suggests the same strategy, but without a working example.
Edit 2: I managed not to get an error with
julia> using Enzyme, Libdl
julia> file = """
double square(double x) {
return x * x;
}
""";
julia> run(pipeline(`clang -x c - -fPIC -fembed-bitcode -shared -o libsquare.dylib`; stdin=IOBuffer(file)))
Process(`clang -x c - -fPIC -fembed-bitcode -shared -o libsquare.dylib`, ProcessExited(0))
julia> fptr = dlsym(dlopen("./libsquare.dylib"), :square)
Ptr{Nothing} @0x0000000104833f9c
julia> csquare(x::Cdouble) = ccall(fptr, Cdouble, (Cdouble,), x)
csquare (generic function with 1 method)
julia> first(first(autodiff(Reverse, csquare, Active, Active(3.14))))
0.0
but the result is still wrong, so I’m clearly still missing something.
If Enzyme is not working, I recommend differentiating your C project using the source-to-source tool Tapenade, and then ccall
the differentiated routines from Julia.
In general, you have to write a custom differentiation rule for those calls. (with Enzyme rules or ChainRules.jl or others, depending on what AD package you are using).
1 Like
It appears julia changed its FFI runtime calls since last someone tried, but in any case on latest Enzyme.jl (0.13.48) and Julia 1.10 or 1.11, this should work:
(base) wmoses@hydra:~/git/Enzyme.jl$ julia +1.10 --project
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.10.9 (2025-03-10)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
julia> using Enzyme
# Enzyme.Compiler.DebugLTO[] = true
julia> file = """
double square(double x) {
return x * x;
}
""";
julia> run(pipeline(`/home/wmoses/llvms/llvm15/buildD/bin/clang -Xclang -no-opaque-pointers -x c - -fPIC -fembed-bitcode -shared -o libsquare.dylib`; stdin=IOBuffer(file)))
Process(`/home/wmoses/llvms/llvm15/buildD/bin/clang -Xclang -no-opaque-pointers -x c - -fPIC -fembed-bitcode -shared -o libsquare.dylib`, ProcessExited(0))
julia> csquare(x::Cdouble) = @ccall "./libsquare.dylib".square(x::Cdouble)::Cdouble
csquare (generic function with 1 method)
julia> @show first(first(autodiff(Reverse, csquare, Active, Active(3.14))))
first(first(autodiff(Reverse, csquare, Active, Active(3.14)))) = 6.28
6.28
Note that you need the version of clang to be less than or equal to the version of LLVM used by Julia (e.g. 15 for Julia 1.10 or 16 for 1.11).
Thanks everyone for the suggestions. I am glad to see that there are options I can explore.
I have been trying the suggested Julia commands, but I am having problems with the -fembed-bitcode
flag:
ld: warning: -bitcode_bundle is no longer supported and will be ignored
which leads to an error in the AD:
ERROR: LoadError:
No reverse pass found for square
at context: %4 = call double @square(double %0) #5, !dbg !14
I suspect this may be a known limitation of Macs. Is there a workaround for this?
I am using LLVM version 15.0.7 on a MacBook Pro M3 (installed with Homebrew).