From bbd4ae1bc12dc6b34d4a455015096b7113a5cec5 Mon Sep 17 00:00:00 2001 From: Nathan Whitaker <17734409+nathanwhit@users.noreply.github.com> Date: Wed, 2 Oct 2024 10:43:42 -0700 Subject: [PATCH] fix(node): implement libuv APIs needed to support `npm:sqlite3` (#25893) Fixes #24740. Implements the `uv_mutex_*` and `uv_async_*` APIs. The mutex API is implemented exactly as libuv, a thin wrapper over the OS's native mutex. The async API is implemented in terms of napi_async_work. As documented in the napi docs, you really shouldn't call `napi_queue_async_work` multiple times (it is documented as undefined behavior). However, our implementation doesn't have any issue with this, so I believe it suits our purpose here. --- Cargo.lock | 47 +++- Cargo.toml | 2 +- cli/Cargo.toml | 2 + cli/build.rs | 2 + .../generated_symbol_exports_list_linux.def | 2 +- .../generated_symbol_exports_list_macos.def | 9 +- .../generated_symbol_exports_list_windows.def | 9 +- cli/napi/mod.rs | 1 + cli/napi/node_api.rs | 13 +- cli/napi/sym/symbol_exports.json | 9 +- cli/napi/uv.rs | 231 ++++++++++++++++++ tests/napi/Cargo.toml | 1 + tests/napi/src/lib.rs | 3 + tests/napi/src/uv.rs | 206 ++++++++++++++++ tests/napi/uv_test.js | 18 ++ 15 files changed, 539 insertions(+), 16 deletions(-) create mode 100644 cli/napi/uv.rs create mode 100644 tests/napi/src/uv.rs create mode 100644 tests/napi/uv_test.js diff --git a/Cargo.lock b/Cargo.lock index 9bb75aac86..f264f41e38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -471,6 +471,26 @@ dependencies = [ "which 4.4.2", ] +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease 0.2.17", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.72", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -688,7 +708,7 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading 0.8.3", + "libloading 0.8.5", ] [[package]] @@ -1064,7 +1084,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b28bfe653d79bd16c77f659305b195b82bb5ce0c0eb2a4846b82ddbd77586813" dependencies = [ "bitflags 2.6.0", - "libloading 0.8.3", + "libloading 0.8.5", "winapi", ] @@ -1205,6 +1225,7 @@ dependencies = [ "lazy-regex", "libc", "libsui", + "libuv-sys-lite", "libz-sys", "log", "lsp-types", @@ -1253,6 +1274,7 @@ dependencies = [ "walkdir", "which 4.4.2", "winapi", + "windows-sys 0.52.0", "winres", "yoke", "zeromq", @@ -4039,7 +4061,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ "libc", - "libloading 0.8.3", + "libloading 0.8.5", "pkg-config", ] @@ -4144,9 +4166,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", "windows-targets 0.52.4", @@ -4192,6 +4214,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "libuv-sys-lite" +version = "1.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8dfd1a173826d193e3b955e07c22765829890f62c677a59c4a410cb4f47c01" +dependencies = [ + "bindgen 0.70.1", + "libloading 0.8.5", +] + [[package]] name = "libz-sys" version = "1.1.16" @@ -7205,6 +7237,7 @@ dependencies = [ name = "test_napi" version = "0.1.0" dependencies = [ + "libuv-sys-lite", "napi-build", "napi-sys", "test_server", @@ -7891,7 +7924,7 @@ version = "0.106.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a381badc47c6f15acb5fe0b5b40234162349ed9d4e4fd7c83a7f5547c0fc69c5" dependencies = [ - "bindgen", + "bindgen 0.69.4", "bitflags 2.6.0", "fslock", "gzip-header", @@ -8158,7 +8191,7 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading 0.8.3", + "libloading 0.8.5", "log", "metal", "naga", diff --git a/Cargo.toml b/Cargo.toml index f605229077..22f3575b98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -225,7 +225,7 @@ nix = "=0.26.2" # windows deps junction = "=0.2.0" winapi = "=0.3.9" -windows-sys = { version = "0.52.0", features = ["Win32_Foundation", "Win32_Media", "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_System_WindowsProgramming", "Wdk", "Wdk_System", "Wdk_System_SystemInformation", "Win32_System_Pipes", "Wdk_Storage_FileSystem", "Win32_System_Registry"] } +windows-sys = { version = "0.52.0", features = ["Win32_Foundation", "Win32_Media", "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_System_WindowsProgramming", "Wdk", "Wdk_System", "Wdk_System_SystemInformation", "Win32_System_Pipes", "Wdk_Storage_FileSystem", "Win32_System_Registry", "Win32_System_Kernel"] } winres = "=0.1.12" # NB: the `bench` and `release` profiles must remain EXACTLY the same. diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 41636c961e..6167baa0b2 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -170,12 +170,14 @@ zstd.workspace = true [target.'cfg(windows)'.dependencies] junction.workspace = true winapi = { workspace = true, features = ["knownfolders", "mswsock", "objbase", "shlobj", "tlhelp32", "winbase", "winerror", "winsock2"] } +windows-sys.workspace = true [target.'cfg(unix)'.dependencies] nix.workspace = true [dev-dependencies] deno_bench_util.workspace = true +libuv-sys-lite = "=1.48.2" pretty_assertions.workspace = true test_util.workspace = true diff --git a/cli/build.rs b/cli/build.rs index c3b6f8b045..aa5d3d18c6 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -387,6 +387,8 @@ fn main() { "Missing symbols list! Generate using tools/napi/generate_symbols_lists.js", ); + println!("cargo:rustc-rerun-if-changed={}", symbols_path.display()); + #[cfg(target_os = "windows")] println!( "cargo:rustc-link-arg-bin=deno=/DEF:{}", diff --git a/cli/napi/generated_symbol_exports_list_linux.def b/cli/napi/generated_symbol_exports_list_linux.def index 06e94f05bb..614880ebfa 100644 --- a/cli/napi/generated_symbol_exports_list_linux.def +++ b/cli/napi/generated_symbol_exports_list_linux.def @@ -1 +1 @@ -{ "node_api_create_syntax_error"; "napi_make_callback"; "napi_has_named_property"; "napi_async_destroy"; "napi_coerce_to_object"; "napi_get_arraybuffer_info"; "napi_detach_arraybuffer"; "napi_get_undefined"; "napi_reference_unref"; "napi_fatal_error"; "napi_open_callback_scope"; "napi_close_callback_scope"; "napi_get_value_uint32"; "napi_create_function"; "napi_create_arraybuffer"; "napi_get_value_int64"; "napi_get_all_property_names"; "napi_resolve_deferred"; "napi_is_detached_arraybuffer"; "napi_create_string_utf8"; "napi_create_threadsafe_function"; "node_api_throw_syntax_error"; "napi_create_bigint_int64"; "napi_wrap"; "napi_set_property"; "napi_get_value_bigint_int64"; "napi_open_handle_scope"; "napi_create_error"; "napi_create_buffer"; "napi_cancel_async_work"; "napi_is_exception_pending"; "napi_acquire_threadsafe_function"; "napi_create_external"; "napi_get_threadsafe_function_context"; "napi_get_null"; "napi_create_string_utf16"; "node_api_create_external_string_utf16"; "napi_get_value_bigint_uint64"; "napi_module_register"; "napi_is_typedarray"; "napi_create_external_buffer"; "napi_get_new_target"; "napi_get_instance_data"; "napi_close_handle_scope"; "napi_get_value_string_utf16"; "napi_get_property_names"; "napi_is_arraybuffer"; "napi_get_cb_info"; "napi_define_properties"; "napi_add_env_cleanup_hook"; "node_api_get_module_file_name"; "napi_get_node_version"; "napi_create_int64"; "napi_create_double"; "napi_get_and_clear_last_exception"; "napi_create_reference"; "napi_get_typedarray_info"; "napi_call_threadsafe_function"; "napi_get_last_error_info"; "napi_create_array_with_length"; "napi_coerce_to_number"; "napi_get_global"; "napi_is_error"; "napi_set_instance_data"; "napi_create_typedarray"; "napi_throw_type_error"; "napi_has_property"; "napi_get_value_external"; "napi_create_range_error"; "napi_typeof"; "napi_ref_threadsafe_function"; "napi_create_bigint_uint64"; "napi_get_prototype"; "napi_adjust_external_memory"; "napi_release_threadsafe_function"; "napi_delete_async_work"; "napi_create_string_latin1"; "node_api_create_external_string_latin1"; "napi_is_array"; "napi_unref_threadsafe_function"; "napi_throw_error"; "napi_has_own_property"; "napi_get_reference_value"; "napi_remove_env_cleanup_hook"; "napi_get_value_string_utf8"; "napi_is_promise"; "napi_get_boolean"; "napi_run_script"; "napi_get_element"; "napi_get_named_property"; "napi_get_buffer_info"; "napi_get_value_bool"; "napi_reference_ref"; "napi_create_object"; "napi_create_promise"; "napi_create_int32"; "napi_escape_handle"; "napi_open_escapable_handle_scope"; "napi_throw"; "napi_get_value_double"; "napi_set_named_property"; "napi_call_function"; "napi_create_date"; "napi_object_freeze"; "napi_get_uv_event_loop"; "napi_get_value_string_latin1"; "napi_reject_deferred"; "napi_add_finalizer"; "napi_create_array"; "napi_delete_reference"; "napi_get_date_value"; "napi_create_dataview"; "napi_get_version"; "napi_define_class"; "napi_is_date"; "napi_remove_wrap"; "napi_delete_property"; "napi_instanceof"; "napi_create_buffer_copy"; "napi_delete_element"; "napi_object_seal"; "napi_queue_async_work"; "napi_get_value_bigint_words"; "napi_is_buffer"; "napi_get_array_length"; "napi_get_property"; "napi_new_instance"; "napi_set_element"; "napi_create_bigint_words"; "napi_strict_equals"; "napi_is_dataview"; "napi_close_escapable_handle_scope"; "napi_get_dataview_info"; "napi_get_value_int32"; "napi_unwrap"; "napi_throw_range_error"; "napi_coerce_to_bool"; "napi_create_uint32"; "napi_has_element"; "napi_create_external_arraybuffer"; "napi_create_symbol"; "node_api_symbol_for"; "napi_coerce_to_string"; "napi_create_type_error"; "napi_fatal_exception"; "napi_create_async_work"; "napi_async_init"; "node_api_create_property_key_utf16"; "napi_type_tag_object"; "napi_check_object_type_tag"; "node_api_post_finalizer"; "napi_add_async_cleanup_hook"; "napi_remove_async_cleanup_hook"; }; \ No newline at end of file +{ "node_api_create_syntax_error"; "napi_make_callback"; "napi_has_named_property"; "napi_async_destroy"; "napi_coerce_to_object"; "napi_get_arraybuffer_info"; "napi_detach_arraybuffer"; "napi_get_undefined"; "napi_reference_unref"; "napi_fatal_error"; "napi_open_callback_scope"; "napi_close_callback_scope"; "napi_get_value_uint32"; "napi_create_function"; "napi_create_arraybuffer"; "napi_get_value_int64"; "napi_get_all_property_names"; "napi_resolve_deferred"; "napi_is_detached_arraybuffer"; "napi_create_string_utf8"; "napi_create_threadsafe_function"; "node_api_throw_syntax_error"; "napi_create_bigint_int64"; "napi_wrap"; "napi_set_property"; "napi_get_value_bigint_int64"; "napi_open_handle_scope"; "napi_create_error"; "napi_create_buffer"; "napi_cancel_async_work"; "napi_is_exception_pending"; "napi_acquire_threadsafe_function"; "napi_create_external"; "napi_get_threadsafe_function_context"; "napi_get_null"; "napi_create_string_utf16"; "node_api_create_external_string_utf16"; "napi_get_value_bigint_uint64"; "napi_module_register"; "napi_is_typedarray"; "napi_create_external_buffer"; "napi_get_new_target"; "napi_get_instance_data"; "napi_close_handle_scope"; "napi_get_value_string_utf16"; "napi_get_property_names"; "napi_is_arraybuffer"; "napi_get_cb_info"; "napi_define_properties"; "napi_add_env_cleanup_hook"; "node_api_get_module_file_name"; "napi_get_node_version"; "napi_create_int64"; "napi_create_double"; "napi_get_and_clear_last_exception"; "napi_create_reference"; "napi_get_typedarray_info"; "napi_call_threadsafe_function"; "napi_get_last_error_info"; "napi_create_array_with_length"; "napi_coerce_to_number"; "napi_get_global"; "napi_is_error"; "napi_set_instance_data"; "napi_create_typedarray"; "napi_throw_type_error"; "napi_has_property"; "napi_get_value_external"; "napi_create_range_error"; "napi_typeof"; "napi_ref_threadsafe_function"; "napi_create_bigint_uint64"; "napi_get_prototype"; "napi_adjust_external_memory"; "napi_release_threadsafe_function"; "napi_delete_async_work"; "napi_create_string_latin1"; "node_api_create_external_string_latin1"; "napi_is_array"; "napi_unref_threadsafe_function"; "napi_throw_error"; "napi_has_own_property"; "napi_get_reference_value"; "napi_remove_env_cleanup_hook"; "napi_get_value_string_utf8"; "napi_is_promise"; "napi_get_boolean"; "napi_run_script"; "napi_get_element"; "napi_get_named_property"; "napi_get_buffer_info"; "napi_get_value_bool"; "napi_reference_ref"; "napi_create_object"; "napi_create_promise"; "napi_create_int32"; "napi_escape_handle"; "napi_open_escapable_handle_scope"; "napi_throw"; "napi_get_value_double"; "napi_set_named_property"; "napi_call_function"; "napi_create_date"; "napi_object_freeze"; "napi_get_uv_event_loop"; "napi_get_value_string_latin1"; "napi_reject_deferred"; "napi_add_finalizer"; "napi_create_array"; "napi_delete_reference"; "napi_get_date_value"; "napi_create_dataview"; "napi_get_version"; "napi_define_class"; "napi_is_date"; "napi_remove_wrap"; "napi_delete_property"; "napi_instanceof"; "napi_create_buffer_copy"; "napi_delete_element"; "napi_object_seal"; "napi_queue_async_work"; "napi_get_value_bigint_words"; "napi_is_buffer"; "napi_get_array_length"; "napi_get_property"; "napi_new_instance"; "napi_set_element"; "napi_create_bigint_words"; "napi_strict_equals"; "napi_is_dataview"; "napi_close_escapable_handle_scope"; "napi_get_dataview_info"; "napi_get_value_int32"; "napi_unwrap"; "napi_throw_range_error"; "napi_coerce_to_bool"; "napi_create_uint32"; "napi_has_element"; "napi_create_external_arraybuffer"; "napi_create_symbol"; "node_api_symbol_for"; "napi_coerce_to_string"; "napi_create_type_error"; "napi_fatal_exception"; "napi_create_async_work"; "napi_async_init"; "node_api_create_property_key_utf16"; "napi_type_tag_object"; "napi_check_object_type_tag"; "node_api_post_finalizer"; "napi_add_async_cleanup_hook"; "napi_remove_async_cleanup_hook"; "uv_mutex_init"; "uv_mutex_lock"; "uv_mutex_unlock"; "uv_mutex_destroy"; "uv_async_init"; "uv_async_send"; "uv_close"; }; \ No newline at end of file diff --git a/cli/napi/generated_symbol_exports_list_macos.def b/cli/napi/generated_symbol_exports_list_macos.def index cac7100c6f..36b2f37fa5 100644 --- a/cli/napi/generated_symbol_exports_list_macos.def +++ b/cli/napi/generated_symbol_exports_list_macos.def @@ -150,4 +150,11 @@ _napi_type_tag_object _napi_check_object_type_tag _node_api_post_finalizer _napi_add_async_cleanup_hook -_napi_remove_async_cleanup_hook \ No newline at end of file +_napi_remove_async_cleanup_hook +_uv_mutex_init +_uv_mutex_lock +_uv_mutex_unlock +_uv_mutex_destroy +_uv_async_init +_uv_async_send +_uv_close \ No newline at end of file diff --git a/cli/napi/generated_symbol_exports_list_windows.def b/cli/napi/generated_symbol_exports_list_windows.def index 5386b46e54..b7355112e7 100644 --- a/cli/napi/generated_symbol_exports_list_windows.def +++ b/cli/napi/generated_symbol_exports_list_windows.def @@ -152,4 +152,11 @@ EXPORTS napi_check_object_type_tag node_api_post_finalizer napi_add_async_cleanup_hook - napi_remove_async_cleanup_hook \ No newline at end of file + napi_remove_async_cleanup_hook + uv_mutex_init + uv_mutex_lock + uv_mutex_unlock + uv_mutex_destroy + uv_async_init + uv_async_send + uv_close \ No newline at end of file diff --git a/cli/napi/mod.rs b/cli/napi/mod.rs index 122d2ff060..811efb1ecc 100644 --- a/cli/napi/mod.rs +++ b/cli/napi/mod.rs @@ -18,3 +18,4 @@ pub mod js_native_api; pub mod node_api; pub mod util; +pub mod uv; diff --git a/cli/napi/node_api.rs b/cli/napi/node_api.rs index 81cb800a7d..2efb71c267 100644 --- a/cli/napi/node_api.rs +++ b/cli/napi/node_api.rs @@ -547,11 +547,16 @@ fn napi_delete_async_work(env: *mut Env, work: napi_async_work) -> napi_status { } #[napi_sym] -fn napi_get_uv_event_loop(env: *mut Env, uv_loop: *mut *mut ()) -> napi_status { - let env = check_env!(env); +fn napi_get_uv_event_loop( + env_ptr: *mut Env, + uv_loop: *mut *mut (), +) -> napi_status { + let env = check_env!(env_ptr); check_arg!(env, uv_loop); - // There is no uv_loop in Deno - napi_set_last_error(env, napi_generic_failure) + unsafe { + *uv_loop = env_ptr.cast(); + } + 0 } #[napi_sym] diff --git a/cli/napi/sym/symbol_exports.json b/cli/napi/sym/symbol_exports.json index 64b548d496..00946b8ed7 100644 --- a/cli/napi/sym/symbol_exports.json +++ b/cli/napi/sym/symbol_exports.json @@ -152,6 +152,13 @@ "napi_check_object_type_tag", "node_api_post_finalizer", "napi_add_async_cleanup_hook", - "napi_remove_async_cleanup_hook" + "napi_remove_async_cleanup_hook", + "uv_mutex_init", + "uv_mutex_lock", + "uv_mutex_unlock", + "uv_mutex_destroy", + "uv_async_init", + "uv_async_send", + "uv_close" ] } diff --git a/cli/napi/uv.rs b/cli/napi/uv.rs new file mode 100644 index 0000000000..d4cb5c0b36 --- /dev/null +++ b/cli/napi/uv.rs @@ -0,0 +1,231 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use deno_core::parking_lot::Mutex; +use deno_runtime::deno_napi::*; +use std::mem::MaybeUninit; +use std::ptr::addr_of_mut; + +#[allow(clippy::print_stderr)] +fn assert_ok(res: c_int) -> c_int { + if res != 0 { + eprintln!("bad result in uv polyfill: {res}"); + // don't panic because that might unwind into + // c/c++ + std::process::abort(); + } + res +} + +use crate::napi::js_native_api::napi_create_string_utf8; +use crate::napi::node_api::napi_create_async_work; +use crate::napi::node_api::napi_delete_async_work; +use crate::napi::node_api::napi_queue_async_work; +use std::ffi::c_int; + +const UV_MUTEX_SIZE: usize = { + #[cfg(unix)] + { + std::mem::size_of::() + } + #[cfg(windows)] + { + std::mem::size_of::( + ) + } +}; + +#[repr(C)] +struct uv_mutex_t { + mutex: Mutex<()>, + _padding: [MaybeUninit; const { + (UV_MUTEX_SIZE - size_of::>()) / size_of::() + }], +} + +#[no_mangle] +unsafe extern "C" fn uv_mutex_init(lock: *mut uv_mutex_t) -> c_int { + unsafe { + addr_of_mut!((*lock).mutex).write(Mutex::new(())); + 0 + } +} + +#[no_mangle] +unsafe extern "C" fn uv_mutex_lock(lock: *mut uv_mutex_t) { + unsafe { + let guard = (*lock).mutex.lock(); + // forget the guard so it doesn't unlock when it goes out of scope. + // we're going to unlock it manually + std::mem::forget(guard); + } +} + +#[no_mangle] +unsafe extern "C" fn uv_mutex_unlock(lock: *mut uv_mutex_t) { + unsafe { + (*lock).mutex.force_unlock(); + } +} + +#[no_mangle] +unsafe extern "C" fn uv_mutex_destroy(_lock: *mut uv_mutex_t) { + // no cleanup required +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +#[allow(dead_code)] +enum uv_handle_type { + UV_UNKNOWN_HANDLE = 0, + UV_ASYNC, + UV_CHECK, + UV_FS_EVENT, + UV_FS_POLL, + UV_HANDLE, + UV_IDLE, + UV_NAMED_PIPE, + UV_POLL, + UV_PREPARE, + UV_PROCESS, + UV_STREAM, + UV_TCP, + UV_TIMER, + UV_TTY, + UV_UDP, + UV_SIGNAL, + UV_FILE, + UV_HANDLE_TYPE_MAX, +} + +const UV_HANDLE_SIZE: usize = 96; + +#[repr(C)] +struct uv_handle_t { + // public members + pub data: *mut c_void, + pub r#loop: *mut uv_loop_t, + pub r#type: uv_handle_type, + + _padding: [MaybeUninit; const { + (UV_HANDLE_SIZE + - size_of::<*mut c_void>() + - size_of::<*mut uv_loop_t>() + - size_of::()) + / size_of::() + }], +} + +#[cfg(unix)] +const UV_ASYNC_SIZE: usize = 128; + +#[cfg(windows)] +const UV_ASYNC_SIZE: usize = 224; + +#[repr(C)] +struct uv_async_t { + // public members + pub data: *mut c_void, + pub r#loop: *mut uv_loop_t, + pub r#type: uv_handle_type, + // private + async_cb: uv_async_cb, + work: napi_async_work, + _padding: [MaybeUninit; const { + (UV_ASYNC_SIZE + - size_of::<*mut c_void>() + - size_of::<*mut uv_loop_t>() + - size_of::() + - size_of::() + - size_of::()) + / size_of::() + }], +} + +type uv_loop_t = Env; +type uv_async_cb = extern "C" fn(handle: *mut uv_async_t); +#[no_mangle] +unsafe extern "C" fn uv_async_init( + r#loop: *mut uv_loop_t, + // probably uninitialized + r#async: *mut uv_async_t, + async_cb: uv_async_cb, +) -> c_int { + unsafe { + addr_of_mut!((*r#async).r#loop).write(r#loop); + addr_of_mut!((*r#async).r#type).write(uv_handle_type::UV_ASYNC); + addr_of_mut!((*r#async).async_cb).write(async_cb); + + let mut resource_name: MaybeUninit = MaybeUninit::uninit(); + assert_ok(napi_create_string_utf8( + r#loop, + c"uv_async".as_ptr(), + usize::MAX, + resource_name.as_mut_ptr(), + )); + let resource_name = resource_name.assume_init(); + + let res = napi_create_async_work( + r#loop, + None::>.into(), + resource_name, + Some(async_exec_wrap), + None, + r#async.cast(), + addr_of_mut!((*r#async).work), + ); + -res + } +} + +#[no_mangle] +unsafe extern "C" fn uv_async_send(handle: *mut uv_async_t) -> c_int { + unsafe { -napi_queue_async_work((*handle).r#loop, (*handle).work) } +} + +type uv_close_cb = unsafe extern "C" fn(*mut uv_handle_t); + +#[no_mangle] +unsafe extern "C" fn uv_close(handle: *mut uv_handle_t, close: uv_close_cb) { + unsafe { + if handle.is_null() { + close(handle); + return; + } + if let uv_handle_type::UV_ASYNC = (*handle).r#type { + let handle: *mut uv_async_t = handle.cast(); + napi_delete_async_work((*handle).r#loop, (*handle).work); + } + close(handle); + } +} + +unsafe extern "C" fn async_exec_wrap(_env: napi_env, data: *mut c_void) { + let data: *mut uv_async_t = data.cast(); + unsafe { + ((*data).async_cb)(data); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sizes() { + assert_eq!( + std::mem::size_of::(), + UV_MUTEX_SIZE + ); + assert_eq!( + std::mem::size_of::(), + UV_HANDLE_SIZE + ); + assert_eq!( + std::mem::size_of::(), + UV_ASYNC_SIZE + ); + assert_eq!(std::mem::size_of::(), UV_MUTEX_SIZE); + assert_eq!(std::mem::size_of::(), UV_HANDLE_SIZE); + assert_eq!(std::mem::size_of::(), UV_ASYNC_SIZE); + } +} diff --git a/tests/napi/Cargo.toml b/tests/napi/Cargo.toml index 611d6d5507..e3de253683 100644 --- a/tests/napi/Cargo.toml +++ b/tests/napi/Cargo.toml @@ -13,6 +13,7 @@ repository.workspace = true crate-type = ["cdylib"] [dependencies] +libuv-sys-lite = "=1.48.2" napi-sys = { version = "=2.2.2", default-features = false, features = ["napi7"] } [dev-dependencies] diff --git a/tests/napi/src/lib.rs b/tests/napi/src/lib.rs index f6fe6e189a..8c6190ad3e 100644 --- a/tests/napi/src/lib.rs +++ b/tests/napi/src/lib.rs @@ -31,6 +31,7 @@ pub mod strings; pub mod symbol; pub mod tsfn; pub mod typedarray; +pub mod uv; #[macro_export] macro_rules! cstr { @@ -138,6 +139,7 @@ unsafe extern "C" fn napi_register_module_v1( #[cfg(windows)] { napi_sys::setup(); + libuv_sys_lite::setup(); } // We create a fresh exports object and leave the passed @@ -169,6 +171,7 @@ unsafe extern "C" fn napi_register_module_v1( symbol::init(env, exports); make_callback::init(env, exports); object::init(env, exports); + uv::init(env, exports); init_cleanup_hook(env, exports); diff --git a/tests/napi/src/uv.rs b/tests/napi/src/uv.rs new file mode 100644 index 0000000000..555470c008 --- /dev/null +++ b/tests/napi/src/uv.rs @@ -0,0 +1,206 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use crate::assert_napi_ok; +use crate::napi_get_callback_info; +use crate::napi_new_property; +use libuv_sys_lite::uv_async_init; +use libuv_sys_lite::uv_async_t; +use libuv_sys_lite::uv_close; +use libuv_sys_lite::uv_handle_t; +use libuv_sys_lite::uv_mutex_destroy; +use libuv_sys_lite::uv_mutex_lock; +use libuv_sys_lite::uv_mutex_t; +use libuv_sys_lite::uv_mutex_unlock; +use napi_sys::*; +use std::mem::MaybeUninit; +use std::ptr; +use std::ptr::addr_of_mut; +use std::ptr::null_mut; +use std::time::Duration; + +struct KeepAlive { + tsfn: napi_threadsafe_function, +} + +impl KeepAlive { + fn new(env: napi_env) -> Self { + let mut name = null_mut(); + assert_napi_ok!(napi_create_string_utf8( + env, + c"test_uv_async".as_ptr(), + 13, + &mut name + )); + + unsafe extern "C" fn dummy( + _env: napi_env, + _cb: napi_callback_info, + ) -> napi_value { + ptr::null_mut() + } + + let mut func = null_mut(); + assert_napi_ok!(napi_create_function( + env, + c"dummy".as_ptr(), + usize::MAX, + Some(dummy), + null_mut(), + &mut func, + )); + + let mut tsfn = null_mut(); + assert_napi_ok!(napi_create_threadsafe_function( + env, + func, + null_mut(), + name, + 0, + 1, + null_mut(), + None, + null_mut(), + None, + &mut tsfn, + )); + assert_napi_ok!(napi_ref_threadsafe_function(env, tsfn)); + Self { tsfn } + } +} + +impl Drop for KeepAlive { + fn drop(&mut self) { + assert_napi_ok!(napi_release_threadsafe_function( + self.tsfn, + ThreadsafeFunctionReleaseMode::release, + )); + } +} + +struct Async { + mutex: *mut uv_mutex_t, + env: napi_env, + value: u32, + callback: napi_ref, + _keep_alive: KeepAlive, +} + +#[derive(Clone, Copy)] +struct UvAsyncPtr(*mut uv_async_t); + +unsafe impl Send for UvAsyncPtr {} + +fn new_raw(t: T) -> *mut T { + Box::into_raw(Box::new(t)) +} + +unsafe extern "C" fn close_cb(handle: *mut uv_handle_t) { + let handle = handle.cast::(); + let async_ = (*handle).data as *mut Async; + let env = (*async_).env; + assert_napi_ok!(napi_delete_reference(env, (*async_).callback)); + + uv_mutex_destroy((*async_).mutex); + let _ = Box::from_raw((*async_).mutex); + let _ = Box::from_raw(async_); + let _ = Box::from_raw(handle); +} + +unsafe extern "C" fn callback(handle: *mut uv_async_t) { + eprintln!("callback"); + let async_ = (*handle).data as *mut Async; + uv_mutex_lock((*async_).mutex); + let env = (*async_).env; + let mut js_cb = null_mut(); + assert_napi_ok!(napi_get_reference_value( + env, + (*async_).callback, + &mut js_cb + )); + let mut global: napi_value = ptr::null_mut(); + assert_napi_ok!(napi_get_global(env, &mut global)); + + let mut result: napi_value = ptr::null_mut(); + let value = (*async_).value; + eprintln!("value is {value}"); + let mut value_js = ptr::null_mut(); + assert_napi_ok!(napi_create_uint32(env, value, &mut value_js)); + let args = &[value_js]; + assert_napi_ok!(napi_call_function( + env, + global, + js_cb, + 1, + args.as_ptr(), + &mut result, + )); + uv_mutex_unlock((*async_).mutex); + if value == 5 { + uv_close(handle.cast(), Some(close_cb)); + } +} + +unsafe fn uv_async_send(ptr: UvAsyncPtr) { + assert_napi_ok!(libuv_sys_lite::uv_async_send(ptr.0)); +} + +fn make_uv_mutex() -> *mut uv_mutex_t { + let mutex = new_raw(MaybeUninit::::uninit()); + assert_napi_ok!(libuv_sys_lite::uv_mutex_init(mutex.cast())); + mutex.cast() +} + +#[allow(unused_unsafe)] +extern "C" fn test_uv_async( + env: napi_env, + info: napi_callback_info, +) -> napi_value { + let (args, argc, _) = napi_get_callback_info!(env, info, 1); + assert_eq!(argc, 1); + + let mut loop_ = null_mut(); + assert_napi_ok!(napi_get_uv_event_loop(env, &mut loop_)); + let uv_async = new_raw(MaybeUninit::::uninit()); + let uv_async = uv_async.cast::(); + let mut js_cb = null_mut(); + assert_napi_ok!(napi_create_reference(env, args[0], 1, &mut js_cb)); + // let mut tsfn = null_mut(); + + let data = new_raw(Async { + env, + callback: js_cb, + mutex: make_uv_mutex(), + value: 0, + _keep_alive: KeepAlive::new(env), + }); + unsafe { + addr_of_mut!((*uv_async).data).write(data.cast()); + assert_napi_ok!(uv_async_init(loop_.cast(), uv_async, Some(callback))); + let uv_async = UvAsyncPtr(uv_async); + std::thread::spawn({ + move || { + let data = (*uv_async.0).data as *mut Async; + for _ in 0..5 { + uv_mutex_lock((*data).mutex); + (*data).value += 1; + uv_mutex_unlock((*data).mutex); + std::thread::sleep(Duration::from_millis(10)); + uv_async_send(uv_async); + } + } + }); + } + + ptr::null_mut() +} + +pub fn init(env: napi_env, exports: napi_value) { + let properties = &[napi_new_property!(env, "test_uv_async", test_uv_async)]; + + assert_napi_ok!(napi_define_properties( + env, + exports, + properties.len(), + properties.as_ptr() + )); +} diff --git a/tests/napi/uv_test.js b/tests/napi/uv_test.js new file mode 100644 index 0000000000..6d8ee26713 --- /dev/null +++ b/tests/napi/uv_test.js @@ -0,0 +1,18 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assertEquals, loadTestLibrary } from "./common.js"; + +const uv = loadTestLibrary(); + +Deno.test("napi uv async", async () => { + let called = false; + await new Promise((resolve) => { + uv.test_uv_async((value) => { + called = true; + if (value === 5) { + resolve(); + } + }); + }); + assertEquals(called, true); +});