Manually add custom bindings#
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
.pyi
file),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) |
---|---|---|
|
The current bound C++ class object |
|
|
The current submodule (for a namespace) |
|
|
The main Python module object |
|
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_code
is 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::arg
vsnb::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_CLASS
to 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 @staticmethod to indicate they are module-level, not instance methods.pydef_code
: Custom binding code in C++ (pybind11/nanobind syntax). You can use the placeholderLG_SUBMODULE
to 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_MODULE
to 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)
namespace RootNs
{
struct Foo
{
int mValue = 0;
};
}
# <submodule root_ns>
class root_ns: # Proxy class that introduces typings for the *submodule* root_ns
pass # (This corresponds to a C++ namespace. All methods are static!)
class Foo:
m_value: int = 0
def __init__(self, m_value: int = 0) -> None:
"""Auto-generated default constructor with named params"""
pass
def get_value(self) -> int:
"""Get the value"""
...
def set_value(self, value: int) -> None:
"""Set the value"""
...
@staticmethod # We **must** use @staticmethod here
def foo_namespace_function() -> int:
"""A custom function in the submodule"""
...
# </submodule root_ns>
def global_function() -> int:
"""A custom function in the main module"""
...
{ // <namespace RootNs>
py::module_ pyNsRootNs = m.def_submodule("root_ns", "");
auto pyNsRootNs_ClassFoo =
py::class_<RootNs::Foo>
(pyNsRootNs, "Foo", "")
.def(py::init<>([](
int mValue = 0)
{
auto r_ctor_ = std::make_unique<RootNs::Foo>();
r_ctor_->mValue = mValue;
return r_ctor_;
})
, py::arg("m_value") = 0
)
.def_readwrite("m_value", &RootNs::Foo::mValue, "")
;
pyNsRootNs_ClassFoo.def("get_value", [](const RootNs::Foo& self){ return self.mValue; });
pyNsRootNs_ClassFoo.def("set_value", [](RootNs::Foo& self, int value){ self.mValue = value; }, nb::arg("value"));
// Example of adding a custom function to the submodule
pyNsRootNs.def("foo_namespace_function", [](){ return 53; });
} // </namespace RootNs>
// Example of adding a custom function to the main module
m.def("global_function", [](){ return 64; });
{ // <namespace RootNs>
nb::module_ pyNsRootNs = m.def_submodule("root_ns", "");
auto pyNsRootNs_ClassFoo =
nb::class_<RootNs::Foo>
(pyNsRootNs, "Foo", "")
.def("__init__", [](RootNs::Foo * self, int mValue = 0)
{
new (self) RootNs::Foo(); // placement new
auto r_ctor_ = self;
r_ctor_->mValue = mValue;
},
nb::arg("m_value") = 0
)
.def_rw("m_value", &RootNs::Foo::mValue, "")
;
pyNsRootNs_ClassFoo.def("get_value", [](const RootNs::Foo& self){ return self.mValue; });
pyNsRootNs_ClassFoo.def("set_value", [](RootNs::Foo& self, int value){ self.mValue = value; }, nb::arg("value"));
// Example of adding a custom function to the submodule
pyNsRootNs.def("foo_namespace_function", [](){ return 53; });
} // </namespace RootNs>
// Example of adding a custom function to the main module
m.def("global_function", [](){ return 64; });
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.