-
Notifications
You must be signed in to change notification settings - Fork 283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature request: expose per-instance data #728
Comments
@jrose-signal This looks like a good minimal API. I think it might need to return some type of borrow guard to ensure borrowed instance data lives long enough, unless Also, I could see benefits of only providing an immutable reference, otherwise, we would need to bind to the lifetime of It would be really neat to make this typesafe without |
In this most limited form it's safe because it's using the lifetime of the
I'm not sure how it's related to Context, but yeah, doing this without boxing would mean a custom collection type like hlist. I decided it ultimately wasn't so bad since |
I think borrowing against the lifetime In order to get around that, the type would need to be clone-able, but then, maybe it should be consistent with It might be possible for Neon to have |
I really like this idea, but I think we could make it a single function with an API similar to Also, I don't think |
Yeah, InstanceKey is mostly just to avoid exposing the |
I love this idea too! I like the I’m a little concerned if we totally eliminate One possible idea: what if instead of a impl InstanceData for Foo {
fn init<‘a, C: Context<‘a>>(cx: &mut C) -> Self {
unimplemented!()
}
} |
@jrose-signal BTW can you say a little bit about your specific use cases for instance data? Concrete examples can he helpful for thinking about the design. |
Heh. My specific use case is probably not the best example: I wanted to store a unique value per-instance so that I could safely store a "current context" using something like scoped-tls and do synchronous calls when a future is resumed. Without a disambiguator between instances, there'd be no way to tell whether the Future can be resumed on the current context or if it's actually a worker context. All of this is related to @indutny-signal's work making EventQueue/Channel have less overhead in Electron; the fastest way to execute something on a context is to already be on that context. There's more thinking for me to do here: it's not generally recommended to synchronously resume futures in Rust because you could deadlock yourself. But if we know the future that was fulfilled came from a JavaScript future, we're already on the microtask queue and don't have any other pending work. But in general anything that can be computed once and cached benefits from being in PerInstanceData, such as recording a set of JavaScript-defined Error types to use for Rust errors. Though I suppose you could also do it generically by adding JsBox properties to the global object. |
I made this diagram to help me understand the entities and instances at runtime: @kjvalencik This makes me feel like some sort of extensibility could be important, especially since Neon-related library crates might want to be able to leverage that global storage without stepping on each other's toes. If we just expose it as a mutable slot and allow anyone to overwrite it, it's hard for non-coordinating entities like libraries to use it cooperatively. |
I think you found the crucial use case here. Without this, crates in the Neon ecosystem that aren't part of Neon proper can't use instance data without coordination with the addon. I'm convinced. This also makes it clear to me that some type of marker (either suggestion works fine) instead of being stringly typed is important. It's better to statically prove they won't conflict. |
I’m still thinking about this but a few early thoughts based on the idea of separate modules being able to publish their own singleton data with this API:
This is just where my thinking is so far. I’ll sketch up a mock API soon we can look at and discuss. |
The mutability thing is tricky. Neon can safely give mutable access to the instance data only if the lifetime is associated with a borrow of However, this isn't super useful because you wouldn't be able to use Additionally, even with only immutable references, it's important to be careful not to simply let the value be replaced because this could result in either dropping the previous value while something still references it or leaking the previous value. I'm thinking that we might be able to borrow ideas of the |
@dherman and I had a call today and @dherman said something really insightful. Instead of trying to make a surrogate key, we can make the key the actual type used for accessing the data. I really like this design because it feels familiar and matches how users typically use statics. static MY_DATA: neon::Cell<MyData> = neon::Cell::new();
/* ... */
let data = MY_DATA.get_or_init(&mut cx, MyData::new); This could be implemented by internally holding a lazily initialized use std::marker::PhantomData;
use once_cell::sync::OnceCell;
struct Cell<T> {
id: OnceCell<usize>,
_data: PhantomData<T>,
}
impl<T> Cell<T> {
pub const fn new() -> Self {
Self {
id: OnceCell::new(),
_data: PhantomData,
}
}
}
impl <T: Send + 'static> Cell<T> {
fn get_or_init<'a, C, F>(&'static self, cx: &mut C, f: F) -> &T
where
C: Context<'a>,
F: FnOnce() -> T,
{
todo!()
}
} Once catch that makes this different from normal Also, I realize this is essentially @jrose-signal's originally design, just slightly tweaked / inverted, so maybe I'm just slow to catch up. 😆 |
Unfortunately this isn't quite correct because you should be able to access the same data in multiple instances in the same program, which means you can't store an offset in the global data (because it might be different for each instance). That's why I went with using the address of the global itself as the key in my original plan, though I forgot that as a zero-sized type (in my version) it might not have a unique address! So there'd have to be a placeholder byte in there too. |
@jrose-signal Yes, there would need to be a placeholder, but this is effectively the same design. The index isn't stored in global data, it's stored in the key. The But, these are all implementation details. It could easily be a hashmap or a vec with ids or address or whatever. The piece that clarifies this more for me is treating the key like it's actually holding the data. |
On the one hand I feel like that's weird since you could have code like this: let x = MY_DATA.get_or_init(&mut main_cx, || …).clone();
worker_queue.send(|mut worker_cx| {
let y = MY_DATA.get_or_init(&mut worker_cx, || x.clone());
eprintln!("{} {}", x, y); // may be different
} On the other hand, that's basically how thread-local storage works, and people have been treating that like a global in Rust (and not in Rust) forever. |
Oh right, TLS is an interesting precedent, great point. Our situation is very analogous, it’s basically “worker-local storage.” Should we consider modeling the API after #[neon::local]
static MY_DATA: Foo = /* ... */; It does have that weirdness that you mention, @jrose-signal, that |
I agree that it matches up nicely with TLS and naming the type With that said, I think the Maybe we can split the difference and borrow a little from both? |
Updated summary of discussion: pub struct InstanceKey<T: 'static> {
_data: PhantomData<&'static mut T>,
_empty: u8,
}
impl<T: 'static> InstanceKey<T> {
pub const fn new() {
Self {
_data: PhantomData,
_empty: 0,
}
}
pub fn get_or_init<'a, C>(&'static self, cx: &C, f: F) -> &'a T
where
C: Context<'a>,
F: FnOnce() -> T,
{
todo!()
}
} Usage: use neon::InstanceKey;
static RUNTIME: InstanceKey<Runtime> = InstanceKey::new();
fn run_async(mut cx: FunctionContext) -> JsResult<JsUndefined> {
let runtime = RUNTIME.get_or_init(|| Runtime::new());
runtime.spawn(async {
println!("Hello, World!");
});
Ok(cx.undefined())
} Other potentially useful additions:
Mutability is omitted because there isn't an ergonomic way to provide it with static checks. Static checks would require blocking any calls that require |
N-API provides the rather limited
napi_set_instance_data
to allow associating a single word of data with an instance. Neon already uses this API to store its own per-instance data, but it would be great if that were exposed to users in a convenient way. A possible shape for the API:An alternate version of the API would include a function to construct the initial value if not present, more like
thread_local!
, but this version is the simplest I could think of.The text was updated successfully, but these errors were encountered: