User Guide
This guide covers the common workflows for using RepliBuild.jl.
Basic Workflow
The standard workflow involves three steps: discovery, building, and wrapping.
1. Discovery
The discover function scans your directory for C++ files and generates a replibuild.toml configuration file.
RepliBuild.discover()You can also specify a directory:
RepliBuild.discover("path/to/project")2. Building
Once configured, the build function compiles your C++ code into a shared library.
RepliBuild.build()This step performs:
- Compilation of C++ to LLVM IR (
.cfiles useclang,.cppfiles useclang++). - Linking and optimization.
- Generation of the shared library.
- Extraction of metadata for wrapping.
3. Wrapping
Finally, generate the Julia wrapper module.
RepliBuild.wrap()This will create a Julia file in the julia/ directory that you can include and use.
Automated Workflow
You can chain these steps together using the flags in discover:
# Discover, Build, and Wrap in one go
RepliBuild.discover(build=true, wrap=true)Package Registry
RepliBuild includes a global package registry (~/.replibuild/registry/) that caches build artifacts so repeated loads are instant.
# Register a project (discover() does this automatically)
RepliBuild.register("replibuild.toml")
# Load a registered package — builds on first call, cached thereafter
Lua = RepliBuild.use("lua_wrapper")
# List all registered packages with hash, source, and build status
RepliBuild.list_registry()
# Remove a package from the registry and clean cached builds
RepliBuild.unregister("lua_wrapper")
# Scaffold a distributable Julia package from a registered project
RepliBuild.scaffold_package("LuaWrapper")The use() function handles the full lifecycle: resolve dependencies, build (or load from cache), wrap, and return a loaded Julia module. The REPLIBUILD_HOME environment variable can override the default registry location.
Configuration
The replibuild.toml file controls the build process. You can edit this file to customize:
- Compiler flags
- Include directories
- Output names
- Optimization levels
See the Configuration Reference for a complete list of available options and sections.
C vs C++ Projects
RepliBuild uses wrap.language as an extensible dispatch key to select the generator, compiler toolchain, and build defaults for a project. "c" and "cpp" are the first two targets:
[wrap]
language = "c" # pure-C project: uses clang, LTO on by default
language = "cpp" # C++ project (default): uses clang++discover() sets this automatically from the scanned source file extensions. For C projects the enable_lto default is true, so you get zero-cost llvmcall dispatch out of the box without any extra configuration.
Additional language targets are planned — the language field is the hook that will route each new language to its own generator and toolchain.
Git & External Dependencies
RepliBuild can automatically pull external C/C++ libraries from git, local paths, or your system into the compilation pipeline. Declare them in replibuild.toml under [dependencies]:
[dependencies.cjson]
type = "git"
url = "https://github.com/DaveGamble/cJSON"
tag = "v1.7.18"
exclude = ["test", "fuzzing"]Run the normal pipeline — the dependency is cloned, filtered, and compiled transparently:
RepliBuild.build("replibuild.toml")
RepliBuild.wrap("replibuild.toml")The first build clones into .replibuild_cache/deps/cjson/. Subsequent builds are cached and only re-clone when the tag changes.
For a local library:
[dependencies.mylib]
type = "local"
path = "../vendor/mylib"
exclude = ["docs", "examples"]For a system library (uses pkg-config):
[dependencies.zlib]
type = "system"
pkg_config = "zlib"See the Configuration Reference for all fields.
Idiomatic Julia Class Wrappers
When your C++ library exposes a class through factory/destructor pairs, RepliBuild automatically generates an idiomatic mutable struct wrapper on top of the raw FFI bindings.
Detection: the wrapper generator scans for:
- Factory functions whose name matches
create_X,new_X,make_X,alloc_X,init_X, or whose return type isX*. - Destructor/deleter functions whose name matches
delete_X,destroy_X,free_X,dealloc_X, orX_destroy. - Instance methods associated with the same class via the DWARF
classfield.
Generated output for a Circle class:
# Raw bindings (always generated)
function create_circle(radius::Cdouble)::Ptr{Cvoid} ... end
function get_area(this::Ptr{Cvoid})::Cdouble ... end
function delete_shape(this::Ptr{Cvoid}) ... end
# Idiomatic wrapper (generated automatically on top)
mutable struct Circle
handle::Ptr{Cvoid}
function Circle(radius::Cdouble)
handle = create_circle(radius)
obj = new(handle)
finalizer(obj) do o
delete_shape(o.handle)
end
return obj
end
end
# Method proxies via multiple dispatch
get_area(c::Circle) = get_area(c.handle)User code needs no manual memory management:
c = Circle(5.0) # allocates C++ object, registers GC finalizer
get_area(c) # dispatch on Circle type, no .handle needed
# c goes out of scope → GC calls delete_shape automaticallyReplacing Manual Shims
When wrapping C/C++ libraries, developers often have to write manual C wrappers ("shims") for things that aren't native functions: templates, varargs, and preprocessor macros. RepliBuild handles all of these automatically via replibuild.toml without you having to write a single line of C/C++ code.
Template Instantiation
C++ templates are only emitted into DWARF if the compiler actually instantiates them. To force instantiation for types you want to wrap, add them to [types] in your config:
[types]
templates = ["std::vector<int>", "std::vector<double>", "std::pair<int,float>"]
template_headers = ["<vector>", "<utility>"]RepliBuild auto-generates a stub .cpp file that explicitly instantiates each requested type, ensuring it appears in the DWARF metadata and is available in the generated Julia module.
Varargs Interception
Julia's ccall cannot call C ... (varargs) functions natively without knowing the exact types at the call site. Instead of writing a manual C wrapper for each type combination, you can configure overloads in [wrap.varargs]:
[wrap.varargs]
printf = [
["const char*", "int"],
["const char*", "double", "int"]
]RepliBuild generates concrete Julia bindings for printf for each of these signatures, completely bypassing the varargs limitation.
Macro Expansion
C/C++ preprocessor macros don't exist in compiled binaries or DWARF metadata. To expose them to Julia, you can configure [wrap.macros]:
[wrap]
shim_headers = ["<stdio.h>"]
[wrap.macros.MY_MATH_MACRO]
ret = "int"
args = ["int", "float"]RepliBuild automatically generates a C/C++ source file that wraps MY_MATH_MACRO inside a typed function and compiles it alongside your project. The resulting wrapper gives you a native Julia function.
Zero-Cost LTO Dispatch
When enable_lto = true (or for C projects, where it is the default), the linker emits both the shared library and LLVM bitcode (<name>_lto.bc) in the julia/ output directory.
[link]
enable_lto = true
optimization_level = "3"The generated Julia wrapper loads the bitcode at module parse time:
const LTO_IR_PATH = joinpath(@__DIR__, "mylib_lto.bc")
const LTO_IR = isfile(LTO_IR_PATH) ? read(LTO_IR_PATH) : UInt8[]For every eligible function (primitive/pointer types, no Cstring, no virtual dispatch, no struct-by-value return), the wrapper emits a dual-dispatch body:
function vector_dot(a::Ptr{Cvoid}, b::Ptr{Cvoid}, n::Cint)::Cdouble
if !isempty(LTO_IR)
return Base.llvmcall((LTO_IR, "_Z10vector_dotPdPdi"),
Cdouble, Tuple{Ptr{Cvoid}, Ptr{Cvoid}, Cint},
a, b, n)
else
return ccall((:_Z10vector_dotPdPdi, LIBRARY_PATH),
Cdouble, (Ptr{Cvoid}, Ptr{Cvoid}, Cint),
a, b, n)
end
endWhen the bitcode is present, Julia's LLVM JIT merges the C++ IR directly into the calling Julia function's IR, enabling full cross-language inlining and vectorization. The ccall fallback fires automatically if the .bc file is missing (e.g., an LTO-disabled build was deployed).
AOT Thunks for Virtual Dispatch
Virtual method dispatch normally requires the MLIR JIT to compile thunks at runtime. Setting aot_thunks = true pre-compiles those thunks at build time:
[compile]
aot_thunks = trueDuring RepliBuild.build(), the JLCS MLIR dialect generates and compiles all virtual dispatch thunks into a companion <name>_thunks.so placed alongside the main library. The generated wrapper emits purely static ccall bindings that load from THUNKS_LIBRARY_PATH — no JITManager startup, no lock, no on-demand compilation:
function Circle_area(this::Ptr{Cvoid})::Cdouble
return ccall((:thunk_Circle_area, THUNKS_LIBRARY_PATH), Cdouble, (Ptr{Cvoid},), this)
endThis is the recommended setting for production deployments where predictable latency matters. Requires src/mlir/build/libJLCS.so to be built first (cd src/mlir && ./build.sh).
Running Tests
The CI suite (stress test + MLIR unit tests + registry tests):
julia --project=. test/runtests.jlThe full developer integration suite (Lua, SQLite, cJSON, Duktape, vtable, JIT edge cases):
julia --project=. test/devtests.jlExternal sources are downloaded on first run via setup.jl scripts.