Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 45 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,30 @@

Automatically generate PyBind11 Python wrapper code for C++ projects.

## Example
## Installation
Clone the repository and install cppwg:

`examples/shapes/` is a full example project, demonstrating how to generate a Python package `pyshapes` from
C++ source code. It is recommended that you use it as a template project when getting started.
```bash
git clone https://github.com/Chaste/cppwg.git
cd cppwg
pip install .
```

## Usage

This project generates PyBind11 wrapper code, saving lots of boilerplate in
bigger projects. Please see the [PyBind11 documentation](https://pybind11.readthedocs.io/en/stable/)
for help on the generated wrapper code.

### First Example

The `examples/shapes/` directory is a full example project, demonstrating how to
generate a Python package `pyshapes` from C++ source code. It is recommended
that you use it as a template project when getting started.

As a small example, we can start with a free function in
`examples/shapes/src/math_funcs/SimpleMathFunctions.hpp`:

As a small example, we can start with a free function in `examples/shapes/src/math_funcs/SimpleMathFunctions.hpp`:
```c++
#ifndef _SIMPLEMATHFUNCTIONS_HPP
#define _SIMPLEMATHFUNCTIONS_HPP
Expand All @@ -28,7 +46,7 @@ int add(int i, int j)
#endif // _SIMPLEMATHFUNCTIONS_HPP
```

add a package description to `examples/shapes/wrapper/package_info.yaml`:
Add a package description to `examples/shapes/wrapper/package_info.yaml`:

```yaml
name: pyshapes
Expand All @@ -37,9 +55,19 @@ modules:
free_functions: cppwg_ALL
```

and do `python examples/shapes/wrapper/generate.py` (with some suitable arguments).
Generate the wrappers with:

```bash
cd examples/shapes
cppwg src/ \
--wrapper_root wrapper/ \
--package_info wrapper/package_info.yaml \
--includes src/math_funcs/
```

The following PyBind11 wrapper code will be output to
`examples/shapes/wrapper/math_funcs/math_funcs.main.cpp`:

The generator will make the following PyBind11 wrapper code in `examples/shapes/wrapper/math_funcs/math_funcs.main.cpp`:
```c++
#include <pybind11/pybind11.h>
#include "wrapper_header_collection.hpp"
Expand All @@ -52,7 +80,8 @@ PYBIND11_MODULE(_pyshapes_math_funcs, m)
}
```

which can be built into a Python module and (with some import tidying) used as follows:
The wrapper code can be built into a Python module and used as follows:

```python
from pyshapes import math_funcs
a = 4
Expand All @@ -62,49 +91,19 @@ print c
>>> 9
```

## Usage
It is recommended that you [learn how to use PyBind11 first](https://pybind11.readthedocs.io/en/stable/). This project
generates PyBind11 wrapper code, saving lots of boilerplate in bigger projects.

### Dependencies
Developed on the [latest Ubuntu LTS](https://ubuntu.com/about/release-cycle)
version and tested with [supported versions of Python 3](https://devguide.python.org/versions/).

The main dependencies are [pyyaml](https://github.com/yaml/pyyaml),
[pygccxml](https://github.com/CastXML/pygccxml), and [castxml](https://github.com/CastXML/CastXML),
which will be automatically pip-installed along with cppwg. Alternatively,
they can be installed directly with:

```bash
pip install pyyaml pygccxml castxml
```

### Test the Installation
First, clone the repository with:
### Full Example

```bash
git clone https://github.com/Chaste/cppwg.git
```

Install cppwg by doing:

```bash
cd cppwg
pip install .
```

To generate the full `pyshapes` wrapper, do:
To generate Pybind11 wrappers for all the C++ code in `examples/shapes`:

```bash
cd examples/shapes
cppwg src/ \
--wrapper_root wrapper/ \
--package_info wrapper/package_info.yaml \
--includes src/geometry/ src/math_funcs/ src/primitives/ \
--std c++17
--includes src/geometry/ src/math_funcs/ src/mesh/ src/primitives
```

To build the example package do:
To build the example `pyshapes` package:

```bash
mkdir build
Expand All @@ -113,16 +112,9 @@ cmake ..
make
```

To test the resulting package do:

```bash
python test_functions.py
python test_classes.py
```

### Starting a New Project
## Starting a New Project
* Make a wrapper directory in your source tree e.g. `mkdir wrappers`
* Copy the template in `examples/shapes/wrapper/generate.py` to the wrapper directory and fill it in.
* Copy the template in `examples/shapes/wrapper/package_info.yaml` to the wrapper directory and fill it in.
* Do `python wrappers/generate.py` to generate the PyBind11 wrapper code in the wrapper directory.
* Copy the template in `examples/shapes/wrapper/generate.py` to the wrapper directory and fill it in as appropriate.
* Copy the template in `examples/shapes/wrapper/package_info.yaml` to the wrapper directory and fill it in as appropriate.
* Run `cppwg` with appropriate arguments to generate the PyBind11 wrapper code in the wrapper directory.
* Follow the [PyBind11 guide](https://pybind11.readthedocs.io/en/stable/compiling.html) for building with CMake, using `examples/shapes/CMakeLists.txt` as an initial guide.
130 changes: 81 additions & 49 deletions cppwg/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
from pathlib import Path
from typing import List, Optional

import pygccxml.utils
from pygccxml import __version__ as pygccxml_version
import pygccxml

from cppwg.input.class_info import CppClassInfo
from cppwg.input.free_function_info import CppFreeFunctionInfo
Expand Down Expand Up @@ -95,7 +94,7 @@ def __init__(
r"castxml version \d+\.\d+\.\d+", castxml_version
).group(0)
logger.info(castxml_version)
logger.info(f"pygccxml version {pygccxml_version}")
logger.info(f"pygccxml version {pygccxml.version}")

# Sanitize castxml_cflags
self.castxml_cflags: str = ""
Expand Down Expand Up @@ -203,6 +202,7 @@ def extract_templates_from_source(self) -> None:
info_helper = CppInfoHelper(module_info)
for class_info in module_info.class_info_collection:
info_helper.extract_templates_from_source(class_info)
class_info.update_names()

def map_classes_to_hpp_files(self) -> None:
"""
Expand Down Expand Up @@ -247,69 +247,96 @@ def parse_package_info(self) -> None:
# If no package info file exists, create a PackageInfo object with default settings
self.package_info = PackageInfo("cppwg_package", self.source_root)

def update_class_info(self) -> None:
def add_discovered_classes(self) -> None:
"""
Add decls to class info objects.
Add discovered classes.

Update the class info with class declarations parsed by pygccxml from
the C++ source code.
Add class info objects for classes discovered by pygccxml from
parsing the C++ source code. This is run for modules which set
`use_all_classes` to True. No class info objects were created for
those modules while parsing the package info yaml file.
"""
for module_info in self.package_info.module_info_collection:
if module_info.use_all_classes:
# Create class info objects for all class declarations found
# from parsing the source code with pygccxml.
# Note: as module_info.use_all_classes == True, no class info
# objects were created while parsing the package info yaml file.
class_decls = self.source_ns.classes(allow_empty=True)

for class_decl in class_decls:
if module_info.is_decl_in_source_path(class_decl):
class_info = CppClassInfo(class_decl.name)
class_info.update_names()
class_info.module_info = module_info
class_info.decl = class_decl
module_info.class_info_collection.append(class_info)

else:
# As module_info.use_all_classes == False, class info objects
# have already been created while parsing the package info file.
# We only need to add the decl from pygccxml's output.
for class_info in module_info.class_info_collection:
class_decls = self.source_ns.classes(
class_info.name, allow_empty=True
)
if len(class_decls) == 1:
class_info.decl = class_decls[0]
def add_class_decls(self) -> None:
"""
Add declarations to class info objects.

def update_free_function_info(self) -> None:
Update all class info objects with their corresponding
declarations found by pygccxml in the C++ source code.
"""
Add decls to free function info objects.
for module_info in self.package_info.module_info_collection:
for class_info in module_info.class_info_collection:
class_info.decls: List["class_t"] = [] # noqa: F821

for full_name in class_info.full_names:
decl_name = full_name.replace(" ", "") # e.g. Foo<2,2>

try:
class_decl = self.source_ns.class_(decl_name)

except pygccxml.declarations.runtime_errors.declaration_not_found_t:
if "=" in class_info.template_signature:
# Try to find the class without default template args
# e.g. for template <int A, int B=A> class Foo {};
# convert Foo<2,2> -> Foo<2 >
pos = 0
for i, s in enumerate(
class_info.template_signature.split(",")
):
if "=" in s:
pos = i
break

decl_name = ",".join(decl_name.split(",")[0:pos]) + " >"
class_decl = self.source_ns.class_(decl_name)

else:
logging.error(
f"Could not find class declaration for {decl_name}"
)

class_info.decls.append(class_decl)

def add_discovered_free_functions(self) -> None:
"""
Add discovered free function.

Update the free function info with declarations parsed by pygccxml from
the C++ source code.
Add free function info objects discovered by pygccxml from
parsing the C++ source code. This is run for modules which set
`use_all_free_functions` to True. No free function info objects were
created for those modules while parsing the package info yaml file.
"""
for module_info in self.package_info.module_info_collection:
if module_info.use_all_free_functions:
# Create free function info objects for all free function
# declarations found from parsing the source code with pygccxml.
# Note: as module_info.use_all_free_functions == True, no class info
# objects were created while parsing the package info yaml file.
free_functions = self.source_ns.free_functions(allow_empty=True)

for free_function in free_functions:
if module_info.is_decl_in_source_path(free_function):
function_info = CppFreeFunctionInfo(free_function.name)
function_info.module_info = module_info
function_info.decl = free_function
module_info.free_function_info_collection.append(function_info)
ff_info = CppFreeFunctionInfo(free_function.name)
ff_info.module_info = module_info
module_info.free_function_info_collection.append(ff_info)

else:
# As module_info.use_all_free_functions == False, free function
# info objects have already been created while parsing the
# package info file. We only need to add the decl from pygccxml's output.
for free_function_info in module_info.free_function_info_collection:
free_functions = self.source_ns.free_functions(
free_function_info.name, allow_empty=True
)
if len(free_functions) == 1:
free_function_info.decl = free_functions[0]
def add_free_function_decls(self) -> None:
"""
Add declarations to free function info objects.

Update all free function info objects with their corresponding
declarations found by pygccxml in the C++ source code.
"""
for module_info in self.package_info.module_info_collection:
for ff_info in module_info.free_function_info_collection:
decls = self.source_ns.free_functions(ff_info.name, allow_empty=True)
ff_info.decls = [decls[0]]

def write_header_collection(self) -> None:
"""Write the header collection to file."""
Expand All @@ -324,7 +351,6 @@ def write_wrappers(self) -> None:
"""Write all the wrappers required for the package."""
for module_info in self.package_info.module_info_collection:
module_writer = CppModuleWrapperWriter(
self.source_ns,
module_info,
wrapper_templates.template_collection,
self.wrapper_root,
Expand All @@ -351,11 +377,17 @@ def generate_wrapper(self) -> None:
# Parse the headers with pygccxml and castxml
self.parse_header_collection()

# Update the Class Info from the parsed code
self.update_class_info()
# Add discovered classes from the parsed code
self.add_discovered_classes()

# Add declarations to class info objects
self.add_class_decls()

# Add discovered free functions from the parsed code
self.add_discovered_free_functions()

# Update the Free Function Info from the parsed code
self.update_free_function_info()
# Add declarations to free function info objects
self.add_free_function_decls()

# Write all the wrappers required
self.write_wrappers()
Loading