Skip to content

Commit 627c2df

Browse files
authored
Merge pull request #414 from alphaville/feature/413-c-bindings-error-handling
C bindings error handling
2 parents 1a2bd54 + b47d13b commit 627c2df

File tree

6 files changed

+341
-92
lines changed

6 files changed

+341
-92
lines changed

docs/python-c.mdx

Lines changed: 94 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ The generated C/C++ bindings are in the auto-generated solver library.
7777
In particular
7878

7979
* The header files are at `the_optimizer/the_optimizer_bindings.{h,hpp}`
80-
* The static and dynamical library files are located in `the_optimizer/target/{debug,release}` (depending on whether it was a [*debug*] or [*release*] build)
80+
* The static and dynamic library files are located in `the_optimizer/target/{debug,release}` (depending on whether it was a [*debug*] or [*release*] build)
8181

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

@@ -112,13 +112,16 @@ typedef struct exampleCache exampleCache;
112112

113113
typedef struct {
114114
exampleExitStatus exit_status;
115+
int error_code;
116+
char error_message[1024];
115117
unsigned long num_outer_iterations;
116118
unsigned long num_inner_iterations;
117-
double last_problem_norm_fpr;
119+
double last_problem_norm_fpr;
118120
unsigned long long solve_time_ns;
119121
double penalty;
120122
double delta_y_norm_over_c;
121123
double f2_norm;
124+
double cost;
122125
const double *lagrange;
123126
} exampleSolverStatus;
124127

@@ -137,10 +140,61 @@ This is designed to follow a new-use-free pattern.
137140
138141
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.
139142
140-
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.
143+
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.
144+
145+
The returned `exampleSolverStatus` always contains a coarse solver outcome in
146+
`exit_status`. On success it also contains `error_code = 0` and an empty
147+
`error_message`. If the solver fails internally, the bindings return a
148+
structured error report with a nonzero `error_code` and a descriptive
149+
`error_message`.
141150
142151
Finally, when done with the solver, use `{optimizer-name}_free` to release the memory allocated by `{optimizer-name}_new`.
143152
153+
## Handling errors
154+
155+
The C bindings always return a value of type `exampleSolverStatus`. This means
156+
that solver calls do not report failure by returning `NULL` or by using a
157+
separate exception-like mechanism. Instead, callers should inspect both
158+
`exit_status` and `error_code`.
159+
160+
- `error_code = 0` means the solver call completed without an internal error
161+
- `error_code != 0` means the solver failed and `error_message` contains a
162+
descriptive explanation
163+
- `exit_status` gives the coarse outcome of the solve attempt, such as
164+
converged, reached the iteration limit, or failed because of a numerical
165+
issue
166+
167+
The recommended pattern is:
168+
169+
1. Call `{optimizer-name}_solve(...)`
170+
2. Check whether `status.error_code != 0`
171+
3. If so, report `status.error_message` and treat the call as failed
172+
4. Otherwise, inspect `status.exit_status` to determine whether the solver
173+
converged or returned the best available non-converged iterate
174+
175+
For example:
176+
177+
```c
178+
exampleSolverStatus status = example_solve(cache, u, p, 0, &initial_penalty);
179+
180+
if (status.error_code != 0) {
181+
fprintf(stderr, "Solver failed: [%d] %s\n",
182+
status.error_code, status.error_message);
183+
example_free(cache);
184+
return EXIT_FAILURE;
185+
}
186+
187+
if (status.exit_status != exampleConverged) {
188+
fprintf(stderr, "Warning: solver did not converge fully\n");
189+
}
190+
```
191+
192+
The generated C example follows exactly this pattern.
193+
194+
At the ABI level, callers are still responsible for passing valid pointers and
195+
correctly sized arrays for `u`, `params`, and optional arguments such as `y0`.
196+
Those are contract violations, not recoverable solver errors.
197+
144198

145199
## Using the bindings in an app
146200

@@ -155,26 +209,41 @@ The auto-generated example has the following form:
155209
/* File: the_optimizer/example_optimizer.c */
156210

157211
#include <stdio.h>
212+
#include <stdlib.h>
158213
#include "example_bindings.h"
159214

