Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Complete rewrite of bindings with nanobind instead of numpyeigen+pybind11 #243

Draft
wants to merge 26 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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
7 changes: 3 additions & 4 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,12 @@ jobs:
runs-on: ${{ matrix.os.runs-on }}

env:
CIBW_BUILD_VERBOSITY: 3
# This is very dubious... It *may* work because these are just cpp libraries that should not depend on the python version. Still, super-dubious.
CIBW_TEST_REQUIRES: "gitpython"
CIBW_TEST_COMMAND: "python {project}/tests/test_basic.py"
CIBW_TEST_COMMAND: "python {project}/tests/test.py"
CIBW_BUILD: "${{ matrix.cpversion }}-${{ matrix.os.cibw-arch }}"
CIBW_TEST_SKIP: "*-macosx_arm64"
CIBW_ENVIRONMENT: "MAX_JOBS=${{ matrix.os.runs-on == 'macos-latest' && 3 || 2 }} PIP_CONSTRAINT=constraints.txt"
CIBW_ENVIRONMENT: "MAX_JOBS=${{ matrix.os.runs-on == 'macos-latest' && 3 || 2 }}"
# Why universal2 here? It's not included above in CIBW_BUILD
CIBW_ARCHS_MACOS: "x86_64 arm64 universal2"
CIBW_ENVIRONMENT_MACOS: "MACOSX_DEPLOYMENT_TARGET=10.13 CMAKE_OSX_ARCHITECTURES=\"${{ matrix.os.cibw-arch == 'macosx_x86_64' && 'x86_64' || matrix.os.cibw-arch == 'macosx_arm64' && 'arm64' || matrix.os.cibw-arch == 'macosx_universal2' && 'arm64;x86_64' || '' }}\""
Expand Down Expand Up @@ -91,7 +90,7 @@ jobs:
- name: Build wheels
shell: bash
run: |
PIP_CONSTRAINT=$GITHUB_WORKSPACE/constraints.txt python -m cibuildwheel --output-dir wheelhouse
python -m cibuildwheel --output-dir wheelhouse

