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
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ include(GNUInstallDirs)
# Perhaps this should permanently be OFF and users can build their own CppInterOp if they want to run the tests?
option(CPPJIT_ENABLE_CPPINTEROP_TESTS "enable CppInterOp tests" OFF)
set(CPPINTEROP_GIT_REPOSITORY "https://github.com/compiler-research/CppInterOp.git" CACHE STRING "")
set(CPPINTEROP_GIT_TAG "c8126249dd18363000b89333b34313ca2a83fb82" CACHE STRING "")
set(CPPINTEROP_GIT_TAG "c57d253e7cba94cc0976a9e36547a960f0fd2f83" CACHE STRING "")
set(CPPINTEROP_SOURCE_DIR "" CACHE PATH
"Override default CppInterOp built by ExternalProject_Add, with a path to local CppInterOp source")

Expand Down
6 changes: 6 additions & 0 deletions python/cppjit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"add_library_path", # add a path to search for libraries
"add_autoload_map", # explicitly include an autoload map
"set_debug", # enable/disable debug output
"use_memory_analyzer", # enable/disable the memory ownership analyzer
]

import ctypes
Expand Down Expand Up @@ -397,6 +398,11 @@ def set_debug(enable=True):
gbl.Cpp.EnableDebugOutput(enable)


def use_memory_analyzer(enable=True):
"""Enable/disable the memory ownership analyzer."""
_backend.UseMemoryAnalyzer(enable)


def _get_name(tt):
if isinstance(tt, str):
return tt
Expand Down
18 changes: 18 additions & 0 deletions src/cpyrt/CPPClassMethod.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,24 @@ PyObject* cpyrt::CPPClassMethod::Call(CPPInstance*& self, cpyrt_PyArgs_t args,
return this->Execute(nullptr, 0, ctxt);
}

//----------------------------------------------------------------------------
Py_ssize_t cpyrt::CPPClassMethod::GetDeallocArgShift(bool isBound,
PyObject* const* args,
Py_ssize_t nargs) {
// a static method takes no object argument, so a bound call drops self
if (isBound)
return 0;

// Call function above drops a leading instance that the call does not need,
// and then args[0] belongs to no parameter. The check below repeats the one
// in Call(), so a change to either one belongs in both.
if (nargs > 0 && CPPInstance_Check(args[0]) && fArgsRequired <= nargs - 1 &&
interop::IsSubclass(((CPPInstance*)args[0])->ObjectIsA(), GetScope()))
return -1;

return 0;
}

