I am trying to interface a CPP library with Python using Ctypes.
The cpp code I am trying to interface is available here. All code used here is available here.
The python code that is used to interface is written in gdist.py.
gdist.py
import ctypes
import glob
import os
import sys
import numpy as np
import scipy.sparse
if sys.platform == 'win32':
libfile = glob.glob('build/*/gdist_c_api.dll')[0]
libfile = os.path.abspath(libfile)
lib = ctypes.CDLL(libfile)
elif sys.platform == 'darwin':
libfile = glob.glob('build/*/gdist*.dylib')[0]
lib = ctypes.CDLL(libfile)
else:
libfile = glob.glob('build/*/gdist*.so')[0]
lib = ctypes.CDLL(libfile)
lib.local_gdist_matrix.argtypes = [
ctypes.c_uint,
ctypes.c_uint,
np.ctypeslib.ndpointer(dtype=np.float64),
np.ctypeslib.ndpointer(dtype=np.uint32),
ctypes.POINTER(ctypes.c_uint),
ctypes.c_double,
]
lib.local_gdist_matrix.restype = ctypes.POINTER(ctypes.c_double)
lib.free_memory.argtypes = [
ctypes.POINTER(ctypes.c_double),
]
lib.free_memory.restype = None
def local_gdist_matrix(
vertices,
triangles,
max_distance=1e100,
):
vertices = vertices.ravel()
triangles = triangles.ravel()
sparse_matrix_size = ctypes.c_uint(0)
data = lib.local_gdist_matrix(
vertices.size,
triangles.size,
vertices,
triangles,
ctypes.byref(sparse_matrix_size),
max_distance,
)
np_data = np.fromiter(
data,
dtype=np.float64,
count=3 * sparse_matrix_size.value,
)
lib.free_memory(data)
assert np_data.size % 3 == 0
sizes = np_data.size // 3
rows = np_data[:sizes]
columns = np_data[sizes: 2 * sizes]
np_data = np_data[2 * sizes:]
return scipy.sparse.csc_matrix(
(np_data, (rows, columns)), shape=(vertices.size // 3, vertices.size // 3)
)
C++ code used to interface:
gdist_c_api.h
#include <iostream>
#include <fstream>
#include <inttypes.h>
#include "geodesic_algorithm_exact.h"
#if defined(_WIN32)
# if defined(DLL_EXPORTS)
# define DLL_EXPORT_API __declspec(dllexport)
# else
# define DLL_EXPORT_API __declspec(dllimport)
# endif
#else
# define DLL_EXPORT_API
#endif
double* local_gdist_matrix_impl(
unsigned number_of_vertices,
unsigned number_of_triangles,
double *vertices,
unsigned *triangles,
unsigned *sparse_matrix_size,
double max_distance
);
void free_memory_impl(double *ptr);
extern "C" {
DLL_EXPORT_API double* local_gdist_matrix(
unsigned number_of_vertices,
unsigned number_of_triangles,
double *vertices,
unsigned *triangles,
unsigned *sparse_matrix_size,
double max_distance
);
DLL_EXPORT_API void free_memory(double *ptr);
};
gdist_c_api.cpp
#include "gdist_c_api.h"
double* local_gdist_matrix_impl(
unsigned number_of_vertices,
unsigned number_of_triangles,
double *vertices,
unsigned *triangles,
unsigned *sparse_matrix_size,
double max_distance
) {
std::vector<double> points (vertices, vertices + number_of_vertices);
std::vector<unsigned> faces (triangles, triangles + number_of_triangles);
geodesic::Mesh mesh;
mesh.initialize_mesh_data(points, faces); // create internal mesh data structure including edges
geodesic::GeodesicAlgorithmExact algorithm(&mesh); // create exact algorithm for the mesh
std::vector <unsigned> rows_vector, columns_vector;
std::vector <double> data_vector;
double distance = 0;
std::vector<geodesic::SurfacePoint> targets(number_of_vertices), source;
for (unsigned i = 0; i < number_of_vertices; ++i) {
targets[i] = geodesic::SurfacePoint(&mesh.vertices()[i]);
}
for (unsigned i = 0; i < number_of_vertices / 3; ++i) {
source.push_back(geodesic::SurfacePoint(&mesh.vertices()[i]));
algorithm.propagate(source, max_distance, NULL);
source.pop_back();
for (unsigned j = 0; j < number_of_vertices / 3; ++j) {
algorithm.best_source(targets[j], distance);
if (distance != geodesic::GEODESIC_INF && distance != 0 && distance <= max_distance) {
rows_vector.push_back(i);
columns_vector.push_back(j);
data_vector.push_back(distance);
}
}
}
double *data;
data = new double[3 * rows_vector.size()];
assert (data != NULL); // memory allocation should not fail
*sparse_matrix_size = rows_vector.size();
std::copy(rows_vector.begin(), rows_vector.end(), data);
std::copy(columns_vector.begin(), columns_vector.end(), data + data_vector.size());
std::copy(data_vector.begin(), data_vector.end(), data + 2 * data_vector.size());
return data;
}
void free_memory_impl(double *ptr) {
delete[] ptr;
}
extern "C" {
double* local_gdist_matrix(
unsigned number_of_vertices,
unsigned number_of_triangles,
double *vertices,
unsigned *triangles,
unsigned *sparse_matrix_size,
double max_distance
) {
return local_gdist_matrix_impl(
number_of_vertices,
number_of_triangles,
vertices,
triangles,
sparse_matrix_size,
max_distance
);
}
void free_memory(double *ptr) {
free_memory_impl(ptr);
}
};
Batch script to create DLL file on Windows:
mkdir build\lib.win32
cd build\lib.win32
cl.exe /LD /DDLL_EXPORTS /EHsc /W4 ..\..\geodesic_library\gdist_c_api.cpp
Script to create dylib on macOS:
mkdir build
cd build
mkdir lib.macos
cd lib.macos
clang++ -std=c++11 -shared -fPIC ../../geodesic_library/gdist_c_api.cpp -o gdist_c_api.dylib
Test file:
test_gdist.py
import numpy as np
import gdist
def test_equality_with_stable():
surface_data = 'inner_skull_642'
expected = np.loadtxt(f'data/{surface_data}/gdist_matrix.txt')
vertices = np.loadtxt(
f'data/{surface_data}/vertices.txt',
dtype=np.float64,
)
triangles = np.loadtxt(
f'data/{surface_data}/triangles.txt',
dtype=np.uint32,
)
actual = gdist.local_gdist_matrix(
vertices=vertices,
triangles=triangles,
)
actual = actual.toarray()
np.testing.assert_array_almost_equal(actual, expected)
def test_flat_triangular_mesh():
data = np.loadtxt("data/flat_triangular_mesh.txt", skiprows=1)
vertices = data[0:121].astype(np.float64)
triangles = data[121:].astype(np.uint32)
distances = gdist.local_gdist_matrix(vertices, triangles)
epsilon = 1e-6 # the default value used in `assert_array_almost_equal`
# test if the obtained matrix is symmetric
assert (abs(distances - distances.T) > epsilon).nnz == 0
np.testing.assert_array_almost_equal(distances.toarray()[1][0], 0.2)
# set max distance as 0.3
distances = gdist.local_gdist_matrix(vertices, triangles, 0.3)
# test if the obtained matrix is symmetric
assert (abs(distances - distances.T) > epsilon).nnz == 0
assert np.max(distances) <= 0.3
def test_hedgehog_mesh():
data = np.loadtxt("data/hedgehog_mesh.txt", skiprows=1)
vertices = data[0:300].astype(np.float64)
triangles = data[300:].astype(np.uint32)
distances = gdist.local_gdist_matrix(vertices, triangles)
epsilon = 1e-6 # the default value used in `assert_array_almost_equal`
# test if the obtained matrix is symmetric
assert (abs(distances - distances.T) > epsilon).nnz == 0
np.testing.assert_array_almost_equal(
distances.toarray()[1][0], 1.40522
)
# set max distance as 1.45
distances = gdist.local_gdist_matrix(vertices, triangles, 1.45)
# test if the obtained matrix is symmetric
assert (abs(distances - distances.T) > epsilon).nnz == 0
assert np.max(distances) <= 1.45
If I run pytest on the code, the tests sometimes fail on Windows with the following error:
Windows fatal exception: access violation
Current thread 0x00001134 (most recent call first):
File "c:\python38\lib\site-packages\gdist.py", line 92 in local_gdist_matrix
File "c:\python38\lib\site-packages\gdist.py", line 146 in local_gdist_matrix
File "C:\Users\travis\build\ayan-b\tvb-geodesic\tests\test_gdist.py", line 64 in test_hedgehog_mesh
File "c:\python38\lib\site-packages\_pytest\python.py", line 182 in pytest_pyfunc_call
File "c:\python38\lib\site-packages\pluggy\callers.py", line 187 in _multicall
File "c:\python38\lib\site-packages\pluggy\manager.py", line 84 in <lambda>
File "c:\python38\lib\site-packages\pluggy\manager.py", line 93 in _hookexec
File "c:\python38\lib\site-packages\pluggy\hooks.py", line 286 in __call__
File "c:\python38\lib\site-packages\_pytest\python.py", line 1477 in runtest
File "c:\python38\lib\site-packages\_pytest\runner.py", line 135 in pytest_runtest_call
File "c:\python38\lib\site-packages\pluggy\callers.py", line 187 in _multicall
File "c:\python38\lib\site-packages\pluggy\manager.py", line 84 in <lambda>
File "c:\python38\lib\site-packages\pluggy\manager.py", line 93 in _hookexec
File "c:\python38\lib\site-packages\pluggy\hooks.py", line 286 in __call__
File "c:\python38\lib\site-packages\_pytest\runner.py", line 217 in <lambda>
File "c:\python38\lib\site-packages\_pytest\runner.py", line 244 in from_call
File "c:\python38\lib\site-packages\_pytest\runner.py", line 216 in call_runtest_hook
File "c:\python38\lib\site-packages\_pytest\runner.py", line 186 in call_and_report
File "c:\python38\lib\site-packages\_pytest\runner.py", line 100 in runtestprotocol
File "c:\python38\lib\site-packages\_pytest\runner.py", line 85 in pytest_runtest_protocol
File "c:\python38\lib\site-packages\pluggy\callers.py", line 187 in _multicall
File "c:\python38\lib\site-packages\pluggy\manager.py", line 84 in <lambda>
File "c:\python38\lib\site-packages\pluggy\manager.py", line 93 in _hookexec
File "c:\python38\lib\site-packages\pluggy\hooks.py", line 286 in __call__
File "c:\python38\lib\site-packages\_pytest\main.py", line 272 in pytest_runtestloop
File "c:\python38\lib\site-packages\pluggy\callers.py", line 187 in _multicall
File "c:\python38\lib\site-packages\pluggy\manager.py", line 84 in <lambda>
File "c:\python38\lib\site-packages\pluggy\manager.py", line 93 in _hookexec
File "c:\python38\lib\site-packages\pluggy\hooks.py", line 286 in __call__
File "c:\python38\lib\site-packages\_pytest\main.py", line 247 in _main
File "c:\python38\lib\site-packages\_pytest\main.py", line 191 in wrap_session
File "c:\python38\lib\site-packages\_pytest\main.py", line 240 in pytest_cmdline_main
File "c:\python38\lib\site-packages\pluggy\callers.py", line 187 in _multicall
File "c:\python38\lib\site-packages\pluggy\manager.py", line 84 in <lambda>
File "c:\python38\lib\site-packages\pluggy\manager.py", line 93 in _hookexec
File "c:\python38\lib\site-packages\pluggy\hooks.py", line 286 in __call__
File "c:\python38\lib\site-packages\_pytest\config\__init__.py", line 124 in main
File "C:\Python38\Scripts\pytest.exe\__main__.py", line 7 in <module>
File "c:\python38\lib\runpy.py", line 85 in _run_code
File "c:\python38\lib\runpy.py", line 192 in _run_module_as_main
Sometimes it also fails on macOS with the following error:
tests/test_equality_with_stable.py Fatal Python error: Segmentation fault
Current thread 0x0000000103ce05c0 (most recent call first):
File "/usr/local/bin/gdist.py", line 98 in local_gdist_matrix
File "/usr/local/bin/gdist.py", line 151 in local_gdist_matrix
File "/Users/travis/build/ayan-b/tvb-geodesic/tests/test_equality_with_stable.py", line 19 in test_equality_with_stable
File "/usr/local/lib/python3.7/site-packages/_pytest/python.py", line 182 in pytest_pyfunc_call
File "/usr/local/lib/python3.7/site-packages/pluggy/callers.py", line 187 in _multicall
File "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 87 in <lambda>
File "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 93 in _hookexec
File "/usr/local/lib/python3.7/site-packages/pluggy/hooks.py", line 286 in __call__
File "/usr/local/lib/python3.7/site-packages/_pytest/python.py", line 1477 in runtest
File "/usr/local/lib/python3.7/site-packages/_pytest/runner.py", line 135 in pytest_runtest_call
File "/usr/local/lib/python3.7/site-packages/pluggy/callers.py", line 187 in _multicall
File "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 87 in <lambda>
File "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 93 in _hookexec
File "/usr/local/lib/python3.7/site-packages/pluggy/hooks.py", line 286 in __call__
File "/usr/local/lib/python3.7/site-packages/_pytest/runner.py", line 217 in <lambda>
File "/usr/local/lib/python3.7/site-packages/_pytest/runner.py", line 244 in from_call
File "/usr/local/lib/python3.7/site-packages/_pytest/runner.py", line 217 in call_runtest_hook
File "/usr/local/lib/python3.7/site-packages/_pytest/runner.py", line 186 in call_and_report
File "/usr/local/lib/python3.7/site-packages/_pytest/runner.py", line 100 in runtestprotocol
File "/usr/local/lib/python3.7/site-packages/_pytest/runner.py", line 85 in pytest_runtest_protocol
File "/usr/local/lib/python3.7/site-packages/pluggy/callers.py", line 187 in _multicall
File "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 87 in <lambda>
File "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 93 in _hookexec
File "/usr/local/lib/python3.7/site-packages/pluggy/hooks.py", line 286 in __call__
File "/usr/local/lib/python3.7/site-packages/_pytest/main.py", line 272 in pytest_runtestloop
File "/usr/local/lib/python3.7/site-packages/pluggy/callers.py", line 187 in _multicall
File "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 87 in <lambda>
File "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 93 in _hookexec
File "/usr/local/lib/python3.7/site-packages/pluggy/hooks.py", line 286 in __call__
File "/usr/local/lib/python3.7/site-packages/_pytest/main.py", line 247 in _main
File "/usr/local/lib/python3.7/site-packages/_pytest/main.py", line 191 in wrap_session
File "/usr/local/lib/python3.7/site-packages/_pytest/main.py", line 240 in pytest_cmdline_main
File "/usr/local/lib/python3.7/site-packages/pluggy/callers.py", line 187 in _multicall
File "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 87 in <lambda>
File "/usr/local/lib/python3.7/site-packages/pluggy/manager.py", line 93 in _hookexec
File "/usr/local/lib/python3.7/site-packages/pluggy/hooks.py", line 286 in __call__
File "/usr/local/lib/python3.7/site-packages/_pytest/config/__init__.py", line 125 in main
File "/usr/local/bin/pytest", line 8 in <module>
/Users/travis/.travis/functions: line 109: 2197 Segmentation fault: 11 pytest --cov=gdist
The command "pytest --cov=gdist" exited with 139.
However, the tests never fail on Linux.
Notes:
pytest the test failed around 50% of the times however with nose2 the test failed
only once of 20+ runs.CDLL and WinDLL but it didn't fix the issue.User contributions licensed under CC BY-SA 3.0