13. Calling a solver from C or C++

A generated FORCESPRO solver comes with a C interface and a C library, allowing the user to call the solver directly from C or C++. This enables direct and efficient integration of a FORCESPRO solver into existing software, and is the most portable way of deploying a solver to any supported hardware platform. This section describes how to call the solver from C, and how to compile your C program. The procedure to deploy to a target platform is described in section Main Targets.

13.1. Generating a solver library

For generating a solver to be used in C, the following options are relevant:

codeoptions.platform = '<platform_name>'; % to specify the target platform
codeoptions.printlevel = 0; % optional, on some platforms printing is not supported
codeoptions.cleanup = 0; % to keep auxiliary source files (not needed for low-level interface)

After having generated a solver with name codeoptions.name = 'myFORCESsolver', the following C libraries are made available for host and target systems:

  • For the host system:

    myFORCESsolver
    ├── include
    ├── lib
    
  • For the target system:

    myFORCESsolver
    ├── include
    ├── lib_target
    

For most platforms (see Target platform), lib and lib_target contain both static and shared/dynamic library files.

For the high-level interface you’ll receive auxiliary sources after solver generation which contain the nonlinear functions of your formulation (if not provided externally). These are generated as follows:

  • From MATLAB, the following auxiliary sources will be generated:

    myFORCESsolver
    ├── myFORCESsolver_adtool2forces.c
    ├── myFORCESsolver_casadi.c
    ├── myFORCESsolver_casadi.h
    
  • From Python, the following auxiliary sources will be generated:

    myFORCESsolver
    ├── myFORCESsolver_interface.c
    ├── myFORCESsolver_model.c
    ├── myFORCESsolver_model.h
    

13.2. C interface

The following code snippets demonstrate how to use the C interface of any generated solver (for simplicity, we assume that the solver has been generated with codeoptions.name = 'myFORCESsolver'). The C variables params (solver input), output (solver output) and info (solver diagnostics) are C structs whose definitions depend on the problem formulation. Their exact definitions can be found in the solver header myFORCESsolver.h.

Low-level interface

#include "myFORCESsolver/include/myFORCESsolver.h"         /* solver symbols definition        */
#include "myFORCESsolver/include/myFORCESsolver_memory.h"  /* handle to internal solver memory */

int main()
{
    int exitflag;
    myFORCESsolver_params params; /* struct for run-time parameters, see myFORCESsolver.h  */
    myFORCESsolver_output output; /* struct for solver output, see myFORCESsolver.h        */
    myFORCESsolver_info info;     /* struct for solver diagnostics, see myFORCESsolver.h   */
    myFORCESsolver_mem * mem;     /* handle to solver memory, from myFORCESsolver_memory.h */

    mem = myFORCESsolver_internal_mem(0);

    /* omitted: assign params field */

    /*  solver call:
     *  - in: params
     *  - out: exitflag, output, info
     *  - 5th argument: optional file pointer for printing, NULL means stdout
     */
    exitflag = myFORCESsolver_solve(&params, &output, &info, mem, NULL);
}

High-level interface

#include "myFORCESsolver/include/myFORCESsolver.h"        /* solver symbols definition        */
#include "myFORCESsolver/include/myFORCESsolver_memory.h" /* handle to internal solver memory */

/* AD tool - FORCESPRO interface
 * Note: if you use your own external C functions, replace 'myFORCESsolver_adtool2forces'
 * with your main callback function name
 */
extern solver_int32_default myFORCESsolver_adtool2forces(
    myFORCESsolver_float *x,        /* primal vars                                         */
    myFORCESsolver_float *y,        /* eq. constraint multiplers                           */
    myFORCESsolver_float *l,        /* ineq. constraint multipliers                        */
    myFORCESsolver_float *p,        /* parameters                                          */
    myFORCESsolver_float *f,        /* objective function (scalar)                         */
    myFORCESsolver_float *nabla_f,  /* gradient of objective function                      */
    myFORCESsolver_float *c,        /* dynamics                                            */
    myFORCESsolver_float *nabla_c,  /* Jacobian of the dynamics (column major)             */
    myFORCESsolver_float *h,        /* inequality constraints                              */
    myFORCESsolver_float *nabla_h,  /* Jacobian of inequality constraints (column major)   */
    myFORCESsolver_float *hess,     /* Hessian (column major)                              */
    solver_int32_default stage,     /* stage number (0 indexed)                            */
    solver_int32_default iteration, /* iteration number of solver                          */
    solver_int32_default threadID   /* Id of caller thread                                 */);

