Litgen normally generates bindings automatically from C++ headers, but sometimes
you may want to extend the API with extra methods or functions that are not
present in the C++ code. LitgenOptions.custom_bindings lets you do this without modifying
your C++ headers.
You can attach:
extra methods/properties to a C++ class,
free functions to a C++ namespace (shown as a Python submodule),
free functions to the main module.
Each injection consists of:
stub code (Python declarations added to the generated
.pyifile),pydef code (C++ binding code inserted into the generated binding .cpp file, using pybind11 or nanobind syntax).
Placeholders are available inside pydef_code:
| Placeholder | Role | Expansion (example) |
|---|---|---|
LG_CLASS | The current bound C++ class object | pyNsRootNs_ClassFoo (of type nb::class_ or py::class_) |
LG_SUBMODULE | The current submodule (for a namespace) | pyNsRootNs (a nb::module_ or py::module_) |
LG_MODULE | The main Python module object | m (the top-level module) |
These placeholders are automatically replaced by litgen during C++ binding code generation. You can safely use them inside your pydef_code snippets without declaring them yourself.
Note about pydef_code syntax:
You can use either pybind11 or nanobind syntax in
pydef_code, depending on which backend you are using.Since the
pydef_codeis inserted into a function, limit yourself to statements that are valid inside a function: no function/class definitions, no#include. Use lambdas for small helpers.When writing lambdas, fully qualify C++ types if the class/namespace isn’t open (e.g.
const RootNs::Foo& self).Argument helpers differ by backend (
py::argvsnb::arg). Use the one matching your active backend.
Example C++ code¶
This page demonstrates how to add custom bindings to the following C++ code.
cpp_code = """
namespace RootNs
{
struct Foo
{
int mValue = 0;
};
}
"""We will be adding custom bindings to the class RootNs::Foo, the namespace RootNs, and to the main module.
Custom bindings for a class¶
options.custom_bindings.add_custom_bindings_to_class(qualified_class, stub_code, pydef_code), lets us extend the generated Python bindings with extra methods, properties, or static methods.
Args:
qualified_class: Fully qualified C++ class name (e.g."RootNs::Foo").stub_code: Python stub declarations to be inserted into the generated stub (“.pyi”) file. These should be written in normal Python syntax with type annotations.pydef_code: Custom binding code in C++ (pybind11/nanobind syntax). You can use the placeholderLG_CLASSto refer to the boundpy::class_/nb::class_object.
import litgen
options = litgen.LitgenOptions()
options.custom_bindings.add_custom_bindings_to_class(
qualified_class="RootNs::Foo",
stub_code='''
def get_value(self) -> int:
"""Get the value"""
...
def set_value(self, value: int) -> None:
"""Set the value"""
...
''',
pydef_code="""
LG_CLASS.def("get_value", [](const RootNs::Foo& self){ return self.mValue; });
LG_CLASS.def("set_value", [](RootNs::Foo& self, int value){ self.mValue = value; }, nb::arg("value"));
""",
)
Custom bindings for C++ namespace / Python submodule¶
options.custom_bindings.add_custom_bindings_to_submodule(qualified_namespace, stub_code, pydef_code), lets us extend the generated Python bindings with extra functions.
Args:
qualified_namespace: Fully qualified C++ namespace name (e.g. “RootNs”).stub_code: Python stub declarations to be inserted into the generated stub (“.pyi”) file. These should be written in normal Python syntax with type annotations. Functions here should be decorated with@staticmethod. Explanation for this: in stubs, namespaces are represented as proxy classes. Thus, functions must be declared as@staticmethodto indicate they are module-level, not instance methods.pydef_code: Custom binding code in C++ (pybind11/nanobind syntax). You can use the placeholderLG_SUBMODULEto refer to the bound submodule object.
Namespace stubs appear as a proxy class.
When declaring functions in a C++ namespace (Python submodule) in the stub, mark them with @staticmethod. These are module-level functions, not instance methods.
options.custom_bindings.add_custom_bindings_to_submodule(
qualified_namespace="RootNs",
stub_code='''
@staticmethod # We **must** use @staticmethod here
def foo_namespace_function() -> int:
"""A custom function in the submodule"""
...
''',
pydef_code="""
// Example of adding a custom function to the submodule
LG_SUBMODULE.def("foo_namespace_function", [](){ return 53; });
""",
)
Custom bindings for the main module¶
options.custom_bindings.add_custom_bindings_to_main_module(stub_code, pydef_code), lets us extend the generated Python bindings with extra functions in the main module.
Args:
stub_code: Python stub declarations to be inserted into the generated stub (“.pyi)” file. These should be written in normal Python syntax with type annotations.pydef_code: Custom binding code in C++ (pybind11/nanobind syntax). You can use the placeholderLG_MODULEto refer to the bound module object.
options.custom_bindings.add_custom_bindings_to_main_module(
stub_code='''
def global_function() -> int:
"""A custom function in the main module"""
...
''',
pydef_code="""
// Example of adding a custom function to the main module
LG_MODULE.def("global_function", [](){ return 64; });
""",
)
Generated code (with custom bindings)¶
We may now call litgen.generate_code to generate the bindings, which will include our custom additions.
The generated code is shown below.
from litgen.demo import litgen_demo
litgen_demo.demo(options, cpp_code)Note about ordering:
If you call add_custom_code_to_* multiple times for the same target (class/namespace/module), snippets are emitted in the order they were added.