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
1 change: 0 additions & 1 deletion Modules/Bridge/VtkGlue/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ set(_required_vtk_libraries
)
if(ITK_WRAP_PYTHON)
list(APPEND _required_vtk_libraries
VTK::WrappingPythonCore
VTK::CommonCore
VTK::CommonDataModel
VTK::CommonExecutionModel)
Expand Down
1 change: 0 additions & 1 deletion Modules/Bridge/VtkGlue/itk-module-init.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ if(ITK_WRAP_PYTHON)
list(
APPEND
_required_vtk_libraries
VTK::WrappingPythonCore
VTK::CommonCore
VTK::CommonDataModel
VTK::CommonExecutionModel
Expand Down
16 changes: 2 additions & 14 deletions Modules/Bridge/VtkGlue/wrapping/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,15 +1,3 @@
itk_wrap_module(ITKVtkGlue)
if(ITK_USE_PYTHON_LIMITED_API)
message(
FATAL_ERROR
"The ITKVtkGlue module can only built without Python limited API due to VTK limitations."
"Please set `ITK_USE_PYTHON_LIMITED_API` to `FALSE`."
)
else()
list(
APPEND
WRAPPER_SWIG_LIBRARY_FILES
"${CMAKE_CURRENT_SOURCE_DIR}/VtkGlue.i"
)
itk_auto_load_and_end_wrap_submodules()
endif()
list(APPEND WRAPPER_SWIG_LIBRARY_FILES "${CMAKE_CURRENT_SOURCE_DIR}/VtkGlue.i")
itk_auto_load_and_end_wrap_submodules()
137 changes: 117 additions & 20 deletions Modules/Bridge/VtkGlue/wrapping/VtkGlue.i
Original file line number Diff line number Diff line change
Expand Up @@ -51,44 +51,141 @@
%module(package="itk",threads="1") VtkGluePython

%{
#include "vtkPythonUtil.h"
#include "vtkVersion.h"
#if (VTK_MAJOR_VERSION > 5 ||((VTK_MAJOR_VERSION == 5)&&(VTK_MINOR_VERSION > 6)))
Comment thread
hjmjohnson marked this conversation as resolved.
#define vtkPythonGetObjectFromPointer vtkPythonUtil::GetObjectFromPointer
#define vtkPythonGetPointerFromObject vtkPythonUtil::GetPointerFromObject
#endif
#include <cinttypes>
#include <cstdio>
#include <cstring>

// Pointer exchange with VTK's Python layer using only the Limited API, so this
// module stays abi3 and needs no link against VTK::WrappingPythonCore.
namespace itkVtkGlueABI3
{

inline PyObject *
ImportClass(const char * moduleName, const char * className)
{
PyObject * mod = PyImport_ImportModule(moduleName);
if (!mod)
{
return nullptr;
}
PyObject * cls = PyObject_GetAttrString(mod, className);
Py_DECREF(mod);
return cls;
}

// Parses the `_<hex>_p_<ClassName>` encoding VTK publishes as `__this__`. The
// isinstance() gate is what makes trusting that string safe: without it any
// object exposing a forged `__this__` would be cast to a native pointer.
inline void *
GetPointerFromObject(PyObject * obj, const char * moduleName, const char * className)
{
PyObject * cls = ImportClass(moduleName, className);
if (!cls)
{
return nullptr;
}
const int isInstance = PyObject_IsInstance(obj, cls);
Py_DECREF(cls);
if (isInstance < 0)
{
return nullptr;
}
if (isInstance == 0)
{
PyErr_Format(PyExc_TypeError, "expected a VTK %s instance", className);
return nullptr;
}

PyObject * thisStr = PyObject_GetAttrString(obj, "__this__");
if (!thisStr)
{
PyErr_Clear();
PyErr_Format(PyExc_TypeError, "expected a VTK %s instance", className);
return nullptr;
}

void * ptr = nullptr;
Py_ssize_t len = 0;
const char * s = PyUnicode_AsUTF8AndSize(thisStr, &len);
if (s && len > 4 && s[0] == '_' && std::strlen(s) == static_cast<size_t>(len))
{
const char * sep = std::strstr(s + 1, "_p_");
if (sep && std::strcmp(sep + 3, className) == 0)
{
std::uintptr_t addr = 0;
// '_' is not a hex digit, so the conversion stops at the separator.
if (std::sscanf(s + 1, "%" SCNxPTR, &addr) == 1 && addr != 0)
{
ptr = reinterpret_cast<void *>(addr);
}
Comment thread
hjmjohnson marked this conversation as resolved.
}
}
Py_DECREF(thisStr);

if (!ptr)
{
PyErr_Format(PyExc_TypeError, "expected a VTK %s instance", className);
}
return ptr;
}

// Reconstructs through VTK's own `Addr=0x...` path so the IsA() check, the
// object map, and reference counting all stay VTK's responsibility.
// `__new__` is called explicitly rather than `cls(addr)`: vtkmodules.util.data_model
// registers keyword-only `override` subclasses for the data-model classes, whose
// __init__ would reject the positional address string.
inline PyObject *
GetObjectFromPointer(void * ptr, const char * moduleName, const char * className)
{
if (!ptr)
{
Py_RETURN_NONE;
}

PyObject * cls = ImportClass(moduleName, className);
if (!cls)
{
return nullptr;
}

char addr[64];
std::snprintf(addr, sizeof(addr), "Addr=0x%" PRIxPTR, reinterpret_cast<std::uintptr_t>(ptr));
PyObject * obj = PyObject_CallMethod(cls, "__new__", "Os", cls, addr);
Py_DECREF(cls);
return obj;
}

} // namespace itkVtkGlueABI3
%}

%typemap(out) vtkImageExport* {
PyImport_ImportModule("vtk");
$result = vtkPythonGetObjectFromPointer ( (vtkImageExport*)$1 );
$result = itkVtkGlueABI3::GetObjectFromPointer($1, "vtkmodules.vtkIOImage", "vtkImageExport");
if (!$result) { SWIG_fail; }
}

%typemap(out) vtkImageImport* {
PyImport_ImportModule("vtk");
$result = vtkPythonGetObjectFromPointer ( (vtkImageImport*)$1 );
$result = itkVtkGlueABI3::GetObjectFromPointer($1, "vtkmodules.vtkIOImage", "vtkImageImport");
if (!$result) { SWIG_fail; }
}

%typemap(out) vtkImageData* {
PyImport_ImportModule("vtk");
$result = vtkPythonGetObjectFromPointer ( (vtkImageData*)$1 );
$result = itkVtkGlueABI3::GetObjectFromPointer($1, "vtkmodules.vtkCommonDataModel", "vtkImageData");
if (!$result) { SWIG_fail; }
}

%typemap(in) vtkImageData* {
$1 = NULL;
$1 = (vtkImageData*) vtkPythonGetPointerFromObject ( $input, "vtkImageData" );
if ( $1 == NULL ) { SWIG_fail; }
$1 = static_cast<vtkImageData *>(itkVtkGlueABI3::GetPointerFromObject($input, "vtkmodules.vtkCommonDataModel", "vtkImageData"));
if (!$1) { SWIG_fail; }
}

%typemap(out) vtkPolyData* {
PyImport_ImportModule("vtk");
$result = vtkPythonGetObjectFromPointer ( (vtkPolyData*)$1 );
$result = itkVtkGlueABI3::GetObjectFromPointer($1, "vtkmodules.vtkCommonDataModel", "vtkPolyData");
if (!$result) { SWIG_fail; }
}

%typemap(in) vtkPolyData* {
$1 = NULL;
$1 = (vtkPolyData*) vtkPythonGetPointerFromObject ( $input, "vtkPolyData" );
if ( $1 == NULL ) { SWIG_fail; }
$1 = static_cast<vtkPolyData *>(itkVtkGlueABI3::GetPointerFromObject($input, "vtkmodules.vtkCommonDataModel", "vtkPolyData"));
if (!$1) { SWIG_fail; }
}
#endif

Expand Down
32 changes: 32 additions & 0 deletions Modules/Bridge/VtkGlue/wrapping/test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
list(FIND ITK_WRAP_IMAGE_DIMS 2 wrap_2_index)
if(
ITK_WRAP_PYTHON
AND
VTK_WRAP_PYTHON
AND
ITK_WRAP_float
AND
wrap_2_index
GREATER
-1
)
itk_python_add_test(
NAME PythonVtkGlueABI3EncodingTest
COMMAND
${CMAKE_CURRENT_SOURCE_DIR}/VtkGlueABI3EncodingTest.py
)
itk_python_add_test(
NAME PythonVtkGlueRoundTripTest
COMMAND
${CMAKE_CURRENT_SOURCE_DIR}/VtkGlueRoundTripTest.py
)
# itkTestDriver prepends ITK's own entries to whatever PYTHONPATH it inherits.
set_property(
TEST
PythonVtkGlueABI3EncodingTest
PythonVtkGlueRoundTripTest
PROPERTY
ENVIRONMENT
"PYTHONPATH=${VTK_PREFIX_PATH}/${VTK_PYTHONPATH}"
)
endif()
100 changes: 100 additions & 0 deletions Modules/Bridge/VtkGlue/wrapping/test/VtkGlueABI3EncodingTest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# ==========================================================================
#
# Copyright NumFOCUS
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0.txt
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# ==========================================================================

"""Canary for the two VTK wrapper encodings the ITKVtkGlue abi3 typemaps rely on.

VtkGlue.i exchanges pointers with VTK through `__this__` and the `Addr=0x...`
argument to `__new__` rather than through vtkPythonUtil, because vtkPythonUtil's
header chain is not usable under Py_LIMITED_API. Neither encoding is documented
VTK API, so this test fails loudly and specifically if VTK changes either one.

`__new__` is used rather than plain construction because vtkmodules.util.data_model
registers keyword-only `override` subclasses for vtkImageData and vtkPolyData;
`cls(addr)` reaches those and raises TypeError.

Both encodings and the `__new__` string branch are present in every VTK 9.x from
9.0.0 onward, so the module's VTK 9.1 floor is unchanged. The branch is gated on
the type not being a heap type, which is the thing to re-check if VTK ever makes
its wrapped types heap types.
"""

import re
import sys

from vtkmodules.vtkCommonDataModel import vtkImageData, vtkPolyData
from vtkmodules.vtkIOImage import vtkImageExport, vtkImageImport

# `_<2*sizeof(void*) hex digits>_p_<ClassName>`, per vtkPythonUtil::ManglePointer.
THIS_RE = re.compile(r"^_([0-9a-fA-F]+)_p_(\w+)$")

failures = []


def check(condition, message):
if not condition:
failures.append(message)


for cls in (vtkImageData, vtkPolyData, vtkImageExport, vtkImageImport):
name = cls.__name__
obj = cls()

this = getattr(obj, "__this__", None)
check(this is not None, f"{name}: instance has no __this__ attribute")
if this is None:
continue

match = THIS_RE.match(this)
check(
match is not None,
f"{name}: __this__ {this!r} does not match _<hex>_p_<ClassName>",
)
if match is None:
continue

address = int(match.group(1), 16)
check(address != 0, f"{name}: __this__ encodes a null address")
check(
match.group(2) == name,
f"{name}: __this__ encodes class {match.group(2)!r}, expected {name!r}",
)

# The reconstruction path VtkGlue.i's `out` typemaps drive.
try:
rebuilt = cls.__new__(cls, f"Addr=0x{address:x}")
except Exception as exception: # noqa: BLE001 - report any refusal verbatim
failures.append(f"{name}: Addr=0x... reconstruction raised {exception!r}")
continue

rebuilt_this = getattr(rebuilt, "__this__", None)
check(
rebuilt_this == this,
f"{name}: reconstruction yielded __this__ {rebuilt_this!r}, expected {this!r}",
)
check(
isinstance(rebuilt, cls),
f"{name}: reconstruction yielded {type(rebuilt)!r}, not a {name} instance",
)

if failures:
print("VTK wrapper encodings assumed by VtkGlue.i have changed:", file=sys.stderr)
for failure in failures:
print(f" - {failure}", file=sys.stderr)
sys.exit(1)

print("VTK __this__ and Addr=0x... encodings are intact.")
Loading
Loading