int main()
{
    int exitflag;
    myFORCESsolver_params params;   /* struct for run-time parameters, see myFORCESsolver.h  */
    myFORCESsolver_output output;   /* struct for solver output, see myFORCESsolver.h        */
    myFORCESsolver_info info;       /* struct for solver diagnostics, see myFORCESsolver.h   */
    myFORCESsolver_mem * mem;       /* handle to solver memory, from myFORCESsolver_memory.h */

    /* handle to external function interface */
    myFORCESsolver_extfunc extfunc_eval = &myFORCESsolver_adtool2forces;

    /* get handle to solver memory, from myFORCESsolver_memory.h */
    mem = myFORCESsolver_internal_mem(0);

    /* omitted: assign params field */

    /*  solver call:
     *  - in: params
     *  - out: exitflag, output, info
     *  - 5th argument: optional file pointer for printing, NULL means stdout
     */
    exitflag = myFORCESsolver_solve(&params, &output, &info, mem, NULL, extfunc_eval);
}

Note

More advanced use cases of the C interface are treated in the sections Memory allocations (for controlling memory allocations) and External parallelism (for concurrency and thread-safety aspects).

13.3. Compiling solver executable

In order to create a standalone executable, all user-provided and generated auxiliary source files need to be compiled and linked against the solver library. The full command for compiling a solver looks as follows (assuming you are in the directory which contains the myFORCESsolver folder):

  • For the low-level interface

    <Compiler> <user sources> <solver library> <additional libraries>
    
  • For the high-level interface

    <Compiler> <user sources> myFORCESsolver_adtool2forces.c myFORCESsolver_casadi.c <solver library> <additional libraries>
    

Where:

  • <Compiler> would be the compiler executable

  • <user sources> would be your sources calling the solver (see C interface)

  • <solver library> would be one of the solver library files from lib or lib_target directory (see Generating a solver library)

  • <additional libraries> would be possible libraries that need to be linked to resolve existing dependencies

    • For Linux/MacOS it’s usually necessary to link the math library (-lm)

    • For Windows you usually need to link the iphlpapi.lib library (it’s distributed with the Intel Compiler, MinGW as well as MATLAB) and unless you’re using MinGW some additional intel libraries (those are included in the FORCESPRO client under the folder libs_Intel – if missing they are downloaded after code generation)

13.3.1. Compiling with C++

Compiling a FORCESPRO solver with a C++ compiler is straightforward: Simply use the C interface as described in section C interface along with the extern "C" syntax to ensure the C linkage conventions are used.

A complete example of compiling a nonlinear FORCESPRO solver with a C++ compiler using CMake can be found in the examples\StandaloneExecution\Cpp folder that comes with your client. In order to run this example, do the following:

  1. Execute OverheadCrane.m in MATLAB to generate a FORCESPRO solver called CraneSolver (uncomment line 96 to generate a solver for Linux).

  2. Run cmake .

  3. Run cmake --build .

  4. An executable CraneSolver_standalone should be available, feel free to run it.

13.4. Memory allocations

The C interface provides some flexibility in how the solver memory is allocated:

  • internal memory: memory buffer is statically allocated inside the solver library

  • external memory: the user is responsible for allocating the memory buffer

The internal memory option is easier to use and thus the default way of interfacing the solver in C. The external memory option is recommended for users who want full control over memory allocation (static or dynamic), or who require multiple memory buffers (e.g. for running a solver in parallel, see External parallelism).