160-
int main() {
161-
double p[EXAMPLE_NUM_PARAMETERS] = {1.0, 10.0}; // parameter
215+
int main(void) {
216+
double p[EXAMPLE_NUM_PARAMETERS] = {0}; // parameter
162217
double u[EXAMPLE_NUM_DECISION_VARIABLES] = {0}; // initial guess
218+
double initial_penalty = 15.0;
163219

164220
exampleCache *cache = example_new();
165-
exampleSolverStatus status = example_solve(cache, u, p);
166-
example_free(cache);
167-
168-
for (int i = 0; i < EXAMPLE_NUM_DECISION_VARIABLES; ++i) {
169-
printf("u[%d] = %g\n", i, u[i]);
221+
if (cache == NULL) {
222+
fprintf(stderr, "Could not allocate solver cache\n");
223+
return EXIT_FAILURE;
170224
}
171225

226+
exampleSolverStatus status = example_solve(cache, u, p, 0, &initial_penalty);
227+
172228
printf("exit status = %d\n", status.exit_status);
229+
printf("error code = %d\n", status.error_code);
230+
printf("error message = %s\n", status.error_message);
173231
printf("iterations = %lu\n", status.num_inner_iterations);
174232
printf("outer iterations = %lu\n", status.num_outer_iterations);
175233
printf("solve time = %f ms\n", (double)status.solve_time_ns / 1e6);
176234

177-
return 0;
235+
if (status.error_code != 0) {
236+
example_free(cache);
237+
return EXIT_FAILURE;
238+
}
239+
240+
for (int i = 0; i < EXAMPLE_NUM_DECISION_VARIABLES; ++i) {
241+
printf("u[%d] = %g\n", i, u[i]);
242+
}
243+
244+
example_free(cache);
245+
246+
return EXIT_SUCCESS;
178247
}
179248
```
180249
@@ -185,19 +254,18 @@ int main() {
185254
To compile your C program you need to link to the auto-generated
186255
C bindings (see [next section](#compile-your-own-code)).
187256
However, OpEn generates automatically a `CMakeLists.txt` file
188-
to facilitate the compilation/linking procedure. To build the
189-
auto-generated example run
257+
to facilitate the compilation/linking procedure. A typical build is:
190258
191259
```bash
192-
cmake .
193-
make
260+
cmake -S . -B build
261+
cmake --build build
194262
```
195263

196-
once you build your optimizer you can run the executable (`optimizer`)
197-
with
264+
Once you build your optimizer you can run the executable (`optimizer`)
265+
with:
198266

199267
```bash
200-
make run
268+
cmake --build build --target run
201269
```
202270

203271
#### Compile your own code
@@ -260,14 +328,18 @@ LD_LIBRARY_PATH=./target/release ./optimizer
260328
The output looks like this:
261329

262330
```text
331+
exit status = 0
332+
error code = 0
333+
error message =
334+
iterations = 69
335+
outer iterations = 5
336+
solve time = 0.140401 ms
263337
u[0] = 0.654738
264338
u[1] = 0.982045
265339
u[2] = 0.98416
266340
u[3] = 0.984188
267341
u[4] = 0.969986
268-
exit status = 0
269-
iterations = 69
270-
outer iterations = 5
271-
solve time = 0.140401 ms
272342
```
273343

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

open-codegen/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Note: This is the Changelog file of `opengen` - the Python interface of OpEn
2121
- Extended `RosConfiguration` so it can be used for both ROS and ROS2 package generation
2222
- Breaking change: the direct interface (Python bindings) now has an API which mirrors that of the TCP interface: the method `solve` returns either a solution or an error object. Website documentation is updated. New unit tests are implemented. Note that `solver.run()` does not return the solution object directly, but rather works in the same way as the TCP interface: it returns a response object (instance of `SolverResponse`), on which the method `.get()` returns either a `SolverStatus` or `SolverError`.
2323
- Added helpful `__repr__` methods to generated Python binding response/status/error objects, TCP solver response/error objects, and `GeneratedOptimizer` for easier inspection and debugging
24-
- Updated generated TCP server and C interface templates to work with the richer Rust solver error model and expose better failure information to clients
24+
- Updated generated TCP server and C interface templates to work with the richer Rust solver error model and expose better failure information to clients. Updated auto-generated `CMakeLists.txt` file. Tighter unit tests.
2525
- ROS2 generated packages now publish detailed `error_code` and `error_message` fields, plus `STATUS_INVALID_REQUEST`, so invalid requests and solver failures are reported explicitly instead of being silently ignored
2626

2727

open-codegen/opengen/templates/c/example_cmakelists.txt

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,40 @@ cmake_minimum_required(VERSION 3.5)
33
# Project name
44
project({{meta.optimizer_name}})
55

6+
# Build the generated example as C99.
7+
set(CMAKE_C_STANDARD 99)
8+
set(CMAKE_C_STANDARD_REQUIRED ON)
9+
10+
# Resolve the generated static library name for the current platform.
11+
if(WIN32)
12+
set(OPEN_STATIC_LIB ${CMAKE_CURRENT_SOURCE_DIR}/target/{{ build_config.build_mode }}/{{meta.optimizer_name}}.lib)
13+
else()
14+
set(OPEN_STATIC_LIB ${CMAKE_CURRENT_SOURCE_DIR}/target/{{ build_config.build_mode }}/lib{{meta.optimizer_name}}.a)
15+
endif()
16+
17+
find_package(Threads REQUIRED)
18+
619
# Add the executable
720
add_executable(optimizer example_optimizer.c)
821

922
# Add libraries to the executable
10-
target_link_libraries(optimizer ${CMAKE_SOURCE_DIR}/target/{{ build_config.build_mode }}/lib{{meta.optimizer_name}}.a)
11-
target_link_libraries(optimizer m)
12-
target_link_libraries(optimizer dl)
13-
target_link_libraries(optimizer pthread)
23+
target_link_libraries(
24+
optimizer
25+
PRIVATE
26+
${OPEN_STATIC_LIB}
27+
Threads::Threads
28+
)
29+
30+
if(UNIX)
31+
target_link_libraries(optimizer PRIVATE m)
32+
endif()
33+
34+
if(CMAKE_DL_LIBS)
35+
target_link_libraries(optimizer PRIVATE ${CMAKE_DL_LIBS})
36+
endif()
1437

1538
add_custom_target(run
16-
COMMAND optimizer
39+
COMMAND $<TARGET_FILE:optimizer>
1740
DEPENDS optimizer
18-
WORKING_DIRECTORY ${CMAKE_PROJECT_DIR}
19-
)
41+
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
42+
)

open-codegen/opengen/templates/c/example_optimizer_c_bindings.c

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,35 @@
2020
*/
2121

2222
#include <stdio.h>
23+
#include <stdlib.h>
2324
#include "{{meta.optimizer_name}}_bindings.h"
2425

2526
/*
2627
* Feel free to customize the following code...
2728
*/
2829

30+
static const char *exit_status_to_string({{meta.optimizer_name}}ExitStatus exit_status) {
31+
switch (exit_status) {
32+
case {{meta.optimizer_name}}Converged:
33+
return "Converged";
34+
case {{meta.optimizer_name}}NotConvergedIterations:
35+
return "NotConvergedIterations";
36+
case {{meta.optimizer_name}}NotConvergedOutOfTime:
37+
return "NotConvergedOutOfTime";
38+
case {{meta.optimizer_name}}NotConvergedCost:
39+
return "NotConvergedCost";
40+
case {{meta.optimizer_name}}NotConvergedNotFiniteComputation:
41+
return "NotConvergedNotFiniteComputation";
42+
default:
43+
return "Unknown";
44+
}
45+
}
46+
2947
int main(void) {
3048
int i;
3149

3250
/* parameters */
33-
double p[{{meta.optimizer_name|upper}}_NUM_PARAMETERS] = {2.0, 10.0};
51+
double p[{{meta.optimizer_name|upper}}_NUM_PARAMETERS] = {0};
3452

3553
/* initial guess */
3654
double u[{{meta.optimizer_name|upper}}_NUM_DECISION_VARIABLES] = {0};
@@ -45,28 +63,18 @@ int main(void) {
4563

4664
/* obtain cache */
4765
{{meta.optimizer_name}}Cache *cache = {{meta.optimizer_name}}_new();
66+
if (cache == NULL) {
67+
fprintf(stderr, "Could not allocate solver cache\n");
68+
return EXIT_FAILURE;
69+
}
4870

4971
/* solve */
5072
{{meta.optimizer_name}}SolverStatus status = {{meta.optimizer_name}}_solve(cache, u, p, {% if problem.dim_constraints_aug_lagrangian() > 0 %}y{% else %}0{% endif %}, &init_penalty);
5173

52-
/* print results */
53-
printf("\n\n-------------------------------------------------\n");
54-
printf(" Solution\n");
55-
printf("-------------------------------------------------\n");
56-
57-
for (i = 0; i < {{meta.optimizer_name|upper}}_NUM_DECISION_VARIABLES; ++i) {
58-
printf("u[%d] = %g\n", i, u[i]);
59-
}
60-
61-
printf("\n");
62-
for (i = 0; i < {{meta.optimizer_name|upper}}_N1; ++i) {
63-
printf("y[%d] = %g\n", i, status.lagrange[i]);
64-
}
65-
6674
printf("\n\n-------------------------------------------------\n");
6775
printf(" Solver Statistics\n");
6876
printf("-------------------------------------------------\n");
69-
printf("exit status : %d\n", status.exit_status);
77+
printf("exit status : %d (%s)\n", status.exit_status, exit_status_to_string(status.exit_status));
7078
printf("error code : %d\n", status.error_code);
7179
printf("error message : %s\n", status.error_message);
7280
printf("iterations : %lu\n", status.num_inner_iterations);
@@ -78,10 +86,34 @@ int main(void) {
7886
printf("Cost : %f\n", status.cost);
7987
printf("||FRP|| : %f\n\n", status.last_problem_norm_fpr);
8088

89+
if (status.error_code != 0) {
90+
fprintf(stderr, "Solver returned an error; solution vector is not printed.\n");
91+
{{meta.optimizer_name}}_free(cache);
92+
return EXIT_FAILURE;
93+
}
94+
95+
if (status.exit_status != {{meta.optimizer_name}}Converged) {
96+
fprintf(stderr, "Warning: solver did not converge, printing best available iterate.\n");
97+
}
98+
99+
/* print results */
100+
printf("\n\n-------------------------------------------------\n");
101+
printf(" Solution\n");
102+
printf("-------------------------------------------------\n");
103+
104+
for (i = 0; i < {{meta.optimizer_name|upper}}_NUM_DECISION_VARIABLES; ++i) {
105+
printf("u[%d] = %g\n", i, u[i]);
106+
}
107+
108+
printf("\n");
109+
for (i = 0; i < {{meta.optimizer_name|upper}}_N1; ++i) {
110+
printf("y[%d] = %g\n", i, status.lagrange[i]);
111+
}
112+
81113

82114

83115
/* free memory */
84116
{{meta.optimizer_name}}_free(cache);
85117

86-
return 0;
118+
return EXIT_SUCCESS;
87119
}

open-codegen/opengen/templates/c/optimizer_cinterface.rs.jinja

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{% if activate_clib_generation -%}
2-
// ---Export functionality from Rust to C/C++------------------------------------------------------------
2+
{% set error_message_capacity = 1024 -%}
3+
// ---Export functionality from Rust to C/C++ --------------
34

45
/// Solver cache (structure `{{meta.optimizer_name}}Cache`)
56
///
@@ -10,21 +11,21 @@ pub struct {{meta.optimizer_name}}Cache {
1011

1112
const {{meta.optimizer_name|upper}}_NO_ERROR_CODE: c_int = 0;
1213
const {{meta.optimizer_name|upper}}_SOLVER_ERROR_CODE: c_int = 2000;
13-
const {{meta.optimizer_name|upper}}_ERROR_MESSAGE_CAPACITY: usize = 256;
14+
const {{meta.optimizer_name|upper}}_ERROR_MESSAGE_CAPACITY: usize = {{ error_message_capacity }};
1415

1516
impl {{meta.optimizer_name}}Cache {
1617
pub fn new(cache: AlmCache) -> Self {
1718
{{meta.optimizer_name}}Cache { cache }
1819
}
1920
}
2021

21-
fn empty_error_message() -> [c_char; 256] {
22-
[0 as c_char; 256]
22+
fn empty_error_message() -> [c_char; {{ error_message_capacity }}] {
23+
[0 as c_char; {{ error_message_capacity }}]
2324
}
2425

2526
fn error_message_to_c_array(
2627
message: &str,
27-
) -> [c_char; 256] {
28+
) -> [c_char; {{ error_message_capacity }}] {
2829
let mut buffer = empty_error_message();
2930
let max_len = {{meta.optimizer_name|upper}}_ERROR_MESSAGE_CAPACITY - 1;
3031
for (idx, byte) in message.as_bytes().iter().copied().take(max_len).enumerate() {
@@ -63,7 +64,7 @@ pub struct {{meta.optimizer_name}}SolverStatus {
6364
/// Detailed error code (0 on success)
6465
error_code: c_int,
6566
/// Detailed error message (empty string on success)
66-
error_message: [c_char; 256],
67+
error_message: [c_char; {{ error_message_capacity }}],
6768
/// Number of outer iterations
6869
num_outer_iterations: c_ulong,
6970
/// Total number of inner iterations

0 commit comments

Comments
 (0)