neon-bindings / neon

Rust bindings for writing safe and fast native Node.js modules.
https://www.neon-bindings.com/
Apache License 2.0
8k stars 283 forks source link

Type tagging for arbitrary `Object`s. #969

Closed cutsoy closed 1 year ago

cutsoy commented 1 year ago

This PR implements type tagging for arbitrary Objects. My use case for this is that I have two Node modules written in Rust that share an ABI-safe JsObject-like value. Instead of using TypeId<T> (which is not equal across builds), I generate a u128 for each ABI version and tag ABI-safe objects with this number.

Example code:

let obj: Handle<JsObject> = cx.empty_object();

// Checking an untagged object always returns false.
assert_eq!(obj.check_type_tag(42)?, false);

// Tagging this object a first time.
obj.type_tag(&mut cx, 42)?;

// Now, checking with the same tag will return true.
assert_eq!(obj.check_type_tag(42)?, true);

// Checking with a different tag will return false.
assert_eq!(obj.check_type_tag(24)?, false);

// Tagging this object a second time will fail.
obj.type_tag(&mut cx, 42).err().unwrap();
obj.type_tag(&mut cx, 24).err().unwrap();
kjvalencik commented 1 year ago

@cutsoy Thanks for this contribution! This seems like an interesting idea.

Can you help me understand the use case a bit better? Are these Object using napi_wrap or similar? What is guaranteeing they are ABI compatible?

My hope is that there might be a higher level design that is possible that doesn't need to expose type tags directly, but rather as an implementation detail.

cutsoy commented 1 year ago

Of course!

My use case is basically this:

import { foo } from 'foo';
import { bar } from 'bar';

const ptr = foo();
bar(ptr);

In this case, foo and bar are different NAPI modules (not even necessarily Rust) that can safely operate on the same native value (assuming the type tag is correct and that ptr is ABI-safe and/or has an ABI-safe interface).

The ptr here can indeed be a wrap, an external or even just a tagged & sealed { ptr: ... }, doesn't really matter.


A concrete example of why this may be useful is something like numpy, a C library for Python where N-dimensional arrays are stored "in C" with an interface in Python but which also allows other 3rd-party C libraries to directly manipulate the data "in C". Rewritten in JS, this would be similar to the following:

import { randn } from 'numpy';
import { pinv } from 'linalg';

const a = randn(9, 6);
const B = pinv(a);
cutsoy commented 1 year ago

Note that type tagging alone is not an unsafe operation. It's essentially just a way to tag an object with a 128-bit number in a way that it cannot be changed/removed. I think you could even tag an object that originated from the JS-side, etc.

In order to get the entire thing working, some sort of JsUnsafeCell would also be necessary (where JsBox is essentially JsUnsafeCell + type tagging).

kjvalencik commented 1 year ago

@cutsoy Thanks for sharing more details on your interesting use case!

@dherman and I discussed this on a call this morning. Our concern is that this is a lower level feature that doesn't seem to fit with the rest of Neon (predominantly high-level, safe features). While this feature is safe by itself, most usages of it would require unsafe elsewhere to accomplish a goal.

A different idea we had is to expose a new feature in Neon called sys. When this feature flag is enabled, Neon will provide:

This feature would allow building extensions to Neon out-of-tree.

What do you think about this? Is this something that would sufficiently solve your use case? Thanks for your feedback!

cutsoy commented 1 year ago

Yes, that would be very helpful!

It would indeed solve my particular use case and it's probably also very useful for others.

Let me know if I can help out!

kjvalencik commented 1 year ago

https://github.com/neon-bindings/neon/pull/970 provides a workaround.