Skip to content

Commit b38989f

Browse files
authored
recipe: cffi 2.1.1 with working ffi.callback() on iOS (#129)
## What `cffi` 2.1.1 with `ffi.callback()` working on iOS, plus two CI fixes: testing rebuilt `flet-lib*` wheels, and pinned Ubuntu runners. ## Why On iOS every `ffi.callback()` raised `MemoryError: Cannot allocate write+execute memory for ffi.callback()`, and variadic ABI-mode calls returned garbage on arm64. cffi uses libffi's closure API on Apple only when `FFI_AVAILABLE_APPLE` is defined, which only Apple's own macOS `ffi.h` does. With the upstream libffi our iOS builds use, cffi falls back to its own allocator, which maps writable+executable memory, and iOS refuses. libffi itself needs no such memory there (arm64 uses trampoline tables, which is why CPython's ctypes callbacks work). Upstream fixed this in python-cffi/cffi#265, merged but not in any release yet. ## Changes - **`recipes/cffi`**: 2.1.1, build 1, new `ios-ffi-closure-alloc.patch` enabling `ffi_closure_alloc` / `ffi_prep_closure_loc` / `ffi_prep_cif_var` under `TARGET_OS_IPHONE` (Android's preprocessed source is byte-identical). `mobile.patch` regenerated for 2.1.1, no behaviour change. - 2.1.1 rather than rebuilding 2.0.0: PyPI ships official cffi 2.1.1 iOS wheels (cp313-cp315) without the fix, and they outrank 2.0.0. At the same version, our build tag wins. - The patch is a `TARGET_OS_IPHONE` variant rather than #265 verbatim: forge's non-interactive `patch` reverse-applies a hunk it sees as already present, so a future bump to a release containing #265 would silently remove the fix. This variant lands as dead code instead. Drop it once a release includes #265. - Tests: a C-invoked callback (`qsort`), a callback from a native thread, 3000 closures allocated and freed, and a variadic `snprintf`. Four of five fail on the old wheels. - **`recipes/soundfile`**: file-object reads (`sf.read(io.BytesIO(...))`) now work on iOS. Removed the test asserting the iOS `MemoryError`, dropped the iOS skip, updated README, example README and a comment. - **CI**: `dist-test/` now bumps `py3-none` (build.sh `flet-lib*`) wheels to build 9999 too, and Android's loop also matches `abi3`. Before, a rebuilt `flet-lib*` at an already-published version and build number could lose to its published copy in the device test. - **CI**: Ubuntu runners pinned to `ubuntu-24.04` (what `ubuntu-latest` resolves to today). `ubuntu-latest` will move to 26.04, which would change the Android lane (NDK builds, KVM emulator) for every recipe at once; macOS is already pinned to `macos-26`. ## Validation - On-device CI, Android + iOS, Python 3.12/3.13/3.14 (`packages=cffi:,soundfile:`, `prebuild_recipes=cffi`): all green on the fork. Note soundfile's iOS tests need the new cffi, so they only pass when cffi is built in the same run or published. - iOS simulator, before/after: callbacks raise `MemoryError` and `snprintf` returns garbage on forge 2.0.0-1 and PyPI 2.1.1; all pass with this build. - Not run: physical iOS devices, x86_64 simulator. ## Consumer notes Apps get working `ffi.callback()` on iOS automatically once cffi 2.1.1-1 is on pypi.flet.dev; for soundfile that means in-memory reads work on both platforms. Details: [`recipes/soundfile/README.md`](recipes/soundfile/README.md).
1 parent b5c3f20 commit b38989f

9 files changed

Lines changed: 231 additions & 83 deletions

File tree

‎.github/workflows/build-wheels-version.yml‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -495,10 +495,11 @@ jobs:
495495
for w in "$GITHUB_WORKSPACE"/dist/*.whl; do
496496
[[ -e "$w" ]] || continue
497497
base=$(basename "$w")
498-
# name-version[-buildtag]-cpXY-cpXY-plat.whl
499-
# → name-version-9999-cpXY-cpXY-plat.whl
498+
# name-version[-buildtag]-{cpXY-cpXY|cpXY-abi3|py3-none}-plat.whl
499+
# → name-version-9999-...-plat.whl. py3-none covers the build.sh
500+
# flet-lib* wheels a prebuild_recipes chain puts here.
500501
new=$(printf '%s\n' "$base" \
501-
| sed -E 's/^([^-]+-[^-]+)(-[0-9]+)?-(cp[0-9]+-cp[0-9]+)-/\1-9999-\3-/')
502+
| sed -E 's/^([^-]+-[^-]+)(-[0-9]+)?-((cp[0-9]+|py3)-(cp[0-9]+|abi[0-9]+|none))-/\1-9999-\3-/')
502503
cp "$w" "$GITHUB_WORKSPACE/dist-test/$new"
503504
done
504505
@@ -562,7 +563,7 @@ jobs:
562563
[[ -e "$w" ]] || continue
563564
base=$(basename "$w")
564565
new=$(printf '%s\n' "$base" \
565-
| sed -E 's/^([^-]+-[^-]+)(-[0-9]+)?-(cp[0-9]+-(cp[0-9]+|abi[0-9]+))-/\1-9999-\3-/')
566+
| sed -E 's/^([^-]+-[^-]+)(-[0-9]+)?-((cp[0-9]+|py3)-(cp[0-9]+|abi[0-9]+|none))-/\1-9999-\3-/')
566567
cp "$w" "$GITHUB_WORKSPACE/dist-test/$new"
567568
done
568569

‎recipes/cffi/meta.yaml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package:
22
name: cffi
3-
version: "2.0.0"
3+
version: "2.1.1"
44

55
build:
66
number: 1
@@ -10,4 +10,5 @@ requirements:
1010
- libffi
1111

1212
patches:
13-
- mobile.patch
13+
- mobile.patch
14+
- ios-ffi-closure-alloc.patch
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
Make ffi.callback() and variadic calls work on iOS (python-cffi/cffi#265).
2+
3+
cffi uses libffi's ffi_closure_alloc()/ffi_prep_closure_loc()/ffi_prep_cif_var()
4+
on Apple only when ffi.h defines FFI_AVAILABLE_APPLE, which only the macOS SDK's
5+
ffi.h does. iOS links the support tree's upstream libffi, so every
6+
ffi.callback() fell back to malloc_closure.h's mmap(PROT_WRITE|PROT_EXEC),
7+
which iOS refuses ("MemoryError: Cannot allocate write+execute memory"), and
8+
variadic calls went through ffi_prep_cif(), which passes garbage on arm64,
9+
where Apple puts varargs on the stack. The fallback's ffi_prep_closure() is
10+
unusable on arm64 even with W+X allowed: that libffi uses trampoline tables.
11+
12+
Same defines as upstream #265 (90e8cc434a, merged 2026-07-26, in no release
13+
as of 2.1.1), narrowed to TARGET_OS_IPHONE rather than upstream's hunk
14+
verbatim, because forge applies patches with a non-interactive `patch -p1`:
15+
on a release that already carries #265 the verbatim hunk is taken as
16+
"previously applied", answered "Assume -R? [y]", and silently REVERTED with
17+
exit 0 (measured on the 2.1.1 tree with #265 applied). This variant lands
18+
there as a redundant, dead branch instead. Android is not __APPLE__ and forge
19+
builds no macOS, so behaviour on every forge target equals upstream's. Drop
20+
this patch on a cffi release that contains #265.
21+
22+
--- a/src/c/_cffi_backend.c
23+
+++ b/src/c/_cffi_backend.c
24+
@@ -99,6 +99,10 @@
25+
* This is also used on macOS, provided we are executing on macOS 10.15 or
26+
* above. It's a mess because it needs runtime checks in that case.
27+
*/
28+
+#ifdef __APPLE__
29+
+# include <TargetConditionals.h>
30+
+#endif
31+
+
32+
#ifdef __NetBSD__
33+
34+
# define CFFI_CHECK_FFI_CLOSURE_ALLOC 1
35+
@@ -117,6 +121,19 @@
36+
# define CFFI_CHECK_FFI_PREP_CIF_VAR __builtin_available(macos 10.15, ios 13, watchos 6, tvos 13, *)
37+
# define CFFI_CHECK_FFI_PREP_CIF_VAR_MAYBE 1
38+
39+
+#elif defined(__APPLE__) && TARGET_OS_IPHONE
40+
+
41+
+/* iOS links upstream libffi, whose ffi.h lacks FFI_AVAILABLE_APPLE (only the
42+
+ * macOS SDK's has it). Falling to malloc_closure.h means a W+X mmap iOS
43+
+ * refuses, and ffi_prep_cif() for varargs, which arm64 Apple passes on the
44+
+ * stack. The static libffi always has all three. python-cffi/cffi#265. */
45+
+# define CFFI_CHECK_FFI_CLOSURE_ALLOC 1
46+
+# define CFFI_CHECK_FFI_CLOSURE_ALLOC_MAYBE 1
47+
+# define CFFI_CHECK_FFI_PREP_CLOSURE_LOC 1
48+
+# define CFFI_CHECK_FFI_PREP_CLOSURE_LOC_MAYBE 1
49+
+# define CFFI_CHECK_FFI_PREP_CIF_VAR 1
50+
+# define CFFI_CHECK_FFI_PREP_CIF_VAR_MAYBE 1
51+
+
52+
#elif defined(__EMSCRIPTEN__)
53+
54+
# define CFFI_CHECK_FFI_CLOSURE_ALLOC 1

‎recipes/cffi/patches/mobile.patch‎

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1-
diff --git a/setup.py b/setup.py
2-
index f9cabaa..c592203 100644
1+
Cross-compile cffi against the target's libffi, not the build machine's.
2+
3+
setup.py: drop the /usr/include/ffi and /usr/include/libffi defaults and the
4+
pkg-config probe, which would point a cross build at the host's libffi
5+
headers; forge's cross environment supplies the target paths.
6+
7+
malloc_closure.h: guard the Linux-only PaX getline() scan by
8+
__ANDROID_API__ >= 18, as Chaquopy does (bionic gained getline() at API 18).
9+
A no-op at forge's API 24.
10+
11+
Regenerated against 2.1.1, where the setup.py hunk moved by 6 lines.
12+
13+
diff -ruN a/setup.py b/setup.py
314
--- a/setup.py
415
+++ b/setup.py
5-
@@ -9,8 +9,7 @@ import setuptools
16+
@@ -9,8 +9,7 @@
617

718
sources = ['src/c/_cffi_backend.c']
819
libraries = ['ffi']
@@ -12,7 +23,7 @@ index f9cabaa..c592203 100644
1223
define_macros = [('FFI_BUILDING', '1')] # for linking with libffi static library
1324
library_dirs = []
1425
extra_compile_args = []
15-
@@ -144,7 +143,7 @@ if sys.platform == "win32" and uses_msvc():
26+
@@ -150,7 +149,7 @@
1627
sources.extend(os.path.join(COMPILE_LIBFFI, filename)
1728
for filename in _filenames)
1829
else:
@@ -21,11 +32,10 @@ index f9cabaa..c592203 100644
2132
ask_supports_thread()
2233
ask_supports_sync_synchronize()
2334

24-
diff --git a/src/c/malloc_closure.h b/src/c/malloc_closure.h
25-
index bebb93d..f82dbe4 100644
35+
diff -ruN a/src/c/malloc_closure.h b/src/c/malloc_closure.h
2636
--- a/src/c/malloc_closure.h
2737
+++ b/src/c/malloc_closure.h
28-
@@ -42,6 +42,8 @@ emutramp_enabled_check (void)
38+
@@ -42,6 +42,8 @@
2939
return 0;
3040
ret = 0;
3141

@@ -34,12 +44,12 @@ index bebb93d..f82dbe4 100644
3444
while (getline (&buf, &len, f) != -1)
3545
if (!strncmp (buf, "PaX:", 4))
3646
{
37-
@@ -51,6 +53,8 @@ emutramp_enabled_check (void)
47+
@@ -51,6 +53,8 @@
3848
break;
3949
}
4050
free (buf);
4151
+ #endif
4252
+
4353
fclose (f);
4454
return ret;
45-
}
55+
}

