├── .gitignore ├── LICENSE ├── README.md ├── pyproject.toml └── src └── easy_pybind ├── __init__.py ├── file_contents.py └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | venv/* 2 | *.so 3 | **/build/* 4 | **/*egg-info* 5 | **/*pytest_cache/ 6 | **/__pycache__/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hieu Pham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Easy PyBind11 2 | 3 | A simple package to generate the boilerplate research code needed to 4 | build a Python/C++ project with PyBind11. 5 | 6 | ## Installation 7 | 8 | From source: 9 | ```bash 10 | $ git clone https://github.com/hyhieu/easy_pybind.git 11 | $ cd easy_pybind 12 | $ pip install . 13 | ``` 14 | 15 | From PyPI: 16 | ```bash 17 | $ pip install easy_pybind 18 | ``` 19 | 20 | ## Usage 21 | 22 | The intended usage for `easy_pybind` is to generate a new project: 23 | ```plaintext 24 | $ easy-pybind create \ 25 | --module-name="cpp_example" \ 26 | [--cuda] [--with-gitignore] [--with-pytest] [--with-pymain] 27 | ``` 28 | 29 | This will create a folder called `cpp_example` with the following files: 30 | ```bash 31 | cpp_example/ 32 | ├─ .gitignore # if you have --with-ignore 33 | ├─ build.sh # script to build the module 34 | ├─ clean.sh # script to clean up the build 35 | ├─ src/ 36 | │ ├─ cpp_example.cc # entry file to the module 37 | │ ├─ cpp_example_impl.cc # main implementation of the module, will 38 | │ │ # be cpp_example_impl.cu if --cuda is given 39 | │ └─ cpp_example_impl.h 40 | ├─ cpp_example_test.py # if you have --with-pytest 41 | └─ main.py # if you have --with-pymain 42 | ``` 43 | 44 | For further usage options, please refer to `easy-pybind --help`. 45 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "easy-pybind" 3 | version = "1.0.0" 4 | description = "A simple command line tool to generate PyBind examples." 5 | authors = [ 6 | { name = "Hieu Pham", email = "hyhieu@gmail.com" } 7 | ] 8 | dependencies = [] 9 | 10 | [build-system] 11 | requires = ["setuptools>=42", "wheel", "pybind11>=2.11"] 12 | build-backend = "setuptools.build_meta" 13 | 14 | [tool.setuptools.packages.find] 15 | where = ["src"] 16 | 17 | [project.scripts] 18 | easy-pybind = "easy_pybind.main:main" 19 | -------------------------------------------------------------------------------- /src/easy_pybind/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyhieu/easy_pybind/45b7bd0491aabf35aaca525f191ce904c024d550/src/easy_pybind/__init__.py -------------------------------------------------------------------------------- /src/easy_pybind/file_contents.py: -------------------------------------------------------------------------------- 1 | """Content of the files to be generated.""" 2 | 3 | PROJECT_CC = r"""#include "pybind11/pybind11.h" 4 | 5 | #include "{module_name}_impl.h" 6 | 7 | PYBIND11_MODULE({module_name}, m) {{ 8 | m.doc() = "pybind11 {module_name} plugin"; // module docstring 9 | 10 | m.def("add", &add, "An example function to add two numbers."); 11 | }} 12 | """ 13 | 14 | 15 | IMPL_H = r"""#pragma once 16 | 17 | template 18 | T add(T a, T b); 19 | 20 | template<> int add(int, int); 21 | """ 22 | 23 | 24 | IMPL_CC = r"""#include "{module_name}_impl.h" 25 | 26 | template 27 | T add(T a, T b) {{ return a + b; }} 28 | 29 | template<> int add(int a, int b) {{ 30 | return a + b; 31 | }} 32 | """ 33 | 34 | 35 | BUILD_SH = r"""#!/bin/bash 36 | 37 | MODULE_NAME="{module_name}" 38 | 39 | PYBIND11_INCLUDES=$(python3 -m pybind11 --includes) 40 | 41 | # If the system is MacOS, add an extra flag to the final compilation command. 42 | if [[ "$(uname)" == "Darwin" ]]; then 43 | undefined_dynamic_lookup="-undefined dynamic_lookup" 44 | else 45 | undefined_dynamic_lookup="" 46 | fi 47 | 48 | g++ \ 49 | -O3 \ 50 | -Wall \ 51 | -shared \ 52 | --std=c++17 \ 53 | -fPIC \ 54 | ${{undefined_dynamic_lookup}} \ 55 | ${{PYBIND11_INCLUDES}} \ 56 | src/${{MODULE_NAME}}.cc \ 57 | src/${{MODULE_NAME}}_impl.cc \ 58 | -o ${{MODULE_NAME}}.so 59 | """ 60 | 61 | 62 | BUILD_SH_CU = r"""#!/bin/bash 63 | 64 | MODULE_NAME="{module_name}" 65 | 66 | PYBIND11_INCLUDES=$(python3 -m pybind11 --includes) 67 | 68 | # If the system is MacOS, add an extra flag to the final compilation command. 69 | if [[ "$(uname)" == "Darwin" ]]; then 70 | undefined_dynamic_lookup="-undefined dynamic_lookup" 71 | else 72 | undefined_dynamic_lookup="" 73 | fi 74 | 75 | # Compile the CUDA source file to an object file 76 | nvcc -c src/${{MODULE_NAME}}_impl.cu \ 77 | -std=c++17 \ 78 | -O3 \ 79 | -lineinfo \ 80 | -I/usr/local/cuda/include \ 81 | -o ${{MODULE_NAME}}_impl.o 82 | 83 | # Compile the C++ source file to an object file 84 | g++ -c -std=c++17 -O3 -fPIC ${{PYBIND11_INCLUDES}} \ 85 | src/${{MODULE_NAME}}.cc -o ${{MODULE_NAME}}.o 86 | 87 | # Link the object files into a shared library 88 | g++ ${{MODULE_NAME}}.o ${{MODULE_NAME}}_impl.o \ 89 | -o ${{MODULE_NAME}}.so \ 90 | -shared \ 91 | -std=c++17 \ 92 | -O3 \ 93 | -L/usr/local/cuda/lib64 \ 94 | -lcuda \ 95 | -lcudart \ 96 | ${{undefined_dynamic_lookup}} \ 97 | ${{PYBIND11_INCLUDES}} 98 | 99 | # remove intermediate .o files 100 | rm -rf ${{MODULE_NAME}}.o ${{MODULE_NAME}}_impl.o 101 | """ 102 | 103 | 104 | CLEAN_SH = r"""#!/bin/bash 105 | 106 | rm -rf *.so build/* 107 | """ 108 | 109 | 110 | GITIGNORE = r"""# Do not add SO files to GIT. 111 | *.so 112 | 113 | # Do not add build outputs to GIT, if using setuptools. 114 | build/* 115 | """ 116 | 117 | 118 | TEST_PY = r'''"""Tests for {module_name}. 119 | 120 | Usage: pytest {module_name}_test.py 121 | """ 122 | 123 | import sys 124 | from pathlib import Path 125 | 126 | 127 | # Add the directory where {module_name}.so is located to sys.path 128 | sys.path.insert(0, Path(__file__).absolute().parent) 129 | 130 | 131 | def test_add(): 132 | import {module_name} 133 | assert {module_name}.add(1, 2) == 3 134 | ''' 135 | 136 | 137 | MAIN_PY = r'''"""Entry point to run {module_name}.""" 138 | import {module_name} 139 | 140 | 141 | def main(): 142 | output = {module_name}.add(1, 2) 143 | print(f"{module_name}.add(1, 2) = {{output}}.") 144 | 145 | 146 | if __name__ == "__main__": 147 | main() 148 | ''' 149 | 150 | 151 | def build_sh(module_name: str, use_cuda: bool = False): 152 | if use_cuda: 153 | return BUILD_SH_CU.format(module_name=module_name) 154 | else: 155 | return BUILD_SH.format(module_name=module_name) 156 | 157 | def clean_sh(module_name: str): 158 | return CLEAN_SH.format(module_name=module_name) 159 | 160 | def project_cc(module_name: str): 161 | return PROJECT_CC.format(module_name=module_name) 162 | 163 | def impl_h(module_name: str): 164 | return IMPL_H.format(module_name=module_name) 165 | 166 | def impl_cc(module_name: str): 167 | return IMPL_CC.format(module_name=module_name) 168 | 169 | def gitignore(): 170 | return GITIGNORE 171 | 172 | def test_py(module_name: str): 173 | return TEST_PY.format(module_name=module_name) 174 | 175 | def main_py(module_name: str): 176 | return MAIN_PY.format(module_name=module_name) 177 | -------------------------------------------------------------------------------- /src/easy_pybind/main.py: -------------------------------------------------------------------------------- 1 | """Entry point to generate PyBind11 bindings. 2 | 3 | python3 main.py --module-name example 4 | """ 5 | 6 | import argparse 7 | from pathlib import Path 8 | 9 | from . import file_contents as fc 10 | 11 | 12 | def _parse_args() -> argparse.Namespace: 13 | class _CustomHelpFormatter(argparse.HelpFormatter): 14 | def _format_action_invocation(self, action): 15 | if action.option_strings == ["--with-gitignore"]: 16 | return "--[no-]with-gitignore" 17 | if action.option_strings == ["--with-pytest"]: 18 | return "--[no-]with-pytest" 19 | if action.option_strings == ["--with-pymain"]: 20 | return "--[no-]with-pymain" 21 | if action.option_strings == ["--cuda"]: 22 | return "--[no-]cuda" 23 | return super()._format_action_invocation(action) 24 | 25 | parser = argparse.ArgumentParser( 26 | prog="easy_pybind", formatter_class=_CustomHelpFormatter, 27 | ) 28 | subparsers = parser.add_subparsers(dest="command", title="command") 29 | create_parser = subparsers.add_parser("create", help="Create a pybind project.") 30 | 31 | create_parser.add_argument( 32 | "--module-name", 33 | type=str, 34 | required=True, 35 | help=("Name of the module to generate bindings for. For instance, if the name " 36 | "is `cpp_example`, then a folder called `cpp_example` will be created. " 37 | "Later, when the module is built, you can `import cpp_example` from " 38 | "to use the module."), 39 | ) 40 | 41 | create_parser.add_argument( 42 | "--module-path", 43 | type=Path, 44 | default=Path("."), 45 | required=False, 46 | help="Where the module should be. Default to the current directory.", 47 | ) 48 | 49 | # --with-gitignore 50 | group = create_parser.add_mutually_exclusive_group(required=False) 51 | group.add_argument( 52 | "--with-gitignore", 53 | dest="with_gitignore", 54 | action="store_true", 55 | help=("If given, will add a `.gitignore` file to the module directory to " 56 | "avoid committing build outputs into GIT."), 57 | ) 58 | group.add_argument( 59 | "--no-with-gitignore", 60 | dest="with_gitignore", 61 | action="store_false", 62 | help=argparse.SUPPRESS) 63 | create_parser.set_defaults(with_gitignore=True) 64 | 65 | # --with-pytest 66 | group = create_parser.add_mutually_exclusive_group(required=False) 67 | group.add_argument( 68 | "--with-pytest", 69 | dest="with_pytest", 70 | action="store_true", 71 | help=("If given, will generate a pytest to smoke test the module.") 72 | ) 73 | group.add_argument( 74 | "--no-with-pytest", 75 | dest="with_pytest", 76 | action="store_false", 77 | help=argparse.SUPPRESS) 78 | create_parser.set_defaults(with_pytest=False) 79 | 80 | # --with-pymain 81 | group = create_parser.add_mutually_exclusive_group(required=False) 82 | group.add_argument( 83 | "--with-pymain", 84 | dest="with_pymain", 85 | action="store_true", 86 | help=("If given, will generate a main.py to import and run the module.") 87 | ) 88 | group.add_argument( 89 | "--no-with-pymain", 90 | dest="with_pymain", 91 | action="store_false", 92 | help=argparse.SUPPRESS) 93 | create_parser.set_defaults(with_pymain=False) 94 | 95 | # --cuda 96 | group = create_parser.add_mutually_exclusive_group(required=False) 97 | group.add_argument( 98 | "--cuda", 99 | dest="cuda", 100 | action="store_true", 101 | help=("If given, will generate a CUDA project instead of a CC project.") 102 | ) 103 | group.add_argument( 104 | "--no-cuda", 105 | dest="cuda", 106 | action="store_false", 107 | help=argparse.SUPPRESS) 108 | create_parser.set_defaults(cuda=False) 109 | 110 | args = parser.parse_args() 111 | return args 112 | 113 | 114 | def create(args: argparse.Namespace): 115 | module_name = args.module_name 116 | module_path = args.module_path / module_name 117 | 118 | print(f"Generating PyBind11 bindings for project {module_name}...", flush=True) 119 | module_path.mkdir(parents=True, exist_ok=True) 120 | 121 | (module_path / "build.sh").write_text(fc.build_sh(module_name, use_cuda=args.cuda)) 122 | (module_path / "build.sh").chmod(0o755) 123 | 124 | (module_path / "clean.sh").write_text(fc.clean_sh(module_name)) 125 | (module_path / "clean.sh").chmod(0o755) 126 | 127 | src = module_path / "src" 128 | src.mkdir(parents=True, exist_ok=True) 129 | (src / f"{module_name}.cc").write_text(fc.project_cc(module_name)) 130 | (src / f"{module_name}_impl.h").write_text(fc.impl_h(module_name)) 131 | 132 | if args.cuda: 133 | (src / f"{module_name}_impl.cu").write_text(fc.impl_cc(module_name)) 134 | else: 135 | (src / f"{module_name}_impl.cc").write_text(fc.impl_cc(module_name)) 136 | 137 | if args.with_gitignore: 138 | (module_path / ".gitignore").write_text(fc.GITIGNORE) 139 | 140 | if args.with_pytest: 141 | (module_path / f"{module_name}_test.py").write_text(fc.test_py(module_name)) 142 | 143 | if args.with_pymain: 144 | (module_path / "main.py").write_text(fc.main_py(module_name)) 145 | 146 | 147 | def main(): 148 | args = _parse_args() 149 | if args.command == "create": 150 | create(args) 151 | else: 152 | raise ValueError(f"Unkonwn command {args.command}.") 153 | 154 | 155 | if __name__ == "__main__": 156 | main() 157 | --------------------------------------------------------------------------------