Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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