Skip to main content

sundials-devs-python-callbacks

Generate nanobind C++ wrapper code for user-supplied callback functions in the sundials4py Python bindings. Use this skill whenever adding a new user-supplied function (callback) to any sundials4py submodule (arkode, cvodes, idas, kinsol, core/sundials), wrapping a C function-pointer-based API for Python, adding a `python` member to a SUNDIALS C struct that does not yet have one, or when a function cannot be autogenerated by litgen because it takes a function pointer argument. Also trigger when the user mentions "callback wrapper", "user_supplied_fn_caller", "BIND_ARKODE_CALLBACK", "BIND_CVODE_CALLBACK", "BIND_IDA_CALLBACK", "BIND_KINSOL_CALLBACK", or references any *_usersupplied.hpp or *_impl.h file in the context of Python bindings. If the user says something like "wrap this Set callback for Python", "add a new user function to cvodes", or "this object needs a python member for callbacks", this is the skill to use.

Ir a la instalación

Datos de origen

Repositorio
llnl/sundials
Última actividad en el origen
17 de agosto de 2026 a las 19:20
Idioma detectado de SKILL.md
inglés
Estrellas
687
Forks
167

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
sundials-devs-python-callbacks
description
Generate nanobind C++ wrapper code for user-supplied callback functions in the sundials4py Python bindings. Use this skill whenever adding a new user-supplied function (callback) to any sundials4py submodule (arkode, cvodes, idas, kinsol, core/sundials), wrapping a C function-pointer-based API for Python, adding a `python` member to a SUNDIALS C struct that does not yet have one, or when a function cannot be autogenerated by litgen because it takes a function pointer argument. Also trigger when the user mentions "callback wrapper", "user_supplied_fn_caller", "BIND_ARKODE_CALLBACK", "BIND_CVODE_CALLBACK", "BIND_IDA_CALLBACK", "BIND_KINSOL_CALLBACK", or references any *_usersupplied.hpp or *_impl.h file in the context of Python bindings. If the user says something like "wrap this Set callback for Python", "add a new user function to cvodes", or "this object needs a python member for callbacks", this is the skill to use.
# sundials4py User-Supplied Function Wrapper Skill This skill guides you through creating the nanobind C++ wrapper code needed to expose a SUNDIALS C callback function to Python. These wrappers cannot be autogenerated by litgen because they accept function pointers, so they must be hand-coded following the patterns described below. ## Why these wrappers exist nanobind cannot directly bind C functions that accept raw function pointers. The solution is a multi-layer indirection pattern: 0. **`python` member on the C struct** - a `void*` field on the SUNDIALS object that holds a pointer to the function table. Some objects (e.g. `SUNStepper`) already have this; others may need it added first. 1. **Function table** - a struct of `nb::object` members, one per callback, stored either in the integrator's `user_data` or in the object's `python` member. 2. **Wrapper function** - a C-linkage function matching the original C typedef signature that retrieves the Python callable from the function table and invokes it. 3. **nanobind `m.def`** - a lambda registered with nanobind that accepts a `std::function`, stores it in the function table, and passes the wrapper function to the underlying C "Set" API. Every new user-supplied function requires additions to layers 1-3. If the target object does not yet have a `python` member, layer 0 must be addressed first. ## Step-by-step process > **Note on examples**: The examples below use ARKODE and CVODES to illustrate > the patterns, but the same structure applies identically to all packages > (ARKODE, CVODES, IDAS, KINSOL). Each has its own function table struct, > wrapper functions, convenience macro, and `get_*_fn_table` helper. When > working in a different package, substitute the corresponding names > (e.g. `ida_user_supplied_fn_table`, `get_ida_fn_table`, `BIND_IDA_CALLBACK`, > etc.). When the user asks you to wrap a new callback, gather these inputs first: - **Module**: which submodule (arkode, cvodes, idas, kinsol, or a core module like sundials). - **C "Set" function name**: e.g. `ARKodeSetPostprocessStepFn`. - **C function-pointer typedef**: e.g. `ARKPostProcessFn`. - **Parameter list of the typedef**: needed to detect "special" parameters (see below). - **Storage mechanism**: whether the function table is accessed via `user_data` (like ARKODE/CVODE/IDA/KINSOL) or via a `python` member on the struct (standalone objects like `SUNStepper`, `SUNAdaptController`, etc.). Then produce code fragments as described in the following sections. If the target object does not yet have a `python` member, start with Step 0. Otherwise, skip to Step 1. ### 0. Add a `python` member to the C struct (if it does not already have one) Some SUNDIALS objects (e.g. `SUNStepper`) already carry a `void* python` member for the function table. Others may not. If the struct you are targeting has no `python` member, you must add one before any binding work can proceed. This involves changes to three C-side files. Use `SUNStepper` as the reference implementation for the pattern. All of the relevant code can be found in `include/sundials/sundials_stepper.h`, `src/sundials/sundials_stepper_impl.h`, and `src/sundials/sundials_stepper.c`. #### 0a. Add the field to the private implementation struct Open the `*_impl.h` header that defines the struct internals (e.g. `src/sundials/sundials_logger_impl.h`). Add a `void* python;` field. Place it near the `content` field if one exists, since they serve analogous purposes (`content` is for user-supplied C data, `python` is for the binding-layer function table). **Before** (SUNLogger example): ```c struct SUNLogger_ { /* ... existing fields ... */ void* content; SUNLoggerQueueMsgFn queue_msg; SUNLoggerFlushMsgFn flush_msg; }; ``` **After**: ```c struct SUNLogger_ { /* ... existing fields ... */ void* content; /* Python binding function table (managed by sundials4py) */ void* python; SUNLoggerQueueMsgFn queue_msg; SUNLoggerFlushMsgFn flush_msg; }; ``` #### 0b. Initialize `python` to NULL in the Create function Open the `.c` source file containing the object's `Create` function. Initialize the new field to `NULL` alongside the other field initializations. Also add a forward declaration for the destroy helper, guarded by `#if defined(SUNDIALS_ENABLE_PYTHON)`. This function will be defined on the C++ binding side (see Step 0c) and is called during object destruction to prevent leaks. **Example** - modifying `SUNLogger_Create` in `src/sundials/sundials_logger.c`: ```c /* Forward declaration of function used to destroy any data allocated for Python */ #if defined(SUNDIALS_ENABLE_PYTHON) void SUNLoggerFunctionTable_Destroy(void* ptr); #endif SUNErrCode SUNLogger_Create(SUNComm comm, int output_rank, SUNLogger* logger_ptr) { /* ... existing allocation code ... */ logger->content = NULL; logger->python = NULL; /* <-- add this */ /* ... rest of initialization ... */ } ``` The naming convention for the forward-declared destroy function is `<ObjectName>FunctionTable_Destroy`. Follow what `SUNStepper` uses: `SUNStepperFunctionTable_Destroy`. #### 0c. Clean up `python` in the Destroy function In the same `.c` file, find the object's `Destroy` function. Add a guarded block that calls the destroy helper and nulls out the pointer. This must happen **before** the `free()` of the object itself, but **after** any user-level destroy ops have run (so the user callback is not called after its function table is freed). **Example** - modifying `SUNLogger_Destroy` in `src/sundials/sundials_logger.c`: ```c SUNErrCode SUNLogger_Destroy(SUNLogger* logger_ptr) { /* ... existing checks and cleanup ... */ SUNLogger logger = *logger_ptr; if (logger) { #if SUNDIALS_LOGGING_LEVEL > 0 if (sunLoggerIsOutputRank(logger, NULL)) { SUNHashMap_Destroy(&logger->filenames); } #endif #if defined(SUNDIALS_ENABLE_PYTHON) SUNLoggerFunctionTable_Destroy(logger->python); #endif logger->python = NULL; #if SUNDIALS_MPI_ENABLED if (logger->comm != SUN_COMM_NULL) { MPI_Comm_free(&logger->comm); } #endif free(logger); *logger_ptr = NULL; } return retval; } ``` #### 0d. Define the FunctionTable_Destroy function on the binding side The forward-declared `*FunctionTable_Destroy` function must be defined in a C++ source file in the bindings. Typically this goes in the same `.cpp` file where the nanobind `m.def` registrations live, or in a dedicated helpers file for that module. It casts the `void*` to the function table struct and deletes it. **Example** - in `bindings/sundials4py/sundials/sundials_logger.cpp`: ```cpp // Must be extern "C" since the forward declaration in the C source is in a C file extern "C" void SUNLoggerFunctionTable_Destroy(void* ptr) { delete static_cast<SUNLoggerFunctionTable*>(ptr); } ``` For reference, `SUNStepper` follows this exact pattern - `SUNStepperFunctionTable_Destroy` is forward-declared in `sundials_stepper.c` and defined in the binding-side C++ code. ### 1. Add a member to the function table struct Open the appropriate `*_usersupplied.hpp` header (e.g. `bindings/sundials4py/arkode/arkode_usersupplied.hpp`). Add an `nb::object` member to the function table struct. The member name should be a lowercase, underscore-separated version of the callback's short name. **File**: `bindings/sundials4py/<module>/<module>_usersupplied.hpp` ```cpp struct <module>_user_supplied_fn_table { // ... existing members ... nb::object <member_name>; // <-- add this }; ``` **Example** - adding `postprocessstepfn` to ARKODE: ```cpp struct arkode_user_supplied_fn_table { // ... existing members ... nb::object postprocessstepfn; }; ``` ### 2. Write the wrapper function Immediately below the struct (in the same `*_usersupplied.hpp` file), add a wrapper function template. The wrapper delegates to `sundials4py::user_supplied_fn_caller`. #### Standard callbacks (no special parameters) For callbacks whose parameters map 1:1 between C and C++, use the generic template form: ```cpp template<typename... Args> inline <return_type> <module>_<member_name>_wrapper(Args... args) { return sundials4py::user_supplied_fn_caller< std::remove_pointer_t<CTypeDef>, // the C function-pointer typedef, de-pointered <FunctionTableStruct>, // the function table struct type <MemStruct>, // integrator memory struct (e.g., ARKodeMem) or object type (e.g., SUNStepper) <user_data_offset> // (optional) positional offset of user_data arg; omit or set to 0 for `python`-member objects >(&<FunctionTableStruct>::<member_name>, args...); } ``` **Key parameters of `user_supplied_fn_caller`**: | Template param | Meaning | |----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1st | The C typedef with pointer removed (`std::remove_pointer_t<...>`) | | 2nd | The function table struct type | | 3rd | The integrator memory struct type or SUNDIALS object type | | 4th (optional) | Integer indicating which argument (0-indexed from the end) is the `user_data` pointer. Only needed for integrator callbacks; omit for `python`-member objects. Check the C typedef to find the position. | **Example** - standard ARKODE callback via `user_data`: ```cpp template<typename... Args> inline int arkode_postprocessstepfn_wrapper(Args... args) { return sundials4py::user_supplied_fn_caller< std::remove_pointer_t<ARKPostProcessFn>, arkode_user_supplied_fn_table, ARKodeMem, 1>(&arkode_user_supplied_fn_table::postprocessstepfn, args...); } ``` **Example** - SUNStepper callback via `python` member (no offset needed): ```cpp template<typename... Args> inline SUNErrCode sunstepper_evolve_wrapper(Args... args) { return sundials4py::user_supplied_fn_caller< std::remove_pointer_t<SUNStepperEvolveFn>, SUNStepperFunctionTable, SUNStepper>(&SUNStepperFunctionTable::evolve, std::forward<Args>(args)...); } ``` #### Special callbacks (array or output-pointer parameters) Some C typedefs have parameters that need translation for Python: | C parameter pattern | C++ / Python analog | |-----------------------------------------------------------------|------------------------------------------------------------| | `N_Vector*` used as a 1D array of N_Vectors | `std::vector<N_Vector>` | | Output pointer (e.g., `sunbooleantype*` used to return a value) | Pack into `std::tuple` return | | `sunrealtype*` used as an array | `std::vector<sunrealtype>` or pointer-to-first with length | When any of these appear, you cannot use the generic variadic template. Instead: 1. **Define a custom `std::function` type alias** that mirrors the C typedef but replaces the special parameters with their C++ analogs. Output parameters become additional elements in a `std::tuple` return type. 2. **Write a fully-explicit wrapper function** (not a variadic template) that: - Casts `user_data` / accesses the `python` member to get the function table. - Extracts the Python callable and casts it to the custom `std::function` type. - Converts C arrays to `std::vector` before calling. - Unpacks `std::tuple` return values back into the output pointers after calling. **Example** - CVODES `CVLsPrecSetupFnBS` with an N_Vector array and an output pointer: ```cpp // Custom std::function type: replaces N_Vector* array with std::vector, // and moves the jcurPtrB output pointer into a tuple return value. using CVLsPrecSetupStdFnBS = std::tuple<int, sunbooleantype>( sunrealtype t, N_Vector y, std::vector<N_Vector> yS_1d, N_Vector yB, N_Vector fyB, sunbooleantype jokB, sunrealtype gammaB, void* user_dataB); inline int cvode_lsprecsetupfnBS_wrapper(sunrealtype t, N_Vector y, N_Vector* yS_1d, N_Vector yB,
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub