Python is where ideas move fast. C++ is where performance lives. For years, gluing the two together meant choosing between power, safety, and sanity. Enter nanobind—a modern, minimalist bridge that lets C++ and Python talk to each other cleanly, efficiently, and without ceremony.
nanobind is a C++17-first binding library, inspired by pybind11 but redesigned with a sharper focus on performance, compile times, and modern C++ practices. It’s built for developers who care about control—over lifetimes, memory, ABI stability, and error handling—without drowning in boilerplate.
What makes nanobind special is how unapologetically low-level it is, while still feeling elegant. It avoids heavy template metaprogramming where possible, compiles fast, and produces smaller binaries. If you’re working on large C++ codebases—CAD kernels, physics engines, solvers, graphics pipelines—this matters a lot.
Memory management is another strong point. nanobind gives you explicit control over object ownership between C++ and Python, making it ideal for systems where Python is embedded inside a C++ application (think Blender, FreeCAD, game engines, or simulation tools). You’re not just exporting functions—you’re designing an API boundary.
Compared to older tools like SWIG, nanobind doesn’t try to “auto-magically” generate bindings. That’s a feature, not a bug. You write bindings intentionally, so the Python API feels Pythonic, not like a leaked C++ header. And compared to pybind11, nanobind is leaner, stricter, and more future-facing.
In short:
Python for orchestration and scripting
C++ for performance-critical logic
nanobind as the clean, modern handshake between them
If you’re building serious software where Python is a first-class citizen—but not the performance bottleneck—nanobind is one of the best bridges you can choose today.
Here's my today's exploration on nanobind - the Factory pattern written in C++ being given a python interface using nanobind.
Enjoy...
The Factory Pattern written in C++.
/*
* Food.h
*
* Created on: Mar 10, 2021
* Author: som
*/
#ifndef FOOD_H_
#define FOOD_H_
#include <string>
using namespace std;
class Food {
public:
virtual string getName() = 0;
virtual ~Food(){
}
};
#endif /* FOOD_H_ */
/*
* Biscuit.h
*
* Created on: Mar 10, 2021
* Author: som
*/
#ifndef BISCUIT_H_
#define BISCUIT_H_
#include "Food.h"
class Biscuit: public Food {
public:
Biscuit();
string getName();
~Biscuit();
};
#endif /* BISCUIT_H_ */
/*
* Biscuit.cpp
*
* Created on: Mar 10, 2021
* Author: som
*/
#include <iostream>
#include "Biscuit.h"
using namespace std;
Biscuit::Biscuit() {
// TODO Auto-generated constructor stub
cout<<"Biscuit is made..."<<endl;
}
Biscuit::~Biscuit(){}
string Biscuit::getName(){
return "It's a Biscuit";
}
/*
* Chocolate.h
*
* Created on: Mar 10, 2021
* Author: som
*/
#ifndef CHOCOLATE_H_
#define CHOCOLATE_H_
#include <iostream>
#include "Food.h"
class Chocolate: public Food {
public:
Chocolate();
virtual ~Chocolate();
string getName();
};
#endif /* CHOCOLATE_H_ */
/*
* Chocolate.cpp
*
* Created on: Mar 10, 2021
* Author: som
*/
#include "Chocolate.h"
Chocolate::Chocolate() {
// TODO Auto-generated constructor stub
cout<<"Chocolate is made..."<<endl;
}
Chocolate::~Chocolate() {
// TODO Auto-generated destructor stub
}
string Chocolate::getName(){
return "It's a Chocolate";
}
/*
* Factory.h
*
* Created on: Mar 10, 2021
* Author: som
*/
#ifndef FACTORY_H_
#define FACTORY_H_
#include <nanobind/nanobind.h>
#include <iostream>
#include <string>
#include "Biscuit.h"
#include "Chocolate.h"
using namespace std;
class Factory{
public:
static Factory* instance;
static Factory* getInstance();
Food* makeFood(const string& type);
private:
Factory(){}
// Delete copy constructor & assignment operator (Singleton pattern)
Factory(const Factory&) = delete;
Factory& operator=(const Factory&) = delete;
};
//Factory* Factory:: instance = NULL;
#endif /* FACTORY_H_ */
/*
* Factory.cpp
*
* Created on: Jan 30, 2025
* Author: som
*/
#include "Factory.h"
Factory* Factory::instance = NULL;
Factory* Factory:: getInstance(){
if(Factory::instance == NULL){
Factory::instance = new Factory();
}
return Factory::instance;
}
Food* Factory::makeFood(const string& type){
if(type.compare("bi") == 0){
return new Biscuit();
}
if(type.compare("ch") == 0){
return new Chocolate();
}
return NULL;
}
The bindings.cpp - defining the bridge...
#include <nanobind/nanobind.h>
#include <nanobind/stl/string.h> // Required for std::string support
#include "Factory.h"
namespace nb = nanobind;
// Use NB_MODULE to define the extension
NB_MODULE(foodfactory, m) {
// 1. Wrap the Base Class
// We use nb::class_<T> and provide the name it will have in Python
nb::class_<Food>(m, "Food")
.def("getName", &Food::getName);
// 2. Wrap the Subclasses
// Note: We specify the base class <Biscuit, Food> so Python knows the relationship
nb::class_<Biscuit, Food>(m, "Biscuit")
.def(nb::init<>());
nb::class_<Chocolate, Food>(m, "Chocolate")
.def(nb::init<>());
// 3. Wrap the Singleton Factory
nb::class_<Factory>(m, "Factory")
.def_static("get_instance", &Factory::getInstance, // Renamed to snake_case for Python idiomatic style
nb::rv_policy::reference)
.def("make_food", &Factory::makeFood, // Match your Python script's call
nb::rv_policy::take_ownership);}
And here is the CMakeLists.txt for compiling and creating the shared object
cmake_minimum_required(VERSION 3.15)
project(FactoryPattern_nanobind)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 1. Find Python
find_package(Python 3.9 COMPONENTS Interpreter Development REQUIRED)
# 2. Get nanobind paths manually to bypass the "Target Not Found" bug
execute_process(
COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir
OUTPUT_VARIABLE NB_DIR OUTPUT_STRIP_TRAILING_WHITESPACE
)
execute_process(
COMMAND "${Python_EXECUTABLE}" -m nanobind --include_dir
OUTPUT_VARIABLE NB_INC OUTPUT_STRIP_TRAILING_WHITESPACE
)
# 3. Load the nanobind logic
list(APPEND CMAKE_PREFIX_PATH "${NB_DIR}")
find_package(nanobind REQUIRED CONFIG)
# 4. Create the module
nanobind_add_module(foodfactory
LTO
Biscuit.cpp
Chocolate.cpp
Factory.cpp
bindings.cpp
)
# 5. MANUALLY fix the missing target link
# This bypasses the error by providing the includes and libraries directly
target_include_directories(foodfactory PRIVATE "${NB_INC}" "${Python_INCLUDE_DIRS}")
target_link_libraries(foodfactory PRIVATE Python::Module)
# If nanobind still complains about the target, we define it as an alias
if(NOT TARGET nanobind::nanobind)
add_library(nanobind::nanobind INTERFACE IMPORTED)
set_target_properties(nanobind::nanobind PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${NB_INC}"
)
endif()
After compiling and creating the shared object, you need to put it
inside the Python project and import it.
The Python code looks as follows.
import foodfactory
# Access get_instance THROUGH the Factory class
factory = foodfactory.Factory.get_instance()
biscuit = factory.make_food("bi")
print(biscuit.getName())
chocolate = factory.make_food("ch")
print(chocolate.getName())
No comments:
Post a Comment