//----------------------------------------------------------------------------
PyObject* cpyrt::CPPClassMethod::GetTypeName() {
PyObject* cppname =
Expand Down
5 changes: 5 additions & 0 deletions src/cpyrt/CPPClassMethod.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ class CPPClassMethod : public CPPMethod {
PyObject* GetTypeName() override;

public:
Py_ssize_t GetDeallocArgShift(bool isBound, PyObject* const* args,
Py_ssize_t nargs) override;
// a static method has no object argument.
Py_ssize_t GetDeallocParamShift() override { return 0; }

PyCallable* Clone() override { return new CPPClassMethod(*this); }
PyObject* Call(CPPInstance*& self, cpyrt_PyArgs_t args, size_t nargsf,
PyObject* kwds, CallContext* ctxt = nullptr) override;
Expand Down
5 changes: 5 additions & 0 deletions src/cpyrt/CPPFunction.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ class CPPFunction : public CPPMethod {
PyObject* GetTypeName() override;

public:
// A free function takes its object as a plain first parameter, so it has no
// implicit this slot. The shift of a bound call stays the one of CPPMethod,
// as AdjustSelf() puts self in that first parameter.
Py_ssize_t GetDeallocParamShift() override { return 0; }

PyCallable* Clone() override { return new CPPFunction(*this); }
PyObject* Call(CPPInstance*& self, cpyrt_PyArgs_t args, size_t nargsf,
PyObject* kwds, CallContext* ctxt = nullptr) override;
Expand Down
33 changes: 32 additions & 1 deletion src/cpyrt/CPPInstance.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ using namespace cppjit;

// Standard
#include <algorithm>
#include <cstdlib>
#include <sstream>

//- data _____________________________________________________________________
Expand Down Expand Up @@ -227,7 +228,17 @@ void cpyrt::op_dealloc_nofree(CPPInstance* pyobj) {
if (pyobj->fFlags & CPPInstance::kIsValue) {
interop::CallDestructor(klass, cppobj);
interop::Deallocate(klass, cppobj);
} else
} else if (pyobj->fFlags & CPPInstance::kIsMalloc)
std::free(cppobj);
else if (pyobj->fFlags & CPPInstance::kIsNoConstruct) {
if (pyobj->fFlags & CPPInstance::kIsArrayAlloc)
::operator delete[](cppobj);
else
::operator delete(cppobj);
} else if (pyobj->fFlags & CPPInstance::kIsArrayAlloc)
interop::Destruct(klass, cppobj, 1);
// Default case: just kIsOwner set in all of memory-ownership flags
else
interop::Destruct(klass, cppobj);
}
cppobj = nullptr;
Expand Down Expand Up @@ -938,10 +949,30 @@ static int op_setownership(CPPInstance* pyobj, PyObject* value, void*) {
return 0;
}

// Added for testing purposes
//-----------------------------------------------------------------------------
static PyObject* op_get_array_alloc(CPPInstance* pyobj, void*) {
return PyBool_FromLong((long)(pyobj->fFlags & CPPInstance::kIsArrayAlloc));
}
//-----------------------------------------------------------------------------
static PyObject* op_get_no_construct(CPPInstance* pyobj, void*) {
return PyBool_FromLong((long)(pyobj->fFlags & CPPInstance::kIsNoConstruct));
}
//-----------------------------------------------------------------------------
static PyObject* op_get_malloc(CPPInstance* pyobj, void*) {
return PyBool_FromLong((long)(pyobj->fFlags & CPPInstance::kIsMalloc));
}
//-----------------------------------------------------------------------------
static PyGetSetDef op_getset[] = {
{(char*)"__python_owns__", (getter)op_getownership, (setter)op_setownership,
(char*)"If true, python manages the life time of this object", nullptr},
{(char*)"__is_array_alloc__", (getter)op_get_array_alloc, nullptr,
(char*)"If true, the object was allocated with new[]/operator new[]",
nullptr},
{(char*)"__is_no_construct__", (getter)op_get_no_construct, nullptr,
(char*)"If true, the memory is raw: no constructor was run", nullptr},
{(char*)"__is_malloc__", (getter)op_get_malloc, nullptr,
(char*)"If true, the object was allocated with malloc", nullptr},
{(char*)nullptr, nullptr, nullptr, nullptr, nullptr}};

//= cpyrt type number stubs to allow dynamic overrides =====================
Expand Down
32 changes: 18 additions & 14 deletions src/cpyrt/CPPInstance.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,24 @@ class CPPInstance {
public:
enum EFlags {
kDefault = 0x0000,
kNoWrapConv = 0x0001, // use type as-is (eg. no smart ptr wrap)
kIsOwner = 0x0002, // Python instance owns C++ object/memory
kIsExtended = 0x0004, // has extended data
kIsValue = 0x0008, // was created from a by-value return
kIsReference = 0x0010, // represents one indirection
kIsArray = 0x0020, // represents an array of objects
kIsSmartPtr = 0x0040, // is or embeds a smart pointer
kIsPtrPtr = 0x0080, // represents two indirections
kIsRValue = 0x0100, // can be used as an r-value
kIsLValue = 0x0200, // can be used as an l-value
kNoMemReg = 0x0400, // do not register with memory regulator
kIsRegulated = 0x0800, // is registered with memory regulator
kIsActual = 0x1000, // has been downcasted to actual type
kHasLifeLine = 0x2000, // has a life line set
kNoWrapConv = 0x0001, // use type as-is (eg. no smart ptr wrap)
kIsOwner = 0x0002, // Python instance owns C++ object/memory
kIsExtended = 0x0004, // has extended data
kIsValue = 0x0008, // was created from a by-value return
kIsReference = 0x0010, // represents one indirection
kIsArray = 0x0020, // represents an array of objects
kIsSmartPtr = 0x0040, // is or embeds a smart pointer
kIsPtrPtr = 0x0080, // represents two indirections
kIsRValue = 0x0100, // can be used as an r-value
kIsLValue = 0x0200, // can be used as an l-value
kNoMemReg = 0x0400, // do not register with memory regulator
kIsRegulated = 0x0800, // is registered with memory regulator
kIsActual = 0x1000, // has been downcasted to actual type
kHasLifeLine = 0x2000, // has a life line set
kIsArrayAlloc = 0x4000, // represents a heap allocated array of objects
kIsNoConstruct =
0x8000, // represents constructor is not called in the allocation
kIsMalloc = 0x10000, // is allocated with malloc
};

public: // public, as the python C-API works with C structs
Expand Down
61 changes: 61 additions & 0 deletions src/cpyrt/CPPMethod.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ extern PyObject* gBusException;
extern PyObject* gSegvException;
extern PyObject* gIllException;
extern PyObject* gAbrtException;
extern bool gUseMemoryAnalyzer;
} // namespace cppjit::cpyrt

//- public helper ------------------------------------------------------------
Expand Down Expand Up @@ -85,6 +86,8 @@ inline void cpyrt::CPPMethod::Copy_(const CPPMethod& /* other */) {
fExecutor = nullptr;
fArgIndices = nullptr;
fArgsRequired = -1;
fAllocType.reset();
fDeallocType.reset();
}

//----------------------------------------------------------------------------
Expand Down Expand Up @@ -703,6 +706,64 @@ PyObject* cpyrt::CPPMethod::GetArgDefault(int iarg, bool silent) {

bool cpyrt::CPPMethod::IsConst() { return interop::IsConstMethod(GetMethod()); }

//----------------------------------------------------------------------------
interop::AllocType cpyrt::CPPMethod::GetAllocBehaviour() {
if (fAllocType.has_value())
return *fAllocType;
interop::TCppMethod_t method = GetMethod();
if (!method) {
fAllocType = interop::AllocType::None;
return interop::AllocType::None;
}
if (!IsPointerType(interop::GetMethodReturnType(method))) {
fAllocType = interop::AllocType::None;
return interop::AllocType::None;
}
interop::AllocType attrResult = interop::IsAllocator(method);
// an attribute always wins, so ask the analyzer when there is no
// relevant attribute
if (attrResult == interop::AllocType::Unknown && gUseMemoryAnalyzer)
attrResult = interop::GetAllocType(method);
fAllocType = attrResult;
return attrResult;
}

//----------------------------------------------------------------------------
void cpyrt::CPPMethod::FillDeallocBehaviour() {
fDeallocType.emplace();
interop::TCppMethod_t method = GetMethod();
if (!method)
return;

interop::IsDeallocator(method, *fDeallocType);
if (!gUseMemoryAnalyzer)
return;

std::vector<interop::DeallocType> analyzeRes;
if (!interop::GetDeallocType(method, analyzeRes))
return;

// IsDeallocator puts the implicit this in slot 0, GetDeallocType does not,
// so each analyzer answer moves up by one slot for a method
const size_t shift = (size_t)GetDeallocParamShift();
assert(analyzeRes.size() + shift == fDeallocType->size() &&
"the analyzer answer must line up with the attribute slots");
for (size_t i = 0; i < analyzeRes.size(); i++) {
// an attribute always wins, so fill only the slots there is no attribute
if ((*fDeallocType)[i + shift] == interop::DeallocType::Opaque)
(*fDeallocType)[i + shift] = analyzeRes[i];
}
}

//----------------------------------------------------------------------------
interop::DeallocType cpyrt::CPPMethod::GetDeallocBehaviour(size_t index) {
if (!fDeallocType)
FillDeallocBehaviour();
if (index < fDeallocType->size())
return (*fDeallocType)[index];
return interop::DeallocType::Opaque;
}

//----------------------------------------------------------------------------
PyObject* cpyrt::CPPMethod::GetScopeProxy() {
// Get or build the scope of this method.
Expand Down
16 changes: 16 additions & 0 deletions src/cpyrt/CPPMethod.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "PyCallable.h"

// Standard
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
Expand Down Expand Up @@ -62,6 +63,17 @@ class CPPMethod : public PyCallable {
PyObject* GetCoVarNames() override;
PyObject* GetArgDefault(int iarg, bool silent = true) override;
bool IsConst() override;
interop::AllocType GetAllocBehaviour() override;
interop::DeallocType GetDeallocBehaviour(size_t index) override;
// A bound call passes self on its own, so args[0] lines up with the second
// slot. A direct call goes through the vector call, which skips the
// descriptor, and then self sits in args[0] itself.
Py_ssize_t GetDeallocArgShift(bool isBound, PyObject* const* /* args */,
Py_ssize_t /* nargs */) override {
return isBound ? 1 : 0;
}
// the first slot belongs to the implicit object argument of the method
Py_ssize_t GetDeallocParamShift() override { return 1; }

PyObject* GetScopeProxy() override;
interop::TCppFuncAddr_t GetFunctionAddress() override;
Expand Down Expand Up @@ -106,6 +118,8 @@ class CPPMethod : public PyCallable {

void SetPyError_(PyObject* msg);

void FillDeallocBehaviour();

private:
// representation
interop::TCppMethod_t fMethod;
Expand All @@ -121,6 +135,8 @@ class CPPMethod : public PyCallable {
protected:
// cached value that doubles as initialized flag (uninitialized if -1)
int fArgsRequired;
std::optional<interop::AllocType> fAllocType;
std::optional<std::vector<interop::DeallocType>> fDeallocType;
};

} // namespace cppjit::cpyrt
Expand Down
Loading
Loading