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
69 changes: 68 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,76 @@ jobs:
cmake -S . -B build-external -DLLVM_DIR="$LLVM_DIR" -DCppInterOp_DIR="$RUNNER_TEMP/cppinterop-build/lib/cmake/CppInterOp"
cmake -S . -B build-external -DLLVM_DIR="$LLVM_DIR" -DCppInterOp_DIR="$RUNNER_TEMP/cppinterop-build/lib/cmake/CppInterOp"

# The wheel toolchain on the manylinux floor and on the current release:
# AlmaLinux 8 builds with gcc-toolset-14 and JIT-compiles against the GCC 8
# headers and runtime, AlmaLinux 10 uses its system compiler throughout.
build-test-almalinux:
name: almalinux:${{ matrix.release }}/llvm21/py3.12/c++${{ matrix.cxx }}
strategy:
fail-fast: false
matrix:
include:
- { release: '8', cxx: '17', python: python3.12, packages: gcc-toolset-14-gcc-c++, bin: /opt/rh/gcc-toolset-14/root/usr/bin }
- { release: '10', cxx: '20', python: python3, packages: eigen3-devel, bin: /usr/bin }
runs-on: ubuntu-24.04
container: almalinux:${{ matrix.release }}
steps:
- name: Install the build and test dependencies
env:
PACKAGES: ${{ matrix.packages }}
PYTHON: ${{ matrix.python }}
run: dnf -y install gcc-c++ make git zstd zlib-devel boost-devel "$PYTHON-devel" "$PYTHON-pip" "$PACKAGES"

- uses: actions/checkout@v7
with:
persist-credentials: false

- name: Put the distribution's python on PATH
env:
PYTHON: ${{ matrix.python }}
run: |
"$PYTHON" -m venv "$RUNNER_TEMP/venv"
echo "$RUNNER_TEMP/venv/bin" >> "$GITHUB_PATH"

- uses: compiler-research/ci-workflows/actions/setup-recipe@main
id: llvm
with:
recipe: llvm-wheel
version: '21.1.8'
os: ubuntu-24.04
arch: x86_64
ref: 02f380b2e46746cd1c948c085894b8594a867fba

- name: Build and install cppjit with the test requirements
env:
CC: ${{ matrix.bin }}/gcc
CXX: ${{ matrix.bin }}/g++
LLVM_DIR: ${{ steps.llvm.outputs.path }}/lib/cmake/llvm
run: python -m pip install . -v --config-settings=cmake.define.LLVM_DIR="$LLVM_DIR" -r requirements.txt

- name: Run the test suite
env:
CPPINTEROP_EXTRA_INTERPRETER_ARGS: -std=c++${{ matrix.cxx }}
run: |
set -o pipefail
cd test
make -j"$(nproc)" PYTHON=python
python -m pytest -ra --tb=short -p no:cacheprovider --durations=25 \
-n auto --max-worker-restart 2 | tee "$RUNNER_TEMP/pytest.log"

- name: Capture summary
if: always()
run: tail -n1 "$RUNNER_TEMP/pytest.log" > pytest-summary.txt 2>/dev/null || echo 'no summary (build or setup failed)' > pytest-summary.txt

- uses: actions/upload-artifact@v7
if: always()
with:
name: result-almalinux-${{ matrix.release }}-llvm21-py3.12-cxx${{ matrix.cxx }}
path: pytest-summary.txt

report:
if: ${{ always() && github.event_name == 'pull_request' }}
needs: build-test
needs: [build-test, build-test-almalinux]
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ if(CMAKE_COMPILER_IS_GNUCXX)
target_link_options(cppjit PRIVATE -Wl,-Bsymbolic-functions)
endif()

# Keep the statically linked toolchain runtime (libstdc++_nonshared, libgcc)
# out of the dynamic symbol table, or the JIT resolves against it.
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_options(cppjit PRIVATE -Wl,--exclude-libs,ALL)
endif()

