Demo Project
This walkthrough will take you through all the steps to create a simple semiwrap project that autogenerates a working wrapper around a C++ class and it’s methods.
This demo should work on Linux, OSX, and Windows. Make sure you have
semiwrap installed first!
Note
This demo shows building a python wrapper around C++ code that is
self-contained in this project. However, semiwrap also
supports wrapping externally compiled libraries and inter-package
shared library dependencies available through pypi-pkgconf
Files + descriptions
Note
If you’re lazy, the files for this demo are checked into the
semiwrap repository at examples/demo.
All of the content required for this demo is contained inline below. Let’s start by creating a new directory for your project, and we’re going to create the following files:
pyproject.toml
Projects that use semiwrap must add a pyproject.toml to the root of
their project as specified in PEP 518.
This file is used to configure your project. The semiwrap configuration is
in the tool.semiwrap table, the rest of the file are hatchling configuration
directives to load the semiwrap and hatch-meson plugins.
Comments describing the function of each section can be found inline below.
[build-system]
# semiwrap is a hatchling plugin and is launched by the hatchling build backend
build-backend = "hatchling.build"
# compilation is handled by meson
requires = ["semiwrap", "hatch-meson", "hatchling"]
# Standard python packaging metadata
[project]
name = "swdemo"
description = "Demo program"
version = "0.0.1"
#
# Configure hatchling build hooks here
# - semiwrap is just a hatchling build hook and can be used along with
# other hatchling build hooks
#
# semiwrap build hook will autogenerate meson.build files in semiwrap directory
[tool.hatch.build.hooks.semiwrap]
# meson build hook will build python extension modules as generated by semiwrap
[tool.hatch.build.hooks.meson]
#
# semiwrap code generation configuration
#
[tool.semiwrap]
# This tells the `semiwrap update-init` command which files to update
# .. the first item is the python package to be updated, and the second
# item is the name of the compiled python extension module
update_init = ["swdemo swdemo._demo"]
# This tells semiwrap to generate a python extension module `swdemo._demo`
[tool.semiwrap.extension_modules."swdemo._demo"]
name = "demo"
# semiwrap will parse any header files mentioned here and autogenerate pybind11
# wrappers around the contents of the header
# - You can use `python -m semiwrap scan-headers` to autogenerate a list of
# headers that are available to be wrapped, and paste the list here
[tool.semiwrap.extension_modules."swdemo._demo".headers]
demo = "include/demo.h"
See also
For detailed information about the contents of pyproject.toml
see pyproject.toml.
meson.build
Semiwrap generates python modules that are built using the meson build system,
and you must provide your own meson.build that includes the build files that
semiwrap generates and any other build customizations required for your project.
project('demo', ['cpp'],
default_options: ['warning_level=1', 'cpp_std=c++20',
'b_colorout=auto', 'optimization=2', 'b_pie=true'])
# Include autogenerated semiwrap/meson.build
subdir('semiwrap')
# Add additional source files to predefined variable in semiwrap/meson.build
demo_sources += files(
'swdemo/src/demo.cpp',
'swdemo/src/main.cpp',
)
# You can add extra compilation arguments by adding a dependency to predefined
# variable
demo_deps += [
declare_dependency(include_directories: ['swdemo/include'])
]
# Include autogenerated semiwrap/modules/meson.build
# - Builds the extension modules
# - Generates the pyi file for the extension modules
subdir('semiwrap/modules')
swdemo/__init__.py
# file is empty for now
swdemo/src/demo.cpp
This is the (very simple) C++ code that we will wrap so that it can be called from python.
#include "demo.h"
int add2(int x) {
return x + 2;
}
namespace demo {
void DemoClass::setX(int x) {
m_x = x;
}
int DemoClass::getX() const {
return m_x;
}
} // namespace demo
swdemo/include/demo.h
This is the C++ header file for the code that we’re wrapping. In pyproject.toml
we told semiwrap to parse this file and autogenerate wrappers for it.
For simple C++ code such as this, autogeneration will ‘just work’ and no other customization is required. However, certain C++ code (templates and sometimes code that depends on templated types, and other complex circumstances) will require providing customization in a YAML file.
#pragma once
/** Adds 2 to the first parameter and returns it */
int add2(int x);
namespace demo {
/**
Doxygen documentation is automatically added to your python objects
when the bindings are autogenerated.
*/
class DemoClass {
public:
/** Sets X */
void setX(int x);
/** Gets X */
int getX() const;
private:
int m_x = 0;
};
} // namespace demo
swdemo/src/main.cpp
Finally, you need to define your pybind11 python module. Custom pybind11
projects would use a PYBIND11_MODULE macro to define a module, but it’s
easier to use the SEMIWRAP_PYBIND11_MODULE macro which automatically sets
the module name when semiwrap compiles the file.
// This header file is automatically generated by semiwrap and provides the
// `initWrapper` function which is used to instantiate all the wrapper code
// autogenerated by semiwrap
//
// The name of the header will be `semiwrap_init.PACKAGE.NAME.hpp`
#include <semiwrap_init.swdemo._demo.hpp>
SEMIWRAP_PYBIND11_MODULE(m) {
initWrapper(m);
}
Note
If you wanted to add your own handwritten pybind11 code here, you
can add it in addition to the initWrapper call made here. See
the pybind11 documentation for more details.
Install the project
When developing a new project, it’s easiest to just install in ‘develop’ mode which will build/install everything in the currect directory.
$ python3 -m pip install -v -e .
If you’ve been following our instructions so far, this will fail with an error similar to this:
semiwrap.makeplan.PlanError: swdemo._demo failed
- caused by FileNotFoundError: semiwrap/demo.yml: use `python3 -m semiwrap update-yaml --write` to generate
semiwrap requires all headers listed in tool.semiwrap.extension_modules."PACKAGE.NAME".headers
to have an associated YAML file in the semiwrap directory. You can create them manually,
or just use the following command to autogenerate it.
python3 -m semiwrap update-yaml --write
Now there will be a semiwrap generation configuration YAML file at semiwrap/demo.yml:
---
functions:
add2:
classes:
demo::DemoClass:
methods:
setX:
getX:
Now if you run the install command again it should build and install the package:
$ python3 -m pip install -v -e .
Adjust the project
As we’ve currently built the project, the CPython extension will be built
as swdemo._swdemo. For example:
>>> from swdemo._demo import DemoClass
>>> DemoClass
<class 'swdemo._demo.DemoClass'>
While that works, we really would like users to be able to access our module
directly by importing them into __init__.py. The semiwrap update-init
command can automatically add this to specified packages. Ensure that your
pyproject.toml contains the package names in update_init:
[tool.semiwrap]
update_init = ["swdemo swdemo._demo"]
Then run this command:
$ python -m semiwrap update-init
This will add the following to your __init__.py:
# autogenerated by 'robotpy-build create-imports rpydemo rpydemo._rpydemo'
from ._demo import DemoClass, add2
__all__ = ["DemoClass", "add2"]
Now when we put this in our __init__.py, that allows this to work instead:
>>> from swdemo import DemoClass
>>> DemoClass
<class 'swdemo._demo.DemoClass'>
Trying out the project
Alright, now that all the pieces are assembled, we can try out our project:
>>> import swdemo
>>> swdemo.add2(2)
4
>>> d = swdemo.DemoClass()
>>> d.setX(2)
>>> d.getX()
2
More Examples
The integration tests in tests/cpp contains a several projects that contains
autogenerated wrappers packages and various customizations.