# Upload binaries to github
- uses: actions/upload-artifact@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ junit
.*.swo
*~
dist/
igl/*
202 changes: 125 additions & 77 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,56 @@
cmake_minimum_required(VERSION 3.16.0)
# Refactor the cmake/setup.py build to use scikit-build
# https://github.com/wjakob/nanobind_example
# Rather than creating multiple modules and dealing with multiple __init__.py
# files, we can create a single module and use __init__.py to import the
# submodules, create one extension module with submodules
# https://stackoverflow.com/a/77020918/148668

cmake_minimum_required(VERSION 3.15...3.27)
project(pyigl)

if (NOT DEFINED PYLIBIGL_EXTERNAL)
set(PYLIBIGL_EXTERNAL ${CMAKE_CURRENT_SOURCE_DIR}/external)
# For std::filesystem::path (generic_string)
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15")
# ≥17 for return value optimization
# <20 for embree
set(CMAKE_CXX_STANDARD 17)

if (CMAKE_VERSION VERSION_LESS 3.18)
set(DEV_MODULE Development)
else()
set(DEV_MODULE Development.Module)
endif()

find_package(Python 3.8 COMPONENTS Interpreter ${DEV_MODULE} REQUIRED)

if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
endif()

# Enable FetchContent to download dependencies at configure time
include(FetchContent)

# Download and set up nanobind
FetchContent_Declare(
nanobind
GIT_REPOSITORY https://github.com/wjakob/nanobind.git
GIT_TAG v2.2.0
)
FetchContent_MakeAvailable(nanobind)

# Download and set up libigl
option(LIBIGL_COPYLEFT_CORE "Build target igl_copyleft::core" ON)
option(LIBIGL_COPYLEFT_CGAL "Build target igl_copyleft::cgal" ON)
option(LIBIGL_EMBREE "Build target igl::embree" ON)
option(LIBIGL_COPYLEFT_TETGEN "Build target igl_copyleft::tetgen" ON)
option(LIBIGL_RESTRICTED_TRIANGLE "Build target igl_restricted::triangle" ON)
FetchContent_Declare(
libigl
GIT_REPOSITORY https://github.com/libigl/libigl.git
GIT_TAG 1f75bc3b617f1966d7c1d8c586463c2a5f11554b
)
FetchContent_MakeAvailable(libigl)

message(STATUS "PYIGL_OUTPUT_DIRECTORY: ${PYIGL_OUTPUT_DIRECTORY}")
if (NOT DEFINED PYIGL_OUTPUT_DIRECTORY)
message(FATAL_ERROR "PYIGL_OUTPUT_DIRECTORY must be defined externally")
Expand All @@ -23,28 +69,7 @@ include(CXXFeatures)
# Generate position independent code by default
set(CMAKE_POSITION_INDEPENDENT_CODE ON CACHE INTERNAL "")

option(LIBIGL_COPYLEFT_CGAL "Build target igl_copyleft::cgal" ON)
option(LIBIGL_COPYLEFT_TETGEN "Build target igl_copyleft::tetgen" ON)
option(LIBIGL_RESTRICTED_TRIANGLE "Build target igl_restricted::triangle" ON)
# libigl options must come before include(PyiglDependencies)
include(PyiglDependencies)
if(NOT TARGET igl::core)
include(libigl)
endif()

# A module for writing bindings with our framework
file(GLOB PYIGL_SOURCES src/*.cpp)
npe_add_module(pyigl
BINDING_SOURCES
${PYIGL_SOURCES})
target_link_libraries(pyigl PRIVATE igl::core)
target_include_directories(pyigl PRIVATE "src/include")
set_target_properties(pyigl PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${PYIGL_OUTPUT_DIRECTORY}"
RUNTIME_OUTPUT_DIRECTORY "${PYIGL_OUTPUT_DIRECTORY}"
LIBRARY_OUTPUT_DIRECTORY_RELEASE "${PYIGL_OUTPUT_DIRECTORY}"
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${PYIGL_OUTPUT_DIRECTORY}"
)

# don't need to worry about nested modules (opengl/** are the only ones and
# those probably aren't ever getting python bindings)
Expand All @@ -57,67 +82,90 @@ function(pyigl_include prefix name)
string(PREPEND prefix_uc _)
endif()
string(TOLOWER "${prefix_uc}" prefix_lc)
if(LIBIGL${prefix_uc}_${name_uc})
if(${prefix} STREQUAL "copyleft")
set(subpath "copyleft/${name}")
# if(LIBIGL${prefix_uc}_${name_uc}) or name == "core"
if(LIBIGL${prefix_uc}_${name_uc} OR name STREQUAL "core")
if("${prefix}" STREQUAL "copyleft")
if("${name}" STREQUAL "core")
set(subpath "copyleft")
else()
set(subpath "copyleft/${name}")
endif()
elseif("${name}" STREQUAL "core")
set(subpath "")
else() # "" or "restricted"
set(subpath "${name}")
endif()
file(GLOB sources src/${subpath}/*.cpp)
file(GLOB sources "${CMAKE_CURRENT_SOURCE_DIR}/src/${subpath}/*.cpp")

set(BINDING_SOURCES ${sources})
list(FILTER BINDING_SOURCES EXCLUDE REGEX ".*/module\\.cpp$")
# Generate the function calls based on filenames
set(BINDING_DECLARATIONS "")
foreach(source_file ${BINDING_SOURCES})
get_filename_component(filename ${source_file} NAME_WE)
set(BINDING_DECLARATIONS "${BINDING_DECLARATIONS}extern void bind_${filename}(nb::module_ &m);\n")
set(BINDING_INVOCATIONS "${BINDING_INVOCATIONS} bind_${filename}(m);\n")
endforeach()
# make a temporary folder in the build directory to store the generated files
set(generated_dir "${CMAKE_CURRENT_BINARY_DIR}/include/${subpath}")
file(MAKE_DIRECTORY "${generated_dir}")
# write contents into BINDING_DECLARATIONS.h and BINDING_INVOCATIONS.h
file(WRITE "${generated_dir}/BINDING_DECLARATIONS.in" "${BINDING_DECLARATIONS}")
file(WRITE "${generated_dir}/BINDING_INVOCATIONS.in" "${BINDING_INVOCATIONS}")

