diff --git a/autowrap/CodeGenerator.py b/autowrap/CodeGenerator.py index 5fa60fa..50d9fd5 100644 --- a/autowrap/CodeGenerator.py +++ b/autowrap/CodeGenerator.py @@ -671,8 +671,23 @@ 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 + 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 @@ -680,9 +695,9 @@ def create_wrapper_for_class(self, r_class: ResolvedClass, out_codes: CodeDict) | 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( @@ -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 "" 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( """ diff --git a/docs/README.md b/docs/README.md index 6d47d38..a35e1e8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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` template + specialization for the class. This requires that `std::hash` 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 @@ -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` 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 { + size_t operator()(const MyClass& obj) const noexcept { + return std::hash{}(obj.getValue()); + } + }; +} +``` + ### Docstrings Docstrings can be added to classes and methods using the `wrap-doc` statement. Multi line docs are supported with diff --git a/tests/test_files/hash_test.hpp b/tests/test_files/hash_test.hpp new file mode 100644 index 0000000..e638cdd --- /dev/null +++ b/tests/test_files/hash_test.hpp @@ -0,0 +1,95 @@ +#ifndef HASH_TEST_HPP +#define HASH_TEST_HPP + +#include +#include + +// 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 { + size_t operator()(const StdHashClass& obj) const noexcept { + // Combine id and label hashes + size_t h1 = std::hash{}(obj.getId()); + size_t h2 = std::hash{}(obj.getLabel()); + return h1 ^ (h2 << 1); + } + }; +} + +// Another class with std::hash to test template instantiations +template +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& other) const { + return data_ == other.data_; + } + bool operator!=(const TemplatedHashClass& other) const { + return !(*this == other); + } +}; + +// std::hash specialization for TemplatedHashClass +namespace std { + template <> + struct hash> { + size_t operator()(const TemplatedHashClass& obj) const noexcept { + return std::hash{}(obj.getData()); + } + }; +} + +#endif // HASH_TEST_HPP diff --git a/tests/test_files/hash_test.pxd b/tests/test_files/hash_test.pxd new file mode 100644 index 0000000..c930355 --- /dev/null +++ b/tests/test_files/hash_test.pxd @@ -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]) diff --git a/tests/test_hash.py b/tests/test_hash.py new file mode 100644 index 0000000..d32e096 --- /dev/null +++ b/tests/test_hash.py @@ -0,0 +1,228 @@ +from __future__ import print_function +from __future__ import absolute_import + +import pytest + +__license__ = """ + +Copyright (c) 2012-2014, Uwe Schmitt, ETH Zurich, all rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the mineway GmbH nor the names of its contributors may be +used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +import autowrap.DeclResolver +import autowrap.CodeGenerator +import autowrap.PXDParser +import autowrap.Utils +import autowrap.Code +import autowrap + +import os + +test_files = os.path.join(os.path.dirname(__file__), "test_files") + + +def test_hash_functionality(): + """Test wrap-hash directive with both expression-based and std::hash approaches""" + # Use a different module name than the pxd file to avoid Cython conflicts + target = os.path.join(test_files, "generated", "hash_test_wrapper.pyx") + + include_dirs = autowrap.parse_and_generate_code( + ["hash_test.pxd"], root=test_files, target=target, debug=True + ) + + hash_mod = autowrap.Utils.compile_and_import( + "hash_test_wrapper", + [target], + include_dirs, + ) + assert hash_mod.__name__ == "hash_test_wrapper" + print(dir(hash_mod)) + + # Test expression-based hash (old behavior - regression test) + sub_test_expr_hash(hash_mod) + + # Test std::hash-based hash (new behavior) + sub_test_std_hash(hash_mod) + + # Test templated class with std::hash + sub_test_templated_hash(hash_mod) + + +def sub_test_expr_hash(hash_mod): + """Test expression-based hash (old behavior, regression test)""" + print("Testing expression-based hash (ExprHashClass)...") + + # Create instances with different values + obj1 = hash_mod.ExprHashClass(42, b"test") + obj2 = hash_mod.ExprHashClass(42, b"test") + obj3 = hash_mod.ExprHashClass(100, b"other") + + # Test __hash__ is defined + assert hasattr(obj1, "__hash__"), "ExprHashClass should have __hash__ method" + + # Test hash() works + h1 = hash(obj1) + h2 = hash(obj2) + h3 = hash(obj3) + + # Equal objects should have same hash (same getValue()) + # Note: obj1 and obj2 have same value (42), so hash should be the same + assert h1 == h2, f"Equal objects should have equal hash: {h1} != {h2}" + + # Different objects can have different hashes + assert h3 != h1, f"Different values should have different hashes: {h3} == {h1}" + + # Test using in set + s = {obj1, obj2} + assert len(s) == 1, f"Set should dedupe equal objects, got {len(s)}" + + s.add(obj3) + assert len(s) == 2, f"Set should have 2 different objects, got {len(s)}" + + # Test using as dict key + d = {obj1: "first", obj3: "second"} + assert len(d) == 2, f"Dict should have 2 entries, got {len(d)}" + assert d[obj1] == "first" + assert d[obj3] == "second" + + # Using obj2 (equal to obj1) should find the same entry + assert d[obj2] == "first", "Equal object should find same dict entry" + + print(" Expression-based hash tests passed!") + + +def sub_test_std_hash(hash_mod): + """Test std::hash-based hash (new behavior)""" + print("Testing std::hash-based hash (StdHashClass)...") + + # Create instances with different values + obj1 = hash_mod.StdHashClass(1, b"label1") + obj2 = hash_mod.StdHashClass(1, b"label1") + obj3 = hash_mod.StdHashClass(2, b"label2") + + # Test __hash__ is defined + assert hasattr(obj1, "__hash__"), "StdHashClass should have __hash__ method" + + # Test hash() works + h1 = hash(obj1) + h2 = hash(obj2) + h3 = hash(obj3) + + # Equal objects should have same hash + assert h1 == h2, f"Equal objects should have equal hash: {h1} != {h2}" + + # Different objects should (likely) have different hashes + # Note: Not guaranteed by hash contract, but expected for this implementation + assert h3 != h1, f"Different values should have different hashes: {h3} == {h1}" + + # Test using in set + s = {obj1, obj2} + assert len(s) == 1, f"Set should dedupe equal objects, got {len(s)}" + + s.add(obj3) + assert len(s) == 2, f"Set should have 2 different objects, got {len(s)}" + + # Test using as dict key + d = {obj1: "first", obj3: "second"} + assert len(d) == 2, f"Dict should have 2 entries, got {len(d)}" + assert d[obj1] == "first" + assert d[obj3] == "second" + + # Using obj2 (equal to obj1) should find the same entry + assert d[obj2] == "first", "Equal object should find same dict entry" + + print(" std::hash-based hash tests passed!") + + +def sub_test_templated_hash(hash_mod): + """Test templated class with std::hash""" + print("Testing templated class with std::hash (TemplatedHashInt)...") + + # Create instances with different values + obj1 = hash_mod.TemplatedHashInt(42) + obj2 = hash_mod.TemplatedHashInt(42) + obj3 = hash_mod.TemplatedHashInt(100) + + # Test __hash__ is defined + assert hasattr(obj1, "__hash__"), "TemplatedHashInt should have __hash__ method" + + # Test hash() works + h1 = hash(obj1) + h2 = hash(obj2) + h3 = hash(obj3) + + # Equal objects should have same hash + assert h1 == h2, f"Equal objects should have equal hash: {h1} != {h2}" + + # Different objects should have different hashes + assert h3 != h1, f"Different values should have different hashes: {h3} == {h1}" + + # Test using in set + s = {obj1, obj2} + assert len(s) == 1, f"Set should dedupe equal objects, got {len(s)}" + + s.add(obj3) + assert len(s) == 2, f"Set should have 2 different objects, got {len(s)}" + + # Test using as dict key + d = {obj1: "first", obj3: "second"} + assert len(d) == 2, f"Dict should have 2 entries, got {len(d)}" + + print(" Templated class std::hash tests passed!") + + +def test_hash_consistency(): + """Test that hash values are consistent across multiple calls""" + target = os.path.join(test_files, "generated", "hash_test_wrapper.pyx") + + include_dirs = autowrap.parse_and_generate_code( + ["hash_test.pxd"], root=test_files, target=target, debug=True + ) + + hash_mod = autowrap.Utils.compile_and_import( + "hash_test_wrapper", + [target], + include_dirs, + ) + + # Test expression-based hash consistency + expr_obj = hash_mod.ExprHashClass(42, b"test") + h1 = hash(expr_obj) + h2 = hash(expr_obj) + assert h1 == h2, "Hash should be consistent across calls" + + # Test std::hash consistency + std_obj = hash_mod.StdHashClass(42, b"test") + h3 = hash(std_obj) + h4 = hash(std_obj) + assert h3 == h4, "Hash should be consistent across calls" + + # Test templated hash consistency + tmpl_obj = hash_mod.TemplatedHashInt(42) + h5 = hash(tmpl_obj) + h6 = hash(tmpl_obj) + assert h5 == h6, "Hash should be consistent across calls"