ruby-bindgen tries its best to generate compilable Rice code. It has been battle tested against OpenCV, which is a large, complex C++ API with over a thousand classes and ten thousand methods.
However, complex libraries may require some customization. Customizations fall into three categories:
- Configuration
- Refinements
- Fixes
Some issues are best solved via the configuration file rather than editing generated code:
- Symbol filtering: Skip functions that cause linker errors or aren’t meant for external use by name or regex pattern
- Version guards: Wrap symbols in
#if VERSION >= Nguards so bindings compile against multiple library versions - Export macros: Limit bindings to exported symbols, preventing linker errors from internal functions
- Name mappings: Override generated Ruby class and method names with exact strings or regex patterns
Ruby classes are open, meaning you can reopen an existing class and add methods to it. Refinements take advantage of this to extend generated bindings with additional functionality. Note that this is not the same as Ruby's built-in refinements, which are scoped. These additions are global.
Common reasons to add refinements include:
| Addition | What It Adds |
|---|---|
| Expected methods | inspect, to_s |
| Modules | include_module(rb_mComparable), define_method("<=>") |
| Template instantiations | Mat<unsigned char> |
| Type conversions | to_* |
| Exceptions | Custom exception class hierarchy |
| Custom type handling | Type<T>, From_Ruby<T>, and To_Ruby<T> |
| Custom STL names | define_vector<cv::Vec3b>(...) |
| Non-member operators | +, -, *, /, == |
| Iteration | Add std::iterator_types |
| Method renames | rb_alias to rename methods to match Ruby idioms |
Just like in Ruby, you can take advantage of Ruby’s open classes to add new functionality.
To do this, create a directory to contain additions. The directory can be named anything, but a suggested convention is to name it refinements. Here is an example layout:
ext/
├── generated/ # Generated files (output: ./ext/generated)
│ ├── matrix-rb.hpp
│ ├── matrix-rb.cpp
│ ├── matrix-rb.ipp
│ └── my_extension-rb.cpp
├── include/
│ └── matrix.hpp
├── refinements/ # Manual additions (never overwritten)
│ ├── CMakeLists.txt
│ ├── matrix_refinements.hpp
│ ├── matrix_refinements.cpp
│ └── ...
Create a new file in refinements/ for each generated Rice file you want to extend. For example, if you want to add functionality to matrix-rb.cpp, create refinements/matrix_refinements.hpp and refinements/matrix_refinements.cpp. Define a new init function such as Init_Matrix_Refinements in those files.
Next add the .cpp file to refinements/CMakeLists.txt:
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
"matrix_refinements.cpp")Then in the generated generated/my_extension-rb.cpp file, include the refinement header and call Init_Matrix_Refinements().
Using refinements has a lot of advantages:
- Regeneration Safe: Your updates will not be overwritten with the exception of the
my_extension-rb.cppfile - Reuse
_instantiatemethods - Refinements can#includegenerated.ippfiles to call_instantiatefunctions with new type arguments.
Here is an example refinements/matrix_refinements.cpp file:
#include <sstream>
#include "../include/matrix.hpp"
#include "../generated/matrix-rb.hpp"
#include "../generated/matrix-rb.ipp"
#include "matrix_refinements.hpp"
using namespace Rice;
void Init_Matrix_Refinements()
{
// Reopen the class that was already defined by generated code
Rice::Data_Type<Matrix> matrix;
matrix.
define_method("to_s", [](const Matrix& self) -> std::string
{
std::ostringstream stream;
stream << self;
return stream.str();
});
// Rename a method to better match Ruby idioms
rb_alias(matrix, rb_intern("[]"), rb_intern("call"));
// Instantiate additional templates not in the generated code
Matrix_instantiate<double>(rb_mMyExtension, "MatrixDouble");
}Sometimes you need to modify the generated Rice code. Common reasons include:
| Change Type | Example | Why |
|---|---|---|
| Missing includes | #include <core.hpp> |
Generated file references types from headers it doesn't include |
| Version guards | #if VERSION >= 2 |
API only available in certain library versions |
| Default values | Remove Stream::Null() default |
Avoid hardware-dependent functions such as CUDA initialization |
- Mark manual changes with
// Manualcomments so they're searchable:grep -r "// Manual" ext/ - Include order matters: System/library headers should come before the primary header, local project headers after
- Watch fluent chain syntax when commenting out methods: change the preceding
.to;to terminate the chain - Refinements can include generated
.ippfiles to reuse_instantiatefunctions with additional type arguments
If you have manual edits, you will need a strategy for preserving them when you regenerate bindings.