A Journey from Pybind11 Envy to Wrapping OpenCV

It started with pybind11 envy.

While at Pattern Computer, I built a pattern discovery engine in C++. Our data scientists needed access to its internals, so I created Python bindings using pybind11. It is magical when you can access a compiled library from a REPL like IRB – the code comes alive! Using the bindings, our data scientists were able to discover new ways to treat colon cancer and a host of other diseases.

Pybind11 made it remarkably easy to create the bindings – it took me just a couple of days to have a prototype and a couple of weeks for a complete solution.

Rice

Since I am a big fan of Ruby, I went searching for a similar solution and found Rice. Sadly, it was not up to the task. Rice go off to a great start in 2008, but had failed to keep up with modern C++. And rest of the ecosystem was not in great shape either – there simply wasn’t a Ruby/C++ toolchain you could use to easily create C++ based Ruby extensions. It all had to be done by hand.

That was 5 years ago. I spent 6 months modernizing Rice and converting it to a header only library (important in the C++ world) and adding ownership support. That ushered in Rice 4.0 and made it good enough to wrap simple C++ APIs. Then in 2023, the Rice bug bit me again, and I spent another 6 months working on it and adding much better STL support. The work paid off, enabling Andrew Kane and others to use Rice to wrap a number of AI and ML libraries for Ruby, including PyTorch.

The OpenCV Challenge

Then in November 2024, I wanted to test Rice against something more ambitious. I looked for a large, complicated C++ API and found OpenCV. OpenCV is a uniquely challenging library to wrap:

  • Scale: Thousands of classes, tens of thousands of methods and functions across dozens of modules
  • Mixed vintage: Modern C++ with STL containers sits alongside legacy APIs using raw pointers and C-style arrays
  • Template heavy: cv::Mat_<T>, cv::Vec<T,n>, cv::Matx<T,m,n> and countless other template classes
  • Buffer-oriented: Matrices are fundamentally raw memory buffers with metadata—no RAII, careful lifetime management required
  • Overloading everywhere: Methods like cv::resize() have 4+ signatures; cv::Mat::create() has even more
  • No Ruby bindings – The existing Ruby bindings were based on the old, no longer existent C API.

Detours

OpenCV is massive – there was no way I was going to write the bindings by hand. So I went on my first detour – creating a gem called ruby-bindgen that automatically creates FFI bindings for C code and Rice bindings for C++. And since I wasn’t about to write C or C++ parsing code – that led to my second detour. Working with Samuel Williams, I added a number of API endpoints to the ffi-clang gem so ruby-bindgen could do its job.

After a few months I finally could generate bindings and ended up with this:

TypeCount
Classes1,058
Methods3,145
Module functions4,006
Constructors1,359
Attributes913
Enums160

Now I needed a build system – clearly mkmf was not going to work. Since OpenCV uses CMake, and CMake is arguably the de-facto C++ build system, I used CMake. And soon discovered its Ruby support hadn’t received much love over the years. That led to my third detour – modernizing CMake’s Ruby support.

The Moment of Truth

Finally, with all the pieces in place – ffi-clang, ruby-bindgen, Rice and CMake – I could actually create OpenCV bindings.

And then Rice completely and totally failed. There were a lot of problems:

  • It lacked method overloading support
  • It did not support buffers (pointers to fundamental types like integers)
  • It did not support C++ template classes (ie, Matrix<T>)
  • It did not support C style callbacks

A Year Later

So began a year-long journey to update Rice and create a complete Ruby <=> C++ toolchain to make it easy to wrap C++ libraries for use in Ruby.

Today marks a big milestone in this journey, with the release of rice 4.9.1 and opencv-ruby 1.0.0. Both projects have excellent documentation and are ready for production use.

But the journey isn’t over. I’m working on upstreaming additional CMake improvements to make Ruby a first-class citizen in the C++ build ecosystem. And ruby-bindgen is approaching its 1.0 release, which will make it even easier to automatically generate bindings for your own C++ libraries.

If you’re interested in bringing C++ libraries to Ruby, I hope you’ll give this toolchain a try! A great place to start is the Rice documentation and the BitmapPlusPlus sample bindings!

Leave a Reply

Your email address will not be published. Required fields are marked *

Top