Using SWIG with CMake
Contents
Using SWIG with CMake¶
There is a UseSWIG file providing support in CMake for system-installed SWIG, providing new bindings and methods for use in the project.
Table of Contents¶
Building with CMake¶
our aim is to install with pip, so we can manage the module conventiently
As usual, developing with OSX, SWIG 4.0.2, CMake 3.18.4. Consider the example project structure
─ examplesrc
├── CMakeLists.txt
├── examplelib.cpp
└── swig
├── CMakeLists.txt
├── example.i
├── MANIFEST.in
└── setup.py.in
- CMakeLists.txt
- venv/
We use a virtual environment, since on OSX the hardened code security prevents local imports with dlopen
, thus we must install it into the environment (or have some wacky absolute paths).
We start with the root cmake file:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(my_proj)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -O2")
add_subdirectory(examplesrc)
Then, in our source directory we compile our pure C++ code into a statically linked library. In theory you could very well make this shared, but then would have to remember to install both ELFs in the setup.py
.
# examplesrc/CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(example_proj)
message("Standard: ${CMAKE_CXX_STANDARD}")
set(examplelibsrc examplelib.cpp)
add_library(examplelib STATIC ${examplelibsrc})
add_subdirectory(swig)
Then finally, in the swig
directory we write our wrapper script
// examplesrc/swig/example.i
%module example
%{
#define SWIG_FILE_WITH_INIT
#include "examplelib.cpp" // use headers generally; source for this example
%}
// wrapper declarations
We incude from the local path, so we need to remember to tell cmake to include the correct directories. Implicit in this is also the relevant Python headers, which SWIG will need. To find these packages, and thus to compile our library, we need to locate the Python header files. CMake has a directive for us, find_package
, which we also use to discover the SWIG tools.
Note that the package detection will locate the active virtual environment, provided we ran
source venv/bin/activate
before proceeding with the build.
The find_packages
command will set common variables for us, including <package>_FOUND
to check if cmake was able to successfully find the package. I have written more notes on this directive for more details.
# examplesrc/swig/CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(my_proj)
find_package(Python COMPONENTS Development Interpreter)
if (Python_FOUND)
message(${Python_SITELIB}) # print sitepath
else()
message("No Python found.")
return()
endif()
find_package(SWIG)
include(UseSWIG)
if(SWIG_FOUND)
message("Found SWIG ${SWIG_VERSION}")
else()
message("No SWIG found.")
return()
endif()
set(UseSWIG_TARGET_NAME_PREFERENCE STANDARD)
set_property(SOURCE example.i PROPERTY CPLUSPLUS ON)
message("${CMAKE_SOURCE_DIR}/examplesrc")
include_directories("${CMAKE_SOURCE_DIR}/examplesrc" "${Python_INCLUDE_DIRS}")
# convenience variable
set(example_build_dir ${CMAKE_CURRENT_BINARY_DIR}/example)
swig_add_library(example
TYPE SHARED
LANGUAGE python
OUTPUT_DIR ${example_build_dir} # important! else find_packages() wont find
SOURCES example.i
)
set_property(TARGET example PROPERTY SUFFIX ".so")
# and again, so that the .so file is in the right place
set_property(TARGET example PROPERTY LIBRARY_OUTPUT_DIRECTORY ${example_build_dir})
target_link_libraries(example PRIVATE examplelib ${Python_LIBRARIES})
# ----------------------- installation specific ----------------------- #
# copy files
configure_file(setup.py ${CMAKE_CURRENT_BINARY_DIR}/setup.py COPYONLY)
configure_file(MANIFEST.in ${CMAKE_CURRENT_BINARY_DIR}/MANIFEST.in COPYONLY)
# and make it a module
install(CODE "file(COPY ${example_build_dir}/example.py ${example_build_dir}/__init__.py)")
# handle with pip
install(
CODE "execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMAND ${Python_EXECUTABLE} -m pip install .)"
)
More details as to why e.g. target_link_libraries
is used over swig_link_libraries
can be found in the documentation pages; in short, it is related to using the STANDARD
name preference.
We write a minimal setup.py
file:
# examplesrc/swig/setup.py
from setuptools import setup, find_packages
print(find_packages())
if __name__ == "__main__":
setup(
name='example',
version='1.0.0',
packages=find_packages(),
include_package_data=True
)
and so that the binary .so
file is included in the distribution, we also define
# MANIFEST.in
include example/*.so
make install
When we run make
a few things will now happen
first, our library will be compiled as normally.
then, SWIG will generate the wrapper code for python
this wrapper is then compiled and statically linked with our library
During make install
, two additional steps happen, as defined by the install
directives in the above CMakeLists.txt
copy
example.py
to__init__.py
so that the wrapped code is at the root of the module namespace (c.f.example.example.fact
vsexample.fact
).run the envionment specific
pip install
In theory, you only need to cart around the example
directory with a minimal
example
├── __init__.py
├── _example.so
└── example.py
for the module to work, but I have experienced issues with this on OSX, with regard to “hardened code”.
Building with Setuptools¶
Todo. :)