Skip to content
Open
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
83 changes: 83 additions & 0 deletions .github/workflows/serialization.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: Central serialization runtime

on: [pull_request]

jobs:
runtime:
name: Serialization (${{ matrix.os }}, ${{ matrix.linkage }} Boost)
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
linkage: [static, shared]
defaults:
run:
shell: bash -el {0}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v5
with:
repository: microsoft/vcpkg
ref: ef7dbf94b9198bc58f45951adcf1f041fcbc5ea0
path: _vcpkg
- uses: ilammy/msvc-dev-cmd@v1
if: runner.os == 'Windows'
- uses: conda-incubator/setup-miniconda@v3
with:
activate-environment: py312
python-version: '3.12'
auto-activate-base: false
- name: Python and build tools
run: python -m pip install -e . pytest cmake ninja
- name: Bootstrap vcpkg (Unix)
if: runner.os != 'Windows'
run: ./_vcpkg/bootstrap-vcpkg.sh -disableMetrics
- name: Bootstrap vcpkg (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: ./_vcpkg/bootstrap-vcpkg.bat -disableMetrics
- name: Configure Boost triplet
run: |
mkdir -p _triplets
architecture=x64
if [[ "$(uname -m)" == arm64 ]]; then architecture=arm64; fi
linkage=${{ matrix.linkage }}
if [[ "$linkage" == shared ]]; then linkage=dynamic; fi
cat > _triplets/managed.cmake <<EOF
set(VCPKG_TARGET_ARCHITECTURE $architecture)
set(VCPKG_CRT_LINKAGE dynamic)
set(VCPKG_LIBRARY_LINKAGE $linkage)
EOF
if [[ "$RUNNER_OS" == Linux ]]; then
echo 'set(VCPKG_CMAKE_SYSTEM_NAME Linux)' >> _triplets/managed.cmake
echo 'set(VCPKG_C_FLAGS "-fPIC")' >> _triplets/managed.cmake
echo 'set(VCPKG_CXX_FLAGS "-fPIC")' >> _triplets/managed.cmake
elif [[ "$RUNNER_OS" == macOS ]]; then
echo 'set(VCPKG_CMAKE_SYSTEM_NAME Darwin)' >> _triplets/managed.cmake
fi
- uses: actions/cache/restore@v4
id: boost-cache
with:
path: _vcpkg/installed
key: serialization-v1-${{ runner.os }}-${{ runner.arch }}-${{ matrix.linkage }}-ef7dbf94
- name: Install Boost.Serialization
run: ./_vcpkg/vcpkg install boost-serialization:managed --overlay-triplets=_triplets
- uses: actions/cache/save@v4
if: steps.boost-cache.outputs.cache-hit != 'true'
with:
path: _vcpkg/installed
key: serialization-v1-${{ runner.os }}-${{ runner.arch }}-${{ matrix.linkage }}-ef7dbf94
- name: Run required runtime tests
env:
GTWRAP_TEST_SERIALIZATION: '1'
GTWRAP_TEST_BOOST_LINKAGE: ${{ matrix.linkage }}
VCPKG_TARGET_TRIPLET: managed
CMAKE_GENERATOR: Ninja
run: |
# Build against the same native C++ runtime as the vcpkg libraries.
unset CC CXX CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
export CMAKE_TOOLCHAIN_FILE="$PWD/_vcpkg/scripts/buildsystems/vcpkg.cmake"
export VCPKG_OVERLAY_TRIPLETS="$PWD/_triplets"
cmake -S . -B build -DWRAP_PYTHON_VERSION=3.12
cmake --build build --target testSerialization.run --parallel 6
14 changes: 13 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ endif()
# Install wrapping scripts as binaries to `CMAKE_INSTALL_PREFIX/bin` so they can
# be invoked for wrapping. We use DESTINATION (instead of TYPE) so we can
# support older CMake versions.
install(PROGRAMS scripts/pybind_wrap.py scripts/matlab_wrap.py
install(PROGRAMS scripts/pybind_wrap.py scripts/pybind_serialization.py
scripts/matlab_wrap.py
DESTINATION "${GTWRAP_BIN_INSTALL_DIR}")

# Install the matlab.h file to `CMAKE_INSTALL_PREFIX/lib/gtwrap/matlab.h`.
Expand All @@ -89,3 +90,14 @@ message(STATUS "${gtwrap} CMake path : ${GTWRAP_CMAKE_INSTALL_DIR}")
message(STATUS "${gtwrap} library path : ${GTWRAP_LIB_INSTALL_DIR}")
message(STATUS "${gtwrap} binary path : ${GTWRAP_BIN_INSTALL_DIR}")
message(STATUS "${gtwrap} header path : ${GTWRAP_INCLUDE_INSTALL_DIR}")

# Explicit test targets work with the caller's configured Python environment.
add_custom_target(testWrap.run
COMMAND "${PYTHON_EXECUTABLE}" -m pytest tests -q -o "addopts="
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" USES_TERMINAL)
add_custom_target(testSerialization.run
COMMAND "${PYTHON_EXECUTABLE}" -m pytest
tests/test_serialization_discovery.py tests/test_serialization_library.py
tests/test_serialization_bindings.py tests/test_serialization_runtime.py
-q -o "addopts="
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" USES_TERMINAL)
144 changes: 144 additions & 0 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,150 @@ The python wrapper supports keyword arguments for functions/methods. Hence, the
- Unfortunately, this means that aliases can no longer be used.
- Similarly, there can be multiple `preamble.h` and `specializations.h` files. Each of these should match the module file name.

### Centrally managed Python serialization

`pybind_add_serialization_library` creates an opt-in shared library that owns
Boost export registrations **and** the text-archive save/load operations used
by Python bindings. Multiple extensions link that same library. Archive
construction stays in the owner, including when Boost.Serialization itself is
static; consumers do not need their own copy of its registration tables.
Existing wrapping is unchanged unless a serialization library is selected.

The API is independent of any particular C++ project. Supply interface files,
headers defining their types, and the domain-library targets they require:

```cmake
# Use find_package(gtwrap CONFIG REQUIRED) for an installed wrap package,
# or include(PybindWrap) with wrap/cmake on CMAKE_MODULE_PATH for a source tree.
find_package(Boost REQUIRED COMPONENTS serialization)

pybind_add_serialization_library(shared_archives
INTERFACES common.i module_a.i module_b.i
HEADERS example/types.h
LINK_LIBRARIES example_types)

pybind_wrap(module_a "${CMAKE_CURRENT_SOURCE_DIR}/module_a.i"
"module_a.cpp" "module_a" "example" ""
"${CMAKE_CURRENT_SOURCE_DIR}/module.tpl" "example_types" "" ON
SERIALIZATION_LIBRARY shared_archives)
pybind_wrap(module_b "${CMAKE_CURRENT_SOURCE_DIR}/module_b.i"
"module_b.cpp" "module_b" "example" ""
"${CMAKE_CURRENT_SOURCE_DIR}/module.tpl" "example_types" "" ON
SERIALIZATION_LIBRARY shared_archives)
```

`example_types` supplies the C++ headers' include paths, compile definitions,
and domain-library dependencies. The usual wrapping template and preamble
requirements still apply. The source-tree module resolves the generator script
paths itself; no extra script-variable assignment is necessary.

The library helper accepts these keyword arguments:

| Argument | Meaning |
| --- | --- |
| `INTERFACES` | Required list of interfaces containing the serializable types. Relative paths resolve against the current source directory. |
| `HEADERS` | Required C++ header include names defining those types. |
| `LINK_LIBRARIES` | Domain targets and their usage requirements; these must not depend on the consuming Python extensions. |
| `NAMESPACE_PREFIXES` | Optional list of namespace prefixes ending in `::`; absent means all serializable declarations. |
| `IGNORE_CLASSES` | Optional fully qualified instantiated C++ types to exclude. |
| `BOOST_SERIALIZATION_TARGET` | Existing static or shared Boost.Serialization target; defaults to `Boost::serialization`. |

Selection uses the existing `serialize`/`serializable` markers and
`@serializable` template typedefs after instantiation. Duplicate types across
interfaces are coalesced. Empty selections and conflicting archive identifiers
are errors. Export names retain the ordinary wrapper's existing spelling,
including its comma-safe template aliases.

Supply the complete serialization domain: both the types whose Python
save/load methods use the owner and every concrete polymorphic type reachable
through their serialized members. Wrap cannot discover arbitrary C++ runtime
subclasses or inspect the bodies of C++ serialization methods. A family-only
registration list is insufficient if the containing object's archive is still
created in an extension with its own static Boost runtime. Use one owner for
all participating boundary types and their polymorphic dependencies.

For selected types, generated `.serialize()`, `.deserialize()`, and pickle
methods call typed functions in the library. Their normal export macros are
suppressed. Unselected types retain ordinary wrapping. Consumers include a
private generated API header through the existing `{includes}` placeholder;
no new template placeholders, manual key headers, implementation fragments, or
special owner translation units are needed. The library's registration headers
remain private to its implementation.

This mode preserves the existing text-archive protocol and one-element pickle
state. It does not add binary/XML archives or customizable archive adapters.
Boost archive exceptions become `std::runtime_error` at the library boundary
and Python `RuntimeError` through pybind11.

The owner is always a `SHARED` target, regardless of `BUILD_SHARED_LIBS`. Its
Boost dependency is private and can be static or shared. Static dependencies
must be suitable for linking into a shared library (including PIC where
required). Do not combine incompatible Boost versions, C++ ABIs, or C++ runtimes;
on Windows, use a consistent runtime such as `/MD` across all participants.
The typed API uses C linkage for explicit symbol exports but passes C++ objects
and strings, so it is not a cross-toolchain C ABI. Only the bridge functions are
supported consumer entry points. ELF and macOS export lists hide the static
Boost implementation; Windows uses explicit exports.

Precompiled static Boost libraries on Windows can carry MSVC `dllexport`
directives that force additional implementation helpers into the owner's DLL
export table. We permit narrowly identified helpers for `codecvt_null<wchar_t>`,
`singleton_module`, and singleton instances for text-archive serializer maps,
extended type-info registries, and void-cast registrations. They are unsupported
implementation details; consumers must not import or call them. Windows resolves
DLL imports through the named provider, and the required integration test checks
that a separate static Boost runtime retains its own registrations before and
after both managed consumers load.

The CI exception applies only to Windows with static Boost and uses the exact
reviewed symbol names in
[`windows_static_boost_exports.txt`](tests/fixtures/serialization_library/windows_static_boost_exports.txt)
for the pinned Boost/MSVC fixture. Unknown exports and missing bridge functions
still fail. Boost or toolchain upgrades that change these names require review;
there is no blanket Boost namespace exemption. Windows shared Boost, Linux,
and macOS retain exact bridge-only export checks.

Call the helper and pass `SERIALIZATION_LIBRARY` only when serialization is
enabled in the consuming project. Pairing the latter argument with a false
serialization flag is a configuration error. MATLAB generation is independent
of this opt-in Python facility.

#### Installation and migration

The caller owns the runtime package layout. Install the shared owner alongside
or in a location reachable from every consuming extension using ordinary
`install(TARGETS ...)`. Configure relative install RPATHs as appropriate (for
example `$ORIGIN` on ELF or `@loader_path` on macOS), or colocate the DLL with
Windows extensions. Package a shared Boost runtime according to the project's
existing dependency policy. Wrap does not choose installation directories or
rewrite third-party library install names. The generated manifest and headers
are build inputs; Python users do not need them at runtime. Normal linking
keeps the owner loaded while its extension consumers are loaded.

To migrate existing bindings, identify the complete domain, create the owner,
and attach it to each consumer. Remove handwritten keys and export
implementations for the managed types, including any special-case header
exclusions. Keep existing serialization feature guards. Validate stable-only
use, both extension import orders, calls after both are loaded, legacy archive
exchange, and serialization-disabled builds before deploying the migration.

The generic integration example in `tests/fixtures/serialization_library`
builds two independent Python consumers and a legacy archive producer. It also
checks installed source-tree/package consumption and coexistence with an
unrelated static Boost runtime. Run it using the configured Python environment:

```sh
cmake -S . -B build
cd build
GTWRAP_TEST_SERIALIZATION=1 make -j6 testSerialization.run
```

Set `GTWRAP_TEST_BOOST_LINKAGE=static` or `shared` to choose one linkage; by
default both are tested. Supply `CMAKE_PREFIX_PATH`, or a toolchain and
`VCPKG_TARGET_TRIPLET`, to select the intended Boost installation. The dedicated
CI matrix provisions Boost and treats missing dependencies as errors. The
ordinary lightweight suite skips this integration test unless explicitly enabled.

## Pybind Callable Adapters

The Python generator uses explicit full-signature C++ callable-pointer casts for
Expand Down
Loading
Loading