TOML input file
Here we demonstrate how a TOML input file can be used to customize the generated Python bindings.
Basic setup
In all examples we will work with the following 3 files:
// foo.hpp
namespace foo {
void f1();
void f2();
void g1();
void g2();
class A {
int x_;
public:
int x() const { return x_; }
void set_x(int x) { x_ = x; }
};
} // namespace foo
// bar.hpp
namespace bar {
void h1();
void h2();
} // namespace bar
// my_module.cpp
#include <c2py/c2py.hpp>
#include "foo.hpp"
#include "bar.hpp"
#include <iostream>
namespace foo {
void f1() { std::cout << "foo::f1" << std::endl; }
void f2() { std::cout << "foo::f2" << std::endl; }
void g1() { std::cout << "foo::g1" << std::endl; }
void g2() { std::cout << "foo::g2" << std::endl; }
} // namespace foo
namespace bar {
void h1() { std::cout << "bar::h1" << std::endl; }
void h2() { std::cout << "bar::h2" << std::endl; }
} // namespace bar
We declare some simple functions and a class in the namespaces foo and bar in the header files foo.hpp and bar.hpp, respectively.
Their definitions are given in my_module.cpp which is also the file passed to clair-c2py to generate the Python bindings.
Our initial TOML file looks as follows:
# Name of the package [optional].
# Name of the module will be package_name.module_filename
package_name = "my_package"
# The documentation string of the module.
documentation = "This is an awesome module generated by c2py/clair."
# List of namespaces as a space separated string e.g. "A A::B"
# If non-empty, only the elements declared in EXACTLY these namespaces are wrapped.
# NB e.g. in previous example A::detail will be ignored, whatever match_names.
namespaces = "foo bar"
# Match/reject elements according to a regex (LLVM regex)
# If match_names/match_names are both set, the algorithm is
# for X of fully qualified name qname
# if match_names and match_names does not match Qname : skip X
# if reject_names and reject_names matches Qname : skip X
# else keep X.
#
match_names = ""
reject_names = ""
# Only keep elements declared in files matching the pattern
match_files = "" # String must be a regex
# If true, transform methods with no arguments into a read only property
wrap_no_arg_methods_as_properties = false
# [Advanced] If yes (default), all elements (class/enum/functions)
# declared in headers included with -isystem are ignored for better performance.
exclude_system_headers = true
We can then generate the Python bindings and compile the extension module:
clair-c2py my_module.cpp -- -std=c++20 `c2py_flags -i`
clang++ my_module.cpp -std=c++20 -shared -o my_module.so `c2py_flags`
Running python and importing my_module, we can see that everything in both namespaces has been wrapped:
>>> import my_module
>>> dir(my_module)
['A', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'f1', 'f2', 'g1', 'g2', 'h1', 'h2']
>>> print(my_module.__doc__)
This is an awesome module generated by c2py/clair.
For the rest of the examples, we will not repeat the binding generation and compilation commands. We will only show the modifications made to the TOML file and the resulting behavior in Python.
Filters
By default, clair will generate bindings for all non-template classes and functions,
except those defined in system files, i.e. the standard library and more generally
anything the compiler includes via -isystem options.
In order to refine this behaviour, several filters can be used.
Warning
Be careful that some these strings are Regex. For example, N::* does not match N::f.
Also note that the qualified name starts with ::. So to match the start of a name, use ^::N::f.
Filtering by namespace
We can restrict the wrapping to only functions declared in specific namespaces using the namespaces option.
For example, to only wrap functions in the foo namespace, we modify the TOML file as follows:
# List of namespaces as a space separated string e.g. "A A::B"
# If non-empty, only the elements declared in EXACTLY these namespaces are wrapped.
# NB e.g. in previous example A::detail will be ignored, whatever match_names.
namespaces = "foo"
Now only the names in the foo namespace are available in Python:
>>> import my_module
>>> dir(my_module)
['A', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'f1', 'f2', 'g1', 'g2']
Filtering by name
A more fine-grained filtering can be achieved using the match_names and reject_names options.
For example, to further restrict the wrapping to only functions whose name starts with f, we modify the TOML file as follows:
# Match/reject elements according to a regex (LLVM regex)
# If match_names/match_names are both set, the algorithm is
# for each element X of qualified name Qname:
# if match_names and not match_names matches Qname : skip X
# if reject_names and reject_names matches Qname : skip X
# else keep X.
#
match_names = "foo::f.*"
# reject_names = ""
Now only the functions f1 and f2 are available in Python:
>>> import my_module
>>> dir(my_module)
['__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'f1', 'f2']
To only wrap the function f1, we can for example reject foo::f2 explicitly:
# Match/reject elements according to a regex (LLVM regex)
# If match_names/match_names are both set, the algorithm is
# for each element X of qualified name Qname:
# if match_names and not match_names matches Qname : skip X
# if reject_names and reject_names matches Qname : skip X
# else keep X.
#
match_names = "foo::f.*"
reject_names = "foo::f2"
This results in only f1 being available in Python:
>>> import my_module
>>> dir(my_module)
['__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'f1']
Filter by file name
Finally, we can restrict the wrapping to only elements declared in files matching a given regex using the match_files option.
Let’s start again from our initial TOML file without any filtering.
To only wrap functions declared in files whose name contains foo, we modify the TOML file as follows:
# Only keep elements declared in files matching the pattern
match_files = ".*foo.*" # String must be a regex
This should give us the same as filtering by namespace for foo only:
>>> import my_module
>>> dir(my_module)
['A', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'f1', 'f2', 'g1', 'g2']
Wrap no argument methods as properties
If we set the wrap_no_arg_methods_as_properties option to true, all methods of wrapped classes with no argument will be wrapped
as Python properties.
For example, our class A defined in foo.hpp has the getter method A::x() with no argument.
By default, this method is wrapped as a normal method in Python:
>>> import my_module
>>> a = my_module.A()
>>> a.x()
0
>>> a.set_x(5)
>>> a.x()
5
>>> a.x
<built-in method x of my_package.my_module.A object at 0x101248a10>
To wrap it as a property instead, we modify the TOML file as follows:
# If true, all methods with no argument are wrapped as properties
wrap_no_arg_methods_as_properties = true
Now the method A::x() is wrapped as a read-only property in Python:
>>> import my_module
>>> a = my_module.A()
>>> a.x
0
>>> a.set_x(5)
>>> a.x
5
Exclude system headers
The option exclude_system_header is an advanced feature and the default behavior is usually what you want.