# macOS needs to allow undefined Python symbols
if(APPLE)
set_target_properties(cppjit PROPERTIES
Expand Down
11 changes: 10 additions & 1 deletion docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,16 @@ This release supports Python 3.12-3.14 and LLVM 21-22.

## Other changes

- ...
- `import cppjit` raises a `RuntimeError` when the C++ standard headers do
not parse, for example on a host without a C++ toolchain.
- `libcppjit` no longer exports the standard-library symbols it links
statically. A feature the host `libstdc++` lacks, such as
`std::filesystem` on a GCC 8 runtime, raises instead of crashing the
process.
- `cppdef`, `cppexec`, `include` and `c_include` errors carry the
compiler's diagnostic text.
- A call whose JIT wrapper cannot be compiled raises a `RuntimeError`
carrying the JIT's report, whatever the return type.

## Contributors

Expand Down
5 changes: 4 additions & 1 deletion python/cppjit/_cpython_cppjit.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ def _preload_backend_library():
raise ImportError("cannot locate the cppjit.libcppjit extension module")
lib = ctypes.CDLL(spec.origin, ctypes.RTLD_GLOBAL)
if not lib.LoadCppInterOp():
raise RuntimeError("failed to load CppInterOp (LoadCppInterOp returned 0)")
raise RuntimeError(
"cppjit could not initialize its C++ interpreter, see the diagnostic "
"above (LoadCppInterOp failed)"
)
return lib


Expand Down
22 changes: 11 additions & 11 deletions src/cpyrt/CPPInstance.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -718,13 +718,12 @@ static Py_hash_t op_hash(CPPInstance* self) {
// Try to locate an std::hash for this type and use that if it exists
CPPClass* klass = (CPPClass*)Py_TYPE(self);
if (klass->fOperators && klass->fOperators->fHash) {
Py_hash_t h = 0;
PyObject* hashval = PyObject_CallFunctionObjArgs(klass->fOperators->fHash,
(PyObject*)self, nullptr);
if (hashval) {
h = cpyrt_PyLong_AsHash_t(hashval);
Py_DECREF(hashval);
}
if (!hashval)
return -1;
Py_hash_t h = cpyrt_PyLong_AsHash_t(hashval);
Py_DECREF(hashval);
return h;
}

Expand All @@ -737,18 +736,19 @@ static Py_hash_t op_hash(CPPInstance* self) {
Py_DECREF(dct);
if (isValid) {
PyObject* hashobj = PyObject_CallObject(hashcls, nullptr);
Py_DECREF(hashcls);
if (!hashobj)
return -1;
if (!klass->fOperators)
klass->fOperators = new Utility::PyOperators{};
klass->fOperators->fHash = hashobj;
Py_DECREF(hashcls);

Py_hash_t h = 0;
PyObject* hashval =
PyObject_CallFunctionObjArgs(hashobj, (PyObject*)self, nullptr);
if (hashval) {
h = cpyrt_PyLong_AsHash_t(hashval);
Py_DECREF(hashval);
}
if (!hashval)
return -1;
Py_hash_t h = cpyrt_PyLong_AsHash_t(hashval);
Py_DECREF(hashval);
return h;
}
Py_DECREF(hashcls);
Expand Down
134 changes: 87 additions & 47 deletions src/cpyrt/Executors.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ using namespace cppjit;
#include "Utility.h"
#include "cppjit_interop.h"

#include "cpyrt/PyException.h"

// Standard
#include <complex>
#include <cstring>
Expand All @@ -33,43 +35,67 @@ extern std::set<std::string> gIteratorTypes;
//- helpers ------------------------------------------------------------------
namespace {

#ifdef WITH_THREAD
class GILControl {
// releases the GIL around a call when the call context asks for it
class GILRelease {
public:
GILControl() : fSave(PyEval_SaveThread()) {}
~GILControl() { PyEval_RestoreThread(fSave); }
GILRelease(cpyrt::CallContext* ctxt) : fSave(nullptr) {
#ifdef WITH_THREAD
if (ReleasesGIL(ctxt))
fSave = PyEval_SaveThread();
#endif
}
~GILRelease() {
if (fSave)
PyEval_RestoreThread(fSave);
}

private:
PyThreadState* fSave;
};
#endif

// raises when the JIT could not compile the call wrapper. An unsizable
// return type is reported by the executor that expected the temporary.
inline void RaiseOnCallFailure(interop::TCppMethod_t method) {
if (interop::LastCallFailure() != interop::kWrapperJitFailed)
return;
std::string proto = interop::GetName(interop::TCppScope_t(method.data)) +
interop::GetMethodSignature(method, true);
if (!interop::IsConstructor(method))
proto = interop::GetMethodReturnTypeAsString(method) + " " + proto;
std::string report = interop::LastCallFailureReport();
PyErr_Format(PyExc_RuntimeError,
"failed to JIT-compile the call wrapper for '%s'%s%s",
proto.c_str(), report.empty() ? "" : ":\n", report.c_str());
throw cpyrt::PyException();
}

} // unnamed namespace

#ifdef WITH_THREAD
#define CPPJIT_IMPL_GILCALL(rtype, tcode) \
static inline rtype GILCall##tcode(interop::TCppMethod_t method, \
interop::TCppObject_t self, \
cpyrt::CallContext* ctxt) { \
if (!ReleasesGIL(ctxt)) \
return interop::Call##tcode(method, self, ctxt->GetEncodedSize(), \
ctxt->GetArgs()); \
GILControl gc{}; \
return interop::Call##tcode(method, self, ctxt->GetEncodedSize(), \
ctxt->GetArgs()); \
rtype result; \
{ \
GILRelease gil{ctxt}; \
result = interop::Call##tcode(method, self, ctxt->GetEncodedSize(), \
ctxt->GetArgs()); \
} \
RaiseOnCallFailure(method); \
return result; \
}
#else
#define CPPJIT_IMPL_GILCALL(rtype, tcode) \
static inline rtype GILCall##tcode(interop::TCppMethod_t method, \
interop::TCppObject_t self, \
cpyrt::CallContext* ctxt) { \
return interop::Call##tcode(method, self, ctxt->GetEncodedSize(), \
ctxt->GetArgs()); \

