Skip to content
Merged
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
34 changes: 29 additions & 5 deletions autowrap/CodeGenerator.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,18 +671,33 @@ def create_wrapper_for_class(self, r_class: ResolvedClass, out_codes: CodeDict)
)

if len(r_class.wrap_hash) != 0:
class_code.add(
"""
hash_expr = r_class.wrap_hash[0].strip()
# If hash expression is "std" or empty, use std::hash<T>
if hash_expr.lower() == "std" or not hash_expr:
class_code.add(
"""
|
| def __hash__(self):
| # Uses C++ std::hash<$cy_type> specialization
| cdef cpp_hash[$cy_type] hasher
| return hasher(deref(self.inst.get()))
|
""",
locals(),
)
else:
class_code.add(
"""
|
| def __hash__(self):
| # The only required property is that objects which compare equal have
| # the same hash value:
| return hash(deref(self.inst.get()).%s )
|
"""
% r_class.wrap_hash[0],
locals(),
)
% hash_expr,
locals(),
)

if len(r_class.wrap_len) != 0:
class_code.add(
Expand Down Expand Up @@ -2089,6 +2104,15 @@ def create_default_cimports(self):
|from libcpp.memory cimport shared_ptr
"""
)
# Add std::hash declaration for wrap-hash support (named cpp_hash to avoid conflict with Python hash)
code.add(
"""
|cdef extern from "<functional>" namespace "std" nogil:
| cdef cppclass cpp_hash "std::hash" [T]:
| cpp_hash() except +
| size_t operator()(const T&) except +
"""
)
if self.include_numpy:
code.add(
"""
Expand Down
45 changes: 40 additions & 5 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,15 @@ directives are:
`__dealloc__` and inst attribute (but their presence is still expected).
This is useful if you cannot use the shared-ptr approach to store a reference
to the C++ class (as with singletons for example).
- `wrap-hash`: If the produced class should be hashable, give a hint which
method should be used for this. This method will be called on the C++ object
and fed into the Python "hash" function. This implies the class also provides
a `operator==` function. Note that the only requirement for a hash function is
that equal objects produce equal values.
- `wrap-hash`: If the produced class should be hashable, specify how to compute
the hash. This implies the class also provides an `operator==` function. Note
that the only requirement for a hash function is that equal objects produce
equal values. There are two modes:
- **Expression-based**: Provide a C++ expression (e.g., `getValue()`) that will
be called on the C++ object and fed into Python's `hash()` function.
- **std::hash-based**: Use `std` to leverage the C++ `std::hash<T>` template
specialization for the class. This requires that `std::hash<YourClass>` is
specialized in your C++ code.
- `wrap-with-no-gil`: Autowrap will release the GIL (Global interpreter lock)
before calling this method, so that it does not block other Python threads.
It is advised to release the GIL for long running, expensive calls into
Expand Down Expand Up @@ -245,6 +249,37 @@ Additionally, TemplatedClass[U,V] gets additional methods from C[U] and from D w

Finally, the object is hashable in Python (assuming it has a function `getName()` that returns a string).

#### wrap-hash Examples

**Expression-based hash** (calls a method and passes result to Python's `hash()`):

```
cdef cppclass MyClass:
# wrap-hash:
# getValue()
```

**std::hash-based hash** (uses C++ `std::hash<MyClass>` specialization):

```
cdef cppclass MyClass:
# wrap-hash:
# std
```

For the `std` mode to work, you need to provide a `std::hash` specialization in your C++ code:

```cpp
namespace std {
template <>
struct hash<MyClass> {
size_t operator()(const MyClass& obj) const noexcept {
return std::hash<int>{}(obj.getValue());
}
};
}
```

### Docstrings

Docstrings can be added to classes and methods using the `wrap-doc` statement. Multi line docs are supported with
Expand Down
95 changes: 95 additions & 0 deletions tests/test_files/hash_test.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#ifndef HASH_TEST_HPP
#define HASH_TEST_HPP

#include <string>
#include <functional>

// Class that uses expression-based hash (old behavior)
class ExprHashClass {
private:
int value_;
std::string name_;

public:
ExprHashClass() : value_(0), name_("default") {}
ExprHashClass(int value, const std::string& name) : value_(value), name_(name) {}
ExprHashClass(const ExprHashClass& other) : value_(other.value_), name_(other.name_) {}

int getValue() const { return value_; }
std::string getName() const { return name_; }

bool operator==(const ExprHashClass& other) const {
return value_ == other.value_ && name_ == other.name_;
}
bool operator!=(const ExprHashClass& other) const {
return !(*this == other);
}
};

// Class that uses std::hash (new behavior)
class StdHashClass {
private:
int id_;
std::string label_;

public:
StdHashClass() : id_(0), label_("") {}
StdHashClass(int id, const std::string& label) : id_(id), label_(label) {}
StdHashClass(const StdHashClass& other) : id_(other.id_), label_(other.label_) {}

int getId() const { return id_; }
std::string getLabel() const { return label_; }

bool operator==(const StdHashClass& other) const {
return id_ == other.id_ && label_ == other.label_;
}
bool operator!=(const StdHashClass& other) const {
return !(*this == other);
}
};

// std::hash specialization for StdHashClass
namespace std {
template <>
struct hash<StdHashClass> {
size_t operator()(const StdHashClass& obj) const noexcept {
// Combine id and label hashes
size_t h1 = std::hash<int>{}(obj.getId());
size_t h2 = std::hash<std::string>{}(obj.getLabel());
return h1 ^ (h2 << 1);
}
};
}

// Another class with std::hash to test template instantiations
template <typename T>
class TemplatedHashClass {
private:
T data_;

public:
TemplatedHashClass() : data_() {}
TemplatedHashClass(const T& data) : data_(data) {}
TemplatedHashClass(const TemplatedHashClass& other) : data_(other.data_) {}

T getData() const { return data_; }

bool operator==(const TemplatedHashClass<T>& other) const {
return data_ == other.data_;
}
bool operator!=(const TemplatedHashClass<T>& other) const {
return !(*this == other);
}
};

// std::hash specialization for TemplatedHashClass<int>
namespace std {
template <>
struct hash<TemplatedHashClass<int>> {
size_t operator()(const TemplatedHashClass<int>& obj) const noexcept {
return std::hash<int>{}(obj.getData());
}
};
}

#endif // HASH_TEST_HPP
54 changes: 54 additions & 0 deletions tests/test_files/hash_test.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# cython: language_level=3
from libcpp.string cimport string as libcpp_string
from libcpp cimport bool

cdef extern from "hash_test.hpp":

# Test case 1: Expression-based hash (old behavior, regression test)
cdef cppclass ExprHashClass:
# wrap-hash:
# getValue()
#
ExprHashClass()
ExprHashClass(int value, libcpp_string name)
ExprHashClass(ExprHashClass) # wrap-ignore

int getValue()
libcpp_string getName()

bool operator==(ExprHashClass)
bool operator!=(ExprHashClass)


# Test case 2: std::hash-based hash (new behavior)
cdef cppclass StdHashClass:
# wrap-hash:
# std
#
StdHashClass()
StdHashClass(int id, libcpp_string label)
StdHashClass(StdHashClass) # wrap-ignore

int getId()
libcpp_string getLabel()

bool operator==(StdHashClass)
bool operator!=(StdHashClass)


# Test case 3: Template class with std::hash
cdef cppclass TemplatedHashClass[T]:
# wrap-hash:
# std
#
# wrap-instances:
# TemplatedHashInt := TemplatedHashClass[int]
#
TemplatedHashClass()
TemplatedHashClass(T data)
TemplatedHashClass(TemplatedHashClass[T]) # wrap-ignore

T getData()

bool operator==(TemplatedHashClass[T])
bool operator!=(TemplatedHashClass[T])
Loading