Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 example libdiffsol0.7: the shared library libdiffsol.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: libllvm19 on Debian 12, Debian 13 and Ubuntu 24.04, and libllvm15 on Ubuntu 22.04. apt installs it as a dependency.
  • libdiffsol-dev: the header diffsol.h, the pkg-config file diffsol.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-llvm15 to diffsl-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-dynamic to diffsl-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:

FileDescription
include/diffsol.hThe 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.

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_t status: DIFFSOL_OK (0) on success, or a negative value (DIFFSOL_ERR, DIFFSOL_BAD_ARG) on failure. A constructor returns NULL on 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, a SolutionWrapper or a Tensor. Free each object with its free function, for example diffsol_ode_free.
  • The enum values are int32_t constants in the header, for example DIFFSOL_MATRIX_TYPE_NALGEBRA_DENSE, DIFFSOL_LINEAR_SOLVER_TYPE_DEFAULT, DIFFSOL_ODE_SOLVER_TYPE_BDF and DIFFSOL_JIT_BACKEND_TYPE_CRANELIFT.
  • A solution array (Tensor) gives a pointer to its data, its dimensions and its strides. The strides are in bytes. A Tensor shares the data of the solution, with no copy, and stays valid after you free the SolutionWrapper.
  • The arrays use the column-major order of diffsol: the first axis changes fastest.
  • An ODE has nbatch lanes. The last axis of an array of lanes is always the lane, also when nbatch is 1. A state or a parameter vector is an n x nbatch array. The solution values ys are an nout x nt x nbatch array. The times ts are an nt array, 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-major len x lanes array: lane b, value k is at index b * len + k. The number of lanes is the nbatch of 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:

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.