set(target_name "pyigl${prefix_lc}_${name}")
npe_add_module( ${target_name} BINDING_SOURCES ${sources})
target_link_libraries( ${target_name} PRIVATE igl::core igl${prefix_lc}::${name})
target_include_directories( ${target_name} PRIVATE "src/include")
nanobind_add_module(${target_name} ${sources})

if("${name}" STREQUAL "core")
target_link_libraries(${target_name} PRIVATE igl::core)
else()
target_link_libraries(${target_name} PRIVATE igl::core igl${prefix_lc}::${name})
endif()
target_include_directories(${target_name} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/include")
target_include_directories(${target_name} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
set(output_dir "${PYIGL_OUTPUT_DIRECTORY}/${subpath}")
file(MAKE_DIRECTORY ${output_dir})
file(WRITE "${output_dir}/__init__.py" "from .${target_name} import *")
# https://stackoverflow.com/a/56514534/148668
file(WRITE "${output_dir}/__init__.py" "from .${target_name} import *\n")
if("${name}" STREQUAL "core" AND "${prefix}" STREQUAL "")
file(APPEND "${output_dir}/__init__.py" "from ._version import __version__\n")
endif()
set_target_properties(${target_name} PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${output_dir}"
RUNTIME_OUTPUT_DIRECTORY "${output_dir}"
LIBRARY_OUTPUT_DIRECTORY_RELEASE "${output_dir}"
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${output_dir}")
# why do this?
target_link_libraries( pyigl INTERFACE ${target_name})
endif()
# https://stackoverflow.com/a/69736197/148668
# https://cmake.org/cmake/help/latest/manual/cmake.1.html#cmdoption-cmake-E-arg-copy
# until then just needlessly also copy TARGET_FILE in case TARGET_RUNTIME_DLLS is empty
if(${CMAKE_SYSTEM_NAME} MATCHES "Windows")
add_custom_command(TARGET ${target_name} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:${target_name}> $<TARGET_RUNTIME_DLLS:${target_name}> $<TARGET_FILE_DIR:${target_name}>
COMMAND_EXPAND_LISTS)
LIBRARY_OUTPUT_DIRECTORY "${output_dir}"
RUNTIME_OUTPUT_DIRECTORY "${output_dir}"
LIBRARY_OUTPUT_DIRECTORY_RELEASE "${output_dir}"
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${output_dir}"
)
nanobind_add_stub(
${target_name}_stub
MODULE ${target_name}
OUTPUT "${output_dir}/${target_name}.pyi"
PYTHON_PATH $<TARGET_FILE_DIR:${target_name}>
DEPENDS ${target_name}
)

# just to add dependency?
if("${name}" STREQUAL "core")
else()
target_link_libraries(pyigl_core INTERFACE ${target_name})
endif()
endif()
endfunction()

pyigl_include("copyleft" "cgal")
pyigl_include("copyleft" "tetgen")
pyigl_include("restricted" "triangle")


file(GLOB PYIGL_CLASSES_SOURCES classes/*.cpp)
add_library(pyigl_classes MODULE ${PYIGL_CLASSES_SOURCES})
# std::variant
target_compile_features(pyigl_classes PRIVATE cxx_std_17)
target_link_libraries(pyigl_classes PRIVATE npe igl::core)
target_link_libraries(pyigl_classes PRIVATE pybind11::module)
set_target_properties(pyigl_classes PROPERTIES PREFIX "${PYTHON_MODULE_PREFIX}" SUFFIX "${PYTHON_MODULE_EXTENSION}")
target_include_directories(pyigl_classes PRIVATE "src/include")
target_include_directories(pyigl_classes PRIVATE "${PYLIBIGL_EXTERNAL}/numpyeigen/src")
set_target_properties(pyigl_classes PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${PYIGL_OUTPUT_DIRECTORY}"
RUNTIME_OUTPUT_DIRECTORY "${PYIGL_OUTPUT_DIRECTORY}"
LIBRARY_OUTPUT_DIRECTORY_RELEASE "${PYIGL_OUTPUT_DIRECTORY}"
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${PYIGL_OUTPUT_DIRECTORY}"
)

# Sort projects inside the solution
set_property(GLOBAL PROPERTY USE_FOLDERS ON)

if(${CMAKE_SYSTEM_NAME} MATCHES "Windows")
foreach(config ${CMAKE_CONFIGURATION_TYPES})
string(TOUPPER ${config} config)
string(REPLACE /MD /MT CMAKE_C_FLAGS_${config} "${CMAKE_C_FLAGS_${config}}")
string(REPLACE /MD /MT CMAKE_CXX_FLAGS_${config} "${CMAKE_CXX_FLAGS_${config}}")
endforeach()
pyigl_include("" "core")
if(LIBIGL_COPYLEFT_CORE)
pyigl_include("copyleft" "core")
endif()
if(LIBIGL_COPYLEFT_CGAL)
pyigl_include("copyleft" "cgal")
endif()
if(LIBIGL_EMBREE)
pyigl_include("" "embree")
endif()
if(LIBIGL_COPYLEFT_TETGEN)
pyigl_include("copyleft" "tetgen")
endif()
if(LIBIGL_RESTRICTED_TRIANGLE)
pyigl_include("restricted" "triangle")
endif()


47 changes: 0 additions & 47 deletions cmake/PyiglDependencies.cmake

This file was deleted.

2 changes: 0 additions & 2 deletions constraints.txt

This file was deleted.

7 changes: 0 additions & 7 deletions environment.yml

This file was deleted.

11 changes: 0 additions & 11 deletions igl/__init__.py

This file was deleted.

32 changes: 32 additions & 0 deletions include/default_types.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#include <Eigen/Dense>
#include <Eigen/Sparse>
#include <cstdint>

///////////////////////////////////////////////////////////////////////////////////
/// Default numeric and integer types. Passing anything else will result in an
/// implicit copy. Developers insisting on native executation of libigl on
/// single-precision (largely untested and possibly error prone) can change this
/// to float and recompile bindings at their own delight or peril.
///////////////////////////////////////////////////////////////////////////////////
using Numeric = double;
using Integer = int64_t;
using Boolean = bool;
// When using `const nb::DRef<const Eigen::MatrixX*>` this `Options` does not
// affect the input types (either order should result in no copy if `dtype`s
// match). It does affect return types, `Eigen::MatrixXN X; … ; return X;`
constexpr auto Options = Eigen::RowMajor;
namespace Eigen
{
typedef Matrix<Numeric, Dynamic, Dynamic, Options> MatrixXN;
typedef Matrix<Integer, Dynamic, Dynamic, Options> MatrixXI;
typedef Matrix<Boolean, Dynamic, Dynamic, Options> MatrixXB;
typedef Matrix<Numeric, Dynamic, 1, Eigen::ColMajor> VectorXN;
typedef Matrix<Integer, Dynamic, 1, Eigen::ColMajor> VectorXI;
typedef Matrix<Boolean, Dynamic, 1, Eigen::ColMajor> VectorXB;
typedef Matrix<Numeric, 1, Dynamic, Eigen::RowMajor> RowVectorXN;
typedef Matrix<Integer, 1, Dynamic, Eigen::RowMajor> RowVectorXI;
typedef Matrix<Boolean, 1, Dynamic, Eigen::RowMajor> RowVectorXB;
typedef SparseMatrix<Numeric> SparseMatrixN;
typedef SparseMatrix<Integer> SparseMatrixI;
typedef SparseMatrix<Boolean> SparseMatrixB;
}
Loading
Loading