Skip to content

Commit 9606fe0

Browse files
Merge pull request #87 from pyscript/issue-83
Fix #83 - Explain in details our pyscript.ffi
2 parents 238a8f6 + 168cd12 commit 9606fe0

File tree

2 files changed

+123
-0
lines changed

2 files changed

+123
-0
lines changed

Diff for: docs/user-guide/ffi.md

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# PyScript FFI
2+
3+
The reason we decided to incrementally provide a unified *pyscript.ffi* utility is that [Pyodide](https://pyodide.org/en/stable/usage/api/python-api/ffi.html)'s *ffi* is only partially implemented in *MicroPython* but there are fundamental differences even with the few common utilities both projects provide, hence our intention to smooth out their usage with all the features or caveats one needs to be aware of.
4+
5+
Our `pyscript.ffi` offers the following utilities:
6+
7+
* `ffi.to_js(reference)` to convert any Python reference to its *JS* counterpart
8+
* `ffi.create_proxy(def_or_lambda)` to proxy a generic Python's function into a *JS* one, without destroying its reference right away
9+
10+
## to_js
11+
12+
In the [Pyodide project](https://pyodide.org/en/stable/usage/api/python-api/ffi.html#pyodide.ffi.to_js), this utility converts Python dictionaries into a *Map*. The *Map* object more accurately reflects the `obj.get(field)` native Python way to retrieve a value from a dictionary.
13+
14+
However, we have noticed that this default conversion plays very badly with pretty much any native or user made *JS* API, so that most of the code around `to_js` needs to explicitly define a `dict_converter=js.Object.fromEntries` instead of mapping Python's dictionaries directly as JS' objects literal.
15+
16+
On top of that, in *MicroPython* the default conversion already produces *object literals*, making the need to specify a different converter pretty pointless.
17+
18+
As we all known though, *explicit is better than implicit*, so whenever an object literal is expected and no reason to hold a Python reference in the JS world is needed, using `to_js(python_dictionary)` will guarantee the entity to be copied or passed as *object literal* to the consumer of that dictionary.
19+
20+
```html title="to_js: pyodide.ffi VS pyscript.ffi"
21+
<!-- works on Pyodide (type py) only -->
22+
<script type="py">
23+
from pyodide.ffi import to_js
24+
25+
# default into JS new Map([["a", 1], ["b", 2]])
26+
to_js({"a": 1, "b": 2})
27+
</script>
28+
29+
<!-- works on both Pyodide and MicroPython -->
30+
<script type="py">
31+
from pyscript.ffi import to_js
32+
33+
# always default into JS {"a": 1, "b": 2}
34+
to_js({"a": 1, "b": 2})
35+
</script>
36+
```
37+
38+
!!! Note
39+
40+
It is still possible to specify a different `dict_converter` or use Pyodide
41+
specific features while converting Python references by simply overriding
42+
the explicit field for `dict_converter`. However, we cannot guarantee
43+
all fields and features provided by Pyodide will work the same on MicroPython.
44+
45+
## create_proxy
46+
47+
In the [Pyodide project](https://pyodide.org/en/stable/usage/api/python-api/ffi.html#pyodide.ffi.create_proxy), this utility guarantees that a Python lambda or callback won't be garbage collected immediately after.
48+
49+
There are, still in *Pyodide*, dozens ad-hoc utilities that work around that implementation detail, such as `create_once_callable` or `pyodide.ffi.wrappers.add_event_listener` or `pyodide.ffi.wrappers.set_timeout` and others, all methods whose goal is to automatically handle the lifetime of the passed callback.
50+
51+
There are also implicit helpers like in `Promise` where `.then` or `.catch` callbacks are implicitly handled a part to guarantee no leaks once executed.
52+
53+
Because the amount of details could be easily overwhelming, we decided to provide an `experimental_create_proxy = "auto"` configuration option that should never require our users to care about all these details while all the generic APIs in the *JS* world should "*just work*".
54+
55+
This flag is strictly coupled with the *JS* garbage collector and it will eventually destroy all proxies that were previously created through the [FinalizationRegistry](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry) native utility.
56+
57+
This flag also won't affect *MicroPython* because it rarely needs a `create_proxy` at all, when Python functions are passed to JS, but there are still cases where that conversion might be needed or explicit.
58+
59+
For these reasons we expose also the `create_proxy` utility which does not change much in the *MicroPython* world or code, but it might be still needed in the *Pyodide* runtime.
60+
61+
The main difference with these two different runtime is that *MicroPython* doesn't provide (yet) a way to explicitly destroy the proxy reference, while in *Pyodide* it's still desirable to explicitly invoke that `proxy.destroy()` when the function is not needed/called anymore.
62+
63+
```html title="A classic Pyodide failure VS MicroPython"
64+
<!-- Throws:
65+
Uncaught Error: This borrowed proxy was automatically destroyed
66+
at the end of a function call. Try using create_proxy or create_once_callable.
67+
-->
68+
<script type="py">
69+
import js
70+
js.setTimeout(lambda x: print(x), 1000, "fail");
71+
</script>
72+
73+
<!-- logs "success" after a second -->
74+
<script type="mpy">
75+
import js
76+
js.setTimeout(lambda x: print(x), 1000, "success");
77+
</script>
78+
```
79+
80+
To address the difference in Pyodide's behaviour, we can use the experimental flag:
81+
82+
```html title="experimental create_proxy"
83+
<py-config>
84+
experimental_create_proxy = "auto"
85+
</py-config>
86+
87+
<!-- logs "success" after a second -->
88+
<script type="py">
89+
import js
90+
js.setTimeout(lambda x: print(x), 1000, "success");
91+
</script>
92+
```
93+
94+
Alternatively, it is possible to create a proxy via our *ffi* in both interpreters, but only in Pyodide can we then destroy such proxy:
95+
96+
```html title="pyscript.ffi.create_proxy"
97+
<!-- success in both Pyodide and MicroPython -->
98+
<script type="py">
99+
from pyscript.ffi import create_proxy
100+
import js
101+
102+
def log(x):
103+
try:
104+
proxy.destroy()
105+
except:
106+
pass # MicroPython
107+
108+
print(x)
109+
110+
proxy = create_proxy(log)
111+
js.setTimeout(proxy, 1000, "success");
112+
</script>
113+
```
114+
115+
!!! warning
116+
117+
In MicroPython proxies might leak due to the lack of a `destroy()` method.
118+
Usually proxies are better off created explicitly for event listeners
119+
or other utilities that won't need to be destroyed in the future.
120+
Until we have a `destroy()` in MicroPython, it's still suggested to try
121+
and test if the experimental flag is good enough for Pyodide and let
122+
both runtime handle possible leaks behind the scene automatically.

Diff for: mkdocs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ nav:
7474
- The DOM &amp; JavaScript: user-guide/dom.md
7575
- Workers: user-guide/workers.md
7676
- Builtin helpers: user-guide/builtins.md
77+
- FFI in detail: user-guide/ffi.md
7778
- Python terminal: user-guide/terminal.md
7879
- Python editor: user-guide/editor.md
7980
- Plugins: user-guide/plugins.md

0 commit comments

Comments
 (0)