‎recipes/cffi/tests/test_cffi.py‎

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,101 @@
1-
def test_basic():
2-
from cffi import FFI
1+
import gc
2+
import sys
3+
import threading
4+
from typing import Any
35

6+
from cffi import FFI
7+
8+
CDEF = """
9+
size_t strlen(char *str);
10+
void qsort(void *base, size_t nmemb, size_t size,
11+
int (*compar)(const void *, const void *));
12+
typedef unsigned long pthread_t;
13+
int pthread_create(pthread_t *thread, void *attr,
14+
void *(*start)(void *), void *arg);
15+
int pthread_join(pthread_t thread, void **retval);
16+
int snprintf(char *str, size_t size, const char *format, ...);
17+
"""
18+
19+
20+
def _libc() -> tuple[FFI, Any]:
21+
"""ABI-mode handle on the C library: bionic's public libc.so on Android, the
22+
process's own symbols (libSystem) on iOS."""
423
ffi = FFI()
5-
ffi.cdef("size_t strlen(char *str);")
6-
lib = ffi.dlopen(None)
24+
ffi.cdef(CDEF)
25+
# Not sys.platform: Android's Python 3.12 still reports "linux".
26+
android = hasattr(sys, "getandroidapilevel")
27+
return ffi, ffi.dlopen("libc.so" if android else None)
28+
29+
30+
def test_basic() -> None:
31+
"""ABI-mode call into libc through a dlopen'd handle."""
32+
ffi, lib = _libc()
733
assert lib.strlen(ffi.new("char[]", b"hello world")) == 11
34+
35+
36+
def test_callback_called_from_c() -> None:
37+
"""C calls a Python ffi.callback() (qsort's comparator). On iOS this needs
38+
libffi's ffi_closure_alloc(): cffi's own allocator asks for W+X pages."""
39+
ffi, lib = _libc()
40+
41+
@ffi.callback("int(const void *, const void *)")
42+
def cmp(a: Any, b: Any) -> int:
43+
x, y = ffi.cast("int *", a)[0], ffi.cast("int *", b)[0]
44+
return (x > y) - (x < y)
45+
46+
arr = ffi.new("int[]", [5, 3, 9, 1, 7, 2])
47+
lib.qsort(arr, 6, ffi.sizeof("int"), cmp)
48+
assert list(arr) == [1, 2, 3, 5, 7, 9]
49+
50+
51+
def test_callback_from_native_thread() -> None:
52+
"""A callback entered on a thread Python did not create, as PortAudio and
53+
libsndfile's virtual I/O do."""
54+
ffi, lib = _libc()
55+
seen: list[tuple[int, int]] = []
56+
57+
@ffi.callback("void *(void *)")
58+
def start(arg: Any) -> Any:
59+
seen.append((threading.get_ident(), int(ffi.cast("intptr_t", arg))))
60+
return ffi.NULL
61+
62+
tid = ffi.new("pthread_t *")
63+
assert lib.pthread_create(tid, ffi.NULL, start, ffi.cast("void *", 1234)) == 0
64+
assert lib.pthread_join(tid[0], ffi.NULL) == 0
65+
assert len(seen) == 1
66+
assert seen[0][1] == 1234
67+
assert seen[0][0] != threading.get_ident()
68+
69+
70+
def test_many_callbacks_freed_and_reallocated() -> None:
71+
"""3000 live closures, freed, then 3000 more. On arm64 iOS a libffi trampoline
72+
table holds 1024, so this grows past one table, releases them and starts again."""
73+
ffi, _ = _libc()
74+
cbs = [
75+
ffi.callback("int(int)", (lambda k: lambda x: x + k)(k)) for k in range(3000)
76+
]
77+
assert [cbs[k](1) for k in (0, 1023, 1024, 2999)] == [1, 1024, 1025, 3000]
78+
del cbs
79+
gc.collect()
80+
again = [ffi.callback("int(int)", lambda x: -x) for _ in range(3000)]
81+
assert again[0](5) == -5
82+
assert again[-1](7) == -7
83+
84+
85+
# Last on purpose: a backend without ffi_prep_cif_var() can crash here, not just fail.
86+
def test_variadic_call() -> None:
87+
"""snprintf with a string, an int and a double in the variadic part. arm64
88+
Apple passes varargs on the stack, so this is garbage or a crash unless cffi
89+
prepares the call with ffi_prep_cif_var()."""
90+
ffi, lib = _libc()
91+
buf = ffi.new("char[64]")
92+
n = lib.snprintf(
93+
buf,
94+
64,
95+
b"[%s] [%d] [%.2f]",
96+
ffi.new("char[]", b"hi"),
97+
ffi.cast("int", 42),
98+
ffi.cast("double", 3.5),
99+
)
100+
assert ffi.string(buf) == b"[hi] [42] [3.50]"
101+
assert n == 16

