]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-111545: Add Py_HashPointer() function (#112096)
authorVictor Stinner <vstinner@python.org>
Wed, 6 Dec 2023 14:09:22 +0000 (15:09 +0100)
committerGitHub <noreply@github.com>
Wed, 6 Dec 2023 14:09:22 +0000 (15:09 +0100)
* Implement _Py_HashPointerRaw() as a static inline function.
* Add Py_HashPointer() tests to test_capi.test_hash.
* Keep _Py_HashPointer() function as an alias to Py_HashPointer().

Doc/c-api/hash.rst
Doc/whatsnew/3.13.rst
Include/cpython/pyhash.h
Include/internal/pycore_pyhash.h
Lib/test/test_capi/test_hash.py
Misc/NEWS.d/next/C API/2023-11-15-01-26-59.gh-issue-111545.iAoFtA.rst [new file with mode: 0644]
Modules/_testcapi/hash.c
Python/hashtable.c
Python/pyhash.c

index 3bfaf8b9f54c14f25aadde878492fd4b87a580c6..91d88ae27bc9f493674c347cb96ae2bc75b1e212 100644 (file)
@@ -49,3 +49,13 @@ See also the :c:member:`PyTypeObject.tp_hash` member.
       :pep:`456` "Secure and interchangeable hash algorithm".
 
    .. versionadded:: 3.4
+
+
+.. c:function:: Py_hash_t Py_HashPointer(const void *ptr)
+
+   Hash a pointer value: process the pointer value as an integer (cast it to
+   ``uintptr_t`` internally). The pointer is not dereferenced.
+
+   The function cannot fail: it cannot return ``-1``.
+
+   .. versionadded:: 3.13
index 534813f3659c9d945bab89e117d36f91bee9e150..c9facad6375ef3f151afbb4825de5f53aff83dbc 100644 (file)
@@ -1249,6 +1249,9 @@ New Features
   :exc:`KeyError` if the key missing.
   (Contributed by Stefan Behnel and Victor Stinner in :gh:`111262`.)
 
+* Add :c:func:`Py_HashPointer` function to hash a pointer.
+  (Contributed by Victor Stinner in :gh:`111545`.)
+
 
 Porting to Python 3.13
 ----------------------
index 6f7113daa5fe4de8c701e0df301a970831c681d7..396c208e1b106a93b18f79d0f9577ec88d72adb3 100644 (file)
@@ -21,7 +21,9 @@
 
 /* Helpers for hash functions */
 PyAPI_FUNC(Py_hash_t) _Py_HashDouble(PyObject *, double);
-PyAPI_FUNC(Py_hash_t) _Py_HashPointer(const void*);
+
+// Kept for backward compatibility
+#define _Py_HashPointer Py_HashPointer
 
 
 /* hash function definition */
@@ -33,3 +35,5 @@ typedef struct {
 } PyHash_FuncDef;
 
 PyAPI_FUNC(PyHash_FuncDef*) PyHash_GetFuncDef(void);
+
+PyAPI_FUNC(Py_hash_t) Py_HashPointer(const void *ptr);
index c3b72d90de3a694c29feb07db07c394c7d21972b..0ce08900e96f0b28b0f2c34a0151ccbe2906798b 100644 (file)
@@ -5,8 +5,20 @@
 #  error "this header requires Py_BUILD_CORE define"
 #endif
 
-// Similar to _Py_HashPointer(), but don't replace -1 with -2
-extern Py_hash_t _Py_HashPointerRaw(const void*);
+// Similar to Py_HashPointer(), but don't replace -1 with -2.
+static inline Py_hash_t
+_Py_HashPointerRaw(const void *ptr)
+{
+    uintptr_t x = (uintptr_t)ptr;
+    Py_BUILD_ASSERT(sizeof(x) == sizeof(ptr));
+
+    // Bottom 3 or 4 bits are likely to be 0; rotate x by 4 to the right
+    // to avoid excessive hash collisions for dicts and sets.
+    x = (x >> 4) | (x << (8 * sizeof(uintptr_t) - 4));
+
+    Py_BUILD_ASSERT(sizeof(x) == sizeof(Py_hash_t));
+    return (Py_hash_t)x;
+}
 
 // Export for '_datetime' shared extension
 PyAPI_FUNC(Py_hash_t) _Py_HashBytes(const void*, Py_ssize_t);
index 59dec15bc21445fd3d600394b051792ec409c384..8436da7c32df10fce13f9663cfc5fabe46ebc6ea 100644 (file)
@@ -4,7 +4,8 @@ from test.support import import_helper
 _testcapi = import_helper.import_module('_testcapi')
 
 
-SIZEOF_PY_HASH_T = _testcapi.SIZEOF_VOID_P
+SIZEOF_VOID_P = _testcapi.SIZEOF_VOID_P
+SIZEOF_PY_HASH_T = SIZEOF_VOID_P
 
 
 class CAPITest(unittest.TestCase):
@@ -31,3 +32,48 @@ class CAPITest(unittest.TestCase):
         self.assertEqual(func_def.name, hash_info.algorithm)
         self.assertEqual(func_def.hash_bits, hash_info.hash_bits)
         self.assertEqual(func_def.seed_bits, hash_info.seed_bits)
+
+    def test_hash_pointer(self):
+        # Test Py_HashPointer()
+        hash_pointer = _testcapi.hash_pointer
+
+        UHASH_T_MASK = ((2 ** (8 * SIZEOF_PY_HASH_T)) - 1)
+        HASH_T_MAX = (2 ** (8 * SIZEOF_PY_HASH_T - 1) - 1)
+
+        def python_hash_pointer(x):
+            # Py_HashPointer() rotates the pointer bits by 4 bits to the right
+            x = (x >> 4) | ((x & 15) << (8 * SIZEOF_VOID_P - 4))
+
+            # Convert unsigned uintptr_t (Py_uhash_t) to signed Py_hash_t
+            if HASH_T_MAX < x:
+                x = (~x) + 1
+                x &= UHASH_T_MASK
+                x = (~x) + 1
+            return x
+
+        if SIZEOF_VOID_P == 8:
+            values = (
+                0xABCDEF1234567890,
+                0x1234567890ABCDEF,
+                0xFEE4ABEDD1CECA5E,
+            )
+        else:
+            values = (
+                0x12345678,
+                0x1234ABCD,
+                0xDEADCAFE,
+            )
+
+        for value in values:
+            expected = python_hash_pointer(value)
+            with self.subTest(value=value):
+                self.assertEqual(hash_pointer(value), expected,
+                                 f"hash_pointer({value:x}) = "
+                                 f"{hash_pointer(value):x} != {expected:x}")
+
+        # Py_HashPointer(NULL) returns 0
+        self.assertEqual(hash_pointer(0), 0)
+
+        # Py_HashPointer((void*)(uintptr_t)-1) doesn't return -1 but -2
+        VOID_P_MAX = -1 & (2 ** (8 * SIZEOF_VOID_P) - 1)
+        self.assertEqual(hash_pointer(VOID_P_MAX), -2)
diff --git a/Misc/NEWS.d/next/C API/2023-11-15-01-26-59.gh-issue-111545.iAoFtA.rst b/Misc/NEWS.d/next/C API/2023-11-15-01-26-59.gh-issue-111545.iAoFtA.rst
new file mode 100644 (file)
index 0000000..7bde249
--- /dev/null
@@ -0,0 +1,2 @@
+Add :c:func:`Py_HashPointer` function to hash a pointer. Patch by Victor
+Stinner.
index d0b8127020c5c1454752c7a62f47074ec6bc6e7d..aee76787dcddb3b4e238d1e66778013115047231 100644 (file)
@@ -44,8 +44,24 @@ hash_getfuncdef(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
     return result;
 }
 
+
+static PyObject *
+hash_pointer(PyObject *Py_UNUSED(module), PyObject *arg)
+{
+    void *ptr = PyLong_AsVoidPtr(arg);
+    if (ptr == NULL && PyErr_Occurred()) {
+        return NULL;
+    }
+
+    Py_hash_t hash = Py_HashPointer(ptr);
+    Py_BUILD_ASSERT(sizeof(long long) >= sizeof(hash));
+    return PyLong_FromLongLong(hash);
+}
+
+
 static PyMethodDef test_methods[] = {
     {"hash_getfuncdef", hash_getfuncdef, METH_NOARGS},
+    {"hash_pointer", hash_pointer, METH_O},
     {NULL},
 };
 
index 8f5e8168ba1339ece71e0880129819acd4039cdf..faf68fe4ff0bca17f5bc6edc005100ffcbafb85b 100644 (file)
@@ -45,7 +45,7 @@
 */
 
 #include "Python.h"
-#include "pycore_hashtable.h"
+#include "pycore_hashtable.h"     // export _Py_hashtable_new()
 #include "pycore_pyhash.h"        // _Py_HashPointerRaw()
 
 #define HASHTABLE_MIN_SIZE 16
index f9060b8003a0a7d927d3d6790b045fb11639eb9c..141407c265677a147e866937282160d862c9f0c6 100644 (file)
@@ -83,8 +83,6 @@ static Py_ssize_t hashstats[Py_HASH_STATS_MAX + 1] = {0};
 
    */
 
-Py_hash_t _Py_HashPointer(const void *);
-
 Py_hash_t
 _Py_HashDouble(PyObject *inst, double v)
 {
@@ -132,23 +130,13 @@ _Py_HashDouble(PyObject *inst, double v)
 }
 
 Py_hash_t
-_Py_HashPointerRaw(const void *p)
-{
-    size_t y = (size_t)p;
-    /* bottom 3 or 4 bits are likely to be 0; rotate y by 4 to avoid
-       excessive hash collisions for dicts and sets */
-    y = (y >> 4) | (y << (8 * SIZEOF_VOID_P - 4));
-    return (Py_hash_t)y;
-}
-
-Py_hash_t
-_Py_HashPointer(const void *p)
+Py_HashPointer(const void *ptr)
 {
-    Py_hash_t x = _Py_HashPointerRaw(p);
-    if (x == -1) {
-        x = -2;
+    Py_hash_t hash = _Py_HashPointerRaw(ptr);
+    if (hash == -1) {
+        hash = -2;
     }
-    return x;
+    return hash;
 }
 
 Py_hash_t