static inline void GILCallV(interop::TCppMethod_t method,
interop::TCppObject_t self,
cpyrt::CallContext* ctxt) {
{
GILRelease gil{ctxt};
interop::CallV(method, self, ctxt->GetEncodedSize(), ctxt->GetArgs());
}
#endif
RaiseOnCallFailure(method);
}

// clang-format off
CPPJIT_IMPL_GILCALL(void, V)
CPPJIT_IMPL_GILCALL(unsigned char, B)
CPPJIT_IMPL_GILCALL(char, C)
CPPJIT_IMPL_GILCALL(short, H)
Expand All @@ -87,31 +113,27 @@ static inline interop::TCppObject_t GILCallO(interop::TCppMethod_t method,
cpyrt::CallContext* ctxt,
interop::TCppScope_t klass) {
interop::TCppType_t klass_ty = interop::GetTypeFromScope(klass);
#ifdef WITH_THREAD
if (!ReleasesGIL(ctxt))
#endif
return interop::CallO(method, self, ctxt->GetEncodedSize(), ctxt->GetArgs(),
klass_ty);
#ifdef WITH_THREAD
GILControl gc{};
return interop::CallO(method, self, ctxt->GetEncodedSize(), ctxt->GetArgs(),
klass_ty);
#endif
interop::TCppObject_t result;
{
GILRelease gil{ctxt};
result = interop::CallO(method, self, ctxt->GetEncodedSize(),
ctxt->GetArgs(), klass_ty);
}
RaiseOnCallFailure(method);
return result;
}

