Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
v8.log
coverage/
dist/
env/
node_modules/
__pycache__/
coincident-*
test/index-timeout.js
reflected_ffi/
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,15 @@ const { direct, reflect, terminate } = local({
});

remote.onmessage = ({ data: [i32a, [trap, ...rest]] }) => {
// retrieve the result
const result = reflect(trap, ...rest);

// ignore `unref` (its value is `0`) as it doesn't need Atomics
if (!trap) return;

// retrieve the result
const result = reflect(trap, ...rest);

// store it into the SharedArrayBuffer + set written length
i32a[1] = encode(reflect(...args), i32a.buffer);
i32a[1] = encode(result, i32a.buffer);

// notify at index 0 it's all good
i32a[0] = 1;
Atomics.notify(i32a, 0);
Expand Down
27 changes: 18 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,12 @@
},
"scripts": {
"build": "npm run no-debug && npm run types",
"build:py": "rm -rf ./pypi && rm -rf ./wheel && mkdir -p ./wheel/reflected_ffi && cp ./python/*.py ./wheel/reflected_ffi/ && cp ./setup.py ./wheel/ && cd wheel && python setup.py bdist_wheel && sleep 1 && cd .. && mv wheel/dist ./pypi && rm -rf wheel",
"coverage": "mkdir -p ./coverage; c8 report --reporter=text-lcov > ./coverage/lcov.info",
"debug": "echo 'export default true;' > ./src/utils/debug.js",
"no-debug": "echo 'export default false;' > ./src/utils/debug.js",
"test": "npm run no-debug && c8 node --expose-gc test/index.js",
"types": "tsc --allowJs --checkJs --lib dom,esnext --module esnext --target esnext -d --emitDeclarationOnly --outDir ./types ./src/*.js ./src/*/*.js"
"types": "tsc --allowJs --checkJs --lib dom,esnext --module NodeNext --moduleResolution NodeNext --target esnext -d --emitDeclarationOnly --outDir ./types ./src/*.js ./src/*/*.js"
},
"devDependencies": {
"c8": "^11.0.0",
Expand All @@ -115,5 +116,8 @@
"bugs": {
"url": "https://github.com/WebReflection/reflected-ffi/issues"
},
"homepage": "https://github.com/WebReflection/reflected-ffi#readme"
"homepage": "https://github.com/WebReflection/reflected-ffi#readme",
"dependencies": {
"weak-id": "^0.2.1"
}
}
83 changes: 83 additions & 0 deletions pypi.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env sh

# to use this env type either:
# . env.sh
# or
# source env.sh

python -m venv env
source env/bin/activate
pip install --upgrade pip
pip install setuptools wheel twine

echo '
import os
os.unlink(__file__)

from setuptools import setup, find_packages

setup(
name="reflected_ffi",
version="0.0.1",
packages=find_packages(),
description="A remotely reflected Foreign Function Interface",
author="Andrea Giammarchi",
install_requires=[],
)

' > setup.py

npm run build:py

