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)