static inline interop::TCppObject_t
GILCallConstructor(interop::TCppMethod_t method, interop::TCppScope_t klass,
cpyrt::CallContext* ctxt) {
#ifdef WITH_THREAD
if (!ReleasesGIL(ctxt))
#endif
return interop::CallConstructor(method, klass, ctxt->GetEncodedSize(),
ctxt->GetArgs());
#ifdef WITH_THREAD
GILControl gc{};
return interop::CallConstructor(method, klass, ctxt->GetEncodedSize(),
ctxt->GetArgs());
#endif
interop::TCppObject_t result;
{
GILRelease gil{ctxt};
result = interop::CallConstructor(method, klass, ctxt->GetEncodedSize(),
ctxt->GetArgs());
}
RaiseOnCallFailure(method);
return result;
}

static inline PyObject* cpyrt_PyText_FromLong(long cl) {
Expand Down Expand Up @@ -368,7 +390,7 @@ bool cpyrt::RefExecutor::SetAssignable(PyObject* pyobject) {
interop::TCppObject_t self, \
CallContext* ctxt) { \
type* ref = (type*)GILCallR(method, self, ctxt); \
if (!ref) { /* can happen if wrapper compilation fails */ \
if (!ref) { /* a reference bound to a null address */ \
PyErr_SetString(PyExc_ReferenceError, \
"attempt to access a null-pointer"); \
return nullptr; \
Expand Down Expand Up @@ -429,6 +451,10 @@ PyObject* cpyrt::STLStringRefExecutor::Execute(interop::TCppMethod_t method,
interop::GetFullScope("std::string");

std::string* result = (std::string*)GILCallR(method, self, ctxt);
if (!result) { // a reference bound to a null address
PyErr_SetString(PyExc_ReferenceError, "attempt to access a null-pointer");
return nullptr;
}
if (!fAssignable) {
std::string* rescp = new std::string{*result};
return BindCppObjectNoCast((void*)rescp, sSTLStringScope,
Expand Down Expand Up @@ -622,9 +648,13 @@ PyObject* cpyrt::STLStringExecutor::Execute(interop::TCppMethod_t method,
interop::GetFullScope("std::string");
std::string* result =
(std::string*)GILCallO(method, self, ctxt, sSTLStringScope).data;
if (!result)
result = new std::string{};
else if (PyErr_Occurred()) {
if (!result) {
if (!PyErr_Occurred()) // the callee may have set a python error itself
PyErr_SetString(PyExc_ValueError,
"nullptr result where temporary expected");
return nullptr;
}
if (PyErr_Occurred()) {
delete result;
return nullptr;
}
Expand All @@ -644,8 +674,10 @@ PyObject* cpyrt::STLWStringExecutor::Execute(interop::TCppMethod_t method,
std::wstring* result =
(std::wstring*)GILCallO(method, self, ctxt, sSTLWStringScope).data;
if (!result) {
wchar_t w = L'\0';
return PyUnicode_FromWideChar(&w, 0);
if (!PyErr_Occurred()) // the callee may have set a python error itself
PyErr_SetString(PyExc_ValueError,
"nullptr result where temporary expected");
return nullptr;
}

PyObject* pyresult = PyUnicode_FromWideChar(result->c_str(), result->size());
Expand Down Expand Up @@ -773,6 +805,10 @@ PyObject* cpyrt::InstancePtrPtrExecutor::Execute(interop::TCppMethod_t method,
return BindCppObject((void*)result, fClass,
CPPInstance::kIsPtrPtr | CPPInstance::kIsReference);

if (!result) { // nothing to assign to
PyErr_SetString(PyExc_ReferenceError, "attempt to access a null-pointer");
return nullptr;
}
CPPInstance* cppinst = (CPPInstance*)fAssignable;
*result = cppinst->GetObject();

Expand All @@ -792,6 +828,10 @@ PyObject* cpyrt::InstancePtrRefExecutor::Execute(interop::TCppMethod_t method,
return SetInstanceCheckError(fAssignable);

void** result = (void**)GILCallR(method, self, ctxt);
if (!result) { // a reference bound to a null address
PyErr_SetString(PyExc_ReferenceError, "attempt to access a null-pointer");
return nullptr;
}
if (!fAssignable)
return BindCppObject(*result, fClass);

Expand Down
Loading
Loading