Skip to main content

C/C++ Bindings

When using any of the tools to auto-generate a solver it is directly supported to also generate C/C++ bindings for integrating the solver from any language which supports a C or C++ application binary interface (ABI). This is a powerful feature when packaging a solver for distribution, or including it into a larger project.

Generating bindings

Generating bindings is as easy as adding with_build_c_bindings() to your build configuration. Here is a full example configuration for creating a solver. We will create an optimizer for the Rosenbrock function with equality and bound constraints on the decision variables:

import casadi.casadi as cs
import opengen as og

u = cs.SX.sym("u", 5) # Decision variabels
p = cs.SX.sym("p", 2) # Parameters

phi = og.functions.rosenbrock(u, p) # Cost function

# Equality constraints: 1.5 * u[0] = u[1] and u[2] = u[3]
# Can also be inequality constratins using max{c(u, p), 0}, where c(u, p) < 0.
c = cs.vertcat(1.5*u[0] - u[1], u[2] - u[3])

# Bounds constraints on u
umin = [-2.0] * 5 # shorthand notation
umax = [ 2.0] * 5

# Bounds on u: (umin <= u <= umax)
bounds = og.constraints.Rectangle(umin, umax)

# Define the problem
problem = og.builder.Problem(u, p, phi) \
.with_penalty_constraints(c) \
.with_constraints(bounds)

# Meta information for the solver
meta = og.config.OptimizerMeta() \
.with_version("1.0.0") \
.with_authors(["P. Sopasakis", "E. Fresk"]) \
.with_optimizer_name("the_optimizer")

# Lets build in release mode with C bindings
build_config = og.config.BuildConfiguration() \
.with_build_mode("release") \
.with_build_c_bindings() # <--- The important setting

# Solver settings
solver_config = og.config.SolverConfiguration() \
.with_tolerance(1e-5) \
.with_constraints_tolerance(1e-4) \
.with_max_outer_iterations(15) \
.with_penalty_weight_update_factor(8.0) \
.with_initial_penalty_weights([20.0, 5.0])

# Create the solver!
builder = og.builder.OpEnOptimizerBuilder(problem,
metadata=meta,
build_configuration=build_config,
solver_configuration=solver_config)

builder.build()

The generated C/C++ bindings are in the auto-generated solver library. In particular

  • The header files are at the_optimizer/the_optimizer_bindings.{h,hpp}
  • The static and dynamic library files are located in the_optimizer/target/{debug,release} (depending on whether it was a debug or release build)

Note that the_optimizer is the name given to the optimizer in the Python codegen above.

Matlab generation will come soon.

Bindings API

The generated API has the following functions available (for complete definitions see the generated header files) for a solver entitled "example":

/* Contents of header file: example_bindings.h */

#define EXAMPLE_N1 0
#define EXAMPLE_N2 2
#define EXAMPLE_NUM_DECISION_VARIABLES 5
#define EXAMPLE_NUM_PARAMETERS 2

typedef enum {
exampleConverged,
exampleNotConvergedIterations,
exampleNotConvergedOutOfTime,
exampleNotConvergedCost,
exampleNotConvergedNotFiniteComputation,
} exampleExitStatus;

typedef struct exampleCache exampleCache;

typedef struct {
exampleExitStatus exit_status;
int error_code;
char error_message[1024];
unsigned long num_outer_iterations;
unsigned long num_inner_iterations;
double last_problem_norm_fpr;
unsigned long long solve_time_ns;
double penalty;
double delta_y_norm_over_c;
double f2_norm;
double cost;
const double *lagrange;
} exampleSolverStatus;

void example_free(exampleCache *instance);

exampleCache *example_new(void);

exampleSolverStatus example_solve(exampleCache *instance,
double *u,
const double *params,
const double *y0,
const double *c0);

This is designed to follow a new-use-free pattern.

Function {optimizer-name}_new will allocate memory and setup a new solver instance and can be used to create as many solvers as necessary. Each solver instance can be used with {optimizer-name}_solve to solve the corresponding problem as many times as needed.

Parameter u is the starting guess and also the return of the decision variables and params is the array of static parameters. The size of u and params are {optimizer-name}_NUM_DECISION_VARIABLES and {optimizer-name}_NUM_PARAMETERS respectively. Arguments y0 and c0 are optional: pass 0 (or NULL) to use the default initial Lagrange multipliers and penalty parameter.