Henceforth, we assume a generated solver named FORCESNLPsolver and demonstrate how to use external and internal memory buffers. We assume the reader is already acquainted with the C interface (see section C interface).

You can find the full code for working examples for the internal and external memory interfaces including instructions on how to run them in the examples\StandaloneExecution\C folder that comes with your client.

13.4.1. Internal memory

If you don’t need control over the memory buffer, the internal memory C interface is the recommended way of calling a generated solver in C:

/* additional header for internal memory functionality */
#include "FORCESNLPsolver/include/FORCESNLPsolver_memory.h"

/* handle to the solver memory */
FORCESNLPsolver_mem * mem_handle;

/* Get i-th memory buffer */
int i = 0;
mem_handle = FORCESNLPsolver_internal_mem(i);
/* Note: number of available memory buffers is controlled by code option max_num_mem */

/* check that memory is in valid state: */
if (mem_handle == NULL)
{
    /* this happens if i >= max_num_mem */
    return 1;
}
exit_code = FORCESNLPsolver_solve(..., mem_handle, ...)

By default, one memory buffer is available (max_num_mem = 1). For further information on the code option max_num_mem, see Table 13.1.

13.4.2. External memory

The following code sample demonstrates the use of external memory with dynamic allocation:

/* memory buffer allocated by the user of type char (representing bytes) */
char * mem;

/* handle to the solver memory */
FORCESNLPsolver_mem * mem_handle;

/* required memory size in bytes */
size_t mem_size = FORCESNLPsolver_get_mem_size();

/* dynamically allocate memory buffer */
mem = malloc(mem_size);

/* cast memory buffer to solver memory
 * note: i can be set to 0 if no thread safety required */
int i = 0;
mem_handle = FORCESNLPsolver_external_mem(mem, i, mem_size);

/* check that memory is in valid state: */
if (mem_handle == NULL)
{
    return 1;
}
exit_code = FORCESNLPsolver_solve(..., mem_handle, ...)

/* free user-allocated memory */
free(mem);

For static allocation, the memory size MEM_SIZE needs to be set at compile time:

/* memory size in bytes s.t. MEM_SIZE >= FORCESNLPsolver_get_mem_size(): */
#define MEM_SIZE 12345

/* statically allocated memory buffer of size MEM_SIZE bytes */
static char mem[MEM_SIZE];
/* cast buffer to solver memory */
mem_handle = FORCESNLPsolver_external_mem(mem, 0, MEM_SIZE);

/* check that memory is in valid state: */
if (mem_handle == NULL)
{
    /* this happens if MEM_SIZE < FORCESNLPsolver_get_mem_size() */
    return 1;
}

The minimum required MEM_SIZE is system and compiler dependent. It can be easily obtained by compiling FORCESNLPsolver\interface\FORCESNLPsolver_get_mem_size.c and by linking against the solver library on the target device. The output of the obtained executable is equal to the value returned by FORCESNLPsolver_get_mem_size().

For memory-critical systems, we advise to disable the internal memory buffer by setting (see also Table 13.1)

codeoptions.max_num_mem = 0;

Otherwise, the solver library still contains one internal memory buffer required by the client to run the solver.

13.4.4. Obtaining memory size

The required memory size for a solver can be obtained by calling the two utility functions provided by the solver library:

size_t FORCESNLPsolver_get_mem_size( void );
size_t FORCESNLPsolver_get_const_size( void );

These functions return the memory size for all non-const / const variables, respectively. This information is also printed in the solver output if printlevel = 2. Note that the memory size depends on the system and compiler. The actual memory footprint might be larger as reported by these functions since they account only for the memory to store data (data or bss segment in the binary), and not for the full binary size.

The size returned by FORCESNLPsolver_get_mem_size refers to an internal or external memory buffer. The size returned by FORCESNLPsolver_get_const_size refers to additional constant memory that is not exposed to the user. To obtain the total memory size of a solver that is called in parallel, the size of the memory buffer must be multiplied with the number of threads: total_size = NUM_THREADS * FORCESNLPsolver_get_mem_size() + FORCESNLPsolver_get_const_size().