The C API
The diffsol-c crate wraps the
dynamic dispatch API as extern "C" functions so that they can be called from C code.
A language that can call C functions (for example with ctypes, cffi or a foreign
function interface) can also be used.
Install the Debian packages
On Debian 12 (bookworm) and 13 (trixie), and on Ubuntu 22.04 (jammy) and 24.04 (noble), for amd64 and arm64, you can install the library with apt. You do not need Rust. There are two packages:
libdiffsol<version>, for examplelibdiffsol0.7: the shared librarylibdiffsol.so. The library has the Cranelift and LLVM JIT backends, and it can load DiffSL code that was compiled before. It uses the LLVM library of the distribution:libllvm19on Debian 12, Debian 13 and Ubuntu 24.04, andlibllvm15on Ubuntu 22.04. apt installs it as a dependency.libdiffsol-dev: the headerdiffsol.h, the pkg-config filediffsol.pc, and the link to the shared library. Install this package to compile programs.
Add the APT repository of diffsol, then install the development package:
curl -1sLf 'https://dl.cloudsmith.io/public/martinjrobins/diffsol/setup.deb.sh' | sudo -E bash
sudo apt install libdiffsol-dev
You can also get the packages from the assets of the diffsol-c
GitHub release. There are packages for each
distribution: the codename of the distribution is part of the package version, for example
libdiffsol0.7_0.7.0-1~noble_amd64.deb. Install the two packages of your distribution and
architecture, for example on Ubuntu 24.04:
sudo apt install ./libdiffsol0.7_*~noble_amd64.deb ./libdiffsol-dev_*~noble_amd64.deb
Then compile and link a program with pkg-config:
cc main.c $(pkg-config --cflags --libs diffsol) -lm -o main
The packages install the library as libdiffsol.so, so a program links with -ldiffsol. A
library that you build with Cargo has the name libdiffsol_c.so (see below), so a program links
with -ldiffsol_c. The header and the functions are the same.
Build the library
Build the crate with Cargo. By default the ODE equations can be supplied as C callbacks. You can also supply the equations using DiffSL via the additional features:
diffsl-cranelift: compile DiffSL code at runtime with the Cranelift JIT backend.diffsl-llvm15todiffsl-llvm21: compile DiffSL code at runtime with LLVM. LLVM of that version must be on the build machine. These features link LLVM statically.diffsl-llvm15-prefer-dynamictodiffsl-llvm21-prefer-dynamic: the same, but link the shared LLVM library (libLLVM) if it is available. Use these to depend on the LLVM package of a Linux distribution. The library is then much smaller, but the LLVM library must be installed where the program runs.diffsl-external-dynamic(default): load DiffSL code that was compiled before, from a dynamic library.suitesparse: add the KLU sparse linear solver.
cargo build -p diffsol-c --release --features diffsl-cranelift
The build makes these files in target/release:
| File | Description |
|---|---|
include/diffsol.h | The C header. |
libdiffsol_c.so (Linux), libdiffsol_c.dylib (macOS) | The dynamic library. |
diffsol_c.dll and diffsol_c.dll.lib (Windows) | The dynamic library and its import library. |
The build script makes the header with cbindgen. The
header agrees with the features of the build. It defines a macro for each enabled feature, for
example DIFFSOL_DIFFSL_CRANELIFT. The functions of a feature are only in the header when the
macro of that feature is defined. Thus C code can use #ifdef to find which features the
library has.
Compile and link a C program
Link with the dynamic library. On Linux and macOS:
cc main.c -I target/release/include -L target/release -ldiffsol_c -lm \
-Wl,-rpath,$PWD/target/release -o main
The -rpath option tells the program where to find the library at runtime. You can also put
the library in a directory that the system searches, or use LD_LIBRARY_PATH (Linux) or
DYLD_LIBRARY_PATH (macOS).
On Windows, with MSVC:
cl main.c /I target\release\include target\release\diffsol_c.dll.lib
At runtime, diffsol_c.dll must be in the same directory as the program, or in a directory in
PATH.
Static library
A static library is made only on request. This command makes it, and lists the system libraries that a program must also link with:
cargo rustc -p diffsol-c --release --lib --features diffsl-cranelift --crate-type staticlib \
-- --print native-static-libs
The static library is target/release/libdiffsol_c.a (diffsol_c.lib on Windows). Link it,
and the system libraries that the command lists. For example, on macOS:
cc main.c -I target/release/include target/release/libdiffsol_c.a -lc++ -liconv -lm -o main
How to use the API
All C API functions use the same rules:
- A function that can fail returns an
int32_tstatus:DIFFSOL_OK(0) on success, or a negative value (DIFFSOL_ERR,DIFFSOL_BAD_ARG) on failure. A constructor returnsNULLon failure. - After a failure,
diffsol_last_error_message()gives a description of the error. The error is kept for each thread. - The library owns each object that it returns, for example an
OdeWrapper, aSolutionWrapperor aTensor. Free each object with its free function, for examplediffsol_ode_free. - The enum values are
int32_tconstants in the header, for exampleDIFFSOL_MATRIX_TYPE_NALGEBRA_DENSE,DIFFSOL_LINEAR_SOLVER_TYPE_DEFAULT,DIFFSOL_ODE_SOLVER_TYPE_BDFandDIFFSOL_JIT_BACKEND_TYPE_CRANELIFT. - A solution array (
Tensor) gives a pointer to its data, its dimensions and its strides. The strides are in bytes. ATensorshares the data of the solution, with no copy, and stays valid after you free theSolutionWrapper. - The arrays use the column-major order of diffsol: the first axis changes fastest.
- An ODE has
nbatchlanes. The last axis of an array of lanes is always the lane, also whennbatchis 1. A state or a parameter vector is ann x nbatcharray. The solution valuesysare annout x nt x nbatcharray. The timestsare anntarray, because all the lanes use the same times. - A function that takes lanes of values (for example
params) takes a pointer, the number of values in each lane and the number of lanes. The values are a column-majorlen x lanesarray: laneb, valuekis at indexb * len + k. The number of lanes is thenbatchof the ODE, or 1. One lane is used for all the lanes of the ODE.
The functions are in these modules of the crate documentation:
- ode_c: configure, solve and
free
OdeWrapperhandles. - ode_callbacks_c:
create an
OdeWrapperfrom equations that C callbacks give. - ode_diffsl_c: create an
OdeWrapperfrom DiffSL code (JIT) or from DiffSL code that was compiled before. - solution_wrapper_c:
get the solution data, and free
SolutionWrapperhandles. - ode_options_c and initial_condition_options_c: get and set the solver options.
- matrix_type_c, scalar_type_c, linear_solver_type_c, ode_solver_type_c and jit_c: the enum values, their names, and their checks.
- tensor_c: read and
free
Tensordata. - error_c: get and clear the error of the thread.
- string_c: allocate and free strings that Rust owns.
Example: equations from C callbacks
The example logistic_callbacks.c solves the logistic equation \(dy/dt = r \cdot y \cdot (1 - y)\), with equations that C functions give. It does not need DiffSL.
Each callback gets its inputs as arrays, writes its output to y, and returns 0 on success.
Its last argument is the user_data pointer of the descriptor. The
ode_callbacks module
gives the array lengths of each callback, the sign conventions of the adjoint callbacks, and the
ownership rules of user_data.
/* rhs: y = r x (1 - x). The arrays have the lengths that the header gives: x and y have
* nstates values, p has nparams values. */
static int32_t rhs(const double *x, const double *p, double t, double *y, void *user_data) {
(void)t;
((UserData *)user_data)->rhs_calls += 1;
y[0] = p[0] * x[0] * (1.0 - x[0]);
return 0; /* 0 is success; a different value tells the solver that the callback failed */
}
/* rhs_jac: y = J v, with J = dF/dx = r (1 - 2 x). The implicit solvers (for example BDF) need it. */
static int32_t rhs_jac(const double *x, const double *p, double t, const double *v, double *y,
void *user_data) {
(void)t;
(void)user_data;
y[0] = p[0] * (1.0 - 2.0 * x[0]) * v[0];
return 0;
}
/* init: the initial state y0(p, t). */
static int32_t init(const double *p, double t, double *y, void *user_data) {
(void)p;
(void)t;
(void)user_data;
y[0] = 0.1;
return 0;
}
Put the callbacks in a DiffsolOdeCallbacksF64 descriptor, and create the solver. Only rhs
and init are necessary. A NULL callback is not given. If a solve needs a callback that is
not given, the solve returns an error that gives the name of the callback. For example, the
implicit methods (such as BDF) need rhs_jac.
/* Set all fields to zero first: a NULL callback means that it is not given. */
DiffsolOdeCallbacksF64 callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.nstates = 1;
callbacks.nparams = 1;
callbacks.user_data = &data; /* data stays valid until the solver is freed */
callbacks.free_user_data = NULL; /* the program owns data, so the library does not free it */
callbacks.rhs = rhs;
callbacks.rhs_jac = rhs_jac;
callbacks.init = init;
/* The last argument is nbatch, the number of lanes (copies of the system) to solve. */
OdeWrapper *ode = diffsol_ode_new_callbacks_f64(&callbacks, DIFFSOL_MATRIX_TYPE_NALGEBRA_DENSE,
DIFFSOL_LINEAR_SOLVER_TYPE_DEFAULT,
DIFFSOL_ODE_SOLVER_TYPE_BDF, 1);
if (ode == NULL) {
return fail("diffsol_ode_new_callbacks_f64");
}
Solve, and read the solution:
const double params[] = {2.0}; /* r */
const double t_eval[] = {0.0, 0.5, 1.0, 2.0, 4.0};
const size_t n_eval = sizeof(t_eval) / sizeof(t_eval[0]);
/* params has 1 lane of 1 value. All the lanes of the ODE use a single lane of params. */
SolutionWrapper *solution = NULL;
if (diffsol_ode_solve_dense(ode, params, 1, 1, t_eval, n_eval, &solution) != DIFFSOL_OK) {
diffsol_ode_free(ode);
return fail("diffsol_ode_solve_dense");
}
/* ys is an (nout x n_eval x nbatch) array. Read it with its strides, which are in bytes. */
Tensor *ys = NULL;
if (diffsol_solution_wrapper_get_ys(solution, &ys) != DIFFSOL_OK) {
diffsol_solution_wrapper_free(solution);
diffsol_ode_free(ode);
return fail("diffsol_solution_wrapper_get_ys");
}
const uint8_t *data_ptr = diffsol_tensor_ptr(ys);
const size_t col_stride = diffsol_tensor_stride(ys, 1);
int status = 0;
printf("%8s %14s %14s\n", "t", "y", "exact");
for (size_t j = 0; j < diffsol_tensor_dim(ys, 1); j++) {
const double y = *(const double *)(data_ptr + j * col_stride);
const double expected = exact(params[0], t_eval[j]);
printf("%8.3f %14.8f %14.8f\n", t_eval[j], y, expected);
if (fabs(y - expected) > 1e-5) {
status = 1;
}
}
printf("rhs was called %d times\n", data.rhs_calls);
diffsol_tensor_free(ys);
diffsol_solution_wrapper_free(solution);
diffsol_ode_free(ode);
Example: DiffSL code with the JIT
The example
logistic_diffsl_jit.c
solves the same equation from DiffSL code. The library compiles the code at runtime. Thus the
library must have a JIT backend (diffsl-cranelift or diffsl-llvm*).
/* The model in DiffSL. `in_i` are the parameters, `u_i` the state, `F_i` the right-hand side
* and `out_i` the output. */
static const char *CODE = "in_i { r = 1 }\n"
"u_i { y = 0.1 }\n"
"F_i { r * y * (1 - y) }\n"
"out_i { y }\n";
Select a JIT backend that the library has, and create the solver:
/* Use Cranelift if the library has it, otherwise LLVM. The environment variable
* DIFFSOL_JIT_BACKEND can select a different backend. */
#if defined(DIFFSOL_DIFFSL_CRANELIFT)
int32_t backend = DIFFSOL_JIT_BACKEND_TYPE_CRANELIFT;
#else
int32_t backend = DIFFSOL_JIT_BACKEND_TYPE_LLVM;
#endif
const char *requested = getenv("DIFFSOL_JIT_BACKEND");
if (requested != NULL && strcmp(requested, "llvm") == 0) {
backend = DIFFSOL_JIT_BACKEND_TYPE_LLVM;
} else if (requested != NULL && strcmp(requested, "cranelift") == 0) {
backend = DIFFSOL_JIT_BACKEND_TYPE_CRANELIFT;
}
printf("JIT backend: %s\n", diffsol_jit_backend_type_name(backend));
OdeWrapper *ode = diffsol_ode_new_jit(CODE, backend, DIFFSOL_MATRIX_TYPE_NALGEBRA_DENSE,
DIFFSOL_LINEAR_SOLVER_TYPE_DEFAULT,
DIFFSOL_ODE_SOLVER_TYPE_BDF);
if (ode == NULL) {
return fail("diffsol_ode_new_jit");
}
The solve is the same as in the callbacks example.
Run the examples
Build the library, then compile and run an example. On Linux and macOS:
cargo build -p diffsol-c --features diffsl-cranelift
cc crates/diffsol-c/examples/c/logistic_diffsl_jit.c -I target/debug/include \
-L target/debug -ldiffsol_c -lm -Wl,-rpath,$PWD/target/debug -o logistic_diffsl_jit
./logistic_diffsl_jit
The script crates/diffsol-c/examples/c/run_examples.sh compiles and runs the examples against
a Cargo build. It runs the JIT example only if the library has a JIT backend, and also runs it
with LLVM if the library has LLVM. The CI runs it on Linux, macOS and Windows (in Git Bash, with
MSVC).
cargo build -p diffsol-c --features diffsl-cranelift
crates/diffsol-c/examples/c/run_examples.sh target/debug
The JIT example uses Cranelift if the library has it. To select the backend, set the environment
variable DIFFSOL_JIT_BACKEND to cranelift or llvm.