fastconstmap 0.1.0__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {fastconstmap-0.1.0 → fastconstmap-0.7.0}/MANIFEST.in +3 -0
  2. {fastconstmap-0.1.0/fastconstmap.egg-info → fastconstmap-0.7.0}/PKG-INFO +74 -1
  3. {fastconstmap-0.1.0 → fastconstmap-0.7.0}/README.md +73 -0
  4. fastconstmap-0.7.0/benchmarks/benchmark.py +155 -0
  5. fastconstmap-0.7.0/examples/example.py +25 -0
  6. {fastconstmap-0.1.0 → fastconstmap-0.7.0}/fastconstmap/__init__.py +4 -2
  7. {fastconstmap-0.1.0 → fastconstmap-0.7.0}/fastconstmap/_fastconstmap.pyi +13 -4
  8. {fastconstmap-0.1.0 → fastconstmap-0.7.0/fastconstmap.egg-info}/PKG-INFO +74 -1
  9. {fastconstmap-0.1.0 → fastconstmap-0.7.0}/fastconstmap.egg-info/SOURCES.txt +2 -0
  10. {fastconstmap-0.1.0 → fastconstmap-0.7.0}/pyproject.toml +5 -2
  11. {fastconstmap-0.1.0 → fastconstmap-0.7.0}/src/constmap.c +82 -3
  12. {fastconstmap-0.1.0 → fastconstmap-0.7.0}/src/constmap.h +19 -0
  13. {fastconstmap-0.1.0 → fastconstmap-0.7.0}/src/fastconstmap_module.c +148 -20
  14. {fastconstmap-0.1.0 → fastconstmap-0.7.0}/LICENSE +0 -0
  15. {fastconstmap-0.1.0 → fastconstmap-0.7.0}/fastconstmap/py.typed +0 -0
  16. {fastconstmap-0.1.0 → fastconstmap-0.7.0}/fastconstmap.egg-info/dependency_links.txt +0 -0
  17. {fastconstmap-0.1.0 → fastconstmap-0.7.0}/fastconstmap.egg-info/top_level.txt +0 -0
  18. {fastconstmap-0.1.0 → fastconstmap-0.7.0}/setup.cfg +0 -0
  19. {fastconstmap-0.1.0 → fastconstmap-0.7.0}/setup.py +0 -0
  20. {fastconstmap-0.1.0 → fastconstmap-0.7.0}/src/third_party/xxhash/LICENSE +0 -0
  21. {fastconstmap-0.1.0 → fastconstmap-0.7.0}/src/third_party/xxhash/xxhash.h +0 -0
  22. {fastconstmap-0.1.0 → fastconstmap-0.7.0}/tests/test_constmap.py +0 -0
@@ -5,3 +5,6 @@ include setup.py
5
5
  recursive-include src *.c *.h
6
6
  recursive-include src/third_party *
7
7
  recursive-include fastconstmap *.py *.pyi py.typed
8
+ recursive-include tests *.py
9
+ recursive-include examples *.py
10
+ recursive-include benchmarks *.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastconstmap
3
- Version: 0.1.0
3
+ Version: 0.7.0
4
4
  Summary: Fast, immutable, compact map from strings to uint64 (binary fuse filter).
5
5
  Author-email: Daniel Lemire <daniel@lemire.me>
6
6
  License: Apache-2.0
@@ -19,6 +19,7 @@ Dynamic: license-file
19
19
  # fastconstmap
20
20
 
