Code annotations

The following examples demonstrate the use of various code annotations supported by clair/c2py to customize the generated Python bindings.

All examples have been compiled with CMake using the instructions from Compiling the examples.

C2PY_IGNORE

The C2PY_IGNORE annotation can be used to exclude specific functions or classes from being exposed in the Python bindings.

// c2py_ignore.cpp

#include "c2py/c2py.hpp"

// Some function we don't want to wrap.
C2PY_IGNORE int f(int x) { return x * 2; }

// Another function we want to wrap.
int g(int y) { return y * 3; }

After generating the extension module, we can use it in Python:

>>> from c2py_ignore import *
>>> g(2)
6
>>> f(2)
Traceback (most recent call last):
  File "<python-input-2>", line 1, in <module>
    f(2)
    ^
NameError: name 'f' is not defined

As expected, only the function g is available in Python, while f has been ignored.

C2PY_RENAME

The C2PY_RENAME annotation can be used to give a wrapped function or class a different name in the Python bindings.

// c2py_rename.cpp

#include "c2py/c2py.hpp"

// We want to rename f to g in the Python bindings.
C2PY_RENAME(g) int f(int x) { return x * 2; }

After generating the extension module, we can use it in Python:

>>> from c2py_rename import *
>>> g(5)
10
>>> f(5)
Traceback (most recent call last):
File "<python-input-2>", line 1, in <module>
    f(5)
    ^
NameError: name 'f' is not defined

The function f has been rename to g as instructed.

C2PY_MODULE_INIT

The C2PY_MODULE_INIT annotation lets us define a function that will be called when the module is initialized/imported.

// c2py_module_init.cpp

#include "c2py/c2py.hpp"
#include <iostream>

// This function should be called when the module is initialized (imported) in Python.
C2PY_MODULE_INIT void init() { std::cout << "c2py/clair rocks!" << std::endl; }

After generating the extension module, we can use it in Python:

>>> import c2py_module_init
c2py/clair rocks!

Importing the module triggers the execution of the init function, which prints a message to the console.

C2PY_WRAP_AS_METHOD

The C2PY_WRAP_AS_METHOD annotation lets us add a function as a method to the first argument’s class object.

// c2py_wrap_as_method.cpp

#include "c2py/c2py.hpp"

// Some class we want to wrap.
struct myclass {
  int x{};
};

// We want to add this function as a method to myclass in the Python bindings.
C2PY_WRAP_AS_METHOD int f(const myclass &m, int y) { return m.x * y; }

After generating the extension module, we can use it in Python:

>>> from c2py_wrap_as_method import *
>>> m = Myclass(x = 100)
>>> m.f(5)
500
>>> f(5)
Traceback (most recent call last):
File "<python-input-5>", line 1, in <module>
    f(5)
    ^
NameError: name 'f' is not defined

The free C++ function f has been added as a method to the Myclass class in Python.

Compiling the examples

We use the following CMake file:

cmake_minimum_required(VERSION 3.20 FATAL_ERROR)
project(code-annotations VERSION 3.2.0 LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
include(FetchContent)

# Locate Python and NumPy components
find_package(Python COMPONENTS Interpreter Development NumPy)

# Fetch the c2py library
FetchContent_Declare(
  c2py
  GIT_REPOSITORY https://github.com/flatironinstitute/c2py
  GIT_TAG        unstable
  EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(c2py)

# clair-c2py: optionally regenerate bindings
option(Update_Python_Bindings "Use clair python bindings generators" OFF)
if(Update_Python_Bindings)
  find_program(clair-c2py clair-c2py REQUIRED)
endif()

# Add an interface target for all Python modules
add_library(${PROJECT_NAME}_python_modules INTERFACE)

# Build and install any python modules
set(module_sources c2py_ignore.cpp c2py_rename.cpp c2py_module_init.cpp c2py_wrap_as_method.cpp)
foreach(module_src ${module_sources})
  get_filename_component(module_name ${module_src} NAME_WE)
  get_filename_component(module_dir ${module_src} DIRECTORY)

  Python_add_library(${module_name} MODULE ${module_src})
  add_library(${PROJECT_NAME}::${module_name} ALIAS ${module_name})
  target_link_libraries(${module_name} PRIVATE c2py::c2py)
  add_dependencies(${PROJECT_NAME}_python_modules ${module_name})

  if(Update_Python_Bindings)
    set(wrap_cxx "${CMAKE_CURRENT_SOURCE_DIR}/${module_name}.wrap.cxx")
    set(depfile "${CMAKE_CURRENT_BINARY_DIR}/${module_name}.cpp.d")

    add_custom_command(
      OUTPUT ${wrap_cxx} ${depfile}
      COMMAND ${clair-c2py} -p ${PROJECT_BINARY_DIR} --generate-depfile ${depfile} ${module_src}
      WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
      DEPFILE ${depfile}
    )

    add_custom_target(wrap_gen_${module_name} DEPENDS ${wrap_cxx})
    add_dependencies(${module_name} wrap_gen_${module_name})
  endif()
endforeach()

To compile all modules at once, run:

$ mkdir build
$ cd build
$ cmake .. -DUpdate_Python_Bindings=ON
$ make -j 8