fipc-python 1.0.0__py3-none-macosx_14_0_arm64.whl

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.
fipc/__init__.py ADDED
@@ -0,0 +1,75 @@
1
+ """
2
+ FastIPC: shared-memory messages between two processes (streams), broadcasts from one publisher to many
3
+ subscribers, work queues from many producers to many consumers, and services from many clients to a pool of servers,
4
+ with replies, over the C API of include/fipc.h.
5
+
6
+ import fipc
7
+
8
+ with fipc.stream_create("my_name", 1 << 20) as listener: # the server, in one process
9
+ conn = listener.accept()
10
+ conn = fipc.stream_connect("my_name", timeout_ms=5000) # the client, in another (or on another thread)
11
+
12
+ stream_connect waits for the server's accept, so a client and its server run on different threads or processes. A
13
+ broadcast: `broadcast_create` returns the publisher's Conn, and `broadcast_join` subscribes (SUBSCRIBER_LOSSY or
14
+ SUBSCRIBER_GATED). A work queue: `queue_create` returns a Queue, and `queue_join` joins it as a PRODUCER or a CONSUMER
15
+ (the owner too). A service: `service_create` returns a Service, and `service_join` joins it as a CLIENT, whose `call`
16
+ sends a request and returns its reply, or a SERVER (the owner too). The library admits a broadcast's, a queue's or a
17
+ service's joiners itself, in the owner's process.
18
+ """
19
+
20
+ from fipc._fipc import (
21
+ CLIENT,
22
+ CONSUMER,
23
+ FOREVER,
24
+ NO_WAIT,
25
+ PRODUCER,
26
+ SERVER,
27
+ SUBSCRIBER_GATED,
28
+ SUBSCRIBER_LOSSY,
29
+ Conn,
30
+ FipcError,
31
+ Listener,
32
+ Queue,
33
+ Result,
34
+ Service,
35
+ broadcast_create,
36
+ broadcast_join,
37
+ ffi,
38
+ lib,
39
+ queue_create,
40
+ queue_join,
41
+ result_str,
42
+ service_create,
43
+ service_join,
44
+ stream_connect,
45
+ stream_create,
46
+ )
47
+
48
+ __version__ = "1.0.0"
49
+ __all__ = [
50
+ "CLIENT",
51
+ "CONSUMER",
52
+ "FOREVER",
53
+ "NO_WAIT",
54
+ "PRODUCER",
55
+ "SERVER",
56
+ "SUBSCRIBER_GATED",
57
+ "SUBSCRIBER_LOSSY",
58
+ "Conn",
59
+ "FipcError",
60
+ "Listener",
61
+ "Queue",
62
+ "Result",
63
+ "Service",
64
+ "broadcast_create",
65
+ "broadcast_join",
66
+ "ffi",
67
+ "lib",
68
+ "queue_create",
69
+ "queue_join",
70
+ "result_str",
71
+ "service_create",
72
+ "service_join",
73
+ "stream_connect",
74
+ "stream_create",
75
+ ]
fipc/_fipc.py ADDED
@@ -0,0 +1,760 @@
1
+ """
2
+ The FastIPC C API (include/fipc.h) for Python, through CFFI's ABI mode: the shared library is loaded at run time,
3
+ with no build step.
4
+
5
+ A stream's server creates a name (`stream_create`) and accepts one client at a time; a client connects to the name
6
+ (`stream_connect`). A broadcast's publisher (`broadcast_create`) sends every message to each of its subscribers
7
+ (`broadcast_join`). A work queue's owner creates it (`queue_create`), and its producers and consumers join it
8
+ (`queue_join`, the owner too): each message a producer sends goes to one consumer. A service's owner creates it
9
+ (`service_create`), and its clients and servers join it (`service_join`, the owner too): each request a client sends
10
+ goes to one server, whose reply goes back to that client. The library admits a broadcast's, a queue's or a service's
11
+ joiners itself, in the owner's process. Timeouts are int milliseconds: NO_WAIT (0) doesn't wait, FOREVER
12
+ (-1) waits for ever. A call that fails raises FipcError, whose `result` is the library's Result (Result.TIMEOUT when the
13
+ timeout ran out). The header's rules hold: on a connection, one thread at a time sends and one receives; `cancel` may
14
+ be called from any thread; `close` only when no other thread is inside a call on the handle (cancel, join the
15
+ threads, close). A Conn or a Listener left unclosed is closed when it is garbage-collected; a Queue and a Service never
16
+ are: they live until their close or the end of the process.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ import platform
23
+ import time
24
+ from collections import deque
25
+ from enum import IntEnum
26
+ from pathlib import Path
27
+ from typing import Any, Deque, List, Optional, Sequence, Union
28
+
29
+ from cffi import FFI
30
+
31
+ ffi = FFI()
32
+ ffi.cdef("""
33
+ typedef struct fipc_listener fipc_listener_t;
34
+ typedef struct fipc_conn fipc_conn_t;
35
+ typedef struct fipc_queue fipc_queue_t;
36
+ typedef struct fipc_service fipc_service_t;
37
+ typedef int fipc_result_t;
38
+
39
+ const char* fipc_result_str(fipc_result_t result);
40
+
41
+ fipc_result_t fipc_stream_create(const char* name, size_t capacity, fipc_listener_t** out_listener);
42
+ fipc_result_t fipc_stream_accept(fipc_listener_t* listener, fipc_conn_t** out_conn, int timeout_ms);
43
+ void fipc_listener_cancel(fipc_listener_t* listener);
44
+ void fipc_listener_close(fipc_listener_t* listener);
45
+ fipc_result_t fipc_stream_connect(const char* name, fipc_conn_t** out_conn, int timeout_ms);
46
+ size_t fipc_max_piece(const fipc_conn_t* conn);
47
+ void fipc_cancel(fipc_conn_t* conn);
48
+ void fipc_close(fipc_conn_t* conn);
49
+
50
+ fipc_result_t fipc_send(fipc_conn_t* conn, const void* data, size_t len, int timeout_ms);
51
+ fipc_result_t fipc_recv(fipc_conn_t* conn, void* buf, size_t buf_len, size_t* out_len, int timeout_ms);
52
+ fipc_result_t fipc_send_acquire(fipc_conn_t* conn, size_t len, void** out_buf, int timeout_ms);
53
+ fipc_result_t fipc_send_commit(fipc_conn_t* conn, size_t len);
54
+ fipc_result_t fipc_recv_acquire(fipc_conn_t* conn, const void** out_data, size_t* out_len, int timeout_ms);
55
+ void fipc_recv_release(fipc_conn_t* conn);
56
+ fipc_result_t fipc_send_many(fipc_conn_t* conn, const void* const* datas, const size_t* lens, size_t count,
57
+ size_t* out_sent, int timeout_ms);
58
+ fipc_result_t fipc_recv_many(fipc_conn_t* conn, void* buf, size_t buf_len, size_t* lens, size_t max_count,
59
+ size_t* out_count, int timeout_ms);
60
+
61
+ fipc_result_t fipc_broadcast_create(const char* name, size_t capacity, uint32_t max_subscribers, int max_stall_ms,
62
+ fipc_conn_t** out_publisher);
63
+ fipc_result_t fipc_broadcast_join(const char* name, int role, fipc_conn_t** out_conn, int timeout_ms);
64
+ fipc_result_t fipc_broadcast_wait_for_members(fipc_conn_t* publisher, uint32_t at_least, uint32_t* out_count,
65
+ int timeout_ms);
66
+
67
+ fipc_result_t fipc_queue_create(const char* name, size_t capacity, uint32_t max_producers, uint32_t max_consumers,
68
+ fipc_queue_t** out_queue);
69
+ fipc_result_t fipc_queue_join(const char* name, int role, fipc_conn_t** out_conn, int timeout_ms);
70
+ fipc_result_t fipc_queue_wait_for_members(fipc_queue_t* queue, int role, uint32_t at_least, uint32_t* out_count,
71
+ int timeout_ms);
72
+ void fipc_queue_cancel(fipc_queue_t* queue);
73
+ void fipc_queue_close(fipc_queue_t* queue);
74
+
75
+ fipc_result_t fipc_service_create(const char* name, size_t capacity, size_t max_message, uint32_t max_clients,
76
+ uint32_t max_servers, fipc_service_t** out_service);
77
+ fipc_result_t fipc_service_join(const char* name, int role, fipc_conn_t** out_conn, int timeout_ms);
78
+ fipc_result_t fipc_service_wait_for_members(fipc_service_t* service, int role, uint32_t at_least,
79
+ uint32_t* out_count, int timeout_ms);
80
+ void fipc_service_cancel(fipc_service_t* service);
81
+ void fipc_service_close(fipc_service_t* service);
82
+ """)
83
+
84
+
85
+ # Linux and macOS: the library is never unloaded (docs/platform-support.md), but CFFI unloads it when the interpreter
86
+ # exits and frees the handle, and a library thread on its way out of the library's code at that moment crashes the
87
+ # exit. RTLD_NODELETE keeps it loaded for the life of the process; Windows has no such flag and needs none.
88
+ _PIN = getattr(ffi, "RTLD_NODELETE", 0)
89
+
90
+
91
+ def _load() -> Any:
92
+ """The shared library, kept loaded (`_PIN`): in the folder the environment variable FASTIPC_LIB_DIR names, when it
93
+ is set (nowhere else); in a development checkout, the repository's zig-out build (ahead of any copy in _native/,
94
+ which may be older); else the one bundled in the package's _native/ folder (an installed wheel); else the
95
+ system's search (LD_LIBRARY_PATH, DYLD_LIBRARY_PATH, PATH)."""
96
+ package = Path(__file__).parent
97
+ repo = package.parent.parent.parent # fipc -> python -> bindings -> the repository
98
+ names = {"Windows": ["fastipc.dll"], "Darwin": ["libfastipc.dylib"]}.get(platform.system(), ["libfastipc.so"])
99
+ directory = os.environ.get("FASTIPC_LIB_DIR")
100
+ if directory:
101
+ for name in names:
102
+ if (Path(directory) / name).exists():
103
+ return ffi.dlopen(str((Path(directory) / name).resolve()), _PIN)
104
+ raise OSError(f"FastIPC library not found: no {names[0]} in {directory}, the folder FASTIPC_LIB_DIR names")
105
+ folders = [repo / "zig-out" / "lib", repo / "zig-out" / "bin"] if (repo / "build.zig").exists() else []
106
+ for folder in (*folders, package / "_native"):
107
+ for name in names:
108
+ if (folder / name).exists():
109
+ return ffi.dlopen(str(folder / name), _PIN)
110
+ for name in names:
111
+ try:
112
+ return ffi.dlopen(name, _PIN)
113
+ except OSError:
114
+ pass
115
+ raise OSError(f"FastIPC library not found: {names} in {package / '_native'}, zig-out or the system's search")
116
+
117
+
118
+ lib = _load()
119
+
120
+ NO_WAIT = 0
121
+ FOREVER = -1
122
+ # broadcast_join's roles: a lossy subscriber never holds the publisher back (it may lose messages: OVERRUN); a gated
123
+ # one misses nothing (the publisher waits for it)
124
+ SUBSCRIBER_LOSSY = 1
125
+ SUBSCRIBER_GATED = 2
126
+ # queue_join's and Queue.wait_for_members' roles: a producer sends, a consumer receives
127
+ PRODUCER = 3
128
+ CONSUMER = 4
129
+ # service_join's and Service.wait_for_members' roles: a client sends requests and receives their replies, a server
130
+ # receives requests and replies to them
131
+ CLIENT = 5
132
+ SERVER = 6
133
+
134
+
135
+ class Result(IntEnum):
136
+ """fipc_result_t."""
137
+ OK = 0
138
+ TIMEOUT = 1
139
+ DISCONNECTED = 2
140
+ CANCELLED = 3
141
+ TOO_LARGE = 4
142
+ INVALID = 5
143
+ NO_MEMORY = 6
144
+ ADDR_IN_USE = 7
145
+ OVERRUN = 8
146
+ UNSUBSCRIBED = 9
147
+ ABANDONED = 10
148
+
149
+
150
+ def result_str(result: int) -> str:
151
+ """The library's name for a result ("FIPC_OK", ...)."""
152
+ return ffi.string(lib.fipc_result_str(result)).decode()
153
+
154
+
155
+ class FipcError(Exception):
156
+ """A call returned `result`, not OK. `lost`: on OVERRUN, how many messages the lossy subscriber lost (at least 1),
157
+ else None. `count`: when a `wait_for_members` raised TIMEOUT, CANCELLED or DISCONNECTED, how many members were
158
+ joined, else None (INVALID included). `sent`: when a `send_many` raised, how many of its messages went (all those
159
+ before the one that didn't), else None. `why`, if given, says why in the message (a call's INVALID while a reply to
160
+ a send is owed)."""
161
+
162
+ def __init__(self, result: int, call: str, lost: Optional[int] = None, count: Optional[int] = None,
163
+ why: Optional[str] = None, sent: Optional[int] = None) -> None:
164
+ detail = f" ({lost} lost)" if lost is not None else f" ({count} joined)" if count is not None else ""
165
+ detail = f" ({sent} sent)" if sent is not None else detail
166
+ detail = f" ({why})" if why is not None else detail
167
+ super().__init__(f"{call}: {result_str(result)}{detail}")
168
+ self.result = Result(result) if result in Result._value2member_map_ else result
169
+ self.lost = lost
170
+ self.count = count
171
+ self.sent = sent
172
+
173
+
174
+ def _check(result: int, call: str) -> None:
175
+ if result != Result.OK:
176
+ raise FipcError(result, call)
177
+
178
+
179
+ def _check_recv(result: int, length: int, call: str = "fipc_recv") -> None:
180
+ """As _check for fipc_recv (or fipc_recv_many), whose length out-parameter holds OVERRUN's count of the messages
181
+ lost."""
182
+ if result == Result.OVERRUN:
183
+ raise FipcError(result, call, lost=length)
184
+ _check(result, call)
185
+
186
+
187
+ def _left(timeout_ms: int, deadline: float) -> int:
188
+ """What is left of timeout_ms at `deadline` (FOREVER stays FOREVER), in whole milliseconds, at least 0."""
189
+ return timeout_ms if timeout_ms < 0 else max(0, int((deadline - time.monotonic()) * 1000))
190
+
191
+
192
+ def _check_count(result: int, count: int, call: str) -> None:
193
+ """As _check for a wait for members, whose count out-parameter holds the count at every result but INVALID."""
194
+ if result not in (Result.OK, Result.INVALID):
195
+ raise FipcError(result, call, count=count)
196
+ _check(result, call)
197
+
198
+
199
+ Bytes = Union[bytes, bytearray, memoryview]
200
+
201
+ # The least room recv_many receives into: a batch takes the messages that fit it (it grows for a longer first message)
202
+ _BATCH_BYTES = 1 << 16
203
+
204
+
205
+ class Conn:
206
+ """One connection: a stream's (`stream_connect`, a client; `Listener.accept`, a server), with two rings and one
207
+ session; a broadcast's publisher (`broadcast_create`), whose sends go to every subscriber, or one of its subscribers
208
+ (`broadcast_join`), which receives; a work queue's producer or consumer (`queue_join`): a producer sends, and a
209
+ consumer receives, each message going to one consumer; or a service's client or server (`service_join`): a client
210
+ sends requests and receives their replies, in the order of its requests (or `call`s), and a server receives the
211
+ next request of any client and replies to it with its next send.
212
+
213
+ A Conn left unclosed is closed when it is garbage-collected, as by `close` (a publisher's ends its broadcast, a
214
+ server's answers the request it holds ABANDONED). A memoryview that `send_acquire` or `recv_acquire` returned keeps
215
+ its Conn referenced, so the connection's memory stays mapped while the view exists; and a thread inside a call
216
+ holds the Conn, so the collection never closes it under a call."""
217
+
218
+ def __init__(self, handle: Any) -> None:
219
+ self._handle = ffi.gc(handle, lib.fipc_close) # the finalizer, which close() disarms
220
+ # Out-parameters and the receive buffer, per connection (one receiving thread at a time)
221
+ self._len = ffi.new("size_t*")
222
+ self._ptr = ffi.new("void**")
223
+ self._cptr = ffi.new("const void**")
224
+ self._buf = ffi.new("char[]", 4096)
225
+ self._lens = ffi.new("size_t[]", 16) # recv_many's lengths, grown for a larger max_count
226
+
227
+ def __enter__(self) -> "Conn":
228
+ return self
229
+
230
+ def __exit__(self, *exc: Any) -> None:
231
+ self.close()
232
+
233
+ def max_piece(self) -> int:
234
+ """The largest message the zero-copy calls take on this connection: its capacity less 64 bytes (a broadcast's or
235
+ a work queue's: a quarter of its capacity, the largest message it takes; a service's: its max_message, the
236
+ largest request or reply)."""
237
+ return lib.fipc_max_piece(self._handle)
238
+
239
+ def cancel(self) -> None:
240
+ """Every call of this connection that waits, now or later, raises CANCELLED (a service client's `call` too,
241
+ sending nothing). Any thread."""
242
+ lib.fipc_cancel(self._handle)
243
+
244
+ def wait_for_members(self, at_least: int = 0, timeout_ms: int = FOREVER) -> int:
245
+ """A broadcast's publisher: waits up to timeout_ms until at least `at_least` subscribers are joined (0 returns
246
+ the count at once), and returns how many are. To send nothing before N subscribers are there, call it with N
247
+ before the first send. TIMEOUT and CANCELLED (after `cancel`) carry the count in the error's `count`; INVALID
248
+ on any other connection, or for at_least over the broadcast's max_subscribers. Any thread."""
249
+ count = ffi.new("uint32_t*")
250
+ result = lib.fipc_broadcast_wait_for_members(self._handle, at_least, count, timeout_ms)
251
+ _check_count(result, count[0], "fipc_broadcast_wait_for_members")
252
+ return count[0]
253
+
254
+ def close(self) -> None:
255
+ """Ends the connection; the peer gets DISCONNECTED. A publisher's close ends the broadcast and frees its name:
256
+ its subscribers receive what it sent, then DISCONNECTED. A subscriber, a producer or a consumer leaves (a
257
+ producer's messages stay for the consumers). A service's client or server leaves: a client's requests that no
258
+ server took are dropped, and the replies to those taken are lost; the request a server holds is answered
259
+ ABANDONED. No other thread may be inside a call on it. Closing it again does nothing."""
260
+ if self._handle is not None:
261
+ handle, self._handle = self._handle, None
262
+ ffi.gc(handle, None) # disarms the finalizer: closed once
263
+ lib.fipc_close(handle)
264
+
265
+ def _view(self, pointer: Any, length: int) -> memoryview:
266
+ """A memoryview of the `length` bytes at `pointer`, in the connection's shared memory, that keeps the Conn
267
+ referenced while it exists (through the destructor of the pointer it holds), so the finalizer can't unmap that
268
+ memory under it."""
269
+ return memoryview(ffi.buffer(ffi.gc(pointer, self._keep), length))
270
+
271
+ def _keep(self, pointer: Any) -> None:
272
+ """The destructor of a view's pointer: nothing to free; being a bound method, it holds the Conn for the view."""
273
+
274
+ # === Messages ===
275
+
276
+ def send(self, data: Bytes, timeout_ms: int = FOREVER) -> None:
277
+ """Sends one message of any size (at least 1 byte; on a broadcast, a work queue or a service, at most
278
+ `max_piece()`: else TOO_LARGE). A work queue's producer waits for room while the consumers haven't taken its
279
+ earlier messages. A service's client sends a request, waiting for room in its ring (which its replies not yet
280
+ received hold too); a server replies to the request it holds, and never waits (INVALID with none held)."""
281
+ _check(lib.fipc_send(self._handle, ffi.from_buffer(data), len(data), timeout_ms), "fipc_send")
282
+
283
+ def recv(self, timeout_ms: int = FOREVER) -> bytes:
284
+ """Receives one message of any size. A lossy subscriber that fell a lap behind gets OVERRUN instead, with the
285
+ number of messages lost in the error's `lost`; its next receive goes on from a recent message. A work queue's
286
+ consumer takes a message of any producer. A service's client receives the reply to its oldest request whose
287
+ reply it hasn't received, or ABANDONED if that request's server ended before it replied (the request counts as
288
+ answered; it may or may not have been carried out); a receive that times out or is cancelled leaves the reply to
289
+ come, for the next receive, and after a `call` that gave up, the next `recv` returns that call's late reply
290
+ first. A server takes the next request of any client and holds it until it replies: its receives raise INVALID
291
+ until then."""
292
+ length = self._recv(timeout_ms) # may replace the buffer with a bigger one
293
+ return ffi.buffer(self._buf, length)[:]
294
+
295
+ def recv_into(self, buf: Union[bytearray, memoryview], timeout_ms: int = FOREVER) -> int:
296
+ """Receives one message into `buf` and returns its length; TOO_LARGE (the message stays) if it doesn't fit: on
297
+ a work queue another consumer may take it first. OVERRUN as `recv`."""
298
+ _check_recv(lib.fipc_recv(self._handle, ffi.from_buffer(buf), len(buf), self._len, timeout_ms), self._len[0])
299
+ return self._len[0]
300
+
301
+ def _recv(self, timeout_ms: int) -> int:
302
+ """Receives into the connection's buffer, growing it for a message that doesn't fit (on a work queue another
303
+ consumer may take that one first, and the next may be longer still); returns the length. Every receive waits
304
+ only for what is left of timeout_ms."""
305
+ deadline = time.monotonic() + timeout_ms / 1000
306
+ result = lib.fipc_recv(self._handle, self._buf, len(self._buf), self._len, timeout_ms)
307
+ while result == Result.TOO_LARGE:
308
+ self._buf = ffi.new("char[]", self._len[0])
309
+ result = lib.fipc_recv(self._handle, self._buf, len(self._buf), self._len, _left(timeout_ms, deadline))
310
+ _check_recv(result, self._len[0])
311
+ return self._len[0]
312
+
313
+ def send_many(self, messages: Sequence[Bytes], timeout_ms: int = FOREVER) -> int:
314
+ """Sends `messages` (at least one, each of at least 1 byte) in order and publishes them together, with at most
315
+ one wake of the receivers for the batch, and returns how many went: all of them. timeout_ms bounds the whole
316
+ call (NO_WAIT: only what fits now). An error is the first message that didn't go, as `send` raises it, and its
317
+ `sent` is how many went, all those before it. Every message is checked first, before any goes: INVALID for an
318
+ empty one or an empty batch, TOO_LARGE over `max_piece()` on a broadcast, a work queue or a service; on a stream
319
+ a longer message goes in pieces, as `send` sends it. A service's client sends a batch of requests, whose replies
320
+ come in their order and are owed as `send`'s are; a server's batch is INVALID. Batch both ends: a receiver that
321
+ takes one message per call falls behind a batching sender, and the two run slower than without batches."""
322
+ count = len(messages)
323
+ datas = ffi.new("const void*[]", count)
324
+ lens = ffi.new("size_t[]", count)
325
+ keep = [ffi.from_buffer(message) for message in messages] # referenced until the call returns
326
+ for i, data in enumerate(keep):
327
+ datas[i] = data
328
+ lens[i] = len(data)
329
+ sent = ffi.new("size_t*")
330
+ result = lib.fipc_send_many(self._handle, datas, lens, count, sent, timeout_ms)
331
+ if result != Result.OK:
332
+ raise FipcError(result, "fipc_send_many", sent=sent[0])
333
+ return sent[0]
334
+
335
+ def recv_many(self, max_count: int = 16, timeout_ms: int = FOREVER) -> List[bytes]:
336
+ """Waits up to timeout_ms for the next message, as `recv`, then takes every further message already there, up to
337
+ `max_count` (at least 1) and as many as fit the connection's receive buffer (64 KiB, grown for a longer first
338
+ message), and returns them in order. An error is the first message's, as `recv` raises it, with nothing else
339
+ taken: OVERRUN with the number lost in `lost`, a service client's ABANDONED. After the first it never waits,
340
+ and stops before a message that isn't there yet, doesn't fit, is in pieces, or would bring another result: the
341
+ next receive returns it. A broadcast's subscriber takes, after its first, only the messages its publisher has
342
+ moved past (the newest come in the next calls); a work queue's consumer acknowledges each message before the
343
+ call returns, as `recv` does; a service's client receives its replies in its requests' order. INVALID on a
344
+ publisher, a producer and a service's server, which holds one request at a time."""
345
+ if len(self._buf) < _BATCH_BYTES:
346
+ self._buf = ffi.new("char[]", _BATCH_BYTES)
347
+ if len(self._lens) < max_count:
348
+ self._lens = ffi.new("size_t[]", max_count)
349
+ deadline = time.monotonic() + timeout_ms / 1000
350
+ result = lib.fipc_recv_many(self._handle, self._buf, len(self._buf), self._lens, max_count, self._len,
351
+ timeout_ms)
352
+ while result == Result.TOO_LARGE: # the first message stays: make room for it (or the next, maybe longer)
353
+ self._buf = ffi.new("char[]", self._lens[0])
354
+ result = lib.fipc_recv_many(self._handle, self._buf, len(self._buf), self._lens, max_count, self._len,
355
+ _left(timeout_ms, deadline))
356
+ _check_recv(result, self._lens[0], "fipc_recv_many")
357
+ received = ffi.buffer(self._buf)
358
+ messages = []
359
+ at = 0
360
+ for i in range(self._len[0]):
361
+ messages.append(received[at:at + self._lens[i]])
362
+ at += self._lens[i]
363
+ return messages
364
+
365
+ def send_acquire(self, length: int, timeout_ms: int = FOREVER) -> memoryview:
366
+ """Zero-copy send, step 1: `length` bytes (at most `max_piece()`) in the ring to write the message into. The
367
+ memoryview keeps the Conn referenced (it is never collected under it)."""
368
+ _check(lib.fipc_send_acquire(self._handle, length, self._ptr, timeout_ms), "fipc_send_acquire")
369
+ return self._view(self._ptr[0], length)
370
+
371
+ def send_commit(self, length: int) -> None:
372
+ """Zero-copy send, step 2: sends the first `length` bytes of the acquired room as one message."""
373
+ _check(lib.fipc_send_commit(self._handle, length), "fipc_send_commit")
374
+
375
+ def recv_acquire(self, timeout_ms: int = FOREVER) -> memoryview:
376
+ """Zero-copy receive, step 1: the next message in the ring (read-only), valid until `recv_release`, the next
377
+ receive or `close` (the memoryview keeps the Conn referenced: it is never collected under it). TOO_LARGE for a
378
+ message of several pieces: `recv` it. A gated subscriber's message holds the publisher back until its release; a
379
+ lossy subscriber's acquire is INVALID. A work queue's consumer takes the message: it holds its producer back
380
+ once the producer is a ring ahead of it. Once a receive raised UNSUBSCRIBED, never read a memoryview of a
381
+ message held in place: its memory is gone, and reading it crashes the process. A service's client takes its next
382
+ reply in place (or ABANDONED, as `recv`); a server takes the next request, which stays valid until its reply's
383
+ `send` or `send_commit`."""
384
+ _check(lib.fipc_recv_acquire(self._handle, self._cptr, self._len, timeout_ms), "fipc_recv_acquire")
385
+ return self._view(self._cptr[0], self._len[0]).toreadonly()
386
+
387
+ def recv_release(self) -> None:
388
+ """Zero-copy receive, step 2: frees the acquired message's room. On a service's server it does nothing: the
389
+ request stays until the reply."""
390
+ lib.fipc_recv_release(self._handle)
391
+
392
+ def call(self, request: Bytes, timeout_ms: int = FOREVER) -> bytes:
393
+ """A service's client: sends `request` (1 to `max_piece()` bytes) and returns its reply, waiting up to
394
+ timeout_ms in all; CANCELLED, sending nothing, after `cancel`; INVALID on any other connection. See
395
+ `service_join`."""
396
+ raise FipcError(Result.INVALID, "call")
397
+
398
+
399
+ class _ServiceClient(Conn):
400
+ """A service's client (`service_join` with CLIENT), a Conn that keeps, oldest first, the requests whose replies it
401
+ hasn't received yet, so that `call` never takes the reply to another request: True for a request whose `call`
402
+ gave up on its reply (it timed out or was cancelled), which the next `call` receives and drops (at most one, as a
403
+ call first drops what is owed); False for one sent with `send` or `send_commit`, whose reply is the caller's to
404
+ receive. A request's entry is added before it is sent, since a thread that receives may take its reply before the
405
+ send returns, and removed unless the send returned: whatever it raised (FipcError, a TypeError for an argument that
406
+ isn't bytes, KeyboardInterrupt, ...). `_cancelled` once `cancel` has run: a call then sends nothing, as it could
407
+ never receive the reply."""
408
+
409
+ def __init__(self, handle: Any) -> None:
410
+ super().__init__(handle)
411
+ self._owed: Deque[bool] = deque()
412
+ self._cancelled = False
413
+
414
+ def cancel(self) -> None:
415
+ self._cancelled = True # before the native cancel: a call that doesn't see it sent its request before it
416
+ super().cancel()
417
+
418
+ def _counted(self, entry: bool, send: Any, *args: Any) -> None:
419
+ """Calls `send` with `entry` added for its request, and removes it unless the send returned (the sending thread
420
+ is the one that adds, so the newest entry is this one)."""
421
+ self._owed.append(entry)
422
+ sent = False
423
+ try:
424
+ send(*args)
425
+ sent = True
426
+ finally:
427
+ if not sent:
428
+ self._owed.pop()
429
+
430
+ def send(self, data: Bytes, timeout_ms: int = FOREVER) -> None:
431
+ self._counted(False, super().send, data, timeout_ms)
432
+
433
+ def send_commit(self, length: int) -> None:
434
+ self._counted(False, super().send_commit, length)
435
+
436
+ def send_many(self, messages: Sequence[Bytes], timeout_ms: int = FOREVER) -> int:
437
+ """As Conn.send_many, with an entry added for each request before the batch goes, and removed for each one that
438
+ didn't go (the newest entries: the sending thread is the one that adds)."""
439
+ count = len(messages)
440
+ self._owed.extend([False] * count)
441
+ sent = 0
442
+ try:
443
+ sent = super().send_many(messages, timeout_ms)
444
+ except FipcError as e:
445
+ sent = e.sent or 0
446
+ raise
447
+ finally:
448
+ for _ in range(count - sent):
449
+ self._owed.pop()
450
+ return sent
451
+
452
+ def _answered(self, receive: Any, *args: Any) -> Any:
453
+ """Calls `receive`, and takes the oldest request off the list when it returned a reply or raised ABANDONED (an
454
+ answer too)."""
455
+ try:
456
+ value = receive(*args)
457
+ except FipcError as e:
458
+ if e.result == Result.ABANDONED and self._owed:
459
+ self._owed.popleft()
460
+ raise
461
+ if self._owed:
462
+ self._owed.popleft()
463
+ return value
464
+
465
+ def recv(self, timeout_ms: int = FOREVER) -> bytes:
466
+ return self._answered(super().recv, timeout_ms)
467
+
468
+ def recv_into(self, buf: Union[bytearray, memoryview], timeout_ms: int = FOREVER) -> int:
469
+ return self._answered(super().recv_into, buf, timeout_ms)
470
+
471
+ def recv_acquire(self, timeout_ms: int = FOREVER) -> memoryview:
472
+ return self._answered(super().recv_acquire, timeout_ms)
473
+
474
+ def recv_many(self, max_count: int = 16, timeout_ms: int = FOREVER) -> List[bytes]:
475
+ """As Conn.recv_many, taking a request off the list for each reply, or for an ABANDONED."""
476
+ try:
477
+ replies = super().recv_many(max_count, timeout_ms)
478
+ except FipcError as e:
479
+ if e.result == Result.ABANDONED and self._owed:
480
+ self._owed.popleft()
481
+ raise
482
+ for _ in replies:
483
+ if self._owed:
484
+ self._owed.popleft()
485
+ return replies
486
+
487
+ def call(self, request: Bytes, timeout_ms: int = FOREVER) -> bytes:
488
+ deadline = time.monotonic() + timeout_ms / 1000
489
+ data = ffi.from_buffer(request) # a TypeError for a request that isn't bytes, before anything is counted
490
+ length = len(data)
491
+ if length == 0:
492
+ raise FipcError(Result.INVALID, "call")
493
+ if length > self.max_piece():
494
+ raise FipcError(Result.TOO_LARGE, "call")
495
+ if self._cancelled:
496
+ raise FipcError(Result.CANCELLED, "call") # its reply could never be received: the request never goes
497
+ if not all(self._owed): # a reply to a request of send(): the caller's, not ours to drop
498
+ raise FipcError(Result.INVALID, "call", why="a reply to a request of send() is owed: receive it first")
499
+ dropped = False
500
+ while self._owed: # the reply to a call that gave up on it: dropped
501
+ dropped = True
502
+ try:
503
+ self._answered(self._recv, _left(timeout_ms, deadline))
504
+ except FipcError as e:
505
+ if e.result != Result.ABANDONED:
506
+ raise
507
+ if dropped and timeout_ms > 0 and time.monotonic() >= deadline:
508
+ raise FipcError(Result.TIMEOUT, "call") # its time went on the drop: the request never goes
509
+ if self._cancelled:
510
+ raise FipcError(Result.CANCELLED, "call") # cancelled while it dropped
511
+ self._counted(True, _check_send, self, data, length, _left(timeout_ms, deadline))
512
+ length = self._answered(self._recv, _left(timeout_ms, deadline))
513
+ return ffi.buffer(self._buf, length)[:]
514
+
515
+
516
+ def _check_send(conn: Conn, data: Any, length: int, timeout_ms: int) -> None:
517
+ _check(lib.fipc_send(conn._handle, data, length, timeout_ms), "fipc_send")
518
+
519
+
520
+ class Listener:
521
+ """A stream's name, held by its server (`stream_create`): `accept` one client at a time. Its close frees the name;
522
+ the connections it accepted stay open.
523
+
524
+ A Listener left unclosed is closed when it is garbage-collected, as by `close`, which frees the name; a thread
525
+ inside `accept` holds the Listener, so the collection never closes it under a call."""
526
+
527
+ def __init__(self, handle: Any) -> None:
528
+ self._handle: Optional[Any] = ffi.gc(handle, lib.fipc_listener_close) # the finalizer, which close() disarms
529
+
530
+ def __enter__(self) -> "Listener":
531
+ return self
532
+
533
+ def __exit__(self, *exc: Any) -> None:
534
+ self.close()
535
+
536
+ def accept(self, timeout_ms: int = FOREVER) -> Conn:
537
+ """Waits for a client and sets its connection up on the calling thread; a call that times out in the middle
538
+ of a client's setup keeps it for the next call, so NO_WAIT polls. INVALID while the connection it returned
539
+ last is open, NO_MEMORY once the listener has failed for good."""
540
+ out = ffi.new("fipc_conn_t**")
541
+ _check(lib.fipc_stream_accept(self._handle, out, timeout_ms), "fipc_stream_accept")
542
+ return Conn(out[0])
543
+
544
+ def cancel(self) -> None:
545
+ """Every accept that waits, now or later, raises CANCELLED; a client whose setup an accept began is dropped. Any
546
+ thread."""
547
+ lib.fipc_listener_cancel(self._handle)
548
+
549
+ def close(self) -> None:
550
+ """Stops listening and frees the name (on Windows once the connections it accepted are closed too); the
551
+ connections it accepted stay open. No other thread may be inside a call on it."""
552
+ if self._handle is not None:
553
+ handle, self._handle = self._handle, None
554
+ ffi.gc(handle, None) # disarms the finalizer: closed once
555
+ lib.fipc_listener_close(handle)
556
+
557
+
558
+ class Queue:
559
+ """A work queue, held by its owner (`queue_create`). Its producers and consumers, the owner's own included, join
560
+ with `queue_join`. A queue lives until `close()` (or the end of its `with` block) or the end of its process;
561
+ dropping the object doesn't end it (it has no finalizer)."""
562
+
563
+ def __init__(self, handle: Any) -> None:
564
+ self._handle: Optional[Any] = handle
565
+
566
+ def __enter__(self) -> "Queue":
567
+ return self
568
+
569
+ def __exit__(self, *exc: Any) -> None:
570
+ self.close()
571
+
572
+ def wait_for_members(self, role: Optional[int] = None, at_least: int = 0, timeout_ms: int = FOREVER) -> int:
573
+ """Waits up to timeout_ms until at least `at_least` members of `role` (PRODUCER or CONSUMER; None counts both)
574
+ are joined (0 returns the count at once), and returns how many are. TIMEOUT, CANCELLED and DISCONNECTED (the
575
+ queue has ended) carry the count in the error's `count`; INVALID for another role, or for at_least over the
576
+ role's maximum (max_producers, max_consumers, or their sum for None). Any thread."""
577
+ count = ffi.new("uint32_t*")
578
+ result = lib.fipc_queue_wait_for_members(self._handle, role or 0, at_least, count, timeout_ms)
579
+ _check_count(result, count[0], "fipc_queue_wait_for_members")
580
+ return count[0]
581
+
582
+ def cancel(self) -> None:
583
+ """Every `wait_for_members` that waits, now or later, raises CANCELLED. The queue goes on. Any thread."""
584
+ lib.fipc_queue_cancel(self._handle)
585
+
586
+ def close(self) -> None:
587
+ """Ends the queue and frees its name: producers' sends raise DISCONNECTED, and consumers receive what is left,
588
+ then DISCONNECTED. The members' connections, the owner's own included, stay open until their close. No other
589
+ thread may be inside a call on it."""
590
+ if self._handle is not None:
591
+ lib.fipc_queue_close(self._handle)
592
+ self._handle = None
593
+
594
+
595
+ class Service:
596
+ """A service, held by its owner (`service_create`). Its clients and servers, the owner's own included, join with
597
+ `service_join`. A service lives until `close()` (or the end of its `with` block) or the end of its process;
598
+ dropping the object doesn't end it (it has no finalizer)."""
599
+
600
+ def __init__(self, handle: Any) -> None:
601
+ self._handle: Optional[Any] = handle
602
+
603
+ def __enter__(self) -> "Service":
604
+ return self
605
+
606
+ def __exit__(self, *exc: Any) -> None:
607
+ self.close()
608
+
609
+ def wait_for_members(self, role: Optional[int] = None, at_least: int = 0, timeout_ms: int = FOREVER) -> int:
610
+ """Waits up to timeout_ms until at least `at_least` members of `role` (CLIENT or SERVER; None counts both) are
611
+ joined (0 returns the count at once), and returns how many are. TIMEOUT, CANCELLED and DISCONNECTED (the
612
+ service has ended) carry the count in the error's `count`; INVALID for another role, or for at_least over the
613
+ role's maximum (max_clients, max_servers, or their sum for None). Any thread."""
614
+ count = ffi.new("uint32_t*")
615
+ result = lib.fipc_service_wait_for_members(self._handle, role or 0, at_least, count, timeout_ms)
616
+ _check_count(result, count[0], "fipc_service_wait_for_members")
617
+ return count[0]
618
+
619
+ def cancel(self) -> None:
620
+ """Every `wait_for_members` that waits, now or later, raises CANCELLED. The service goes on. Any thread."""
621
+ lib.fipc_service_cancel(self._handle)
622
+
623
+ def close(self) -> None:
624
+ """Ends the service and frees its name: clients' sends and servers' receives raise DISCONNECTED, a client
625
+ receives the replies that came before the end, then DISCONNECTED, and a server's reply raises DISCONNECTED.
626
+ The members' connections, the owner's own included, stay open until their close. No other thread may be inside
627
+ a call on it."""
628
+ if self._handle is not None:
629
+ lib.fipc_service_close(self._handle)
630
+ self._handle = None
631
+
632
+
633
+ # === Streams ===
634
+
635
+
636
+ def stream_create(name: str, capacity: int) -> Listener:
637
+ """A stream's server: claims `name` and listens on it, with rings of `capacity` bytes (a power of two, 1 KiB to
638
+ 2 GiB) for each connection, and returns its Listener. Doesn't wait: clients may connect from now on, and
639
+ `Listener.accept` sets each one up. ADDR_IN_USE if another stream, broadcast, queue or service holds the name."""
640
+ out = ffi.new("fipc_listener_t**")
641
+ _check(lib.fipc_stream_create(name.encode(), capacity, out), "fipc_stream_create")
642
+ return Listener(out[0])
643
+
644
+
645
+ def stream_connect(name: str, timeout_ms: int = FOREVER) -> Conn:
646
+ """A stream's client: connects to the server of the stream on `name` and sets the connection up on the calling
647
+ thread, waiting up to timeout_ms for the server to create the stream and to call accept: the server runs on another
648
+ thread or in another process. FOREVER waits for a server however late it starts. INVALID if a broadcast, a queue or
649
+ a service holds the name."""
650
+ out = ffi.new("fipc_conn_t**")
651
+ _check(lib.fipc_stream_connect(name.encode(), out, timeout_ms), "fipc_stream_connect")
652
+ return Conn(out[0])
653
+
654
+
655
+ # === Broadcasts ===
656
+
657
+
658
+ def broadcast_create(name: str, capacity: int, max_subscribers: int, max_stall_ms: int) -> Conn:
659
+ """A broadcast's publisher: claims `name` for a broadcast of `capacity` bytes of messages (a power of two, 4 KiB to
660
+ 2 GiB; messages of up to a quarter of it) and up to `max_subscribers` subscribers (1 to 1024), and returns the
661
+ publisher's connection, the broadcast's one handle: its sends go to every subscriber, and its close ends the
662
+ broadcast, as does its garbage collection: keep it referenced while the broadcast is in use. Doesn't wait:
663
+ subscribers may join from now on, and this process admits them by itself. A gated subscriber that holds the
664
+ publisher back for max_stall_ms is unsubscribed; FOREVER never, NO_WAIT refuses gated subscribers. ADDR_IN_USE if
665
+ another stream, broadcast, queue or service holds the name."""
666
+ out = ffi.new("fipc_conn_t**")
667
+ _check(lib.fipc_broadcast_create(name.encode(), capacity, max_subscribers, max_stall_ms, out),
668
+ "fipc_broadcast_create")
669
+ return Conn(out[0])
670
+
671
+
672
+ def broadcast_join(name: str, role: int, timeout_ms: int = FOREVER) -> Conn:
673
+ """A subscriber: joins the broadcast on `name` as `role` (SUBSCRIBER_LOSSY, which never holds the publisher back;
674
+ SUBSCRIBER_GATED, which misses nothing), waiting up to timeout_ms for the publisher to create the broadcast, for a
675
+ free slot, and for the publisher's process to admit it. It receives the messages sent from its admission on, then
676
+ DISCONNECTED once the publisher closed. INVALID for another role, a name a queue or a service holds, or a gated
677
+ subscriber on a broadcast that refuses them. On a name a stream holds, the join learns the kind when the stream's
678
+ server accepts it, which brings INVALID; while no accept runs, it waits and raises TIMEOUT."""
679
+ out = ffi.new("fipc_conn_t**")
680
+ _check(lib.fipc_broadcast_join(name.encode(), role, out, timeout_ms), "fipc_broadcast_join")
681
+ return Conn(out[0])
682
+
683
+
684
+ # === Work queues ===
685
+
686
+
687
+ def queue_create(name: str, capacity: int, max_producers: int, max_consumers: int) -> Queue:
688
+ """A work queue's owner: claims `name` for a work queue of up to `max_producers` producers (1 to 63) and
689
+ `max_consumers` consumers (1 to 64), each producer with a ring of `capacity` bytes of messages (a power of two,
690
+ 4 KiB to 2 GiB; messages of up to a quarter of it), and returns the Queue. Doesn't wait: members may join from now
691
+ on, and this process admits them by itself; the owner joins with `queue_join` too, from any thread. A queue lives
692
+ until `close()` or the end of its process; dropping the object doesn't end it. ADDR_IN_USE if another stream,
693
+ broadcast, queue or service holds the name."""
694
+ out = ffi.new("fipc_queue_t**")
695
+ _check(lib.fipc_queue_create(name.encode(), capacity, max_producers, max_consumers, out), "fipc_queue_create")
696
+ return Queue(out[0])
697
+
698
+
699
+ def queue_join(name: str, role: int, timeout_ms: int = FOREVER) -> Conn:
700
+ """A producer or a consumer: joins the work queue on `name` as `role` (PRODUCER or CONSUMER), waiting up to
701
+ timeout_ms for the owner to create the queue, for a free place of the role, and for the owner's process to admit
702
+ it. A consumer takes each message of the queue's producers that no other consumer took, in no promised order, then
703
+ DISCONNECTED once the queue ended; a producer's sends raise DISCONNECTED from then on. INVALID for another role, or
704
+ a name a broadcast or a service holds. On a name a stream holds, the join learns the kind when the stream's server
705
+ accepts it, which brings INVALID; while no accept runs, it waits and raises TIMEOUT."""
706
+ out = ffi.new("fipc_conn_t**")
707
+ _check(lib.fipc_queue_join(name.encode(), role, out, timeout_ms), "fipc_queue_join")
708
+ return Conn(out[0])
709
+
710
+
711
+ # === Services ===
712
+
713
+
714
+ def service_create(name: str, capacity: int, max_message: int, max_clients: int, max_servers: int) -> Service:
715
+ """A service's owner: claims `name` for a service of up to `max_clients` clients (1 to 64) and `max_servers`
716
+ servers (1 to 64), each client with a ring of `capacity` bytes (a power of two, 4 KiB to 2 GiB) for its requests and
717
+ their replies, each of 1 to `max_message` bytes (1 to capacity / 4), and returns the Service. A request holds its
718
+ length rounded up to 64 bytes, plus max_message rounded up to 64, of its client's ring from its send until its
719
+ reply has been received. Doesn't wait: members may join from now on, and this process admits them by itself; the
720
+ owner joins with `service_join` too, from any thread. A service lives until `close()` or the end of its process;
721
+ dropping the object doesn't end it. ADDR_IN_USE if another stream, broadcast, queue or service holds the name."""
722
+ out = ffi.new("fipc_service_t**")
723
+ _check(lib.fipc_service_create(name.encode(), capacity, max_message, max_clients, max_servers, out),
724
+ "fipc_service_create")
725
+ return Service(out[0])
726
+
727
+
728
+ def service_join(name: str, role: int, timeout_ms: int = FOREVER) -> Conn:
729
+ """A client or a server: joins the service on `name` as `role` (CLIENT or SERVER), waiting up to timeout_ms for the
730
+ owner to create the service, for a free place of the role, and for the owner's process to admit it. INVALID for
731
+ another role, or a name a broadcast or a queue holds. On a name a stream holds, the join learns the kind when the
732
+ stream's server accepts it, which brings INVALID; while no accept runs, it waits and raises TIMEOUT.
733
+
734
+ A client's `send` sends a request, and its `recv` (`recv_into`, `recv_acquire`) receives the reply to its oldest
735
+ request whose reply it hasn't received yet, so the n-th reply answers the n-th request; or ABANDONED, if the server
736
+ that took that request ended before it replied (the request counts as answered; it may or may not have been carried
737
+ out, and it is never sent again). `call(request)` does both: it sends the request and returns its reply. A call
738
+ checks its request first: INVALID for an empty one, TOO_LARGE over `max_piece()`, at once, even while a reply is
739
+ owed. Then a call on a cancelled connection raises CANCELLED, whatever its timeout, and sends nothing, as it could
740
+ never receive the reply. A call whose receive timed out or was cancelled leaves its reply owed: the connection keeps
741
+ count, and the next call first receives and drops that reply (at most one is owed, since every call drops it first;
742
+ an ABANDONED in its place counts too), within its own timeout and before it sends, so it never returns another
743
+ request's reply. A call that had to drop an owed reply first sends its request only if its timeout has time left:
744
+ once a positive timeout has run out, it raises TIMEOUT without sending, so no server sees that request; with NO_WAIT
745
+ no step waits, and it sends if there is room. After a call that gave up, a plain `recv` returns that call's late
746
+ reply (or ABANDONED) first: replies come in request order. A call raises INVALID while a request sent with `send`
747
+ still has its reply to be received: receive it first. A call sends and receives, so no other thread may send or
748
+ receive on the connection meanwhile.
749
+
750
+ A server's `recv` takes the next request of any client, and its `send` replies to it, never waiting; a server holds
751
+ one request at a time, so its receives raise INVALID until it replies, and its replies raise INVALID with no request
752
+ held. A request taken in place (`recv_acquire`) stays valid until the reply's `send` or `send_commit`;
753
+ `recv_release` does nothing. A reply to a client that has left is dropped. A server's close answers the request it
754
+ holds ABANDONED, and so does the garbage collection of a server's `Conn` left unclosed: a server holds its request,
755
+ and its client waits for the reply, until the server is closed or garbage-collected (or its process ends). Once the
756
+ service has ended, clients' sends and servers' receives raise DISCONNECTED; a client receives the replies that came
757
+ before the end, then DISCONNECTED."""
758
+ out = ffi.new("fipc_conn_t**")
759
+ _check(lib.fipc_service_join(name.encode(), role, out, timeout_ms), "fipc_service_join")
760
+ return _ServiceClient(out[0]) if role == CLIENT else Conn(out[0])
Binary file
fipc/py.typed ADDED
File without changes
@@ -0,0 +1,325 @@
1
+ Metadata-Version: 2.4
2
+ Name: fipc-python
3
+ Version: 1.0.0
4
+ Summary: Shared-memory IPC for Python: streams, broadcasts, work queues and services between processes on one machine (FastIPC)
5
+ Author: Hayden Donnelly
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://fastipc.github.io/fastipc/
8
+ Project-URL: Repository, https://github.com/fastipc/fastipc
9
+ Project-URL: Documentation, https://fastipc.github.io/fastipc/python.html
10
+ Project-URL: Issues, https://github.com/fastipc/fastipc/issues
11
+ Keywords: ipc,shared-memory,interprocess-communication,zero-copy,messaging
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: System :: Networking
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: cffi>=1.15.0
26
+ Dynamic: license-file
27
+
28
+ # fipc
29
+
30
+ Messages between processes on one machine, through shared memory: the Python binding of
31
+ [FastIPC](https://github.com/fastipc/fastipc), a small library written in Zig. Four kinds, each created on a name:
32
+
33
+ - **Streams:** a server creates a stream (`fipc.stream_create`) and its `Listener` accepts one client at a time; a
34
+ client connects to the name (`fipc.stream_connect`). Each connection has a shared-memory segment with one ring per
35
+ direction, and reports the peer's end as soon as its process exits or closes the connection.
36
+ - **[Broadcasts](#broadcasts):** one publisher (`fipc.broadcast_create`) sends every message to each of its
37
+ subscribers (`fipc.broadcast_join`).
38
+ - **[Work queues](#work-queues):** producers and consumers join a queue (`fipc.queue_create`, `fipc.queue_join`), and
39
+ each message goes to one consumer.
40
+ - **[Services](#services):** clients and servers join a service (`fipc.service_create`, `fipc.service_join`): each
41
+ request goes to one server, and its reply back to its client.
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install fipc-python
47
+ ```
48
+
49
+ The wheels bundle the native library: Windows x64 (Windows 11 / Server 2022 or newer) and Linux x64 (glibc 2.34 or
50
+ newer: Ubuntu 22.04, Debian 12, RHEL 9 and later), which need an x86-64-v3 CPU (AVX2: Intel Haswell, AMD Zen or
51
+ later), Linux on ARM64 (`manylinux_2_34_aarch64`, glibc 2.34 or newer) and macOS 14.4 or newer on Apple Silicon. The
52
+ macOS wheel is tagged `macosx_14_0_arm64` (pip matches macOS 11
53
+ and later by the major version only), so pip also installs it on macOS 14.0 to 14.3, where the library doesn't load.
54
+ The binding loads it through CFFI's ABI mode, so one wheel per platform fits every Python 3.9+. A library of your own
55
+ comes first: set the environment variable `FASTIPC_LIB_DIR` to its folder (when it is set, the binding looks nowhere
56
+ else).
57
+
58
+ ## Use
59
+
60
+ ```python
61
+ # server.py
62
+ import fipc
63
+
64
+ # rings of 1 MiB each way
65
+ with fipc.stream_create("my_channel", 1 << 20) as listener:
66
+ with listener.accept() as conn: # waits for a client
67
+ message = conn.recv()
68
+ conn.send(message.upper())
69
+ ```
70
+
71
+ ```python
72
+ # client.py
73
+ import fipc
74
+
75
+ with fipc.stream_connect("my_channel", timeout_ms=5000) as conn:
76
+ conn.send(b"ping")
77
+ print(conn.recv()) # b'PING'
78
+ ```
79
+
80
+ Run each in its own terminal: `python server.py`, then `python client.py`. The client sends the message `ping`, and the
81
+ server answers with its uppercase, `PING`. Every example of the
82
+ [repository's README](https://github.com/fastipc/fastipc#examples) makes this exchange, so its Zig, C, C++, C#, Java,
83
+ Rust, Lua, JavaScript and Go clients work against this server too, and this client against their servers.
84
+
85
+ A name holds a stream, a broadcast, a work queue or a service, and each kind has its functions: `stream_create`,
86
+ `stream_connect`; `broadcast_create`, `broadcast_join`; `queue_create`, `queue_join`; `service_create`,
87
+ `service_join`. They are the only way to make a `Listener`, a `Conn`, a `Queue` or a `Service`.
88
+
89
+ - **Streams:** `fipc.stream_create(name, capacity)` claims the name and returns its `Listener`, whose `accept()` sets
90
+ one client at a time up on the calling thread (`NO_WAIT` polls); `fipc.stream_connect(name)` connects to it.
91
+ `stream_connect` waits for the server's `accept`, so a client and its server run in different processes or on
92
+ different threads.
93
+ - **Copied messages:** `send` and `recv` (a message of any size: it travels in pieces and arrives whole), or
94
+ `recv_into` a buffer of your own.
95
+ - **Zero-copy** (messages of one piece, up to `max_piece()` bytes): `send_acquire(length)`, write into the
96
+ memoryview, `send_commit(length)`; `recv_acquire()`, read, `recv_release()`.
97
+ - **Batches:** `send_many(messages)` sends a list of messages, published together with at most one wake of the
98
+ receivers, and returns how many went (an error's `sent` says how many went before it); `recv_many(max_count=16)`
99
+ waits for the next message, then takes every further one already there, and returns a list of `bytes`. On every
100
+ kind: a broadcast's subscriber takes those its publisher has moved past, and a service's client sends a batch of
101
+ requests (owed as its sends are: `call` waits until their replies are received) and receives their replies in
102
+ order; a server's batches raise `INVALID`. Batch both ends: a receiver that takes one message per call falls
103
+ behind a batching sender.
104
+
105
+ Timeouts are int milliseconds, the last parameter: `NO_WAIT` (0) doesn't wait, `FOREVER` (-1, the default) waits for
106
+ ever. A call that fails raises `FipcError`, whose `result` is a `Result` (`Result.TIMEOUT`, `Result.DISCONNECTED`,
107
+ ...). On a connection, one thread at a time sends and one receives; `cancel` may be called from any thread, `close`
108
+ only when no other thread is inside a call on the connection. A disconnected connection is final: close it, and
109
+ accept or connect a new one. A `Conn` left unclosed is closed when it is garbage-collected (a thread inside a call on
110
+ it, or a memoryview of `send_acquire` or `recv_acquire`, keeps it referenced), and so is a `Listener`, which frees its
111
+ name; a `Queue` and a `Service` never are.
112
+
113
+ ## Broadcasts
114
+
115
+ A broadcast sends every message of one publisher to each of its subscribers, in any processes of the user: each
116
+ message is stored once, in memory every subscriber maps, and is of one piece (up to a quarter of the capacity).
117
+
118
+ ```python
119
+ # publisher.py
120
+ import fipc
121
+
122
+ # 64 KiB of messages, up to 8 subscribers; one that holds the publisher back 5 s is dropped
123
+ with fipc.broadcast_create("my_broadcast", 1 << 16, 8, 5000) as publisher:
124
+ publisher.wait_for_members(1) # waits for the first subscriber
125
+ for i in (1, 2, 3):
126
+ publisher.send(f"tick {i}".encode())
127
+ # closing the publisher ends the broadcast: subscribers get DISCONNECTED after the ticks
128
+ ```
129
+
130
+ ```python
131
+ # subscriber.py
132
+ import fipc
133
+
134
+ # gated: misses nothing (the publisher waits for it)
135
+ with fipc.broadcast_join("my_broadcast", fipc.SUBSCRIBER_GATED, timeout_ms=5000) as subscriber:
136
+ try:
137
+ while True:
138
+ print(subscriber.recv().decode()) # tick 1, tick 2, tick 3
139
+ except fipc.FipcError as e:
140
+ if e.result != fipc.Result.DISCONNECTED: # the publisher closed
141
+ raise
142
+ ```
143
+
144
+ Run `python publisher.py`, then `python subscriber.py` (or in the other order: `broadcast_join` waits up to its
145
+ timeout for the publisher), which prints `tick 1` to `tick 3`. `fipc.broadcast_create(name, capacity,
146
+ max_subscribers, max_stall_ms)` returns the publisher, a `Conn` and the broadcast's one handle, which the `with` block
147
+ closes.
148
+
149
+ - **Joining:** `fipc.broadcast_join(name, role)`; the publisher's process admits each subscriber by itself, on a
150
+ thread of the library's. `publisher.wait_for_members(at_least)` waits until that many have joined (at a timeout or
151
+ a cancel, the error's `count` holds the count), so that nothing is sent before they are there.
152
+ - **Gated** (`SUBSCRIBER_GATED`): misses nothing, as the publisher waits for it, and may receive in place
153
+ (`recv_acquire`, `recv_release`). One that holds the publisher back for `max_stall_ms` is unsubscribed: its
154
+ receives raise `UNSUBSCRIBED`.
155
+ - **Lossy** (`SUBSCRIBER_LOSSY`): never holds the publisher back. One that falls a lap behind gets `OVERRUN` instead
156
+ of a message, and the error's `lost` says how many messages it lost; the next receive goes on from a recent message.
157
+ - **The end:** closing the publisher ends the broadcast and frees its name; each subscriber receives what was sent,
158
+ then `DISCONNECTED`. The publisher's garbage collection closes it too: keep it referenced while the broadcast is in
159
+ use.
160
+
161
+ ## Work queues
162
+
163
+ A work queue takes messages from up to 63 producers and hands each to one of up to 64 consumers, in any processes of
164
+ the user. Each producer has a ring of its own; a consumer takes a message of any producer, and the queue promises no
165
+ order. Messages are of one piece (up to a quarter of the capacity) and wait in the queue until a consumer takes them.
166
+ Each message goes to one consumer, and again only if that consumer's process ends before acknowledging it
167
+ (**Delivery**, below).
168
+
169
+ ```python
170
+ # producer.py
171
+ import fipc
172
+
173
+ # 4 KiB of messages per producer, 1 producer and 1 consumer: this process owns the queue and is its producer
174
+ with fipc.queue_create("my_queue", 4096, 1, 1) as queue:
175
+ with fipc.queue_join("my_queue", fipc.PRODUCER) as producer:
176
+ for i in (1, 2, 3):
177
+ producer.send(f"job {i}".encode()) # waits in the queue until a consumer takes it
178
+ queue.wait_for_members(fipc.CONSUMER, 1) # waits for the consumer to join
179
+ # closing the queue ends it: the consumer gets DISCONNECTED after the jobs
180
+ ```
181
+
182
+ ```python
183
+ # consumer.py
184
+ import fipc
185
+
186
+ with fipc.queue_join("my_queue", fipc.CONSUMER, timeout_ms=5000) as consumer:
187
+ try:
188
+ while True:
189
+ print(consumer.recv().decode()) # job 1, job 2, job 3
190
+ except fipc.FipcError as e:
191
+ if e.result != fipc.Result.DISCONNECTED: # the queue ended
192
+ raise
193
+ ```
194
+
195
+ Run `python producer.py`, then `python consumer.py` (or in the other order: `queue_join` waits up to its timeout for
196
+ the queue), which prints `job 1` to `job 3`. `fipc.queue_create(name, capacity, max_producers, max_consumers)`
197
+ returns the `Queue`, which the `with` block closes.
198
+
199
+ - **Joining:** `fipc.queue_join(name, PRODUCER)` or `fipc.queue_join(name, CONSUMER)`, the owner's members too, from
200
+ any thread: the owner's process admits each joiner by itself, on a thread of the library's. The example's producer
201
+ sends first (the jobs wait in the queue), then waits for its consumer, so it works in either start order.
202
+ - **The owner's queue:** `queue.wait_for_members(role, at_least)` waits until that many members of the role
203
+ (`PRODUCER`, `CONSUMER`, or `None` for both) have joined (at a timeout or a cancel, the error's `count` holds the
204
+ count); `queue.cancel()` makes its waits raise `CANCELLED`, from any thread. A `Queue` never closes itself (it has
205
+ no finalizer): a queue lives until `close()` or the end of its process, and dropping the object doesn't end it.
206
+ - **Receiving:** `recv()`, `recv_into(buf)` (`TOO_LARGE` takes nothing: another consumer may take that message) or in
207
+ place, `recv_acquire()` until `recv_release()`.
208
+ - **The end:** after the queue's close, a producer's sends raise `DISCONNECTED`, and each consumer receives what is
209
+ left in the queue, then `DISCONNECTED`. A producer's or a consumer's close only leaves the queue.
210
+
211
+ **Delivery.** Each message goes to one consumer. It goes again, to another consumer, only if the process of the consumer
212
+ that took it ends before acknowledging it, and when a consumer acknowledges is its own choice, not the queue's. A
213
+ message received in place is acknowledged at its release (or the consumer's next receive, or its close).
214
+
215
+ | How the consumer receives | When it acknowledges | If its process ends | What the application gets |
216
+ |---|---|---|---|
217
+ | `recv()` or `recv_into()` (copying) | inside the call, before it returns | inside the call: the message goes to another consumer, and this one never saw it; after the call returned: it doesn't go again | **at most once**: no duplicates |
218
+ | `recv_acquire()`, then `recv_release()` at once, then the work | at the release, before the work | as above | **at most once**: no duplicates |
219
+ | `recv_acquire()`, the work, then `recv_release()` | at the release, after the work | before the release: the message goes to another consumer | **at least once**: a duplicate is possible, so the work must be idempotent |
220
+
221
+ - **Choose by the work, per consumer.** Work that may safely run twice (rendering, a computation, re-indexing,
222
+ converting a file) acknowledges after it; work whose effects must never repeat (a payment, an email, a motor command)
223
+ acknowledges before it.
224
+ - **Not covered:** a consumer that hangs while its process runs (there are no timeouts, by design); the owner's process,
225
+ with which the queue ends; a reboot. A consumer holds at most one unacknowledged message at a time, the one received
226
+ in place.
227
+ - **A message that goes again comes in no set order,** after messages its producer sent later: the queue promises no
228
+ order anyway.
229
+ - **A message held in place holds back its producer's room** until its release: a consumer that acknowledges after a
230
+ long job keeps that message's room, and so the room of the producer's messages after it, until then.
231
+ - **A message that crashes every consumer that takes it,** acknowledged after the work, goes to each consumer in turn,
232
+ with no limit: acknowledge before the work where that can happen.
233
+
234
+ ## Services
235
+
236
+ A service takes requests from up to 64 clients and hands each to one of up to 64 servers, in any processes of the
237
+ user; the server that took a request replies to it, and the reply goes back to the client that sent it. A server waits
238
+ for the requests of every client at once (one `recv`), and the servers of a pool share the load: each takes the next
239
+ request of any client. Each client has a ring of its own, which holds its requests and the room for their replies, so a
240
+ server never waits for a client. Requests and replies are of one piece, 1 to `max_message` bytes, the owner's choice.
241
+
242
+ ```python
243
+ # service_server.py
244
+ import fipc
245
+
246
+ # 4 KiB per client, requests and replies of up to 256 bytes, 1 client and 1 server: this process owns the service
247
+ # and is its server
248
+ with fipc.service_create("my_service", 4096, 256, 1, 1):
249
+ with fipc.service_join("my_service", fipc.SERVER, timeout_ms=5000) as server:
250
+ for _ in range(3): # three requests, then the end
251
+ request = server.recv() # the next request of any client
252
+ server.send(request.upper()) # the reply goes to its client, and never waits
253
+ # closing the service ends it: the client still receives the replies sent
254
+ ```
255
+
256
+ ```python
257
+ # service_client.py
258
+ import fipc
259
+
260
+ with fipc.service_join("my_service", fipc.CLIENT, timeout_ms=5000) as client:
261
+ for i in (1, 2, 3):
262
+ # sends the request and returns its reply; FipcError (ABANDONED) if the server ended before it replied
263
+ reply = client.call(f"ping {i}".encode())
264
+ print(reply.decode()) # PING 1, PING 2, PING 3
265
+ ```
266
+
267
+ Run `python service_server.py`, then `python service_client.py` (or in the other order: `service_join` waits up to its
268
+ timeout for the service), which prints `PING 1` to `PING 3`. `fipc.service_create(name, capacity, max_message,
269
+ max_clients, max_servers)` returns the `Service`, which the `with` block closes.
270
+
271
+ - **Joining:** `fipc.service_join(name, CLIENT)` or `fipc.service_join(name, SERVER)`, the owner's members too, from
272
+ any thread: the owner's process admits each joiner by itself, on a thread of the library's. Clients wait while no
273
+ server is joined, and servers while no client is.
274
+ - **The owner's service:** `service.wait_for_members(role, at_least)` waits until that many members of the role
275
+ (`CLIENT`, `SERVER`, or `None` for both) have joined (at a timeout or a cancel, the error's `count` holds the count);
276
+ `service.cancel()` makes its waits raise `CANCELLED`, from any thread. A `Service` never closes itself (it has no
277
+ finalizer): a service lives until `close()` or the end of its process, and dropping the object doesn't end it.
278
+ - **A client:** `send(request)` sends a request, and `recv()` (`recv_into(buf)`, or in place `recv_acquire()` until
279
+ `recv_release()`) receives the reply to its oldest request whose reply it hasn't received yet: the n-th reply answers
280
+ the n-th request, whichever server replies first. A client may send several requests before it receives (as many as
281
+ its ring has room for: each holds its length rounded up to 64 bytes, plus `max_message` rounded up to 64, until its
282
+ reply has been received), and the servers work on them at once. A receive that times out or is cancelled leaves its
283
+ reply to come: the next receive returns it.
284
+ - **`call(request)`** sends a request and returns its reply. A call checks its request first: `INVALID` for an empty
285
+ one, `TOO_LARGE` over `max_piece()`, at once, even while a reply is owed. Then a call on a cancelled connection raises
286
+ `CANCELLED` at once, whatever its timeout, and sends nothing: it could never receive the reply. The connection keeps
287
+ count of the replies still owed: a call whose receive timed out or was cancelled leaves its reply owed, and the next
288
+ call first receives and drops that reply (at most one is owed, since every call drops it first; an `ABANDONED` in its
289
+ place counts too), within its own timeout and before it sends, so a call never returns the reply to another request. A
290
+ call that had to drop an owed reply first sends its request only if its timeout has time left: once a positive timeout
291
+ has run out, it raises `TIMEOUT` without sending, so no server sees that request; with `NO_WAIT` (0) no step waits,
292
+ and it sends if there is room. After a call that gave up, a plain `recv` returns that call's late reply (or
293
+ `ABANDONED`) first: replies come in request order. A call raises `INVALID` while a request sent with `send` still has
294
+ its reply to be received: that reply is yours, so receive it first. A call sends and receives, so no other thread may
295
+ send or receive on the connection meanwhile.
296
+ - **A server:** `recv()` takes the next request of any client (each client's in the order it sent them), and
297
+ `send(reply)` replies to it and never waits. A server holds one request at a time: its receives raise `INVALID` until
298
+ it replies, and a reply with no request held raises `INVALID`. A request received in place (`recv_acquire()`) stays
299
+ valid until the reply's `send` or `send_commit`; `recv_release()` does nothing. A reply to a client that has left is
300
+ dropped. A server may receive on one thread and reply on another. Its `close()` answers the request it holds
301
+ `ABANDONED`, and so does the garbage collection of a server's `Conn` left unclosed: a server holds its request, and
302
+ its client waits for the reply, until it is closed (with `with` or `close()`) or garbage-collected (or its process
303
+ ends).
304
+ - **`ABANDONED`:** a client's receive (or call) raises `FipcError` with `Result.ABANDONED` when the server that took
305
+ the request ended before it replied: it closed its connection, or its process ended. The request counts as answered,
306
+ and the next receive returns the next reply. An abandoned request may or may not have been carried out; the service
307
+ never sends it again, and the client decides whether to.
308
+ - **The end:** after the service's close, clients' sends and servers' receives raise `DISCONNECTED`; a client receives
309
+ the replies that came before the end, then `DISCONNECTED`, and a server's reply raises `DISCONNECTED`. A client's or
310
+ a server's close only leaves the service: a client's requests that no server took are dropped.
311
+ - **Not covered:** a server that hangs while its process runs (there are no timeouts, by design: its client waits,
312
+ within its own timeouts); the owner's process, with which the service ends.
313
+
314
+ ## Documentation
315
+
316
+ - [The website](https://fastipc.github.io/fastipc/python.html): this binding's guide, with examples of every call.
317
+ - [`include/fipc.h`](https://github.com/fastipc/fastipc/blob/main/include/fipc.h): the C API under this binding, and every call's contract (results,
318
+ timeouts, threads, messages in pieces, the peer's end).
319
+ - [Platform support](https://github.com/fastipc/fastipc/blob/main/docs/platform-support.md), the [protocol](https://github.com/fastipc/fastipc/blob/main/docs/protocol.md) and the
320
+ [benchmarks](https://github.com/fastipc/fastipc/blob/main/docs/perf/bindings-baseline.md).
321
+ - [Changelog](https://github.com/fastipc/fastipc/blob/main/CHANGELOG.md), [issues](https://github.com/fastipc/fastipc/issues).
322
+
323
+ ## License
324
+
325
+ MIT. Copyright (c) 2025-2026 Hayden Donnelly. See [LICENSE](https://github.com/fastipc/fastipc/blob/main/LICENSE).
@@ -0,0 +1,9 @@
1
+ fipc/__init__.py,sha256=VnAQDawa-dcqBJUhhiqFA1EowbPPeUru95TqGn0vo4o,2051
2
+ fipc/_fipc.py,sha256=Tmz6WbFVgxnSoJFDnrb9CcZ4STepldH0ThweKAC9eoU,44812
3
+ fipc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ fipc/_native/libfastipc.dylib,sha256=BePaSb5FX0_Mek_pictoc5vhGl5CLb9RPXk7-VvPdMs,539632
5
+ fipc_python-1.0.0.dist-info/licenses/LICENSE,sha256=MOwjp6PAUn5cman8LKZHxggBBEe2qZjai87CKCfm3lA,1077
6
+ fipc_python-1.0.0.dist-info/METADATA,sha256=AZwOo4K_OdhEvwI57z5tBGShrSN6CqD4t2uXKfoU9NU,21010
7
+ fipc_python-1.0.0.dist-info/WHEEL,sha256=FhEgcH91d6QFK-RbYU7zbRbSEPxYLL8iqNY8lJNbOc4,106
8
+ fipc_python-1.0.0.dist-info/top_level.txt,sha256=Au6R2A9qZGata9VKymUS6hxDN1rjJ9wSzHC6y2q2ZNQ,5
9
+ fipc_python-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-macosx_14_0_arm64
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Hayden Donnelly
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ fipc