if [ "$1" = "publish" ]; then
python -m twine upload --verbose --repository reflected_ffi pypi/*
else
cat setup.py
fi

rm -f setup.py

# echo '<!DOCTYPE html>
# <html lang="en">
# <head>
# <meta charset="utf-8">
# <meta name="viewport" content="width=device-width,initial-scale=1">
# <title>reflected_ffi</title>
# <link rel="stylesheet" href="https://pyscript.net/releases/2025.11.2/core.css">
# <script type="module">
# import { encode, decode } from "https://esm.run/reflected-ffi/direct";

# globalThis.encode = encode;

# // MicroPython does not integrate bytes or bytearray properly
# // but it is possible to iterate via their proxy wrapper
# globalThis.decode = value => decode("_ref" in value ? value : [...value]);

# // be sure PyScript core is imported after global utilities are defined
# await import("https://pyscript.net/releases/2025.11.2/core.js");
# </script>
# </head>
# <body>
# <mpy-config>
# packages = ["./reflected_ffi-0.0.1-py3-none-any.whl"]
# </mpy-config>
# <py-config>
# packages = ["./reflected_ffi-0.0.1-py3-none-any.whl"]
# </py-config>
# <script type="mpy">
# import js
# from datetime import datetime
# from reflected_ffi.direct import encode, decode
# print("py", decode(encode(datetime.now())))
# print("js", decode(js.encode(js.Date.new())))
# js.console.log(js.decode(encode(datetime.now())))
# </script>
# </body>
# </html>


# ' > pypi/index.html

# # TODO: this is needed only in coincident
# unzip -o ./pypi/reflected_ffi-0.0.1-py3-none-any.whl -d ./pypi/
# cp -R ./pypi/reflected_ffi ~/git/coincident/src/server/
Binary file added pypi.zip
Binary file not shown.
Binary file added pypi/reflected_ffi-0.0.1-py3-none-any.whl
Binary file not shown.
2 changes: 2 additions & 0 deletions python/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .remote import reflected as remote
from .local import reflected as local
145 changes: 145 additions & 0 deletions python/local.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
def reflected(reflect=lambda id, trap, args=[], kwargs=None: print("reflect", id, trap, args, kwargs)):
import builtins
from builtins import isinstance as _isinstance

from .types import DIRECT, REMOTE, ERROR, FUNCTION

def from_value(value):
if not _isinstance(value, list):
return value

t, v = value

if t == DIRECT:
return v

if t == ERROR:
raise Exception(v)

if t == FUNCTION:
if not v in handlers:
handlers[v] = lambda *args, **kwargs: reflect(v, "__call__", [to_value(a) for a in args], {k: to_value(v) for k, v in kwargs.items()})

return handlers[v]

if v is None:
return builtins

# REMOTE
return handlers[v]

def to_value(value):
if value is None:
return None

if _isinstance(value, (bool, int, float, str, bytes, bytearray, tuple)):
return value

if _isinstance(value, Exception):
return [ERROR, str(value)]

_id = id(value)
if _id in direct:
# TODO: validate this does not conflict in practice with from_value lambda
direct.pop(_id)
return [DIRECT, value]

if not _id in handlers:
handlers[_id] = value

return [REMOTE, _id]


handlers = {}
direct = {}

class Reflected:
def direct(self, value):
direct[id(value)] = True

def reflect(self, id, trap, args=[], kwargs=None):
try:
ref = builtins if id is None else handlers[id]

if trap == "__isinstance__":
return isinstance(ref, (handlers[a] for a in args))

return False

if trap == "__setattr__":
objrefect.__setattr__(ref, args[0], to_value(args[1]))
return True

if trap == "__setitem__":
ref[args[0]] = to_value(args[1])
return True

if trap == "__bool__":
return bool(ref)

if trap == "__delattr__":
ref.__delattr__(args[0])
return True

if trap == "__delitem__":
ref.__delitem__(args[0])
return True

if trap == "__getattr__":
return to_value(getattr(ref, args[0]))

if trap == "__getitem__":
return to_value(ref.__getitem__(args[0]))

if trap == "__hash__":
return hash(ref)

if trap == "__len__":
return len(ref)

if trap == "__mul__":
return to_value(ref.__mul__(args[0]))

if trap == "__rmul__":
return to_value(ref.__rmul__(args[0]))

if trap == "__next__":
return to_value(ref.__next__())

if trap == "__iter__":
return to_value(ref.__iter__())

if trap == "__str__":
return str(ref)

if trap == "__repr__":
return repr(ref)

if trap == "__format__":
return format(ref, args[0])

if trap == "__getattribute__":
return to_value(ref.__getattribute__(args[0]))

if trap == "__getitem__":
return to_value(ref.__getitem__(args[0]))

if trap == "__call__":
fn = ref
args = [from_value(a) for a in args]
kwargs = {k: from_value(v) for k, v in kwargs.items()}
return to_value(fn(*args, **kwargs))

if trap == "__unref__":
if id in handlers:
handlers.pop(id)
return True

return False

except Exception as e:
return to_value(e)


reflected = Reflected()
return reflected
Loading
Loading