|
| 1 | +//! Rust wrappers for Node-API simple asynchronous operations |
| 2 | +//! |
| 3 | +//! Unlike `napi_async_work` which threads a single mutable pointer to a data |
| 4 | +//! struct to both the `execute` and `complete` callbacks, the wrapper follows |
| 5 | +//! a more idiomatic Rust ownership pattern by passing the output of `execute` |
| 6 | +//! into the input of `complete`. |
| 7 | +//! |
| 8 | +//! https://nodejs.org/api/n-api.html#n_api_simple_asynchronous_operations |
| 9 | +
|
| 10 | +use std::ffi::c_void; |
| 11 | +use std::mem; |
| 12 | +use std::ptr; |
| 13 | + |
| 14 | +use crate::napi::bindings as napi; |
| 15 | +use crate::raw::Env; |
| 16 | + |
| 17 | +type Execute<T, O> = fn(input: T) -> O; |
| 18 | +type Complete<O> = fn(env: Env, output: O); |
| 19 | + |
| 20 | +/// Schedule work to execute on the libuv thread pool |
| 21 | +/// |
| 22 | +/// # Safety |
| 23 | +/// * `env` must be a valid `napi_env` for the current thread |
| 24 | +pub unsafe fn schedule<T, O>(env: Env, input: T, execute: Execute<T, O>, complete: Complete<O>) |
| 25 | +where |
| 26 | + T: Send + 'static, |
| 27 | + O: Send + 'static, |
| 28 | +{ |
| 29 | + let mut data = Box::new(Data { |
| 30 | + state: State::Input(input), |
| 31 | + execute, |
| 32 | + complete, |
| 33 | + // Work is initialized as a null pointer, but set by `create_async_work` |
| 34 | + // `data` must not be used until this value has been set. |
| 35 | + work: ptr::null_mut(), |
| 36 | + }); |
| 37 | + |
| 38 | + // Store a pointer to `work` before ownership is transferred to `Box::into_raw` |
| 39 | + let work = &mut data.work as *mut _; |
| 40 | + |
| 41 | + // Create the `async_work` |
| 42 | + assert_eq!( |
| 43 | + napi::create_async_work( |
| 44 | + env, |
| 45 | + ptr::null_mut(), |
| 46 | + super::string(env, "neon_async_work"), |
| 47 | + Some(call_execute::<T, O>), |
| 48 | + Some(call_complete::<T, O>), |
| 49 | + Box::into_raw(data).cast(), |
| 50 | + work, |
| 51 | + ), |
| 52 | + napi::Status::Ok, |
| 53 | + ); |
| 54 | + |
| 55 | + // Queue the work |
| 56 | + match napi::queue_async_work(env, *work) { |
| 57 | + napi::Status::Ok => {} |
| 58 | + status => { |
| 59 | + // If queueing failed, delete the work to prevent a leak |
| 60 | + napi::delete_async_work(env, *work); |
| 61 | + assert_eq!(status, napi::Status::Ok); |
| 62 | + } |
| 63 | + } |
| 64 | +} |
| 65 | + |
| 66 | +/// A pointer to data is passed to the `execute` and `complete` callbacks |
| 67 | +struct Data<T, O> { |
| 68 | + state: State<T, O>, |
| 69 | + execute: Execute<T, O>, |
| 70 | + complete: Complete<O>, |
| 71 | + work: napi::AsyncWork, |
| 72 | +} |
| 73 | + |
| 74 | +/// State of the task that is transitioned by `execute` and `complete` |
| 75 | +enum State<T, O> { |
| 76 | + /// Initial data input passed to `execute` |
| 77 | + Input(T), |
| 78 | + /// Transient state while `execute` is running |
| 79 | + Executing, |
| 80 | + /// Return data of `execute` passed to `complete` |
| 81 | + Output(O), |
| 82 | +} |
| 83 | + |
| 84 | +impl<T, O> State<T, O> { |
| 85 | + /// Return the input if `State::Input`, replacing with `State::Executing` |
| 86 | + fn take_execute_input(&mut self) -> Option<T> { |
| 87 | + match mem::replace(self, Self::Executing) { |
| 88 | + Self::Input(input) => Some(input), |
| 89 | + _ => None, |
| 90 | + } |
| 91 | + } |
| 92 | + |
| 93 | + /// Return the output if `State::Output`, replacing with `State::Executing` |
| 94 | + fn into_output(self) -> Option<O> { |
| 95 | + match self { |
| 96 | + Self::Output(output) => Some(output), |
| 97 | + _ => None, |
| 98 | + } |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +/// Callback executed on the libuv thread pool |
| 103 | +/// |
| 104 | +/// # Safety |
| 105 | +/// * `Env` should not be used because it could attempt to call JavaScript |
| 106 | +/// * `data` is expected to be a pointer to `Data<T, O>` |
| 107 | +unsafe extern "C" fn call_execute<T, O>(_: Env, data: *mut c_void) { |
| 108 | + let data = &mut *data.cast::<Data<T, O>>(); |
| 109 | + // `unwrap` is ok because `call_execute` should be called exactly once |
| 110 | + // after initialization |
| 111 | + let input = data.state.take_execute_input().unwrap(); |
| 112 | + let output = (data.execute)(input); |
| 113 | + |
| 114 | + data.state = State::Output(output); |
| 115 | +} |
| 116 | + |
| 117 | +/// Callback executed on the JavaScript main thread |
| 118 | +/// |
| 119 | +/// # Safety |
| 120 | +/// * `data` is expected to be a pointer to `Data<T, O>` |
| 121 | +unsafe extern "C" fn call_complete<T, O>(env: Env, status: napi::Status, data: *mut c_void) { |
| 122 | + let Data { |
| 123 | + state, |
| 124 | + complete, |
| 125 | + work, |
| 126 | + .. |
| 127 | + } = *Box::<Data<T, O>>::from_raw(data.cast()); |
| 128 | + |
| 129 | + napi::delete_async_work(env, work); |
| 130 | + |
| 131 | + match status { |
| 132 | + // `unwrap` is okay because `call_complete` should be called exactly once |
| 133 | + // if and only if `call_execute` has completed successfully |
| 134 | + napi::Status::Ok => complete(env, state.into_output().unwrap()), |
| 135 | + napi::Status::Cancelled => {} |
| 136 | + _ => assert_eq!(status, napi::Status::Ok), |
| 137 | + } |
| 138 | +} |
0 commit comments