21
21
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
22
+ [![Test](https://github.com/lemire/fastconstmap/actions/workflows/test.yml/badge.svg)](https://github.com/lemire/fastconstmap/actions/workflows/test.yml)
22
23
 
23
24
  Fast, immutable, compact map from strings to 64-bit integers — for Python.
24
25
 
@@ -101,6 +102,78 @@ Keys must be unique (Python dict semantics already guarantee this).
101
102
  Construction raises `ValueError` in the extremely unlikely event of an
102
103
  xxhash collision (~2⁻⁶⁴ per key pair).
103
104
 
105
+ ## Sharing a map across processes (zero-copy)
106
+
107
+ A map's serialized form *is* its in-memory lookup array (plus a small
108
+ header). That means it can live in a
109
+ [`multiprocessing.shared_memory`](https://docs.python.org/3/library/multiprocessing.shared_memory.html)
110
+ block and be opened by any number of processes **without copying** — every
111
+ process reads the same physical pages.
112
+
113
+ Three methods make this work:
114
+
115
+ | Method | Purpose |
116
+ |--|--|
117
+ | `m.serialized_size()` | bytes needed to hold the serialized map |
118
+ | `m.write_into(buffer)` | serialize straight into a writable buffer (no intermediate `bytes`) |
119
+ | `ConstMap.from_buffer(buffer)` | open a **zero-copy** map that reads directly from `buffer` |
120
+
121
+ Producer — build once, publish into a **named** shared-memory block:
122
+
123
+ ```python
124
+ from multiprocessing.shared_memory import SharedMemory
125
+ from fastconstmap import ConstMap
126
+
127
+ SHM_NAME = "fastconstmap_demo"
128
+
129
+ cm = ConstMap({f"key-{i}": i for i in range(1_000_000)})
130
+
131
+ shm = SharedMemory(create=True, size=cm.serialized_size(), name=SHM_NAME)
132
+ cm.write_into(shm.buf)
133
+ # keep `shm` alive (do not close/unlink) while consumers are running
134
+ ```
135
+
136
+ Consumer — attach to the same name with no copy:
137
+
138
+ ```python
139
+ from multiprocessing.shared_memory import SharedMemory
140
+ from fastconstmap import ConstMap
141
+
142
+ SHM_NAME = "fastconstmap_demo"
143
+
144
+ shm = SharedMemory(name=SHM_NAME) # attach by name, no `create=`
145
+ cm = ConstMap.from_buffer(shm.buf) # zero-copy: no per-process copy
146
+ cm["key-42"] # reads straight from shared memory
147
+ ```
148
+
149
+ Choosing the name yourself (rather than letting `SharedMemory` generate
150
+ one) means consumers can hard-code it or read it from config — no need to
151
+ pass the auto-generated name around. Pick a unique name; creating a block
152
+ whose name already exists raises `FileExistsError`.
153
+
154
+ Notes and constraints:
155
+
156
+ - **`from_buffer` does not copy.** The returned map holds a reference to the
157
+ buffer; the buffer (and, for shared memory, the `SharedMemory` object)
158
+ must stay alive and **must not be closed or mutated** while the map is in
159
+ use. Drop the map (`del cm`) before calling `shm.close()`.
160
+ - The map is **immutable** — the intended pattern is *write once in the
161
+ producer, then only read in every process*. Concurrent readers need no
162
+ locking.
163
+ - `from_buffer` verifies the magic bytes and the FNV-1a checksum, so a
164
+ truncated or corrupt block raises `ValueError` rather than returning
165
+ garbage.
166
+ - Requirements: a **little-endian** host (x86-64, ARM64, …) and an
167
+ **8-byte-aligned** buffer. `SharedMemory.buf`, `bytes`, and `bytearray`
168
+ all satisfy the alignment requirement; an offset slice of a buffer may
169
+ not, in which case `from_buffer` raises `ValueError` — use
170
+ `from_bytes()` (which copies) instead.
171
+ - `VerifiedConstMap` supports the same three methods.
172
+
173
+ `from_bytes()` remains available when you *want* an owned copy (or need to
174
+ load on a big-endian host): it copies the data and the resulting map owns
175
+ its memory independently of the source buffer.
176
+
104
177
  ## Benchmark
105
178
 
106
179
  On an Apple M-series CPU, with 1,000,000 string keys
@@ -1,6 +1,7 @@
1
1
  # fastconstmap
2
2
 
3
3
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
4
+ [![Test](https://github.com/lemire/fastconstmap/actions/workflows/test.yml/badge.svg)](https://github.com/lemire/fastconstmap/actions/workflows/test.yml)
4
5
 
5
6
  Fast, immutable, compact map from strings to 64-bit integers — for Python.
6
7
 
@@ -83,6 +84,78 @@ Keys must be unique (Python dict semantics already guarantee this).
83
84
  Construction raises `ValueError` in the extremely unlikely event of an
84
85
  xxhash collision (~2⁻⁶⁴ per key pair).
85
86
 
87
+ ## Sharing a map across processes (zero-copy)
88
+
89
+ A map's serialized form *is* its in-memory lookup array (plus a small
90
+ header). That means it can live in a
91
+ [`multiprocessing.shared_memory`](https://docs.python.org/3/library/multiprocessing.shared_memory.html)
92
+ block and be opened by any number of processes **without copying** — every
93
+ process reads the same physical pages.
94
+
95
+ Three methods make this work:
96
+
97
+ | Method | Purpose |
98
+ |--|--|
99
+ | `m.serialized_size()` | bytes needed to hold the serialized map |
100
+ | `m.write_into(buffer)` | serialize straight into a writable buffer (no intermediate `bytes`) |
101
+ | `ConstMap.from_buffer(buffer)` | open a **zero-copy** map that reads directly from `buffer` |
102
+
103
+ Producer — build once, publish into a **named** shared-memory block:
104
+
105
+ ```python
106
+ from multiprocessing.shared_memory import SharedMemory
107
+ from fastconstmap import ConstMap
108
+
109
+ SHM_NAME = "fastconstmap_demo"
110
+
111
+ cm = ConstMap({f"key-{i}": i for i in range(1_000_000)})
112
+
113
+ shm = SharedMemory(create=True, size=cm.serialized_size(), name=SHM_NAME)
114
+ cm.write_into(shm.buf)
115
+ # keep `shm` alive (do not close/unlink) while consumers are running
116
+ ```
117
+
118
+ Consumer — attach to the same name with no copy:
119
+
120
+ ```python
121
+ from multiprocessing.shared_memory import SharedMemory
122
+ from fastconstmap import ConstMap
123
+
124
+ SHM_NAME = "fastconstmap_demo"
125
+
126
+ shm = SharedMemory(name=SHM_NAME) # attach by name, no `create=`
127
+ cm = ConstMap.from_buffer(shm.buf) # zero-copy: no per-process copy
128
+ cm["key-42"] # reads straight from shared memory
129
+ ```
130
+
131
+ Choosing the name yourself (rather than letting `SharedMemory` generate
132
+ one) means consumers can hard-code it or read it from config — no need to
133
+ pass the auto-generated name around. Pick a unique name; creating a block
134
+ whose name already exists raises `FileExistsError`.
135
+
136
+ Notes and constraints:
137
+
138
+ - **`from_buffer` does not copy.** The returned map holds a reference to the
139
+ buffer; the buffer (and, for shared memory, the `SharedMemory` object)
140
+ must stay alive and **must not be closed or mutated** while the map is in
141
+ use. Drop the map (`del cm`) before calling `shm.close()`.
142
+ - The map is **immutable** — the intended pattern is *write once in the
143
+ producer, then only read in every process*. Concurrent readers need no
144
+ locking.
145
+ - `from_buffer` verifies the magic bytes and the FNV-1a checksum, so a
146
+ truncated or corrupt block raises `ValueError` rather than returning
147
+ garbage.
148
+ - Requirements: a **little-endian** host (x86-64, ARM64, …) and an
149
+ **8-byte-aligned** buffer. `SharedMemory.buf`, `bytes`, and `bytearray`
150
+ all satisfy the alignment requirement; an offset slice of a buffer may
151
+ not, in which case `from_buffer` raises `ValueError` — use
152
+ `from_bytes()` (which copies) instead.
153
+ - `VerifiedConstMap` supports the same three methods.
154
+
155
+ `from_bytes()` remains available when you *want* an owned copy (or need to
156
+ load on a big-endian host): it copies the data and the resulting map owns
157
+ its memory independently of the source buffer.
158
+
86
159
  ## Benchmark
87
160
 
88
161
  On an Apple M-series CPU, with 1,000,000 string keys
@@ -0,0 +1,155 @@
1
+ """Benchmark fastconstmap against the built-in dict.
2
+
3
+ Run as:
4
+ python benchmarks/benchmark.py [N]
5
+
6
+ Defaults to N = 1,000,000 keys.
7
+ """
8
+ from __future__ import annotations
9
+ import argparse
10
+ import random
11
+ import sys
12
+ import time
13
+ from typing import Callable
14
+
15
+ from fastconstmap import ConstMap, VerifiedConstMap
16
+
17
+
18
+ def make_data(n: int, seed: int = 0xC0FFEE) -> tuple[list[str], dict[str, int]]:
19
+ rng = random.Random(seed)
20
+ keys = [f"key-{i}-{rng.randint(0, 1 << 30):x}" for i in range(n)]
21
+ return keys, {k: i for i, k in enumerate(keys)}
22
+
23
+
24
+ def time_loop(label: str, iterations: int, fn: Callable[[int], None]) -> float:
25
+ # Warm-up so the JIT-equivalent (branch predictor, page faults) settles.
26
+ for i in range(min(iterations, 1000)):
27
+ fn(i)
28
+ t0 = time.perf_counter()
29
+ for i in range(iterations):
30
+ fn(i)
31
+ elapsed = time.perf_counter() - t0
32
+ per_op_ns = elapsed * 1e9 / iterations
33
+ print(f" {label:<28} {per_op_ns:8.1f} ns/op ({elapsed:.3f} s total)")
34
+ return per_op_ns
35
+
36
+
37
+ def time_batch(label: str, batches: int, batch_size: int, fn: Callable[[], None]) -> float:
38
+ for _ in range(min(batches, 5)):
39
+ fn()
40
+ t0 = time.perf_counter()
41
+ for _ in range(batches):
42
+ fn()
43
+ elapsed = time.perf_counter() - t0
44
+ per_op_ns = elapsed * 1e9 / (batches * batch_size)
45
+ print(f" {label:<28} {per_op_ns:8.1f} ns/op "
46
+ f"({elapsed*1000/batches:.2f} ms/batch of {batch_size})")
47
+ return per_op_ns
48
+
49
+
50
+ def report_size(label: str, value: int, n: int) -> None:
51
+ print(f" {label:<28} {value:12,} bytes ({value/n:.2f} bytes/key)")
52
+
53
+
54
+ def deep_sizeof(obj, seen: set[int] | None = None) -> int:
55
+ """Recursive sizeof that walks dict / list / tuple / set, deduping by id.
56
+
57
+ Returns the total bytes attributable to ``obj`` and everything it
58
+ transitively references. Small interned ints (-5..256) and interned
59
+ strings are counted once because of the id() dedup, which matches
60
+ reality — they're shared with the rest of the process.
61
+ """
62
+ if seen is None:
63
+ seen = set()
64
+ obj_id = id(obj)
65
+ if obj_id in seen:
66
+ return 0
67
+ seen.add(obj_id)
68
+ size = sys.getsizeof(obj)
69
+ if isinstance(obj, dict):
70
+ for k, v in obj.items():
71
+ size += deep_sizeof(k, seen)
72
+ size += deep_sizeof(v, seen)
73
+ elif isinstance(obj, (list, tuple, set, frozenset)):
74
+ for item in obj:
75
+ size += deep_sizeof(item, seen)
76
+ return size
77
+
78
+
79
+ def main() -> None:
80
+ parser = argparse.ArgumentParser()
81
+ parser.add_argument("n", type=int, nargs="?", default=1_000_000)
82
+ parser.add_argument("--lookups", type=int, default=2_000_000,
83
+ help="number of individual lookups to time")
84
+ parser.add_argument("--batch-size", type=int, default=1024)
85
+ parser.add_argument("--batches", type=int, default=2000)
86
+ args = parser.parse_args()
87
+ n = args.n
88
+
89
+ print(f"=== fastconstmap benchmark — n = {n:,} keys, "
90
+ f"python {sys.version.split()[0]} ===\n")
91
+
92
+ keys, d = make_data(n)
93
+ # Lookup index pattern: shuffled to avoid trivial cache wins.
94
+ rng = random.Random(1234)
95
+ lookup_idx = [rng.randrange(n) for _ in range(args.lookups)]
96
+
97
+ # ---- Construction time ----
98
+ print("Construction:")
99
+ t0 = time.perf_counter()
100
+ cm = ConstMap(d)
101
+ print(f" ConstMap.__init__ {time.perf_counter()-t0:7.3f} s")
102
+
103
+ t0 = time.perf_counter()
104
+ vm = VerifiedConstMap(d)
105
+ print(f" VerifiedConstMap.__init__ {time.perf_counter()-t0:7.3f} s")
106
+
107
+ t0 = time.perf_counter()
108
+ pyd = dict(d)
109
+ print(f" dict(d) {time.perf_counter()-t0:7.3f} s")
110
+
111
+ # ---- Memory ----
112
+ # The ConstMap retains no string objects: cm.nbytes() is its entire
113
+ # footprint. To compare honestly we walk the dict and add up the
114
+ # dict-table size *plus* every str key and int value it references
115
+ # (deduped by id, so interned objects count once).
116
+ print("\nMemory:")
117
+ report_size("ConstMap.nbytes", cm.nbytes(), n)
118
+ report_size("VerifiedConstMap.nbytes", vm.nbytes(), n)
119
+ dict_total = deep_sizeof(pyd)
120
+ report_size("dict (table+keys+values)", dict_total, n)
121
+ print(f" ratio dict / ConstMap {dict_total / cm.nbytes():.1f}x")
122
+
123
+ # ---- Single lookup ----
124
+ print(f"\nSingle lookup, {args.lookups:,} ops:")
125
+
126
+ time_loop("dict[k]", args.lookups, lambda i: pyd[keys[lookup_idx[i]]])
127
+ time_loop("ConstMap[k]", args.lookups, lambda i: cm[keys[lookup_idx[i]]])
128
+ time_loop("VerifiedConstMap[k]", args.lookups, lambda i: vm[keys[lookup_idx[i]]])
129
+
130
+ # ---- Batched lookup ----
131
+ batch_keys = [keys[rng.randrange(n)] for _ in range(args.batch_size)]
132
+ print(f"\nBatched lookup, {args.batches} × {args.batch_size}:")
133
+
134
+ # dict batch: pure Python list comprehension is the natural comparison.
135
+ time_batch("dict comprehension", args.batches, args.batch_size,
136
+ lambda: [pyd[k] for k in batch_keys])
137
+ time_batch("ConstMap.get_many", args.batches, args.batch_size,
138
+ lambda: cm.get_many(batch_keys))
139
+ time_batch("VerifiedConstMap.get_many", args.batches, args.batch_size,
140
+ lambda: vm.get_many(batch_keys))
141
+
142
+ # ---- Serialization ----
143
+ print("\nSerialization:")
144
+ t0 = time.perf_counter()
145
+ blob = cm.to_bytes()
146
+ print(f" ConstMap.to_bytes {time.perf_counter()-t0:7.3f} s ({len(blob):,} bytes)")
147
+ t0 = time.perf_counter()
148
+ cm2 = ConstMap.from_bytes(blob)
149
+ print(f" ConstMap.from_bytes {time.perf_counter()-t0:7.3f} s")
150
+ # Verify round-trip.
151
+ assert cm2[keys[0]] == d[keys[0]]
152
+
153
+
154
+ if __name__ == "__main__":
155
+ main()
@@ -0,0 +1,25 @@
1
+ """Tiny end-to-end example: build, look up, save, reload."""
2
+ from fastconstmap import ConstMap, VerifiedConstMap
3
+
4
+ d = {"apple": 100, "banana": 200, "cherry": 300}
5
+
6
+ # Minimal, no missing-key detection.
7
+ m = ConstMap(d)
8
+ print("apple ->", m["apple"])
9
+ print("batched ->", m.get_many(["banana", "cherry"]))
10
+
11
+ # Dict-like, detects missing keys.
12
+ vm = VerifiedConstMap(d)
13
+ print("vm grape?", vm.get("grape"))
14
+ print("vm has banana?", "banana" in vm)
15
+
16
+ # Round-trip through bytes.
17
+ blob = m.to_bytes()
18
+ m2 = ConstMap.from_bytes(blob)
19
+ assert m2["banana"] == 200
20
+
21
+ # Round-trip through a file.
22
+ m.save("example.cmap")
23
+ m3 = ConstMap.load("example.cmap")
24
+ assert m3["cherry"] == 300
25
+ print("save/load OK")
@@ -14,9 +14,11 @@ Two classes are exposed:
14
14
  works.
15
15
 
16
16
  Both types are immutable after construction and can be serialised to bytes
17
- or to a file path.
17
+ or to a file path. They can also be placed in a ``multiprocessing``
18
+ ``SharedMemory`` block and opened by many processes with no per-process
19
+ copy — see ``serialized_size``, ``write_into`` and ``from_buffer``.
18
20
  """
19
21
  from ._fastconstmap import ConstMap, VerifiedConstMap
20
22
 
21
23
  __all__ = ["ConstMap", "VerifiedConstMap"]
22
- __version__ = "0.1.0"
24
+ __version__ = "0.7.0"
@@ -3,8 +3,9 @@ from __future__ import annotations
3
3
  from os import PathLike
4
4
  from typing import Iterable, Mapping, Optional, Union
5
5
 
6
- KeyT = Union[str, bytes]
7
- PathT = Union[str, bytes, PathLike[str], PathLike[bytes]]
6
+ KeyT = Union[str, bytes]
7
+ PathT = Union[str, bytes, PathLike[str], PathLike[bytes]]
8
+ BufferT = Union[bytes, bytearray, memoryview]
8
9
 
9
10
 
10
11
  class ConstMap:
@@ -21,7 +22,11 @@ class ConstMap:
21
22
  def get_many(self, keys: Iterable[KeyT]) -> list[int]: ...
22
23
  def to_bytes(self) -> bytes: ...
23
24
  @classmethod
24
- def from_bytes(cls, data: bytes) -> "ConstMap": ...
25
+ def from_bytes(cls, data: BufferT) -> "ConstMap": ...
26
+ def serialized_size(self) -> int: ...
27
+ def write_into(self, buffer: BufferT) -> int: ...
28
+ @classmethod
29
+ def from_buffer(cls, buffer: BufferT) -> "ConstMap": ...
25
30
  def save(self, path: PathT) -> None: ...
26
31
  @classmethod
27
32
  def load(cls, path: PathT) -> "ConstMap": ...
@@ -46,7 +51,11 @@ class VerifiedConstMap:
46
51
  def get_many(self, keys: Iterable[KeyT], default: Optional[int] = None) -> list[Optional[int]]: ...
47
52
  def to_bytes(self) -> bytes: ...
48
53
  @classmethod
49
- def from_bytes(cls, data: bytes) -> "VerifiedConstMap": ...
54
+ def from_bytes(cls, data: BufferT) -> "VerifiedConstMap": ...
55
+ def serialized_size(self) -> int: ...
56
+ def write_into(self, buffer: BufferT) -> int: ...
57
+ @classmethod
58
+ def from_buffer(cls, buffer: BufferT) -> "VerifiedConstMap": ...
50
59
  def save(self, path: PathT) -> None: ...
51
60
  @classmethod
52
61
  def load(cls, path: PathT) -> "VerifiedConstMap": ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastconstmap
3
- Version: 0.1.0
3
+ Version: 0.7.0
4
4
  Summary: Fast, immutable, compact map from strings to uint64 (binary fuse filter).
5
5
  Author-email: Daniel Lemire <daniel@lemire.me>
6
6
  License: Apache-2.0
@@ -19,6 +19,7 @@ Dynamic: license-file
19
19
  # fastconstmap
20
20
 
21
21
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
22
+ [![Test](https://github.com/lemire/fastconstmap/actions/workflows/test.yml/badge.svg)](https://github.com/lemire/fastconstmap/actions/workflows/test.yml)
22
23
 
23
24
  Fast, immutable, compact map from strings to 64-bit integers — for Python.
24
25
 
@@ -101,6 +102,78 @@ Keys must be unique (Python dict semantics already guarantee this).
101
102
  Construction raises `ValueError` in the extremely unlikely event of an
102
103
  xxhash collision (~2⁻⁶⁴ per key pair).
103
104
 
105
+ ## Sharing a map across processes (zero-copy)
106
+
107
+ A map's serialized form *is* its in-memory lookup array (plus a small
108
+ header). That means it can live in a
109
+ [`multiprocessing.shared_memory`](https://docs.python.org/3/library/multiprocessing.shared_memory.html)
110
+ block and be opened by any number of processes **without copying** — every
111
+ process reads the same physical pages.
112
+
113
+ Three methods make this work:
114
+
115
+ | Method | Purpose |
116
+ |--|--|
117
+ | `m.serialized_size()` | bytes needed to hold the serialized map |
118
+ | `m.write_into(buffer)` | serialize straight into a writable buffer (no intermediate `bytes`) |
119
+ | `ConstMap.from_buffer(buffer)` | open a **zero-copy** map that reads directly from `buffer` |
120
+
121
+ Producer — build once, publish into a **named** shared-memory block:
122
+
123
+ ```python
124
+ from multiprocessing.shared_memory import SharedMemory
125
+ from fastconstmap import ConstMap
126
+
127
+ SHM_NAME = "fastconstmap_demo"
128
+
129
+ cm = ConstMap({f"key-{i}": i for i in range(1_000_000)})
130
+
131
+ shm = SharedMemory(create=True, size=cm.serialized_size(), name=SHM_NAME)
132
+ cm.write_into(shm.buf)
133
+ # keep `shm` alive (do not close/unlink) while consumers are running
134
+ ```
135
+
136
+ Consumer — attach to the same name with no copy:
137
+
138
+ ```python
139
+ from multiprocessing.shared_memory import SharedMemory
140
+ from fastconstmap import ConstMap
141
+
142
+ SHM_NAME = "fastconstmap_demo"
143
+
144
+ shm = SharedMemory(name=SHM_NAME) # attach by name, no `create=`
145
+ cm = ConstMap.from_buffer(shm.buf) # zero-copy: no per-process copy
146
+ cm["key-42"] # reads straight from shared memory
147
+ ```
148
+
149
+ Choosing the name yourself (rather than letting `SharedMemory` generate
150
+ one) means consumers can hard-code it or read it from config — no need to
151
+ pass the auto-generated name around. Pick a unique name; creating a block
152
+ whose name already exists raises `FileExistsError`.
153
+
154
+ Notes and constraints:
155
+
156
+ - **`from_buffer` does not copy.** The returned map holds a reference to the
157
+ buffer; the buffer (and, for shared memory, the `SharedMemory` object)
158
+ must stay alive and **must not be closed or mutated** while the map is in
159
+ use. Drop the map (`del cm`) before calling `shm.close()`.
160
+ - The map is **immutable** — the intended pattern is *write once in the
161
+ producer, then only read in every process*. Concurrent readers need no
162
+ locking.
163
+ - `from_buffer` verifies the magic bytes and the FNV-1a checksum, so a
164
+ truncated or corrupt block raises `ValueError` rather than returning
165
+ garbage.
166
+ - Requirements: a **little-endian** host (x86-64, ARM64, …) and an
167
+ **8-byte-aligned** buffer. `SharedMemory.buf`, `bytes`, and `bytearray`
168
+ all satisfy the alignment requirement; an offset slice of a buffer may
169
+ not, in which case `from_buffer` raises `ValueError` — use
170
+ `from_bytes()` (which copies) instead.
171
+ - `VerifiedConstMap` supports the same three methods.
172
+
173
+ `from_bytes()` remains available when you *want* an owned copy (or need to
174
+ load on a big-endian host): it copies the data and the resulting map owns
175
+ its memory independently of the source buffer.
176
+
104
177
  ## Benchmark
105
178
 
106
179
  On an Apple M-series CPU, with 1,000,000 string keys
@@ -3,6 +3,8 @@ MANIFEST.in
3
3
  README.md
4
4
  pyproject.toml
5
5
  setup.py
6
+ benchmarks/benchmark.py
7
+ examples/example.py
6
8
  fastconstmap/__init__.py
7
9
  fastconstmap/_fastconstmap.pyi
8
10
  fastconstmap/py.typed
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastconstmap"
7
- version = "0.1.0"
7
+ version = "0.7.0"
8
8
  description = "Fast, immutable, compact map from strings to uint64 (binary fuse filter)."
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -38,7 +38,10 @@ test-command = "pytest {project}/tests"
38
38
  archs = ["x86_64", "universal2", "arm64"]
39
39
 
40
40
  [tool.cibuildwheel.linux]
41
- archs = ["x86_64"]
41
+ # "auto64" = the runner's native 64-bit arch (no 32-bit i686). The wheels
42
+ # workflow runs this on both an x86_64 runner and an aarch64 runner, so
43
+ # this produces x86_64 and aarch64 manylinux wheels — each built natively.
44
+ archs = ["auto64"]
42
45
 
43
46
  [tool.cibuildwheel.windows]
44
47
  archs = ["AMD64"]
@@ -563,15 +563,17 @@ uint64_t fcm_verified_constmap_lookup(const fcm_verified_constmap_t *vm,
563
563
  /* [4] segment_length */
564
564
  /* [4] segment_count */
565
565
  /* [4] data_len */
566
+ /* [4] reserved (zero) — pads the header to 32 bytes so the data array */
567
+ /* starts 8-byte aligned, enabling zero-copy uint64 views */
566
568
  /* [8 * data_len] data */
567
569
  /* (verified only) [8 * data_len] checks */
568
570
  /* [8] FNV-1a-64 checksum of all preceding bytes */
569
571
  /* ------------------------------------------------------------------------- */
570
572
 
571
- static const uint8_t fcm_magic_constmap[8] = {'C','M','A','P','0','0','0','1'};
572
- static const uint8_t fcm_magic_verified_constmap[8] = {'V','C','M','P','0','0','0','1'};
573
+ static const uint8_t fcm_magic_constmap[8] = {'C','M','A','P','0','0','0','2'};
574
+ static const uint8_t fcm_magic_verified_constmap[8] = {'V','C','M','P','0','0','0','2'};
573
575
 
574
- #define FCM_HEADER_SIZE 28u /* 8 + 8 + 4 + 4 + 4 */
576
+ #define FCM_HEADER_SIZE 32u /* 8 magic + 8 seed + 4 seglen + 4 segcount + 4 datalen + 4 pad */
575
577
  #define FCM_TRAILER_SIZE 8u /* checksum */
576
578
 
577
579
  static inline void fcm_write_u32(uint8_t *p, uint32_t v) {
@@ -629,6 +631,7 @@ int fcm_constmap_write(const fcm_constmap_t *cm, void *buf) {
629
631
  fcm_write_u32(p, cm->segment_length); p += 4;
630
632
  fcm_write_u32(p, cm->segment_count); p += 4;
631
633
  fcm_write_u32(p, cm->data_len); p += 4;
634
+ fcm_write_u32(p, 0); p += 4; /* reserved padding */
632
635
  for (uint32_t i = 0; i < cm->data_len; i++) {
633
636
  fcm_write_u64(p, cm->data[i]); p += 8;
634
637
  }
@@ -688,6 +691,7 @@ int fcm_verified_constmap_write(const fcm_verified_constmap_t *vm, void *buf) {
688
691
  fcm_write_u32(p, vm->segment_length); p += 4;
689
692
  fcm_write_u32(p, vm->segment_count); p += 4;
690
693
  fcm_write_u32(p, vm->data_len); p += 4;
694
+ fcm_write_u32(p, 0); p += 4; /* reserved padding */
691
695
  for (uint32_t i = 0; i < vm->data_len; i++) {
692
696
  fcm_write_u64(p, vm->data[i]); p += 8;
693
697
  }
@@ -744,3 +748,78 @@ int fcm_verified_constmap_read(fcm_verified_constmap_t *out, const void *buf, si
744
748
  out->checks = checks;
745
749
  return FCM_OK;
746
750
  }
751
+
752
+ /* ------------------------------------------------------------------------- */
753
+ /* Zero-copy views */
754
+ /* ------------------------------------------------------------------------- */
755
+
756
+ static inline int fcm_host_is_little_endian(void) {
757
+ const uint16_t x = 1;
758
+ return *(const uint8_t *)&x == 1;
759
+ }
760
+
761
+ int fcm_constmap_view(fcm_constmap_t *out, const void *buf, size_t buf_len) {
762
+ if (!out) return FCM_E_INVALID_FORMAT;
763
+ memset(out, 0, sizeof(*out));
764
+ if (!fcm_host_is_little_endian()) return FCM_E_INVALID_FORMAT;
765
+ if (buf_len < FCM_HEADER_SIZE + FCM_TRAILER_SIZE) return FCM_E_SHORT_BUFFER;
766
+
767
+ const uint8_t *p = (const uint8_t *)buf;
768
+ if (memcmp(p, fcm_magic_constmap, 8) != 0) return FCM_E_INVALID_FORMAT;
769
+
770
+ uint32_t data_len = fcm_read_u32(p + 24);
771
+ size_t expected = FCM_HEADER_SIZE + (size_t)data_len * 8u + FCM_TRAILER_SIZE;
772
+ if (buf_len < expected) return FCM_E_SHORT_BUFFER;
773
+
774
+ uint64_t got_sum = fcm_read_u64(p + expected - 8);
775
+ uint64_t expected_sum = fcm_fnv1a64(p, expected - 8);
776
+ if (got_sum != expected_sum) return FCM_E_CHECKSUM;
777
+
778
+ const uint8_t *dp = p + FCM_HEADER_SIZE;
779
+ if (((uintptr_t)dp & 7u) != 0) return FCM_E_UNALIGNED;
780
+
781
+ uint32_t segment_length = fcm_read_u32(p + 16);
782
+ out->seed = fcm_read_u64(p + 8);
783
+ out->segment_length = segment_length;
784
+ out->segment_length_mask = segment_length ? segment_length - 1 : 0;
785
+ out->segment_count = fcm_read_u32(p + 20);
786
+ out->segment_count_length = out->segment_count * segment_length;
787
+ out->data_len = data_len;
788
+ /* Borrowed pointer into `buf`; the integer round-trip launders away the
789
+ * source const-ness. Lookups only read this array. */
790
+ out->data = (uint64_t *)(uintptr_t)dp;
791
+ return FCM_OK;
792
+ }
793
+
794
+ int fcm_verified_constmap_view(fcm_verified_constmap_t *out, const void *buf, size_t buf_len) {
795
+ if (!out) return FCM_E_INVALID_FORMAT;
796
+ memset(out, 0, sizeof(*out));
797
+ if (!fcm_host_is_little_endian()) return FCM_E_INVALID_FORMAT;
798
+ if (buf_len < FCM_HEADER_SIZE + FCM_TRAILER_SIZE) return FCM_E_SHORT_BUFFER;
799
+
800
+ const uint8_t *p = (const uint8_t *)buf;
801
+ if (memcmp(p, fcm_magic_verified_constmap, 8) != 0) return FCM_E_INVALID_FORMAT;
802
+
803
+ uint32_t data_len = fcm_read_u32(p + 24);
804
+ size_t expected = FCM_HEADER_SIZE + (size_t)data_len * 16u + FCM_TRAILER_SIZE;
805
+ if (buf_len < expected) return FCM_E_SHORT_BUFFER;
806
+
807
+ uint64_t got_sum = fcm_read_u64(p + expected - 8);
808
+ uint64_t expected_sum = fcm_fnv1a64(p, expected - 8);
809
+ if (got_sum != expected_sum) return FCM_E_CHECKSUM;
810
+
811
+ const uint8_t *dp = p + FCM_HEADER_SIZE;
812
+ const uint8_t *cp = dp + (size_t)data_len * 8u;
813
+ if (((uintptr_t)dp & 7u) != 0) return FCM_E_UNALIGNED;
814
+
815
+ uint32_t segment_length = fcm_read_u32(p + 16);
816
+ out->seed = fcm_read_u64(p + 8);
817
+ out->segment_length = segment_length;
818
+ out->segment_length_mask = segment_length ? segment_length - 1 : 0;
819
+ out->segment_count = fcm_read_u32(p + 20);
820
+ out->segment_count_length = out->segment_count * segment_length;
821
+ out->data_len = data_len;
822
+ out->data = (uint64_t *)(uintptr_t)dp;
823
+ out->checks = (uint64_t *)(uintptr_t)cp;
824
+ return FCM_OK;
825
+ }
@@ -25,6 +25,7 @@ extern "C" {
25
25
  #define FCM_E_INVALID_FORMAT -5
26
26
  #define FCM_E_CHECKSUM -6
27
27
  #define FCM_E_SHORT_BUFFER -7
28
+ #define FCM_E_UNALIGNED -8
28
29
 
29
30
  #define FCM_NOT_FOUND ((uint64_t)0xFFFFFFFFFFFFFFFFULL)
30
31
 
@@ -85,6 +86,24 @@ size_t fcm_verified_constmap_serialized_size(const fcm_verified_constmap_t *vm);
85
86
  int fcm_verified_constmap_write(const fcm_verified_constmap_t *vm, void *buf);
86
87
  int fcm_verified_constmap_read (fcm_verified_constmap_t *out, const void *buf, size_t buf_len);
87
88
 
89
+ /* Zero-copy views over a serialized buffer.
90
+ *
91
+ * Unlike `*_read`, these do NOT copy: `out->data` (and `out->checks`) point
92
+ * directly into `buf`. This makes a map openable from shared memory with no
93
+ * per-process copy.
94
+ *
95
+ * Requirements: a little-endian host and a buffer whose start is 8-byte
96
+ * aligned (so the embedded uint64 arrays are aligned). The serialized
97
+ * format pads its header to a multiple of 8 to guarantee this.
98
+ *
99
+ * Returns FCM_OK, or FCM_E_UNALIGNED / FCM_E_INVALID_FORMAT (e.g. on a
100
+ * big-endian host) / FCM_E_CHECKSUM / FCM_E_SHORT_BUFFER.
101
+ *
102
+ * The caller MUST keep `buf` alive for the lifetime of `out`, and MUST NOT
103
+ * call `fcm_*_free` on a view (the data is borrowed, not owned). */
104
+ int fcm_constmap_view(fcm_constmap_t *out, const void *buf, size_t buf_len);
105
+ int fcm_verified_constmap_view(fcm_verified_constmap_t *out, const void *buf, size_t buf_len);
106
+
88
107
  #ifdef __cplusplus
89
108
  }
90
109
  #endif
@@ -153,8 +153,22 @@ fail:
153
153
  typedef struct {
154
154
  PyObject_HEAD
155
155
  fcm_constmap_t cm;
156
+ /* When `view.obj` is non-NULL the map is a zero-copy view: `cm.data`
157
+ * points into `view`'s buffer and must NOT be freed. */
158
+ Py_buffer view;
156
159
  } PyConstMap;
157
160
 
161
+ /* Release whatever storage the map currently holds (owned heap data or a
162
+ * borrowed buffer). Safe to call on a zero-initialised struct. */
163
+ static void pyconstmap_release(PyConstMap *self) {
164
+ if (self->view.obj != NULL) {
165
+ /* Borrowed: data lives inside the buffer; just drop our reference. */
166
+ PyBuffer_Release(&self->view); /* sets view.obj = NULL */
167
+ self->cm.data = NULL;
168
+ }
169
+ fcm_constmap_free(&self->cm); /* frees owned data; no-op if NULL */
170
+ }
171
+
158
172
  static int PyConstMap_init(PyConstMap *self, PyObject *args, PyObject *kwargs) {
159
173
  PyObject *dict;
160
174
  static char *kwlist[] = {"mapping", NULL};
@@ -166,7 +180,7 @@ static int PyConstMap_init(PyConstMap *self, PyObject *args, PyObject *kwargs) {
166
180
  PyObject *keep = NULL;
167
181
  if (fcm_dict_to_arrays(dict, &n, &keys, &vals, &keep) < 0) return -1;
168
182
 
169
- fcm_constmap_free(&self->cm);
183
+ pyconstmap_release(self);
170
184
 
171
185
  int rc;
172
186
  Py_BEGIN_ALLOW_THREADS
@@ -182,7 +196,7 @@ static int PyConstMap_init(PyConstMap *self, PyObject *args, PyObject *kwargs) {
182
196
  }
183
197
 
184
198
  static void PyConstMap_dealloc(PyConstMap *self) {
185
- fcm_constmap_free(&self->cm);
199
+ pyconstmap_release(self);
186
200
  Py_TYPE(self)->tp_free((PyObject *)self);
187
201
  }
188
202
 
@@ -363,20 +377,74 @@ static PyObject *PyConstMap_data_bytes(PyConstMap *self, PyObject *Py_UNUSED(ign
363
377
  return PyLong_FromUnsignedLongLong((unsigned long long)self->cm.data_len * 8ULL);
364
378
  }
365
379
 
380
+ static PyObject *PyConstMap_serialized_size(PyConstMap *self, PyObject *Py_UNUSED(ignored)) {
381
+ return PyLong_FromSize_t(fcm_constmap_serialized_size(&self->cm));
382
+ }
383
+
384
+ /* Serialize directly into a caller-provided writable buffer (e.g. a
385
+ * SharedMemory block), avoiding the intermediate bytes object. */
386
+ static PyObject *PyConstMap_write_into(PyConstMap *self, PyObject *arg) {
387
+ Py_buffer view;
388
+ if (PyObject_GetBuffer(arg, &view, PyBUF_WRITABLE) < 0) return NULL;
389
+ size_t need = fcm_constmap_serialized_size(&self->cm);
390
+ if (view.len < 0 || (size_t)view.len < need) {
391
+ PyErr_Format(PyExc_ValueError,
392
+ "buffer too small: need %zu bytes, got %zd",
393
+ need, view.len);
394
+ PyBuffer_Release(&view);
395
+ return NULL;
396
+ }
397
+ fcm_constmap_write(&self->cm, view.buf);
398
+ PyBuffer_Release(&view);
399
+ return PyLong_FromSize_t(need);
400
+ }
401
+
402
+ /* Zero-copy: build a map that reads directly out of `buffer`. The buffer
403
+ * is kept alive by the returned object; lookups touch it directly. */
404
+ static PyObject *PyConstMap_from_buffer(PyTypeObject *type, PyObject *arg) {
405
+ Py_buffer view;
406
+ if (PyObject_GetBuffer(arg, &view, PyBUF_SIMPLE) < 0) return NULL;
407
+ PyConstMap *self = (PyConstMap *)type->tp_alloc(type, 0);
408
+ if (!self) { PyBuffer_Release(&view); return NULL; }
409
+ /* tp_alloc zeroed the struct, so self->view.obj == NULL and cm is clear. */
410
+ int rc = fcm_constmap_view(&self->cm, view.buf, (size_t)view.len);
411
+ if (rc != FCM_OK) {
412
+ PyBuffer_Release(&view);
413
+ Py_DECREF(self);
414
+ if (rc == FCM_E_UNALIGNED) {
415
+ PyErr_SetString(PyExc_ValueError,
416
+ "buffer is not 8-byte aligned; cannot create a zero-copy view "
417
+ "(use from_bytes() to copy instead)");
418
+ } else {
419
+ fcm_set_python_error(rc);
420
+ }
421
+ return NULL;
422
+ }
423
+ self->view = view; /* keep the buffer alive for the map's lifetime */
424
+ return (PyObject *)self;
425
+ }
426
+
366
427
  static PyMethodDef PyConstMap_methods[] = {
367
- {"get", (PyCFunction)PyConstMap_get, METH_O,
428
+ {"get", (PyCFunction)PyConstMap_get, METH_O,
368
429
  "Return the value for `key`. If `key` was not in the original mapping, the return value is undefined."},
369
- {"get_many", (PyCFunction)PyConstMap_get_many, METH_O,
430
+ {"get_many", (PyCFunction)PyConstMap_get_many, METH_O,
370
431
  "Look up an iterable of keys. Returns a list of ints in the same order."},
371
- {"to_bytes", (PyCFunction)PyConstMap_serialize, METH_NOARGS,
432
+ {"to_bytes", (PyCFunction)PyConstMap_serialize, METH_NOARGS,
372
433
  "Serialize the map to bytes."},
373
- {"from_bytes", (PyCFunction)PyConstMap_deserialize, METH_O | METH_CLASS,
374
- "Deserialize a map from a bytes-like object."},
375
- {"save", (PyCFunction)PyConstMap_save, METH_O,
434
+ {"from_bytes", (PyCFunction)PyConstMap_deserialize, METH_O | METH_CLASS,
435
+ "Deserialize a map from a bytes-like object (copies the data)."},
436
+ {"serialized_size", (PyCFunction)PyConstMap_serialized_size, METH_NOARGS,
437
+ "Number of bytes that to_bytes()/write_into() will produce."},
438
+ {"write_into", (PyCFunction)PyConstMap_write_into, METH_O,
439
+ "Serialize directly into a writable buffer (e.g. SharedMemory.buf). Returns bytes written."},
440
+ {"from_buffer", (PyCFunction)PyConstMap_from_buffer, METH_O | METH_CLASS,
441
+ "Create a zero-copy map that reads directly from a buffer (e.g. shared memory). "
442
+ "The buffer is kept alive by the returned map and must not be modified while in use."},
443
+ {"save", (PyCFunction)PyConstMap_save, METH_O,
376
444
  "Save the map to a file path."},
377
- {"load", (PyCFunction)PyConstMap_load, METH_O | METH_CLASS,
445
+ {"load", (PyCFunction)PyConstMap_load, METH_O | METH_CLASS,
378
446
  "Load a map from a file path."},
379
- {"nbytes", (PyCFunction)PyConstMap_data_bytes, METH_NOARGS,
447
+ {"nbytes", (PyCFunction)PyConstMap_data_bytes, METH_NOARGS,
380
448
  "Heap size of the lookup array in bytes."},
381
449
  {NULL, NULL, 0, NULL}
382
450
  };
@@ -407,8 +475,19 @@ static PyTypeObject PyConstMapType = {
407
475
  typedef struct {
408
476
  PyObject_HEAD
409
477
  fcm_verified_constmap_t vm;
478
+ /* Non-NULL view.obj => zero-copy view: data/checks point into the buffer. */
479
+ Py_buffer view;
410
480
  } PyVerifiedConstMap;
411
481
 
482
+ static void pyverified_release(PyVerifiedConstMap *self) {
483
+ if (self->view.obj != NULL) {
484
+ PyBuffer_Release(&self->view);
485
+ self->vm.data = NULL;
486
+ self->vm.checks = NULL;
487
+ }
488
+ fcm_verified_constmap_free(&self->vm);
489
+ }
490
+
412
491
  static int PyVerifiedConstMap_init(PyVerifiedConstMap *self, PyObject *args, PyObject *kwargs) {
413
492
  PyObject *dict;
414
493
  static char *kwlist[] = {"mapping", NULL};
@@ -420,7 +499,7 @@ static int PyVerifiedConstMap_init(PyVerifiedConstMap *self, PyObject *args, PyO
420
499
  PyObject *keep = NULL;
421
500
  if (fcm_dict_to_arrays(dict, &n, &keys, &vals, &keep) < 0) return -1;
422
501
 
423
- fcm_verified_constmap_free(&self->vm);
502
+ pyverified_release(self);
424
503
 
425
504
  int rc;
426
505
  Py_BEGIN_ALLOW_THREADS
@@ -436,7 +515,7 @@ static int PyVerifiedConstMap_init(PyVerifiedConstMap *self, PyObject *args, PyO
436
515
  }
437
516
 
438
517
  static void PyVerifiedConstMap_dealloc(PyVerifiedConstMap *self) {
439
- fcm_verified_constmap_free(&self->vm);
518
+ pyverified_release(self);
440
519
  Py_TYPE(self)->tp_free((PyObject *)self);
441
520
  }
442
521
 
@@ -655,20 +734,69 @@ static PyObject *PyVerifiedConstMap_nbytes(PyVerifiedConstMap *self, PyObject *P
655
734
  return PyLong_FromUnsignedLongLong((unsigned long long)self->vm.data_len * 16ULL);
656
735
  }
657
736
 
737
+ static PyObject *PyVerifiedConstMap_serialized_size(PyVerifiedConstMap *self, PyObject *Py_UNUSED(ignored)) {
738
+ return PyLong_FromSize_t(fcm_verified_constmap_serialized_size(&self->vm));
739
+ }
740
+
741
+ static PyObject *PyVerifiedConstMap_write_into(PyVerifiedConstMap *self, PyObject *arg) {
742
+ Py_buffer view;
743
+ if (PyObject_GetBuffer(arg, &view, PyBUF_WRITABLE) < 0) return NULL;
744
+ size_t need = fcm_verified_constmap_serialized_size(&self->vm);
745
+ if (view.len < 0 || (size_t)view.len < need) {
746
+ PyErr_Format(PyExc_ValueError,
747
+ "buffer too small: need %zu bytes, got %zd",
748
+ need, view.len);
749
+ PyBuffer_Release(&view);
750
+ return NULL;
751
+ }
752
+ fcm_verified_constmap_write(&self->vm, view.buf);
753
+ PyBuffer_Release(&view);
754
+ return PyLong_FromSize_t(need);
755
+ }
756
+
757
+ static PyObject *PyVerifiedConstMap_from_buffer(PyTypeObject *type, PyObject *arg) {
758
+ Py_buffer view;
759
+ if (PyObject_GetBuffer(arg, &view, PyBUF_SIMPLE) < 0) return NULL;
760
+ PyVerifiedConstMap *self = (PyVerifiedConstMap *)type->tp_alloc(type, 0);
761
+ if (!self) { PyBuffer_Release(&view); return NULL; }
762
+ int rc = fcm_verified_constmap_view(&self->vm, view.buf, (size_t)view.len);
763
+ if (rc != FCM_OK) {
764
+ PyBuffer_Release(&view);
765
+ Py_DECREF(self);
766
+ if (rc == FCM_E_UNALIGNED) {
767
+ PyErr_SetString(PyExc_ValueError,
768
+ "buffer is not 8-byte aligned; cannot create a zero-copy view "
769
+ "(use from_bytes() to copy instead)");
770
+ } else {
771
+ fcm_set_python_error(rc);
772
+ }
773
+ return NULL;
774
+ }
775
+ self->view = view;
776
+ return (PyObject *)self;
777
+ }
778
+
658
779
  static PyMethodDef PyVerifiedConstMap_methods[] = {
659
- {"get", (PyCFunction)PyVerifiedConstMap_get, METH_VARARGS,
780
+ {"get", (PyCFunction)PyVerifiedConstMap_get, METH_VARARGS,
660
781
  "Return value for `key`, or `default` (default None) if not present."},
661
- {"get_many", (PyCFunction)(void *)PyVerifiedConstMap_get_many, METH_VARARGS | METH_KEYWORDS,
782
+ {"get_many", (PyCFunction)(void *)PyVerifiedConstMap_get_many, METH_VARARGS | METH_KEYWORDS,
662
783
  "Look up an iterable of keys; missing keys yield `default` (default None)."},
663
- {"to_bytes", (PyCFunction)PyVerifiedConstMap_serialize, METH_NOARGS,
784
+ {"to_bytes", (PyCFunction)PyVerifiedConstMap_serialize, METH_NOARGS,
664
785
  "Serialize the map to bytes."},
665
- {"from_bytes", (PyCFunction)PyVerifiedConstMap_deserialize, METH_O | METH_CLASS,
666
- "Deserialize a map from a bytes-like object."},
667
- {"save", (PyCFunction)PyVerifiedConstMap_save, METH_O,
786
+ {"from_bytes", (PyCFunction)PyVerifiedConstMap_deserialize, METH_O | METH_CLASS,
787
+ "Deserialize a map from a bytes-like object (copies the data)."},
788
+ {"serialized_size", (PyCFunction)PyVerifiedConstMap_serialized_size, METH_NOARGS,
789
+ "Number of bytes that to_bytes()/write_into() will produce."},
790
+ {"write_into", (PyCFunction)PyVerifiedConstMap_write_into, METH_O,
791
+ "Serialize directly into a writable buffer (e.g. SharedMemory.buf). Returns bytes written."},
792
+ {"from_buffer", (PyCFunction)PyVerifiedConstMap_from_buffer, METH_O | METH_CLASS,
793
+ "Create a zero-copy map that reads directly from a buffer (e.g. shared memory). "
794
+ "The buffer is kept alive by the returned map and must not be modified while in use."},
795
+ {"save", (PyCFunction)PyVerifiedConstMap_save, METH_O,
668
796
  "Save the map to a file path."},
669
- {"load", (PyCFunction)PyVerifiedConstMap_load, METH_O | METH_CLASS,
797
+ {"load", (PyCFunction)PyVerifiedConstMap_load, METH_O | METH_CLASS,
670
798
  "Load a map from a file path."},
671
- {"nbytes", (PyCFunction)PyVerifiedConstMap_nbytes, METH_NOARGS,
799
+ {"nbytes", (PyCFunction)PyVerifiedConstMap_nbytes, METH_NOARGS,
672
800
  "Heap size of the data + checks arrays in bytes."},
673
801
  {NULL, NULL, 0, NULL}
674
802
  };
File without changes
File without changes
File without changes