diff --git a/.gitignore b/.gitignore
index 076850d..e39ec55 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,9 @@
v8.log
coverage/
dist/
+env/
node_modules/
+__pycache__/
coincident-*
test/index-timeout.js
+reflected_ffi/
diff --git a/README.md b/README.md
index b5ef18a..1f4e0a0 100644
--- a/README.md
+++ b/README.md
@@ -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);
diff --git a/package-lock.json b/package-lock.json
index 9d1d77d..e901ff3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,9 @@
"name": "reflected-ffi",
"version": "0.7.1",
"license": "MIT",
+ "dependencies": {
+ "weak-id": "^0.2.1"
+ },
"devDependencies": {
"c8": "^11.0.0",
"typescript": "^5.9.3"
@@ -51,9 +54,9 @@
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.30",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
- "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -423,9 +426,9 @@
}
},
"node_modules/lru-cache": {
- "version": "11.2.6",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
- "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
+ "version": "11.2.7",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
+ "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
@@ -554,9 +557,9 @@
}
},
"node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -659,6 +662,12 @@
"node": ">=10.12.0"
}
},
+ "node_modules/weak-id": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/weak-id/-/weak-id-0.2.1.tgz",
+ "integrity": "sha512-tgMSPjzThNo85XCLn9CpxhVnKMATbdlCdAnIex7kdq6U0zvRcn9hUKOv1Gh5iOa2cH9gGTp1Go5jNPjrLHyVJA==",
+ "license": "MIT"
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/package.json b/package.json
index 4cfd1eb..9f603eb 100644
--- a/package.json
+++ b/package.json
@@ -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",
@@ -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"
+ }
}
diff --git a/pypi.sh b/pypi.sh
new file mode 100755
index 0000000..0d9c981
--- /dev/null
+++ b/pypi.sh
@@ -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 '
+#
+#
+#
+#
+# reflected_ffi
+#
+#
+#
+#
+#
+# packages = ["./reflected_ffi-0.0.1-py3-none-any.whl"]
+#
+#
+# packages = ["./reflected_ffi-0.0.1-py3-none-any.whl"]
+#
+#
+#
+#
+
+
+# ' > 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/
diff --git a/pypi.zip b/pypi.zip
new file mode 100644
index 0000000..0c53763
Binary files /dev/null and b/pypi.zip differ
diff --git a/pypi/reflected_ffi-0.0.1-py3-none-any.whl b/pypi/reflected_ffi-0.0.1-py3-none-any.whl
new file mode 100644
index 0000000..6f18eb6
Binary files /dev/null and b/pypi/reflected_ffi-0.0.1-py3-none-any.whl differ
diff --git a/python/__init__.py b/python/__init__.py
new file mode 100644
index 0000000..2cdf5d6
--- /dev/null
+++ b/python/__init__.py
@@ -0,0 +1,2 @@
+from .remote import reflected as remote
+from .local import reflected as local
diff --git a/python/local.py b/python/local.py
new file mode 100644
index 0000000..c6c9b4f
--- /dev/null
+++ b/python/local.py
@@ -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
diff --git a/python/remote.py b/python/remote.py
new file mode 100644
index 0000000..da63e78
--- /dev/null
+++ b/python/remote.py
@@ -0,0 +1,151 @@
+def reflected(reflect=lambda id, trap, args=[], kwargs=None: print("reflect", id, trap, args, kwargs)):
+ import builtins
+ import weakref
+ from builtins import isinstance as _isinstance
+ from .types import DIRECT, REMOTE, ERROR, FUNCTION
+
+ def isinstance(self, type):
+ types = [type] if not _isinstance(type, (list, tuple)) else type
+ types = [t for t in types if _isinstance(t, Handler)]
+
+ if len(types) > 0:
+ reflect(self._, "__isinstance__", [t._ for t in types])
+
+ return False
+
+ builtins.isinstance = lambda self, type: isinstance(self, type) if id(self) in handlers else _isinstance(self, type)
+
+ def finalize(_id, _ref):
+ def cleanup():
+ handlers.pop(_id)
+ reflect(_ref, "__unref__")
+
+ return cleanup
+
+
+ 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 v is None:
+ return reflected.builtins
+
+ return handlers[v]() if v in handlers else Handler(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, Handler):
+ return [REMOTE, value._]
+
+ if _isinstance(value, Exception):
+ return [ERROR, str(value)]
+
+ if _isinstance(value, list):
+ return [DIRECT, list_values(value)]
+
+ if callable(value):
+ return [FUNCTION, id(value)]
+
+ return [DIRECT, dict_values(value)]
+
+ list_values = lambda value: [to_value(v) for v in value]
+ dict_values = lambda value: {k: to_value(v) for k, v in value.items()}
+
+ handlers = {}
+
+ class Handler:
+ def __init__(self, _):
+ object.__setattr__(self, "_", _)
+ _id = id(self)
+ handlers[_id] = weakref.ref(self)
+ weakref.finalize(self, finalize(_id, _))
+
+ def __bool__(self):
+ return reflect(self._, "__bool__")
+
+ def __call__(self, *args, **kwargs):
+ return from_value(reflect(self._, "__call__", list_values(args), dict_values(kwargs)))
+
+ def __delattr__(self, name):
+ return reflect(self._, "__delattr__", [name])
+
+ def __delitem__(self, name):
+ return reflect(self._, "__delitem__", [name])
+
+ def __getattr__(self, name):
+ return from_value(reflect(self._, "__getattr__", [name]))
+
+ def __hash__(self):
+ return reflect(self._, "__hash__")
+
+ def __getitem__(self, index):
+ return from_value(reflect(self._, "__getitem__", [index]))
+
+ def __mul__(self, other):
+ return from_value(reflect(self._, "__mul__", [other]))
+
+ def __rmul__(self, other):
+ return from_value(reflect(self._, "__rmul__", [other]))
+
+ def __setattr__(self, name, value):
+ reflect(self._, "__setattr__", [name, to_value(value)])
+
+ def __setitem__(self, index, value):
+ reflect(self._, "__setitem__", [index, to_value(value)])
+
+ def __str__(self):
+ return reflect(self._, "__str__")
+
+ def __repr__(self):
+ return reflect(self._, "__repr__")
+
+ def __format__(self, format_spec):
+ return reflect(self._, "__format__", [format_spec])
+
+ def __next__(self):
+ return from_value(reflect(self._, "__next__"))
+
+ def __iter__(self):
+ def iterator():
+ value = from_value(reflect(self._, "__iter__"))
+ if value is None:
+ return
+
+ yield value
+
+ return iterator()
+
+ def __len__(self):
+ return reflect(self._, "__len__")
+
+ class Reflected:
+ def __init__(self):
+ self.Handler = Handler
+ self.builtins = Handler(None)
+
+ def reflect(self, id, trap, args=[], kwargs=None):
+ if trap == "__call__":
+ fn = handlers[id]()
+ 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__":
+ handlers.pop(id)
+ return
+
+ reflected = Reflected()
+ return reflected
diff --git a/python/test_remote.py b/python/test_remote.py
new file mode 100644
index 0000000..aa26ae8
--- /dev/null
+++ b/python/test_remote.py
@@ -0,0 +1,34 @@
+from remote import reflected
+
+module = reflected()
+
+### for testing purposes
+
+# simulating a callback
+f = module.Handler(123)
+f('a', [1, 2, 3], b=4)
+
+# simulating a dictionary
+d = module.Handler(456)
+"a" in d
+
+# simulating a list
+l = module.Handler(789)
+for i in range(0, len(l)):
+ print(l[i])
+
+if not l:
+ print("l is empty")
+
+l[0]
+l[0] = 123
+
+l.asd
+l.asd = 123
+
+print(isinstance(l, f))
+
+next(l)
+
+del l[0]
+del l.asd
diff --git a/python/types.py b/python/types.py
new file mode 100644
index 0000000..c79b928
--- /dev/null
+++ b/python/types.py
@@ -0,0 +1,4 @@
+DIRECT = 0
+REMOTE = 1 << 0
+ERROR = 1 << 1
+FUNCTION = 1 << 2
diff --git a/src/utils/heap.js b/src/utils/heap.js
index 2577b14..7d5108a 100644
--- a/src/utils/heap.js
+++ b/src/utils/heap.js
@@ -1,3 +1,5 @@
+import i32 from 'weak-id/i32';
+
/**
* @template T
* @typedef {Object} Heap
@@ -14,24 +16,27 @@
* @param {Map} [refs=new Map] The used map of references to ids.
* @returns {Heap}
*/
-export default (id = 0, ids = new Map, refs = new Map) => ({
- clear: () => {
- ids.clear();
- refs.clear();
- },
- id: ref => {
- let uid = refs.get(ref);
- if (uid === void 0) {
- /* c8 ignore next */
- while (ids.has(uid = id++));
- ids.set(uid, ref);
- refs.set(ref, uid);
- }
- return uid;
- },
- ref: id => ids.get(id),
- unref: id => {
- refs.delete(ids.get(id));
- return ids.delete(id);
- },
-});
+export default (id = 0, ids = new Map, refs = new Map) => {
+ const next = i32(id);
+ return {
+ clear: () => {
+ ids.clear();
+ refs.clear();
+ },
+ id: ref => {
+ let uid = refs.get(ref);
+ if (uid === void 0) {
+ /* c8 ignore next */
+ while (ids.has(uid = next()));
+ ids.set(uid, ref);
+ refs.set(ref, uid);
+ }
+ return uid;
+ },
+ ref: id => ids.get(id),
+ unref: id => {
+ refs.delete(ids.get(id));
+ return ids.delete(id);
+ },
+ };
+};
diff --git a/test/cached.js b/test/cached.js
new file mode 100644
index 0000000..4cb6c59
--- /dev/null
+++ b/test/cached.js
@@ -0,0 +1,22 @@
+import { encode } from '../src/direct/encoder.js';
+import { decode } from '../src/direct/decoder.js';
+
+const error = new Error('test');
+const regexp = new RegExp('test');
+
+const roundtrip = value => {
+ const encoded = encode(value);
+ const decoded = decode(Uint8Array.from(encoded));
+ return decoded;
+};
+
+const input = [1, error, regexp, error, regexp, 2];
+let result = roundtrip(input);
+
+console.assert(result[0] === input[0]);
+console.assert(result.at(-1) === input.at(-1));
+console.assert(result[1] === result[3]);
+console.assert(result[1] instanceof Error);
+console.assert(result[2] === result[4]);
+console.assert(result[2] instanceof RegExp);
+console.assert(result.length === input.length);
diff --git a/test/direct.js b/test/direct.js
index ac61506..1413370 100644
--- a/test/direct.js
+++ b/test/direct.js
@@ -61,7 +61,7 @@ const written = enc('hello encoder', sab);
console.assert(written === 5 + 'hello encoder'.length);
console.assert(dec(written, sab) === 'hello encoder');
-const venc = encoder({ byteOffset: 0, splitViews: true });
+const venc = encoder({ byteOffset: 0 });
console.assert(venc('a'.repeat(256), sab) === 261);
console.assert(dec(261, sab) === 'a'.repeat(256));
console.assert(venc('a'.repeat(10), sab) === 15);
diff --git a/test/python/config.toml b/test/python/config.toml
new file mode 100644
index 0000000..77a1203
--- /dev/null
+++ b/test/python/config.toml
@@ -0,0 +1,7 @@
+debug = true
+
+[files]
+"../../python/__init__.py" = "/reflected_ffi/__init__.py"
+"../../python/types.py" = "/reflected_ffi/types.py"
+"../../python/local.py" = "/reflected_ffi/local.py"
+"../../python/remote.py" = "/reflected_ffi/remote.py"
diff --git a/test/python/index.html b/test/python/index.html
new file mode 100644
index 0000000..bcd29b6
--- /dev/null
+++ b/test/python/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Python - Reflected FFI
+
+
+
+
+
+
+
+
diff --git a/test/python/micro.js b/test/python/micro.js
new file mode 100644
index 0000000..c58628b
--- /dev/null
+++ b/test/python/micro.js
@@ -0,0 +1,14 @@
+import { hooks } from 'https://pyscript.net/releases/2026.3.1/core.js'
+
+hooks.worker.onReady.add(({ interpreter, io, run }, { sync }) => {
+ interpreter.registerJsModule("_pyscript_positron", {
+ server: {"a": 123}
+ });
+
+ run(
+ [
+ "from _pyscript_positron import server",
+ "print(server['a'])",
+ ].join(";"),
+ );
+});
diff --git a/test/python/micro.py b/test/python/micro.py
new file mode 100644
index 0000000..f087b1d
--- /dev/null
+++ b/test/python/micro.py
@@ -0,0 +1,10 @@
+from reflected_ffi import local, remote
+
+worker = local(lambda id, trap, args=[], kwargs=None: server.reflect(id, trap, args, kwargs))
+server = remote(lambda id, trap, args=[], kwargs=None: worker.reflect(id, trap, args, kwargs))
+
+server_globals = server.builtins.globals()
+server_print = server.builtins.print
+
+server_print(server_globals)
+#server.builtins.print(server_globals)