From 88fc7c6254f5d811acd961dd0913f5c4b3cbad92 Mon Sep 17 00:00:00 2001
From: Pantelis Sopasakis
Date: Sat, 28 Mar 2026 02:26:57 +0000
Subject: [PATCH 1/5] update c bindings
---
.../c/example_optimizer_c_bindings.c | 64 ++++++++++++++-----
.../templates/c/optimizer_cinterface.rs.jinja | 2 +-
2 files changed, 49 insertions(+), 17 deletions(-)
diff --git a/open-codegen/opengen/templates/c/example_optimizer_c_bindings.c b/open-codegen/opengen/templates/c/example_optimizer_c_bindings.c
index 282bd2c8..dbd8316b 100644
--- a/open-codegen/opengen/templates/c/example_optimizer_c_bindings.c
+++ b/open-codegen/opengen/templates/c/example_optimizer_c_bindings.c
@@ -20,12 +20,30 @@
*/
#include
+#include
#include "{{meta.optimizer_name}}_bindings.h"
/*
* Feel free to customize the following code...
*/
+static const char *exit_status_to_string({{meta.optimizer_name}}ExitStatus exit_status) {
+ switch (exit_status) {
+ case {{meta.optimizer_name}}Converged:
+ return "Converged";
+ case {{meta.optimizer_name}}NotConvergedIterations:
+ return "NotConvergedIterations";
+ case {{meta.optimizer_name}}NotConvergedOutOfTime:
+ return "NotConvergedOutOfTime";
+ case {{meta.optimizer_name}}NotConvergedCost:
+ return "NotConvergedCost";
+ case {{meta.optimizer_name}}NotConvergedNotFiniteComputation:
+ return "NotConvergedNotFiniteComputation";
+ default:
+ return "Unknown";
+ }
+}
+
int main(void) {
int i;
@@ -45,28 +63,18 @@ int main(void) {
/* obtain cache */
{{meta.optimizer_name}}Cache *cache = {{meta.optimizer_name}}_new();
+ if (cache == NULL) {
+ fprintf(stderr, "Could not allocate solver cache\n");
+ return EXIT_FAILURE;
+ }
/* solve */
{{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);
- /* print results */
- printf("\n\n-------------------------------------------------\n");
- printf(" Solution\n");
- printf("-------------------------------------------------\n");
-
- for (i = 0; i < {{meta.optimizer_name|upper}}_NUM_DECISION_VARIABLES; ++i) {
- printf("u[%d] = %g\n", i, u[i]);
- }
-
- printf("\n");
- for (i = 0; i < {{meta.optimizer_name|upper}}_N1; ++i) {
- printf("y[%d] = %g\n", i, status.lagrange[i]);
- }
-
printf("\n\n-------------------------------------------------\n");
printf(" Solver Statistics\n");
printf("-------------------------------------------------\n");
- printf("exit status : %d\n", status.exit_status);
+ printf("exit status : %d (%s)\n", status.exit_status, exit_status_to_string(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);
@@ -78,10 +86,34 @@ int main(void) {
printf("Cost : %f\n", status.cost);
printf("||FRP|| : %f\n\n", status.last_problem_norm_fpr);
+ if (status.error_code != 0) {
+ fprintf(stderr, "Solver returned an error; solution vector is not printed.\n");
+ {{meta.optimizer_name}}_free(cache);
+ return EXIT_FAILURE;
+ }
+
+ if (status.exit_status != {{meta.optimizer_name}}Converged) {
+ fprintf(stderr, "Warning: solver did not converge, printing best available iterate.\n");
+ }
+
+ /* print results */
+ printf("\n\n-------------------------------------------------\n");
+ printf(" Solution\n");
+ printf("-------------------------------------------------\n");
+
+ for (i = 0; i < {{meta.optimizer_name|upper}}_NUM_DECISION_VARIABLES; ++i) {
+ printf("u[%d] = %g\n", i, u[i]);
+ }
+
+ printf("\n");
+ for (i = 0; i < {{meta.optimizer_name|upper}}_N1; ++i) {
+ printf("y[%d] = %g\n", i, status.lagrange[i]);
+ }
+
/* free memory */
{{meta.optimizer_name}}_free(cache);
- return 0;
+ return EXIT_SUCCESS;
}
diff --git a/open-codegen/opengen/templates/c/optimizer_cinterface.rs.jinja b/open-codegen/opengen/templates/c/optimizer_cinterface.rs.jinja
index 26cbe1f5..1fabbb95 100644
--- a/open-codegen/opengen/templates/c/optimizer_cinterface.rs.jinja
+++ b/open-codegen/opengen/templates/c/optimizer_cinterface.rs.jinja
@@ -1,5 +1,5 @@
{% if activate_clib_generation -%}
-// ---Export functionality from Rust to C/C++------------------------------------------------------------
+// ---Export functionality from Rust to C/C++ --------------
/// Solver cache (structure `{{meta.optimizer_name}}Cache`)
///
From 04bfc4887d466ddf1d5a3faeb44bef174bf72b61 Mon Sep 17 00:00:00 2001
From: Pantelis Sopasakis
Date: Sat, 28 Mar 2026 02:45:09 +0000
Subject: [PATCH 2/5] more unit tests
---
.../c/example_optimizer_c_bindings.c | 2 +-
open-codegen/test/test.py | 199 ++++++++++++++----
2 files changed, 161 insertions(+), 40 deletions(-)
diff --git a/open-codegen/opengen/templates/c/example_optimizer_c_bindings.c b/open-codegen/opengen/templates/c/example_optimizer_c_bindings.c
index dbd8316b..31cf8e66 100644
--- a/open-codegen/opengen/templates/c/example_optimizer_c_bindings.c
+++ b/open-codegen/opengen/templates/c/example_optimizer_c_bindings.c
@@ -48,7 +48,7 @@ int main(void) {
int i;
/* parameters */
- double p[{{meta.optimizer_name|upper}}_NUM_PARAMETERS] = {2.0, 10.0};
+ double p[{{meta.optimizer_name|upper}}_NUM_PARAMETERS] = {0};
/* initial guess */
double u[{{meta.optimizer_name|upper}}_NUM_DECISION_VARIABLES] = {0};
diff --git a/open-codegen/test/test.py b/open-codegen/test/test.py
index f61b354b..79d4373a 100644
--- a/open-codegen/test/test.py
+++ b/open-codegen/test/test.py
@@ -304,7 +304,8 @@ def setUpSolverError(cls):
.with_open_version(local_path=RustBuildTestCase.get_open_local_absolute_path()) \
.with_build_directory(RustBuildTestCase.TEST_DIR) \
.with_build_mode(og.config.BuildConfiguration.DEBUG_MODE) \
- .with_tcp_interface_config(tcp_interface_config=tcp_config)
+ .with_tcp_interface_config(tcp_interface_config=tcp_config) \
+ .with_build_c_bindings()
og.builder.OpEnOptimizerBuilder(problem,
metadata=meta,
build_configuration=build_config,
@@ -791,48 +792,168 @@ def test_rust_build_parametric_halfspace(self):
self.assertTrue(sum([u[i] * c[i] for i in range(5)]) - b <= eps)
self.assertTrue(-sum([u[i] * c[i] for i in range(5)]) + b <= eps)
+ @staticmethod
+ def rebuild_generated_staticlib(optimizer_name):
+ optimizer_dir = os.path.join(RustBuildTestCase.TEST_DIR, optimizer_name)
+ process = subprocess.Popen(
+ ["cargo", "build"],
+ cwd=optimizer_dir,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ _stdout, stderr = process.communicate()
+ return process.returncode, stderr.decode()
+
@staticmethod
def c_bindings_helper(optimizer_name):
- p = subprocess.Popen(["/usr/bin/gcc",
- RustBuildTestCase.TEST_DIR + "/" + optimizer_name + "/example_optimizer.c",
- "-I" + RustBuildTestCase.TEST_DIR + "/" + optimizer_name,
- "-pthread",
- RustBuildTestCase.TEST_DIR + "/" + optimizer_name +
- "/target/debug/lib" + optimizer_name + ".a",
- "-lm",
- "-ldl",
- "-std=c99",
- "-o",
- RustBuildTestCase.TEST_DIR + "/" + optimizer_name + "/optimizer"],
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
-
- # Make sure it compiles
- p.communicate()
- rc1 = p.returncode
-
- # Run the optimizer
- p = subprocess.Popen([RustBuildTestCase.TEST_DIR + "/" + optimizer_name + "/optimizer"],
- stdout=subprocess.DEVNULL)
- p.communicate()
- rc2 = p.returncode
-
- return rc1, rc2
+ compile_process = subprocess.Popen(
+ ["/usr/bin/gcc",
+ RustBuildTestCase.TEST_DIR + "/" + optimizer_name + "/example_optimizer.c",
+ "-I" + RustBuildTestCase.TEST_DIR + "/" + optimizer_name,
+ "-pthread",
+ RustBuildTestCase.TEST_DIR + "/" + optimizer_name +
+ "/target/debug/lib" + optimizer_name + ".a",
+ "-lm",
+ "-ldl",
+ "-std=c99",
+ "-o",
+ RustBuildTestCase.TEST_DIR + "/" + optimizer_name + "/optimizer"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+
+ compile_stdout, compile_stderr = compile_process.communicate()
+
+ run_stdout = b""
+ run_stderr = b""
+ run_returncode = None
+ if compile_process.returncode == 0:
+ run_process = subprocess.Popen(
+ [RustBuildTestCase.TEST_DIR + "/" + optimizer_name + "/optimizer"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ run_stdout, run_stderr = run_process.communicate()
+ run_returncode = run_process.returncode
+
+ return {
+ "compile_returncode": compile_process.returncode,
+ "compile_stdout": compile_stdout.decode(),
+ "compile_stderr": compile_stderr.decode(),
+ "run_returncode": run_returncode,
+ "run_stdout": run_stdout.decode(),
+ "run_stderr": run_stderr.decode(),
+ }
+
+ @staticmethod
+ def patch_c_bindings_example_parameter_initializer(optimizer_name, replacement_line):
+ example_file = os.path.join(
+ RustBuildTestCase.TEST_DIR, optimizer_name, "example_optimizer.c")
+ with open(example_file, "r", encoding="utf-8") as fh:
+ example_source = fh.read()
+
+ original_line = None
+ for line in example_source.splitlines():
+ if "double p[" in line and "= {" in line:
+ original_line = line
+ break
+
+ if original_line is None:
+ raise RuntimeError("Could not locate parameter initializer in example_optimizer.c")
+
+ with open(example_file, "w", encoding="utf-8") as fh:
+ fh.write(example_source.replace(original_line, replacement_line, 1))
+
+ return original_line
+
+ @staticmethod
+ def c_bindings_cmake_helper(optimizer_name):
+ cmake_executable = shutil.which("cmake")
+ if cmake_executable is None:
+ raise unittest.SkipTest("cmake is not available in PATH")
+
+ optimizer_dir = os.path.join(RustBuildTestCase.TEST_DIR, optimizer_name)
+ build_dir = os.path.join(optimizer_dir, "cmake-build-test")
+ if os.path.isdir(build_dir):
+ shutil.rmtree(build_dir)
+ os.makedirs(build_dir)
+
+ configure_process = subprocess.Popen(
+ [cmake_executable, ".."],
+ cwd=build_dir,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ configure_stdout, configure_stderr = configure_process.communicate()
+
+ build_stdout = b""
+ build_stderr = b""
+ build_returncode = None
+ if configure_process.returncode == 0:
+ build_process = subprocess.Popen(
+ [cmake_executable, "--build", "."],
+ cwd=build_dir,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ build_stdout, build_stderr = build_process.communicate()
+ build_returncode = build_process.returncode
+
+ return {
+ "configure_returncode": configure_process.returncode,
+ "configure_stdout": configure_stdout.decode(),
+ "configure_stderr": configure_stderr.decode(),
+ "build_returncode": build_returncode,
+ "build_stdout": build_stdout.decode(),
+ "build_stderr": build_stderr.decode(),
+ }
def test_c_bindings(self):
- rc1, rc2 = RustBuildTestCase.c_bindings_helper(
- optimizer_name="only_f1")
- self.assertEqual(0, rc1)
- self.assertEqual(0, rc2)
-
- rc1, rc2 = RustBuildTestCase.c_bindings_helper(
- optimizer_name="only_f2")
- self.assertEqual(0, rc1)
- self.assertEqual(0, rc2)
-
- rc1, rc2 = RustBuildTestCase.c_bindings_helper(optimizer_name="plain")
- self.assertEqual(0, rc1)
- self.assertEqual(0, rc2)
+ result = RustBuildTestCase.c_bindings_helper(optimizer_name="only_f1")
+ self.assertEqual(0, result["compile_returncode"], msg=result["compile_stderr"])
+ self.assertEqual(0, result["run_returncode"], msg=result["run_stderr"])
+ self.assertIn("Converged", result["run_stdout"])
+ self.assertIn("exit status : 0", result["run_stdout"])
+ self.assertIn("error code : 0", result["run_stdout"])
+
+ result = RustBuildTestCase.c_bindings_helper(optimizer_name="only_f2")
+ self.assertEqual(0, result["compile_returncode"], msg=result["compile_stderr"])
+ self.assertIn("Converged", result["run_stdout"])
+ self.assertEqual(0, result["run_returncode"], msg=result["run_stderr"])
+ self.assertIn("exit status : 0", result["run_stdout"])
+ self.assertIn("error code : 0", result["run_stdout"])
+
+ result = RustBuildTestCase.c_bindings_helper(optimizer_name="plain")
+ self.assertIn("Converged", result["run_stdout"])
+ self.assertEqual(0, result["compile_returncode"], msg=result["compile_stderr"])
+ self.assertEqual(0, result["run_returncode"], msg=result["run_stderr"])
+ self.assertIn("exit status : 0", result["run_stdout"])
+ self.assertIn("error code : 0", result["run_stdout"])
+
+ def test_c_bindings_error_path(self):
+ rebuild_rc, rebuild_stderr = RustBuildTestCase.rebuild_generated_staticlib(
+ optimizer_name="solver_error")
+ self.assertEqual(0, rebuild_rc, msg=rebuild_stderr)
+
+ original_line = RustBuildTestCase.patch_c_bindings_example_parameter_initializer(
+ optimizer_name="solver_error",
+ replacement_line=" double p[SOLVER_ERROR_NUM_PARAMETERS] = {-1.0};")
+ try:
+ result = RustBuildTestCase.c_bindings_helper(optimizer_name="solver_error")
+ finally:
+ RustBuildTestCase.patch_c_bindings_example_parameter_initializer(
+ optimizer_name="solver_error",
+ replacement_line=original_line)
+ self.assertEqual(0, result["compile_returncode"], msg=result["compile_stderr"])
+ self.assertNotEqual(0, result["run_returncode"])
+ self.assertIn("error code : 2000", result["run_stdout"])
+ self.assertIn("forced solver error for TCP test", result["run_stdout"])
+ self.assertIn(
+ "Solver returned an error; solution vector is not printed.",
+ result["run_stderr"])
+
+ def test_c_bindings_cmake_example_builds(self):
+ result = RustBuildTestCase.c_bindings_cmake_helper(optimizer_name="plain")
+ self.assertEqual(0, result["configure_returncode"], msg=result["configure_stderr"])
+ self.assertEqual(0, result["build_returncode"], msg=result["build_stderr"])
def test_tcp_generated_server_builds(self):
tcp_iface_dir = os.path.join(
From e19818a4f0de8df1efd8b2da8e7259d2a3de005c Mon Sep 17 00:00:00 2001
From: Pantelis Sopasakis
Date: Sat, 28 Mar 2026 02:53:53 +0000
Subject: [PATCH 3/5] update autogen CMakeLists.txt
- Threads instead of pthreads
- using PRIVATE
- option for WIN32
---
.../templates/c/example_cmakelists.txt | 37 +++++++++++++++----
1 file changed, 30 insertions(+), 7 deletions(-)
diff --git a/open-codegen/opengen/templates/c/example_cmakelists.txt b/open-codegen/opengen/templates/c/example_cmakelists.txt
index 13a79032..63d2bca9 100644
--- a/open-codegen/opengen/templates/c/example_cmakelists.txt
+++ b/open-codegen/opengen/templates/c/example_cmakelists.txt
@@ -3,17 +3,40 @@ cmake_minimum_required(VERSION 3.5)
# Project name
project({{meta.optimizer_name}})
+# Build the generated example as C99.
+set(CMAKE_C_STANDARD 99)
+set(CMAKE_C_STANDARD_REQUIRED ON)
+
+# Resolve the generated static library name for the current platform.
+if(WIN32)
+ set(OPEN_STATIC_LIB ${CMAKE_CURRENT_SOURCE_DIR}/target/{{ build_config.build_mode }}/{{meta.optimizer_name}}.lib)
+else()
+ set(OPEN_STATIC_LIB ${CMAKE_CURRENT_SOURCE_DIR}/target/{{ build_config.build_mode }}/lib{{meta.optimizer_name}}.a)
+endif()
+
+find_package(Threads REQUIRED)
+
# Add the executable
add_executable(optimizer example_optimizer.c)
# Add libraries to the executable
-target_link_libraries(optimizer ${CMAKE_SOURCE_DIR}/target/{{ build_config.build_mode }}/lib{{meta.optimizer_name}}.a)
-target_link_libraries(optimizer m)
-target_link_libraries(optimizer dl)
-target_link_libraries(optimizer pthread)
+target_link_libraries(
+ optimizer
+ PRIVATE
+ ${OPEN_STATIC_LIB}
+ Threads::Threads
+)
+
+if(UNIX)
+ target_link_libraries(optimizer PRIVATE m)
+endif()
+
+if(CMAKE_DL_LIBS)
+ target_link_libraries(optimizer PRIVATE ${CMAKE_DL_LIBS})
+endif()
add_custom_target(run
- COMMAND optimizer
+ COMMAND $
DEPENDS optimizer
- WORKING_DIRECTORY ${CMAKE_PROJECT_DIR}
-)
\ No newline at end of file
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+)
From 006502b08c71a5c68ef8f43001e5dce86db271b8 Mon Sep 17 00:00:00 2001
From: Pantelis Sopasakis
Date: Sat, 28 Mar 2026 03:21:32 +0000
Subject: [PATCH 4/5] [ci skip] update website docs
---
docs/python-c.mdx | 116 ++++++++++++++++++++++++++++++--------
open-codegen/CHANGELOG.md | 2 +-
2 files changed, 95 insertions(+), 23 deletions(-)
diff --git a/docs/python-c.mdx b/docs/python-c.mdx
index 721b5bf9..74e8d4a7 100644
--- a/docs/python-c.mdx
+++ b/docs/python-c.mdx
@@ -77,7 +77,7 @@ 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 dynamical library files are located in `the_optimizer/target/{debug,release}` (depending on whether it was a [*debug*] or [*release*] build)
+* 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.
@@ -112,13 +112,16 @@ typedef struct exampleCache exampleCache;
typedef struct {
exampleExitStatus exit_status;
+ int error_code;
+ char error_message[256];
unsigned long num_outer_iterations;
unsigned long num_inner_iterations;
- double last_problem_norm_fpr;
+ 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;
@@ -137,10 +140,61 @@ 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.
+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:
+
+```c
+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
@@ -155,26 +209,41 @@ The auto-generated example has the following form:
/* File: the_optimizer/example_optimizer.c */
#include
+#include
#include "example_bindings.h"
-int main() {
- double p[EXAMPLE_NUM_PARAMETERS] = {1.0, 10.0}; // parameter
+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();
- exampleSolverStatus status = example_solve(cache, u, p);
- example_free(cache);
-
- for (int i = 0; i < EXAMPLE_NUM_DECISION_VARIABLES; ++i) {
- printf("u[%d] = %g\n", i, u[i]);
+ 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);
- return 0;
+ 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;
}
```
@@ -185,19 +254,18 @@ int main() {
To compile your C program you need to link to the auto-generated
C bindings (see [next section](#compile-your-own-code)).
However, OpEn generates automatically a `CMakeLists.txt` file
-to facilitate the compilation/linking procedure. To build the
-auto-generated example run
+to facilitate the compilation/linking procedure. A typical build is:
```bash
-cmake .
-make
+cmake -S . -B build
+cmake --build build
```
-once you build your optimizer you can run the executable (`optimizer`)
-with
+Once you build your optimizer you can run the executable (`optimizer`)
+with:
```bash
-make run
+cmake --build build --target run
```
#### Compile your own code
@@ -260,14 +328,18 @@ LD_LIBRARY_PATH=./target/release ./optimizer
The output looks like this:
```text
+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
-exit status = 0
-iterations = 69
-outer iterations = 5
-solve time = 0.140401 ms
```
+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.
diff --git a/open-codegen/CHANGELOG.md b/open-codegen/CHANGELOG.md
index b7dfdaa3..3c38beb8 100644
--- a/open-codegen/CHANGELOG.md
+++ b/open-codegen/CHANGELOG.md
@@ -21,7 +21,7 @@ Note: This is the Changelog file of `opengen` - the Python interface of OpEn
- Extended `RosConfiguration` so it can be used for both ROS and ROS2 package generation
- 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`.
- Added helpful `__repr__` methods to generated Python binding response/status/error objects, TCP solver response/error objects, and `GeneratedOptimizer` for easier inspection and debugging
-- 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 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.
- 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
From b47d13b8b69fa9bba787cfbe22bbcf2e81e01230 Mon Sep 17 00:00:00 2001
From: Pantelis Sopasakis
Date: Sat, 28 Mar 2026 03:34:05 +0000
Subject: [PATCH 5/5] [ci skip] increate c err msg size
---
docs/python-c.mdx | 2 +-
.../opengen/templates/c/optimizer_cinterface.rs.jinja | 11 ++++++-----
2 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/docs/python-c.mdx b/docs/python-c.mdx
index 74e8d4a7..3de4d262 100644
--- a/docs/python-c.mdx
+++ b/docs/python-c.mdx
@@ -113,7 +113,7 @@ typedef struct exampleCache exampleCache;
typedef struct {
exampleExitStatus exit_status;
int error_code;
- char error_message[256];
+ char error_message[1024];
unsigned long num_outer_iterations;
unsigned long num_inner_iterations;
double last_problem_norm_fpr;
diff --git a/open-codegen/opengen/templates/c/optimizer_cinterface.rs.jinja b/open-codegen/opengen/templates/c/optimizer_cinterface.rs.jinja
index 1fabbb95..db2dec64 100644
--- a/open-codegen/opengen/templates/c/optimizer_cinterface.rs.jinja
+++ b/open-codegen/opengen/templates/c/optimizer_cinterface.rs.jinja
@@ -1,4 +1,5 @@
{% if activate_clib_generation -%}
+{% set error_message_capacity = 1024 -%}
// ---Export functionality from Rust to C/C++ --------------
/// Solver cache (structure `{{meta.optimizer_name}}Cache`)
@@ -10,7 +11,7 @@ pub struct {{meta.optimizer_name}}Cache {
const {{meta.optimizer_name|upper}}_NO_ERROR_CODE: c_int = 0;
const {{meta.optimizer_name|upper}}_SOLVER_ERROR_CODE: c_int = 2000;
-const {{meta.optimizer_name|upper}}_ERROR_MESSAGE_CAPACITY: usize = 256;
+const {{meta.optimizer_name|upper}}_ERROR_MESSAGE_CAPACITY: usize = {{ error_message_capacity }};
impl {{meta.optimizer_name}}Cache {
pub fn new(cache: AlmCache) -> Self {
@@ -18,13 +19,13 @@ impl {{meta.optimizer_name}}Cache {
}
}
-fn empty_error_message() -> [c_char; 256] {
- [0 as c_char; 256]
+fn empty_error_message() -> [c_char; {{ error_message_capacity }}] {
+ [0 as c_char; {{ error_message_capacity }}]
}
fn error_message_to_c_array(
message: &str,
-) -> [c_char; 256] {
+) -> [c_char; {{ error_message_capacity }}] {
let mut buffer = empty_error_message();
let max_len = {{meta.optimizer_name|upper}}_ERROR_MESSAGE_CAPACITY - 1;
for (idx, byte) in message.as_bytes().iter().copied().take(max_len).enumerate() {
@@ -63,7 +64,7 @@ pub struct {{meta.optimizer_name}}SolverStatus {
/// Detailed error code (0 on success)
error_code: c_int,
/// Detailed error message (empty string on success)
- error_message: [c_char; 256],
+ error_message: [c_char; {{ error_message_capacity }}],
/// Number of outer iterations
num_outer_iterations: c_ulong,
/// Total number of inner iterations