The returned exampleSolverStatus always contains a coarse solver outcome in exit_status. On success it also contains error_code = 0 and an empty error_message. If the solver fails internally, the bindings return a structured error report with a nonzero error_code and a descriptive error_message.

Finally, when done with the solver, use {optimizer-name}_free to release the memory allocated by {optimizer-name}_new.

Handling errors

The C bindings always return a value of type exampleSolverStatus. This means that solver calls do not report failure by returning NULL or by using a separate exception-like mechanism. Instead, callers should inspect both exit_status and error_code.

  • error_code = 0 means the solver call completed without an internal error
  • error_code != 0 means the solver failed and error_message contains a descriptive explanation
  • exit_status gives the coarse outcome of the solve attempt, such as converged, reached the iteration limit, or failed because of a numerical issue

The recommended pattern is:

  1. Call {optimizer-name}_solve(...)
  2. Check whether status.error_code != 0
  3. If so, report status.error_message and treat the call as failed
  4. Otherwise, inspect status.exit_status to determine whether the solver converged or returned the best available non-converged iterate

For example:

exampleSolverStatus status = example_solve(cache, u, p, 0, &initial_penalty);

if (status.error_code != 0) {
fprintf(stderr, "Solver failed: [%d] %s\n",
status.error_code, status.error_message);
example_free(cache);
return EXIT_FAILURE;
}

if (status.exit_status != exampleConverged) {
fprintf(stderr, "Warning: solver did not converge fully\n");
}

The generated C example follows exactly this pattern.

At the ABI level, callers are still responsible for passing valid pointers and correctly sized arrays for u, params, and optional arguments such as y0. Those are contract violations, not recoverable solver errors.

Using the bindings in an app

Suppose that you have compiled your optimizer using the option with_build_c_bindings() and you have generated C/C++ bindings. OpEn will generate an example C file that you can use as a starting point to build your application. This example file is stored in the directory containing all other files of your optimizer and is called example_optimizer.c. The auto-generated example has the following form:

/* File: the_optimizer/example_optimizer.c  */

#include <stdio.h>
#include <stdlib.h>
#include "example_bindings.h"

int main(void) {
double p[EXAMPLE_NUM_PARAMETERS] = {0}; // parameter
double u[EXAMPLE_NUM_DECISION_VARIABLES] = {0}; // initial guess
double initial_penalty = 15.0;

exampleCache *cache = example_new();
if (cache == NULL) {
fprintf(stderr, "Could not allocate solver cache\n");
return EXIT_FAILURE;
}

exampleSolverStatus status = example_solve(cache, u, p, 0, &initial_penalty);

printf("exit status = %d\n", status.exit_status);
printf("error code = %d\n", status.error_code);
printf("error message = %s\n", status.error_message);
printf("iterations = %lu\n", status.num_inner_iterations);
printf("outer iterations = %lu\n", status.num_outer_iterations);
printf("solve time = %f ms\n", (double)status.solve_time_ns / 1e6);

if (status.error_code != 0) {
example_free(cache);
return EXIT_FAILURE;
}

for (int i = 0; i < EXAMPLE_NUM_DECISION_VARIABLES; ++i) {
printf("u[%d] = %g\n", i, u[i]);
}

example_free(cache);

return EXIT_SUCCESS;
}

Compiling and linking

Using cmake

To compile your C program you need to link to the auto-generated C bindings (see next section). However, OpEn generates automatically a CMakeLists.txt file to facilitate the compilation/linking procedure. A typical build is:

cmake -S . -B build
cmake --build build

Once you build your optimizer you can run the executable (optimizer) with:

cmake --build build --target run

Compile your own code

When building and running an application based on the generated libraries it is good to know that GCC is a bit temperamental when linking libraries, so the following can be used as reference:

gcc example_optimizer.c -l:libthe_optimizer.a \
-L./target/release -pthread -lm -ldl -o optimizer

Running

Which will solve the problem and output the following when run:

./optimizer

The output looks like this:

exit status = 0
error code = 0
error message =
iterations = 69
outer iterations = 5
solve time = 0.140401 ms
u[0] = 0.654738
u[1] = 0.982045
u[2] = 0.98416
u[3] = 0.984188
u[4] = 0.969986

If error_code is nonzero, the solver failed to produce a valid result and error_message contains the propagated reason from the generated Rust solver.