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)

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_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 vs nb::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 placeholder LG_CLASS to refer to the bound py::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 placeholder LG_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 placeholder LG_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.