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.