‎recipes/soundfile/README.md‎

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -61,27 +61,26 @@ file, so it is how you check a samplerate or duration before deciding to decode.
6161
### Reading audio the user picked
6262

6363
[`FilePicker`](https://flet.dev/docs/controls/filepicker/) hands back a path on both
64-
platforms, and `sf.read()` takes it directly. **A real path is the only form that works
65-
everywhere** — see the iOS note below.
64+
platforms, and `sf.read()` takes it directly.
6665

67-
Where you hold bytes instead (a download, a file read out of `assets/`), `soundfile` does
68-
accept any file object, and on Android that works:
66+
Where you hold bytes instead (a download, a file read out of `assets/`), pass any binary
67+
file object:
6968

7069
```python
7170
import io
7271

73-
data, sr = sf.read(io.BytesIO(raw_bytes)) # Android and desktop only
72+
data, sr = sf.read(io.BytesIO(raw_bytes))
7473
```
7574

76-
**On iOS this raises `MemoryError`,** and no version of the package will fix it. That path
77-
uses libsndfile's virtual I/O, which means handing the C library a `ffi.callback()`; cffi
78-
writes that trampoline into memory at runtime, and iOS refuses write+execute pages to an
79-
app without the JIT entitlement. The message names the cause:
75+
A file object goes through libsndfile's virtual I/O, which calls back into Python through
76+
`ffi.callback()`. That works on both platforms with **cffi 2.1.1-1 or later from
77+
pypi.flet.dev**, which pip picks over PyPI's own iOS wheel of the same version. Apps built
78+
against an older cffi raised this on iOS for every file object:
8079

8180
MemoryError: Cannot allocate write+execute memory for ffi.callback().
8281

83-
Write the bytes to a file and read that instead — it costs one write, needs no
84-
per-platform branch, and is the recommended form on both platforms:
82+
A real path is still the simplest form, with no callbacks at all. If the bytes are going to
83+
disk anyway, read them back by path:
8584

8685
```python
8786
import os
@@ -92,9 +91,6 @@ with open(path, "wb") as f:
9291
data, sr = sf.read(path)
9392
```
9493

95-
Only the *file-object* form is affected. Paths, and integer file descriptors (which go
96-
through `sf_open_fd`), use no callbacks and work normally on iOS.
97-
9894
### Storage
9995

10096
`soundfile` reads and writes nothing of its own — no config, no cache, no env vars. Files you
@@ -142,9 +138,10 @@ device, which is not something you can assume for every package.
142138
being killed by the OS. Pass `dtype="float32"` unless you specifically need the precision,
143139
and prefer `blocks()` over reading whole files.
144140

145-
- **iOS cannot read audio from a file object.** Repeating it here because it is the one
146-
thing that behaves differently between the two platforms, and it fails at the point of
147-
use rather than at import. `io.BytesIO` in, `MemoryError` out; write a file first.
141+
- **`MemoryError: Cannot allocate write+execute memory` on iOS means an old cffi.** File
142+
objects need cffi 2.1.1-1 or later; with PyPI's 2.1.1 or our 2.0.0-1 the app imports
143+
fine and fails at the first file object. Check which cffi the build resolved rather
144+
than looking at soundfile.
148145

149146
- **Opus only encodes at 48 kHz.** libsndfile will not resample for you; `sf.write(...,
150147
format="OGG", subtype="OPUS")` at any other rate raises. Resample first — [`soxr`](../soxr)
@@ -218,6 +215,10 @@ download six wheels whose contents already live inside `libsndfile.so`.
218215
`py2.py3-none-any` for an unknown `sys.platform`. forge's `fix_wheel` overwrites the tag,
219216
which is what makes our wheel outrank PyPI's `any` wheel at the same version. Do not
220217
"fix" the tag in a patch.
218+
- **File objects on iOS depend on the cffi recipe's `ios-ffi-closure-alloc.patch`**
219+
(python-cffi/cffi#265, in no cffi release as of 2.1.1). If PyPI ships a newer cffi with
220+
iOS wheels before pypi.flet.dev does, apps resolve it, and `test_read_from_file_object`
221+
goes red on iOS unless that release carries #265.
221222
- **FLAC 1.5.0 needs `-DCMAKE_SYSTEM_VERSION` on Android** and `-DENABLE_MULTITHREADING=OFF`;
222223
both have comments in `recipes/flet-libflac/build.sh` saying what breaks without them.
223224

@@ -241,10 +242,10 @@ download six wheels whose contents already live inside `libsndfile.so`.
241242
### Coverage gaps
242243

243244
The device tests cover loading the library, WAV round trips in three subtypes, every
244-
container and codec, virtual I/O from `BytesIO` (Android) and its `MemoryError` on iOS,
245-
the write-a-file workaround, block reads with seeking, and the format registry. They do not
246-
cover: writing to `FLET_APP_STORAGE_*` (a path question, not a
247-
libsndfile one), any real recorded audio file, `sf.info()`, the `RAW` format's manual
248-
`samplerate`/`channels`/`subtype` arguments, or multi-threaded decoding. The security
249-
backports are compile-verified only — there are no regression tests for the five fixes, since
250-
reproducing them needs the malformed inputs from upstream's fuzz corpus.
245+
container and codec, virtual I/O from `BytesIO` on both platforms, bytes spilled to a path,
246+
block reads with seeking, and the format registry. They do not cover: writing to
247+
`FLET_APP_STORAGE_*` (a path question, not a libsndfile one), any real recorded audio
248+
file, `sf.info()`, the `RAW` format's manual `samplerate`/`channels`/`subtype` arguments,
249+
or multi-threaded decoding. The security backports are compile-verified only — there are
250+
no regression tests for the five fixes, since reproducing them needs the malformed inputs
251+
from upstream's fuzz corpus.

‎recipes/soundfile/examples/codec-roundtrip/README.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ What it demonstrates:
2020
quantisation, not codec loss — a distinction worth seeing once.
2121
- **Writing into Flet's storage.** Each round trip writes a real file under
2222
[`FLET_APP_STORAGE_TEMP`](https://flet.dev/docs/reference/environment-variables#flet_app_storage_temp)
23-
and deletes it again. Files rather than `io.BytesIO` deliberately: a file object routes
24-
through libsndfile's virtual I/O, which needs a cffi callback, which needs write+execute
25-
memory that iOS refuses — so the file-object form works on Android and raises
26-
`MemoryError` on iOS. A path works on both.
23+
and deletes it again. Files rather than `io.BytesIO` deliberately: a path needs no
24+
callbacks, so it works with any cffi. A file object routes through libsndfile's virtual
25+
I/O, which calls back into Python through `ffi.callback()`; that works on both platforms
26+
too, but on iOS only with cffi 2.1.1-1 or later from pypi.flet.dev.
2727
- **Compute off the UI thread.** The sweep runs in
2828
[`page.run_thread(...)`](https://flet.dev/docs/controls/page/#flet.Page.run_thread) with a
2929
spinner up, ending in the explicit

‎recipes/soundfile/examples/codec-roundtrip/src/audio_codecs.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ def roundtrip(label, fmt, subtype, rate):
4141
audible-codec-sized for the lossy ones. Returns an `error` string instead if the
4242
format is not compiled in.
4343
44-
Files, not io.BytesIO: a file object would route through libsndfile's virtual I/O,
45-
which needs a cffi callback, which needs write+execute memory that iOS refuses.
44+
Files, not io.BytesIO: a path needs no virtual I/O callbacks, so it works with any
45+
cffi. A file object works too, but on iOS only with cffi 2.1.1-1 or later.
4646
"""
4747
source = signal(rate)
4848
# FLET_APP_STORAGE_TEMP on device; tempfile's default elsewhere.

0 commit comments

Comments
 (0)