PyScript FFI
The foreign function interface (FFI) gives Python access to JavaScript, and JavaScript access to Python. As a result PyScript is able to access all the standard APIs and capabilities provided by the browser.
We provide a unified pyscript.ffi
because
Pyodide's FFI
is only partially implemented in MicroPython and there are some fundamental
differences. The pyscript.ffi
namespace smooths out such differences into
a uniform and consistent API.
Our pyscript.ffi
offers the following utilities:
ffi.to_js(reference)
converts a Python object into its JavaScript counterpart.ffi.create_proxy(def_or_lambda)
proxies a generic Python function into a JavaScript one, without destroying its reference right away.
Should you require access to Pyodide or MicroPython's specific version of the
FFI you'll find them under the pyodide.ffi
and micropython.ffi
namespaces.
Please refer to the documentation for those projects for further information.
to_js
In the
Pyodide project,
this utility converts Python dictionaries into
JavaScript Map
objects.
Such Map
objects reflect the obj.get(field)
semantics native to Python's
way of retrieving a value from a dictionary.
Unfortunately, this default conversion breaks the vast majority of native and
third party JavaScript APIs. This is because the convention in idiomatic
JavaScript is to use an object
for such key/value data structures (not a Map
instance).
A common complaint has been the repeated need to call to_js
with the long
winded argument dict_converter=js.Object.fromEntries
. It turns out, most
people most of the time simply want to map a Python dict
to a JavaScript
object
(not a Map
).
Furthermore, in MicroPython the default Python dict
conversion is to the
idiomatic and sensible JavaScript object
, making the need to specify a
dictionary converter pointless.
Therefore, if there is no reason to hold a Python reference in a JavaScript
context (which is 99% of the time, for common usage of PyScript) then use the
pyscript.ffi.to_js
function, on both Pyodide and MicroPython, to always
convert a Python dict
to a JavaScript object
.
<!-- works on Pyodide (type py) only -->
<script type="py">
from pyodide.ffi import to_js
# default into JS new Map([["a", 1], ["b", 2]])
to_js({"a": 1, "b": 2})
</script>
<!-- works on both Pyodide and MicroPython -->
<script type="py">
from pyscript.ffi import to_js
# always default into JS {"a": 1, "b": 2}
to_js({"a": 1, "b": 2})
</script>
Note
It is still possible to specify a different dict_converter
or use Pyodide
specific features while converting Python references by simply overriding
the explicit field for dict_converter
.
However, we cannot guarantee all fields and features provided by Pyodide will work in the same way on MicroPython.
create_proxy
In the Pyodide project, this function ensures that a Python callable associated with an event listener, won't be garbage collected immediately after the function is assigned to the event. Therefore, in Pyodide, if you do not wrap your Python function, it is immediately garbage collected after being associated with an event listener.
This is so common a gotcha (see the FAQ for
more on this) that the Pyodide project have already
created many work-arounds to address this situation. For example, the
create_once_callable
, pyodide.ffi.wrappers.add_event_listener
and
pyodide.ffi.set_timeout
are all methods whose goal is to automatically manage
the lifetime of the passed in Python callback.
Add to this situation methods connected to the JavaScript Promise
object
(.then
and .catch
callbacks that are implicitly handled to guarantee no
leaks once executed) and things start to get confusing and overwhelming with
many ways to achieve a common end result.
Ultimately, user feedback suggests folks simply want to do something like this, as they write their Python code:
import js
from pyscript import window
def callback(msg):
"""
A Python callable that logs a message.
"""
window.console.log(msg)
# Use the callback without having to explicitly create_proxy.
js.setTimeout(callback, 1000, 'success')
Therefore, PyScript provides an experimental configuration flag called
experimental_create_proxy = "auto"
. When set, you should never have to care
about these technical details nor use the create_proxy
method and all the
JavaScript callback APIs should just work.
Under the hood, the flag is strictly coupled with the JavaScript garbage collector that will eventually destroy all proxy objects created via the FinalizationRegistry built into the browser.
This flag also won't affect MicroPython because it rarely needs a
create_proxy
at all when Python functions are passed to JavaScript event
handlers. MicroPython automatically handles this situation. However,
there might still be rare and niche cases in MicroPython where such a
conversion might be needed.
Hence, PyScript retains the create_proxy
method, even though it does not
change much in the MicroPython world, although it might be still needed with
the Pyodide runtime is you don't use the experimental_create_proxy = "auto"
flag.
At a more fundamental level, MicroPython doesn't provide (yet) a way to
explicitly destroy a proxy reference, whereas Pyodide still expects to
explicitly invoke proxy.destroy()
when the function is not needed.
Warning
In MicroPython proxies might leak due to the lack of a destroy()
method.
Happily, proxies are usually created explicitly for event listeners or
other utilities that won't need to be destroyed in the future. So the lack
of a destroy()
method in MicroPython is not a problem in this specific,
and most common, situation.
Until we have a destroy()
in MicroPython, we suggest testing the
experimental_create_proxy
flag with Pyodide so both runtimes handle
possible leaks automatically.
For completeness, the following examples illustrate the differences in behaviour between Pyodide and MicroPython:
<!-- Throws:
Uncaught Error: This borrowed proxy was automatically destroyed
at the end of a function call. Try using create_proxy or create_once_callable.
-->
<script type="py">
import js
js.setTimeout(lambda x: print(x), 1000, "fail");
</script>
<!-- logs "success" after a second -->
<script type="mpy">
import js
js.setTimeout(lambda x: print(x), 1000, "success");
</script>
To address the difference in Pyodide's behaviour, we can use the experimental flag:
<py-config>
experimental_create_proxy = "auto"
</py-config>
<!-- logs "success" after a second in both Pyodide and MicroPython -->
<script type="py">
import js
js.setTimeout(lambda x: print(x), 1000, "success");
</script>
Alternatively, create_proxy
via the pyscript.ffi
in both interpreters, but
only in Pyodide can we then destroy such proxy: