GithubHelp home page GithubHelp logo

rfcs's People

Contributors

dherman avatar geovie avatar goto-bus-stop avatar kjvalencik avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

rfcs's Issues

`vargo new --neon`

I'd like to see a proposal for a command-line flag for vargo new that lets you specify a local path to a local development tree of neon, so that you can easily build neon projects with a custom build or fork of neon. This would be useful in particular for testing.

Exclusive and Shared Context References

Exclusive and Shared Context References

Purpose

The purpose of this pre-RFC is to start gathering feedback from maintainers and users about the perception of mutability inside the VM via a Context<'_> in order to guide a future RFC to update Neon's usage of Context<'_>.

Summary

Neon provides the Context<'_> trait for abstracting interactions with the JavaScript engine. Most methods in Neon require a reference to a Context. Some of these references are shared (&Context<'_>) and others are exclusive (&mut Context<'_>). However, there are some inconsistencies in when one or the other is required. For example, most methods that create JavaScript objects require &mut while methods that tend to hold a reference to a Context<'_> (or an underlying Isolate or napi_env) tend to use a &.

Neon should provide a consistent, opinionated API that guides users towards correct implementations with the help of the borrow checker. While I can't speak for all contributors, I know that I have lazily chose a safer exclusive reference (&mut) without thinking critically if it was necessary.

Background

& and &mut in idiomatic Rust

Terminology: For the purpose of this document, & will be referred to as a shared reference and &mut an exclusive reference.

In idiomatic Rust, a shared reference & signals that it is safe for multiple pointers to the same data to exist concurrently. In contrast, only a single exclusive reference &mut may exist. Typically, this aligns 1-to-1 with mutability. There can either be one mutable reference or many immutable reference.

However, Rust also provides a concept of interior mutability. This allows mutating a value from a shared & reference. Examples include, Cell, RefCell, and atomics. An AtomicU32 can be incremented with a shared & reference because it provides internal consistency guarantees.

In application code, it is generally best to only use internal mutability to abstract implementation details in implementations that appear immutable from the user. For example, memoization of a pure function.

JavaScript Engine Throwing State

Unlike Rust, JavaScript makes use of exceptions. When working with the JavaScript Engine, it can be in several states:

  • Running the event loop. JavaScript is executing and native code cannot call into the engine.
  • Locked. Synchronous native code is executing and JavaScript is not. Calls into the engine are safe.
  • Throwing. A JavaScript exception has been thrown. Only some calls into the JavaScript engine are safe.
  • Shutting down. Clean-up is executing and no calls may be made into the JavaScript engine.

Calls into JavaScript from a native extension may trigger an exception. For example, calling a function that executes throw or attempting to read a property from undefined. Neon modules must be sensitive to the state of the VM.

Current

Nan Backend

With the Nan backend, Neon does not prevent calls into the JavaScript engine when in a throwing exception. Making calls when in a throwing state is undefined behavior. The user must be careful to check for the Throw error type in Result<_, _> and avoid using the Context<'_>.

N-API Backend

All N-API methods verify state before executing code that accesses the engine. If the engine is not in a valid state for the call, a status enum other than napi_status::napi_ok will be returned.

Neon functions that use N-API validate the status with assert_eq!(status, napi_status::napi_ok). The call to the engine will not occur, the Rust code will panic, the panic will be caught and returned to JavaScript and no undefined behavior will occur.

Questions

Is throwing considered mutating Context<'_>?

Under normal circumstances, creating a type (e.g., cx.string("hello")) appears pure. A new type is created and the underlying Context<'_> does not change. However, it's possible for the call to transition the engine to the throwing state.

In this case, an unrelated call may start failing (e.g., another cx.string("hello")). This may violate expectations of the user that one "pure" function should not cause another "pure" function to fail.

"Pure" is quoted because these methods are not technically pure functions since they may cause side-effects.

Proposal

In my opinion, this behavior is acceptable and preferred because it mirrors patterns found in the standard library. For example, Mutex can be used with & shared references, but if the lock becomes poisoned, future unrelated locks will fail. With this justification, I propose that most Neon methods should accept &Context<'_> and not &mut Context<'_>.

Using shared references in more places improves Neon's ergonomics because user defined structs and functions may alias Context<'_>.

When should exclusive references be used?

Context<'_> is a trait and some implementations include additional methods and data. For example, ModuleContext<'a> wraps the module as a JsObject. Methods that mutate the underlying JsObject should take an exclusive &mut reference. E.g., export_function.

Safe ownership for EventHandler

As @kjvalencik points out in neon-bindings/neon#551 and neon-bindings/neon#552, the memory management model for EventHandler is still not safe: it's susceptible to leaks and races.

The problem is that the we aren't coordinating the overall lifetime of the queue of pending invocations of the event handler (which all run on the main thread); we're only tracking the places in random other threads where invocations are requested. So when the last request is made, we drop the underlying queue even though there may be pending invocations.

Instead, I propose we expose the ownership model into the public API of EventHandler. That is, the user should understand EventHandler as an atomically refcounted handle to a JS event handler callback. The relevant changes to the API would be:

  • Scheduling an invocation of the event handler consumes self.
  • In order to schedule multiple invocations of the event handler, you have to explicitly .clone() it.

The types would look something like this:

struct InvocationQueue(...);          // private implementation
impl Drop for InvocationQueue { ... } // private implementation

#[derive(Clone)]
pub struct EventHandler(Arc<InvocationQueue>);

impl EventHandler {

    pub fn new<'a, C: Context<'a>, T: Value>(cx: &C, this: Handle<T>, callback: Handle<JsFunction>) -> Self;

    // CHANGE: consumes self (implementation clones the Arc<InvocationQueue> and sends to main thread)
    pub fn schedule<T, F>(self, arg_cb: F)
        where T: Value,
              F: for<'a> FnOnce(&mut EventContext<'a>) -> JsResult<'a, T>,
              F: Send + 'static;

    // CHANGE: consumes self (implementation clones the Arc<InvocationQueue> and sends to main thread)
    pub fn schedule_with<F>(self, arg_cb: F)
        where F: FnOnce(&mut EventContext, Handle<JsValue>, Handle<JsFunction>) -> NeonResult<()>,
              F: Send + 'static;

}

Notice that the implementation of the schedule* methods would need to clone the Arc<InvocationQueue> and send it to the main thread in order to ensure that it's kept alive until all the invocations have terminated. This prevents the race where the invocation queue is shut down before all the invocations have executed.

This should also make EventHandler virtually leak-proof: the only way to keep it alive indefinitely is to either keep a local computation running infinitely or to keep cloning it and passing it on to other computations infinitely. Otherwise, by default it will be shut down once all cloned instances have either dropped or scheduled and executed their invocations.


This is an example of what would look different in user code. The example from the RFC would just need one more line to explicitly clone the EventHandler on each iteration:

    let mut this = cx.this();
    let cb = cx.argument::<JsFunction>(0)?;
    let handler = EventHandler::new(cb);
    // or:      = EventHandler::bind(this, cb);
    thread::spawn(move || {
        for i in 0..100 {
            // do some work ....
            thread::sleep(Duration::from_millis(40));
            // schedule a call into javascript
            let handler = handler.clone(); // CHANGE: clone the handler before scheduling an invocation
            handler.schedule(move |cx| {
                // successful result to be passed to the event handler
                Ok(cx.number(i))
            }
        }
    });

Update docs to access binary data

I spent a while this morning trying to figure out how to get at a JsArrayBuffer as a [u8].

There is this RFC: 0005-array-buffer-views but that doesn't seem to be implemented (at least not as the RFC describes it)

There is also this legacy example: sharing-binary-data but the code there is commented out and wouldn't compile otherwise.

Here's what I arrived at:

if let Ok(buffer) = handle.downcast::<JsArrayBuffer>() {
    let lock = cx.lock();
    let binary = buffer.borrow(&lock);
    let slice: &[u8] = binary.as_slice();
    return Ok(slice.to_owned());
}

Updating this page: Arrays which currently has a broken link to where the legacy example used to be with some explanation of all the nice things you can do with as_slice would be great.

`neon bench`

As mentioned in #1 the design of a benchmarking tool should be considered.

Pre-RFC: Procedural macro classes for N-API backend

Proc macro for classes

Replaces #6

Proc macros might allow creating classes as wrappers around the JsBox concept. For example:

struct MyClass {}

impl neon::JsClass for MyClass {
    fn init(cx: FunctionContext) -> Self { todo!() }
}

#[neon::class]
impl MyClass {
    #[neon::class_method(name = "myMethod")]
    my_method(cx: FunctionContext) -> Self {}
}

A use case to keep in my is re-using implementations. For example, a user might have both MemorySession and a RedisSession classes that share implementations. These should be friendly to generics and re-using the base class.

Support for N-API

As discussed in this issue, I propose making the change to the new N-API.

Supporting N-API has several benefits, the most important being that different node versions can use the same addon without recompiling. It will require some changes to the existing API, but my guess from experience with both projects is it will not be that bad.

`neon test`

As mentioned in #1 the design of neon test should be considered.

Iterator for JsArray

  • Implement IntoIterator for iterating over a JsArray
    • Should provide hint for length
  • Implement FromIterator for collecting an iterator into a JsArray

`neon build --target`

We need an RFC for cross-compilation flags. The design should try to mirror the UX of Cargo as much as possible.

Pre-RFC: Borrowing refactor

Borrow

Problem

The current borrowing API was designed to support borrowing both binary JavaScript data and the internal Rust data of classes. It is intended to prevent aliased borrows.

However, it has multiple soundness holes (JavaScript handles might alias the same object, multiple Lock can be created). It also has an API that includes types and helpers from before NLL that are awkward and unnecessary today.

Lastly, classes were removed in the N-API backend, leaving a generic API that only applies to borrowing binary data.

Proposal

Module

  • Remove neon::borrow
  • Create neon::binary
  • Move binary types (e.g., JsArrayBuffer) to neon::binary
  • Move borrowing into neon::binary

Removal

Buffer is a legacy type from before JavaScript included ArrayBuffer. It is now a sub-class of Uint8Array. Neon does not need any type level distinction for JsBuffer because it can be treated as an array view.

  • Remove JsBuffer
  • Only reference to buffer is a constructor on ArrayBuffer for creating a Buffer. The return type is indistinguishable from a Uint8Array in Neon.

Addition

Neon does not currently understand views. We will need to add this. We most likely do not need to know the view type and can have a simple JsArrayView. If we want to include the type, it could be done with a generic phantom data element. JsArrayView<u8>.

Borrowing

In an initial implementation, we will only support borrowing ArrayBuffer. This way we can avoid complicated set math for determining if any of our borrows overlap. We can simply keep a HashSet of starting pointers.

Lock

The Lock type will be removed from the public API and moved to Context.

Views

Borrowing views will mostly be a convenience API. We will still borrow the entire ArrayBuffer. This will behave very similar to how borrowing a &[u8] from a Vec<u8> borrows the entire Vec.

This is necessary to remove complexity from users. If we don't provide this API, users cannot borrow buffers without first either copying data to an ArrayBuffer or passing the underlying buffer along with a Range.

stdlib RFC

Need to flesh out Rust APIs for working with the basic stdlib types, including:

  • Date
  • RegExp
  • typed arrays
  • promises

What else?

Incorrect constructor functions for typed arrays

When returning a typed array, like JsInt16Array for example, the following JavaScript expression will evaluate to false:

x instanceof Int16Array

Using Jest, this assertion

expect(arr).toBeInstanceOf(Int16Array)

fails and reports the following:

Expected constructor: Int16Array
Received constructor: Int16Array

This makes me suspect that Neon is using a constructor function with an identical name, but different equality identity.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.