diff --git a/.bazelrc b/.bazelrc index cb3a4f0e108..52d2b039f33 100644 --- a/.bazelrc +++ b/.bazelrc @@ -160,6 +160,7 @@ build --cxxopt="-fbracket-depth=512" --host_cxxopt="-fbracket-depth=512" build --@rules_rust//:extra_rustc_flag=-Cdebug-assertions=n build --@rules_rust//:extra_exec_rustc_flags=-Cdebug-assertions=n build --@rules_rust//:rustfmt.toml=//src/rust:rustfmt.toml +build --@rules_rust//rust/toolchain/channel=nightly # We default to not enabling debug assertions and unwinding for Rust. For debug builds this is not # the right setting, but unfortunately we can't set that directly. diff --git a/build/rust_lint.bazelrc b/build/rust_lint.bazelrc index baa22d54362..10d78a95398 100644 --- a/build/rust_lint.bazelrc +++ b/build/rust_lint.bazelrc @@ -23,7 +23,7 @@ build:rust-enable-clippy-checks --output_groups=+clippy_checks # - clippy::non_std_lazy_statics – triggers on lazy_static, non-trivial to replace # - clippy::format_push_string – avoids single memory allocation, but makes code less readable # - clippy::cast_possible_truncation - usize/u64 conversion warning is unbelievably noisy -build:rust-enable-clippy-checks --@rules_rust//:clippy_flags=-Wclippy::pedantic,-Wclippy::redundant_clone,-Wclippy::str_to_string,-Wclippy::string_to_string,-Wclippy::to_string_in_format_args,-Wclippy::unnecessary_to_owned,-Wclippy::implicit_clone,-Wclippy::suspicious_to_owned,-Wclippy::unnecessary_to_owned,-Wclippy::nursery,-Wclippy::dbg_macro,-Wclippy::unwrap_used,-Wclippy::allow_attributes,-Aclippy::missing_const_for_fn,-Aclippy::cognitive_complexity,-Aclippy::trait_duplication_in_bounds,-Aclippy::non_send_fields_in_send_ty,-Aclippy::option_if_let_else,-Aclippy::missing_errors_doc,-Aclippy::must_use_candidate,-Aclippy::future_not_send,-Aclippy::trivial_regex,-Aclippy::literal_string_with_formatting_args,-Aclippy::non_std_lazy_statics,-Aclippy::format_push_string,-Aclippy::cast_possible_truncation,-Dwarnings +build:rust-enable-clippy-checks --@rules_rust//:clippy_flags=-Wclippy::pedantic,-Wclippy::redundant_clone,-Wclippy::str_to_string,-Wclippy::to_string_in_format_args,-Wclippy::unnecessary_to_owned,-Wclippy::implicit_clone,-Wclippy::suspicious_to_owned,-Wclippy::unnecessary_to_owned,-Wclippy::nursery,-Wclippy::dbg_macro,-Wclippy::unwrap_used,-Wclippy::allow_attributes,-Aclippy::missing_const_for_fn,-Aclippy::cognitive_complexity,-Aclippy::trait_duplication_in_bounds,-Aclippy::non_send_fields_in_send_ty,-Aclippy::option_if_let_else,-Aclippy::missing_errors_doc,-Aclippy::must_use_candidate,-Aclippy::future_not_send,-Aclippy::trivial_regex,-Aclippy::literal_string_with_formatting_args,-Aclippy::non_std_lazy_statics,-Aclippy::format_push_string,-Aclippy::cast_possible_truncation,-Dwarnings build --@rules_rust//rust/settings:clippy.toml=//src/rust:clippy.toml # enable rustfmt checks diff --git a/build/wd_rust_proc_macro.bzl b/build/wd_rust_proc_macro.bzl new file mode 100644 index 00000000000..8057f119d76 --- /dev/null +++ b/build/wd_rust_proc_macro.bzl @@ -0,0 +1,54 @@ +load("@rules_rust//rust:defs.bzl", "rust_proc_macro", "rust_test") + +def wd_rust_proc_macro( + name, + deps = [], + data = [], + test_env = {}, + test_tags = [], + test_deps = [], + visibility = None): + """Define rust procedural macro crate. + + Args: + name: crate name. + deps: crate dependencies: rust crates (typically includes proc-macro2, quote, syn). + data: additional data files. + test_env: additional test environment variables. + test_tags: additional test tags. + test_deps: test-only dependencies. + visibility: crate visibility. + """ + srcs = native.glob(["**/*.rs"]) + crate_name = name.replace("-", "_") + + rust_proc_macro( + name = name, + crate_name = crate_name, + srcs = srcs, + deps = deps + ["@workerd//deps/rust:runtime"], + visibility = visibility, + data = data, + target_compatible_with = select({ + "@//build/config:no_build": ["@platforms//:incompatible"], + "//conditions:default": [], + }), + ) + + rust_test( + name = name + "_test", + crate = ":" + name, + env = { + "RUST_BACKTRACE": "1", + # rust test runner captures stderr by default, which makes debugging tests very hard + "RUST_TEST_NOCAPTURE": "1", + # our tests are usually very heavy and do not support concurrent invocation + "RUST_TEST_THREADS": "1", + } | test_env, + tags = test_tags, + deps = test_deps, + experimental_use_cc_common_link = select({ + "@platforms//os:windows": 0, + "//conditions:default": 1, + }), + ) diff --git a/compile_flags.txt b/compile_flags.txt index 17b9295ab6a..1a6527cadf2 100644 --- a/compile_flags.txt +++ b/compile_flags.txt @@ -54,13 +54,14 @@ -isystembazel-bin/src/rust/cxx-integration/_virtual_includes/cxx-include/ -isystembazel-bin/src/rust/cxx-integration/_virtual_includes/cxx-integration@cxx -isystembazel-bin/src/rust/cxx-integration-test/_virtual_includes/cxx-integration-test@cxx --isystembazel-bin/src/rust/dns/_virtual_includes/dns@cxx -isystembazel-bin/src/rust/kj/_virtual_includes/http.rs@cxx -isystembazel-bin/src/rust/kj/_virtual_includes/io.rs@cxx -isystembazel-bin/src/rust/kj/tests/_virtual_includes/lib.rs@cxx -isystembazel-bin/src/rust/python-parser/_virtual_includes/python-parser@cxx -isystembazel-bin/src/rust/net/_virtual_includes/net@cxx -isystembazel-bin/src/rust/transpiler/_virtual_includes/transpiler@cxx +-isystembazel-bin/src/rust/api/_virtual_includes/lib.rs@cxx +-isystembazel-bin/src/rust/jsg/_virtual_includes/modules.rs@cxx -D_FORTIFY_SOURCE=1 -D_LIBCPP_REMOVE_TRANSITIVE_INCLUDES -D_LIBCPP_NO_ABI_TAG diff --git a/justfile b/justfile index d591ebc5435..9bca4ba2d3d 100644 --- a/justfile +++ b/justfile @@ -15,6 +15,12 @@ prepare: cargo install gen-compile-commands watchexec-cli just create-external just compile-commands + just prepare-rust + +prepare-rust: + rustup install 1.90.0 + rustup component add rust-analyzer + rustup update prepare-ubuntu: sudo apt-get install -y --no-install-recommends libc++abi1-19 libc++1-19 libc++-19-dev lld-19 bazelisk python3 lcov fd-find diff --git a/src/rust/dns/BUILD.bazel b/src/rust/api/BUILD.bazel similarity index 58% rename from src/rust/dns/BUILD.bazel rename to src/rust/api/BUILD.bazel index 427568b21e3..0efe32c5610 100644 --- a/src/rust/dns/BUILD.bazel +++ b/src/rust/api/BUILD.bazel @@ -1,11 +1,12 @@ load("//:build/wd_rust_crate.bzl", "wd_rust_crate") wd_rust_crate( - name = "dns", - cxx_bridge_src = "lib.rs", + name = "api", + cxx_bridge_deps = ["//src/rust/jsg"], + cxx_bridge_srcs = ["lib.rs"], visibility = ["//visibility:public"], deps = [ - "//src/rust/cxx-integration", + "//src/rust/jsg", "@crates_vendor//:thiserror", ], ) diff --git a/src/rust/api/dns.rs b/src/rust/api/dns.rs new file mode 100644 index 00000000000..dbc81aa2bc3 --- /dev/null +++ b/src/rust/api/dns.rs @@ -0,0 +1,288 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum DnsParserError { + #[error("Invalid hex string: {0}")] + InvalidHexString(String), + #[error("ParseInt error: {0}")] + ParseIntError(#[from] std::num::ParseIntError), + #[error("Invalid DNS response: {0}")] + InvalidDnsResponse(String), + #[error("unknown dns parser error")] + Unknown, +} + +impl Into for DnsParserError { + fn into(self) -> jsg::Error { + jsg::Error::new("Error".to_string(), self.to_string()) + } +} + +/// CAA record representation +pub struct CaaRecord { + pub critical: u8, + pub field: String, + pub value: String, +} + +impl jsg::Type for CaaRecord {} + +impl jsg::Struct for CaaRecord {} + +/// NAPTR record representation +pub struct NaptrRecord { + pub flags: String, + pub service: String, + pub regexp: String, + pub replacement: String, + pub order: u32, + pub preference: u32, +} + +impl jsg::Type for NaptrRecord {} + +impl jsg::Struct for NaptrRecord {} + +/// Given a vector of strings, converts each slice to UTF-8 from HEX. +/// +/// # Errors +/// `DnsParserError::InvalidHexString` +/// `DnsParserError::ParseIntError` +pub fn decode_hex(input: &[&str]) -> Result, DnsParserError> { + let mut v = Vec::with_capacity(input.len()); + + for slice in input { + let num = u16::from_str_radix(slice, 16)?; + let ch = String::from_utf16(&[num]) + .map_err(|_| DnsParserError::InvalidHexString("Invalid UTF-16 sequence".to_owned()))?; + v.push(ch); + } + + Ok(v) +} + +/// Replacement values needs to be parsed accordingly. +/// +/// It has a similar characteristic to CAA and NAPTR records whereas +/// first character contains the length of the input, and the second character +/// is the starting index of the substring. We need to continue parsing until there +/// are no input left, and later join them using "." +/// +/// It is important that the returning value doesn't end with dot (".") character. +/// +/// # Errors +/// `DnsParserError::InvalidHexString` +/// `DnsParserError::ParseIntError` +pub fn parse_replacement(input: &[&str]) -> jsg::Result { + if input.is_empty() { + return Ok(String::new()); + } + + let mut output: Vec = vec![]; + let mut length_index = 0; + let mut offset_index = 1; + + // Iterate through each character to parse different frames. + // Each frame starts with the length of the remaining frame. + while length_index < input.len() { + let length = usize::from_str_radix(input[length_index], 16)?; + let subset = input[offset_index..length + offset_index].to_vec(); + let decoded = decode_hex(&subset)?.join(""); + + // We omit the trailing "." from replacements. + // Cloudflare DNS returns "_sip._udp.sip2sip.info." whereas Node.js removes trailing dot + if !decoded.is_empty() { + output.push(decoded); + } + + length_index += subset.len() + 1; + offset_index = length_index + 1; + } + + Ok(output.join(".")) +} + +// #[jsg::resource] +pub struct DnsUtil {} + +// Generated code +pub struct DnsUtilWrapper { + // memoized_constructor: Option, + // context_constructor: Option, +} + +impl DnsUtilWrapper { + fn register(&mut self, isolate: &mut jsg::v8::Isolate) { + // Create necessary constructors. + } +} + +impl DnsUtil { + /// Parses an unknown RR format returned from Cloudflare DNS. + /// Specification is available at + /// `` + /// + /// The format of the record is as follows: + /// \# + /// \\# 15 00 05 69 73 73 75 65 70 6b 69 2e 67 6f 6f 67 + /// | | | | + /// | | | - Starting point of the actual data + /// | | - Length of the field. + /// | - Number representation of "`is_critical`" + /// - Length of the data + /// + /// Note: Field can be "issuewild", "issue" or "iodef". + /// + /// ``` + /// let record = parse_caa_record("\\# 15 00 05 69 73 73 75 65 70 6b 69 2e 67 6f 6f 67"); + /// assert_eq!(record.critical, false); + /// assert_eq!(record.field, "issue") + /// assert_eq!(record.value, "pki.goog") + /// ``` + /// # Errors + /// `DnsParserError::InvalidHexString` + /// `DnsParserError::ParseIntError` + pub fn parse_caa_record(record: &str) -> Result { + // Let's remove "\\#" and the length of data from the beginning of the record + let data = record.split_ascii_whitespace().collect::>()[2..].to_vec(); + let critical = data[0].parse::()?; + let prefix_length = data[1].parse::()?; + + let field = decode_hex(&data[2..prefix_length + 2])?.join(""); + let value = decode_hex(&data[(prefix_length + 2)..])?.join(""); + + // Field can be "issuewild", "issue" or "iodef" + if field != "issuewild" && field != "issue" && field != "iodef" { + return Err(DnsParserError::InvalidDnsResponse(format!( + "Received unknown field '{field}'" + ))); + } + + Ok(CaaRecord { + critical, + field, + value, + }) + } + + /// Parses an unknown RR format returned from Cloudflare DNS. + /// Specification is available at + /// `` + /// + /// The format of the record is as follows: + /// \# 37 15 b3 08 ae 01 73 0a 6d 79 2d 73 65 72 76 69 63 65 06 72 65 67 65 78 70 0b 72 65 70 6c 61 63 65 6d 65 6e 74 00 + /// |--| |--| | | | |--------------------------| | |--------------| | |--------------------------------| + /// | | | | | | | | | - Replacement + /// | | | | | | | | - Length of first part of the replacement + /// | | | | | | | - Regexp + /// | | | | | | - Regexp length + /// | | | | | - Service + /// | | | | - Length of service + /// | | | - Flag + /// | | - Length of flags + /// | - Preference + /// - Order + /// + /// ``` + /// let record = parse_naptr_record("\\# 37 15 b3 08 ae 01 73 0a 6d 79 2d 73 65 72 76 69 63 65 06 72 65 67 65 78 70 0b 72 65 70 6c 61 63 65 6d 65 6e 74 00"); + /// assert_eq!(record.flags, "s"); + /// assert_eq!(record.service, "my-service"); + /// assert_eq!(record.regexp, "regexp"); + /// assert_eq!(record.replacement, "replacement"); + /// assert_eq!(record.order, 5555); + /// assert_eq!(record.preference, 2222); + /// ``` + /// + /// # Errors + /// `DnsParserError::InvalidHexString` + /// `DnsParserError::ParseIntError` + pub fn parse_naptr_record(record: &str) -> jsg::Result { + let data = record.split_ascii_whitespace().collect::>()[1..].to_vec(); + + let order_str = data[1..3].to_vec(); + let order = u32::from_str_radix(&order_str.join(""), 16)?; + let preference_str = data[3..5].to_vec(); + let preference = u32::from_str_radix(&preference_str.join(""), 16)?; + + let flag_length = usize::from_str_radix(data[5], 16)?; + let flag_offset = 6; + let flags = decode_hex(&data[flag_offset..flag_length + flag_offset])?.join(""); + + let service_length = usize::from_str_radix(data[flag_offset + flag_length], 16)?; + let service_offset = flag_offset + flag_length + 1; + let service = decode_hex(&data[service_offset..service_length + service_offset])?.join(""); + + let regexp_length = usize::from_str_radix(data[service_offset + service_length], 16)?; + let regexp_offset = service_offset + service_length + 1; + let regexp = decode_hex(&data[regexp_offset..regexp_length + regexp_offset])?.join(""); + + let replacement = parse_replacement(&data[regexp_offset + regexp_length..])?; + + Ok(NaptrRecord { + flags, + service, + regexp, + replacement, + order, + preference, + }) + } +} + +// Generated code. +impl DnsUtil { + fn parse_caa_record_callback( + lock: *mut jsg::ffi::Lock, + args: *mut jsg::ffi::Args, + ) -> jsg::Result { + let lock = unsafe { &mut *lock }; + let args = unsafe { &mut *args }; + let arg0 = args.get_arg(0); + let arg0 = jsg::ffi::string_from_value(lock, arg0); + match Self::parse_caa_record(arg0) { + Ok(record) => Ok(jsg::ffi::value_from_jsg_struct(lock, &record)), + Err(err) => Err(err.into()), + } + } + + fn parse_naptr_record_callback( + lock: *mut jsg::ffi::Lock, + args: *mut jsg::ffi::Args, + ) -> jsg::Result { + let lock = unsafe { &mut *lock }; + let args = unsafe { &mut *args }; + let arg0 = args.get_arg(0); + let arg0 = jsg::ffi::string_from_value(lock, arg0); + match Self::parse_naptr_record(arg0) { + Ok(record) => Ok(jsg::ffi::value_from_jsg_struct(lock, &record)), + Err(err) => Err(err.into()), + } + } +} + +// Generated code. +impl jsg::Resource for DnsUtil { + fn members() -> Vec> + where + Self: Sized, + { + vec![ + jsg::Member::StaticMethod { + name: "parseCaaRecord", + callback: Box::new(Self::parse_caa_record_callback), + }, + jsg::Member::StaticMethod { + name: "parseNaptrRecord", + callback: Box::new(Self::parse_naptr_record_callback), + }, + ] + } +} + +impl jsg::Type for DnsUtil {} + +pub fn register_types(r: &mut jsg::TypeRegistrar) { + r.register_module::("node-internal:dns", jsg::modules::Type::INTERNAL); + r.register_struct::(); + r.register_struct::(); +} diff --git a/src/rust/api/lib.rs b/src/rust/api/lib.rs new file mode 100644 index 00000000000..9a95be2c8f2 --- /dev/null +++ b/src/rust/api/lib.rs @@ -0,0 +1,24 @@ +#![feature(must_not_suspend)] +#![warn(must_not_suspend)] + +use std::pin::Pin; + +pub mod dns; + +#[cxx::bridge(namespace = "workerd::rust::api")] +mod ffi { + #[namespace = "workerd::rust::jsg"] + unsafe extern "C++" { + include!("workerd/rust/jsg/ffi.h"); + type ModuleRegistry = jsg::modules::ffi::ModuleRegistry; + type LocalValue = jsg::v8::ffi::LocalValue; + type ModuleCallback = jsg::modules::ffi::ModuleCallback; + } + extern "Rust" { + pub fn register_nodejs_modules(registry: Pin<&mut ModuleRegistry>); + } +} + +pub fn register_nodejs_modules(registry: Pin<&mut ffi::ModuleRegistry>) { + todo!("register_nodejs_modules") +} diff --git a/src/rust/dns/lib.rs b/src/rust/dns/lib.rs deleted file mode 100644 index c8dafdc83f2..00000000000 --- a/src/rust/dns/lib.rs +++ /dev/null @@ -1,205 +0,0 @@ -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum DnsParserError { - #[error("Invalid hex string: {0}")] - InvalidHexString(String), - #[error("ParseInt error: {0}")] - ParseIntError(#[from] std::num::ParseIntError), - #[error("Invalid DNS response: {0}")] - InvalidDnsResponse(String), - #[error("unknown dns parser error")] - Unknown, -} - -#[cxx::bridge(namespace = "workerd::rust::dns")] -mod ffi { - /// CAA record representation - struct CaaRecord { - critical: u8, - field: String, - value: String, - } - /// NAPTR record representation - struct NaptrRecord { - flags: String, - service: String, - regexp: String, - replacement: String, - order: u32, - preference: u32, - } - extern "Rust" { - fn parse_caa_record(record: &str) -> Result; - fn parse_naptr_record(record: &str) -> Result; - } -} - -/// Given a vector of strings, converts each slice to UTF-8 from HEX. -/// -/// # Errors -/// `DnsParserError::InvalidHexString` -/// `DnsParserError::ParseIntError` -pub fn decode_hex(input: &[&str]) -> Result, DnsParserError> { - let mut v = Vec::with_capacity(input.len()); - - for slice in input { - let num = u16::from_str_radix(slice, 16)?; - let ch = String::from_utf16(&[num]) - .map_err(|_| DnsParserError::InvalidHexString("Invalid UTF-16 sequence".to_owned()))?; - v.push(ch); - } - - Ok(v) -} - -/// Parses an unknown RR format returned from Cloudflare DNS. -/// Specification is available at -/// `` -/// -/// The format of the record is as follows: -/// \# -/// \\# 15 00 05 69 73 73 75 65 70 6b 69 2e 67 6f 6f 67 -/// | | | | -/// | | | - Starting point of the actual data -/// | | - Length of the field. -/// | - Number representation of "`is_critical`" -/// - Length of the data -/// -/// Note: Field can be "issuewild", "issue" or "iodef". -/// -/// ``` -/// let record = parse_caa_record("\\# 15 00 05 69 73 73 75 65 70 6b 69 2e 67 6f 6f 67"); -/// assert_eq!(record.critical, false); -/// assert_eq!(record.field, "issue") -/// assert_eq!(record.value, "pki.goog") -/// ``` -/// # Errors -/// `DnsParserError::InvalidHexString` -/// `DnsParserError::ParseIntError` -pub fn parse_caa_record(record: &str) -> Result { - // Let's remove "\\#" and the length of data from the beginning of the record - let data = record.split_ascii_whitespace().collect::>()[2..].to_vec(); - let critical = data[0].parse::()?; - let prefix_length = data[1].parse::()?; - - let field = decode_hex(&data[2..prefix_length + 2])?.join(""); - let value = decode_hex(&data[(prefix_length + 2)..])?.join(""); - - // Field can be "issuewild", "issue" or "iodef" - if field != "issuewild" && field != "issue" && field != "iodef" { - return Err(DnsParserError::InvalidDnsResponse(format!( - "Received unknown field '{field}'" - ))); - } - - Ok(ffi::CaaRecord { - critical, - field, - value, - }) -} - -/// Parses an unknown RR format returned from Cloudflare DNS. -/// Specification is available at -/// `` -/// -/// The format of the record is as follows: -/// \# 37 15 b3 08 ae 01 73 0a 6d 79 2d 73 65 72 76 69 63 65 06 72 65 67 65 78 70 0b 72 65 70 6c 61 63 65 6d 65 6e 74 00 -/// |--| |--| | | | |--------------------------| | |--------------| | |--------------------------------| -/// | | | | | | | | | - Replacement -/// | | | | | | | | - Length of first part of the replacement -/// | | | | | | | - Regexp -/// | | | | | | - Regexp length -/// | | | | | - Service -/// | | | | - Length of service -/// | | | - Flag -/// | | - Length of flags -/// | - Preference -/// - Order -/// -/// ``` -/// let record = parse_naptr_record("\\# 37 15 b3 08 ae 01 73 0a 6d 79 2d 73 65 72 76 69 63 65 06 72 65 67 65 78 70 0b 72 65 70 6c 61 63 65 6d 65 6e 74 00"); -/// assert_eq!(record.flags, "s"); -/// assert_eq!(record.service, "my-service"); -/// assert_eq!(record.regexp, "regexp"); -/// assert_eq!(record.replacement, "replacement"); -/// assert_eq!(record.order, 5555); -/// assert_eq!(record.preference, 2222); -/// ``` -/// -/// # Errors -/// `DnsParserError::InvalidHexString` -/// `DnsParserError::ParseIntError` -pub fn parse_naptr_record(record: &str) -> Result { - let data = record.split_ascii_whitespace().collect::>()[1..].to_vec(); - - let order_str = data[1..3].to_vec(); - let order = u32::from_str_radix(&order_str.join(""), 16)?; - let preference_str = data[3..5].to_vec(); - let preference = u32::from_str_radix(&preference_str.join(""), 16)?; - - let flag_length = usize::from_str_radix(data[5], 16)?; - let flag_offset = 6; - let flags = decode_hex(&data[flag_offset..flag_length + flag_offset])?.join(""); - - let service_length = usize::from_str_radix(data[flag_offset + flag_length], 16)?; - let service_offset = flag_offset + flag_length + 1; - let service = decode_hex(&data[service_offset..service_length + service_offset])?.join(""); - - let regexp_length = usize::from_str_radix(data[service_offset + service_length], 16)?; - let regexp_offset = service_offset + service_length + 1; - let regexp = decode_hex(&data[regexp_offset..regexp_length + regexp_offset])?.join(""); - - let replacement = parse_replacement(&data[regexp_offset + regexp_length..])?; - - Ok(ffi::NaptrRecord { - flags, - service, - regexp, - replacement, - order, - preference, - }) -} - -/// Replacement values needs to be parsed accordingly. -/// -/// It has a similar characteristic to CAA and NAPTR records whereas -/// first character contains the length of the input, and the second character -/// is the starting index of the substring. We need to continue parsing until there -/// are no input left, and later join them using "." -/// -/// It is important that the returning value doesn't end with dot (".") character. -/// -/// # Errors -/// `DnsParserError::InvalidHexString` -/// `DnsParserError::ParseIntError` -pub fn parse_replacement(input: &[&str]) -> Result { - if input.is_empty() { - return Ok(String::new()); - } - - let mut output: Vec = vec![]; - let mut length_index = 0; - let mut offset_index = 1; - - // Iterate through each character to parse different frames. - // Each frame starts with the length of the remaining frame. - while length_index < input.len() { - let length = usize::from_str_radix(input[length_index], 16)?; - let subset = input[offset_index..length + offset_index].to_vec(); - let decoded = decode_hex(&subset)?.join(""); - - // We omit the trailing "." from replacements. - // Cloudflare DNS returns "_sip._udp.sip2sip.info." whereas Node.js removes trailing dot - if !decoded.is_empty() { - output.push(decoded); - } - - length_index += subset.len() + 1; - offset_index = length_index + 1; - } - - Ok(output.join(".")) -} diff --git a/src/rust/jsg/BUILD.bazel b/src/rust/jsg/BUILD.bazel new file mode 100644 index 00000000000..f86087c1484 --- /dev/null +++ b/src/rust/jsg/BUILD.bazel @@ -0,0 +1,11 @@ +load("//:build/wd_rust_crate.bzl", "wd_rust_crate") + +wd_rust_crate( + name = "jsg", + cxx_bridge_deps = ["//src/workerd/jsg"], + cxx_bridge_srcs = [ + "modules.rs", + "v8.rs", + ], + visibility = ["//visibility:public"], +) diff --git a/src/rust/jsg/ffi.h b/src/rust/jsg/ffi.h new file mode 100644 index 00000000000..40fc065f681 --- /dev/null +++ b/src/rust/jsg/ffi.h @@ -0,0 +1,102 @@ +#pragma once + +#include +#include +#include + +namespace workerd::rust::jsg { + using LocalValue = v8::Local; + using V8Isolate = v8::Isolate; + using ModuleCallback = kj::Function(); + + struct ModuleRegistry { + virtual ~ModuleRegistry() = default; + virtual void addBuiltinModule(::rust::Str specifier) = 0; + }; + + template + struct RustModuleRegistry : public ::workerd::rust::jsg::ModuleRegistry { + virtual ~RustModuleRegistry() = default; + RustModuleRegistry(Registry& registry) : registry(registry) {} + void addBuiltinModule(::rust::Str specifier) override { + auto kj_specifier = kj::str(specifier); + registry.addBuiltinModule(kj_specifier, [&kj_specifier](::workerd::jsg::Lock& js, ::workerd::jsg::ModuleRegistry::ResolveMethod, kj::Maybe&) mutable -> kj::Maybe<::workerd::jsg::ModuleRegistry::ModuleInfo> { + // auto isolate = js.v8Isolate; + KJ_UNIMPLEMENTED(); + // return kj::Maybe(ModuleInfo(js, kj_specifier, kj::none, ObjectModuleInfo(js, wrap))); + }, ::workerd::jsg::ModuleType::BUILTIN); + } + v8::Local getTemplate(v8::Isolate* isolate) { + KJ_UNIMPLEMENTED(); + // v8::Global& slot = memoizedConstructor; + // if (slot.IsEmpty()) { + // // Construct lazily. + // v8::EscapableHandleScope scope(isolate); + + // v8::Local constructor; + // if constexpr (!isContext && hasConstructorMethod((T*)nullptr)) { + // constructor = + // v8::FunctionTemplate::New(isolate, &ConstructorCallback::callback); + // } else { + // constructor = v8::FunctionTemplate::New(isolate, &throwIllegalConstructor); + // } + + // auto prototype = constructor->PrototypeTemplate(); + + // // Signatures protect our methods from being invoked with the wrong `this`. + // auto signature = v8::Signature::New(isolate, constructor); + + // auto instance = constructor->InstanceTemplate(); + + // instance->SetInternalFieldCount(Wrappable::INTERNAL_FIELD_COUNT); + + // auto classname = v8StrIntern(isolate, typeName(typeid(T))); + + // if (getShouldSetToStringTag(isolate)) { + // prototype->Set( + // v8::Symbol::GetToStringTag(isolate), classname, v8::PropertyAttribute::DontEnum); + // } + + // // Previously, miniflare would use the lack of a Symbol.toStringTag on a class to + // // detect a type that came from the runtime. That's obviously a bit problematic because + // // Symbol.toStringTag is required for full compliance on standard web platform APIs. + // // To help use cases where it is necessary to detect if a class is a runtime class, we + // // will add a special symbol to the prototype of the class to indicate. Note that + // // because this uses the global symbol registry user code could still mark their own + // // classes with this symbol but that's unlikely to be a problem in any practical case. + // auto internalMarker = + // v8::Symbol::For(isolate, v8StrIntern(isolate, "cloudflare:internal-class")); + // prototype->Set(internalMarker, internalMarker, + // static_cast(v8::PropertyAttribute::DontEnum | + // v8::PropertyAttribute::DontDelete | v8::PropertyAttribute::ReadOnly)); + + // constructor->SetClassName(classname); + + // static_assert(kj::isSameType(), + // "Name passed to JSG_RESOURCE_TYPE() must be the class's own name."); + + // auto& typeWrapper = static_cast(*this); + + // // ResourceTypeBuilder builder( + // // typeWrapper, isolate, constructor, instance, prototype, signature); + + // // if constexpr (isDetected()) { + // // T::template registerMembers(builder, configuration); + // // } else { + // // T::template registerMembers(builder); + // // } + + // // auto result = scope.Escape(constructor); + // // slot.Reset(isolate, result); + // // return result; + // } else { + // return slot.Get(isolate); + // } + } + Registry& registry; + }; + + inline void register_add_builtin_module(ModuleRegistry& registry, ::rust::Str specifier) { + registry.addBuiltinModule(specifier); + } +} diff --git a/src/rust/jsg/lib.rs b/src/rust/jsg/lib.rs new file mode 100644 index 00000000000..56508eaa210 --- /dev/null +++ b/src/rust/jsg/lib.rs @@ -0,0 +1,182 @@ +#![feature(must_not_suspend)] +#![warn(must_not_suspend)] + +use std::cell::Cell; +use std::num::ParseIntError; +use std::rc::Rc; + +pub mod modules; +pub mod v8; + +pub struct Error { + pub name: String, + pub message: String, +} + +impl Error { + pub fn new(name: String, message: String) -> Self { + Self { name, message } + } +} + +impl Default for Error { + fn default() -> Self { + Self { + name: "Error".to_owned(), + message: "An unknown error occurred".to_owned(), + } + } +} + +impl From for Error { + fn from(err: ParseIntError) -> Self { + Self::new( + "TypeError".to_owned(), + format!("Failed to parse integer: {err}"), + ) + } +} + +pub type Result = std::result::Result; + +pub mod ffi { + + pub struct Lock {} + + pub struct Value {} + + pub fn value_from_string(_lock: &Lock, _value: &str) -> Value { + todo!() + } + + pub fn value_from_jsg_struct(_lock: &Lock, _value: &T) -> Value + where + T: crate::Struct, + { + todo!() + } + + pub struct Args {} + + impl Args { + pub fn get_arg(&self, _index: usize) -> Value { + todo!() + } + } + + pub fn string_from_value(_lock: &mut Lock, _v: Value) -> &str { + todo!() + } +} + +#[must_not_suspend] +pub struct Lock {} + +impl Lock { + // todo: Result? + pub fn alloc(&mut self, t: T) -> Ref { + Ref { + t: Rc::new(Cell::new(t)), + } + } + + pub fn await_io(self, _fut: F, _callback: C) -> Result + where + F: Future, + C: FnOnce(Self, I) -> Result, + { + todo!() + } +} + +pub struct Ref { + t: Rc>, +} + +impl Ref { + pub fn as_mut<'a, 'b>(&'a mut self, _lock: &'b Lock) -> &'b mut T { + todo!() + } + + // self could potentially exist during all isolate lifetime, while + // lock is created anew every time. + // The resulting reference is bound by a lock lifetime to make + // it impossible to hold across different invocations. + pub fn as_ref<'a, 'b>(&'a self, _lock: &'b Lock) -> &'b T + where + 'a: 'b, + { + todo!() + } +} + +impl Clone for Ref { + fn clone(&self) -> Self { + Self { t: self.t.clone() } + } +} + +pub struct TypeRegistrar {} + +impl TypeRegistrar { + pub fn register_module(&mut self, name: &str, module_type: modules::Type) + where + T: Resource, + { + for _member in &T::members() { + // register the member in v8 + } + } + + pub fn register_struct(&mut self) + where + T: Struct, + { + } +} + +/// TODO: Implement `memory_info(jsg::MemoryTracker)` +pub trait Type { + /// Same as jsgGetMemoryName + fn memory_name() -> &'static str { + std::any::type_name::() + } + /// Same as jsgGetMemorySelfSize + fn memory_self_size() -> usize + where + Self: Sized, + { + std::mem::size_of::() + } +} + +pub type MethodCallbackImpl = + dyn FnMut(*mut S, *mut ffi::Lock, *mut ffi::Args) -> Result + 'static; + +pub type StaticMethodCallbackImpl = + dyn FnMut(*mut ffi::Lock, *mut ffi::Args) -> Result + 'static; + +pub enum Member { + Constructor, + Method { + name: &'static str, + callback: Box>, + }, + Property { + name: &'static str, + getter: Box>, + setter: Option>>, + }, + StaticMethod { + name: &'static str, + callback: Box>, + }, +} + +pub trait Resource: Type { + fn members() -> Vec> + where + Self: Sized; +} + +pub trait Struct: Type {} diff --git a/src/rust/jsg/modules.rs b/src/rust/jsg/modules.rs new file mode 100644 index 00000000000..cc2f25bca7f --- /dev/null +++ b/src/rust/jsg/modules.rs @@ -0,0 +1,13 @@ +pub enum Type { + INTERNAL, +} + +#[cxx::bridge(namespace = "workerd::rust::jsg")] +pub mod ffi { + extern "C++" { + include!("workerd/rust/jsg/ffi.h"); + + type ModuleRegistry; + type ModuleCallback; + } +} diff --git a/src/rust/jsg/v8.rs b/src/rust/jsg/v8.rs new file mode 100644 index 00000000000..1fafc8c9c78 --- /dev/null +++ b/src/rust/jsg/v8.rs @@ -0,0 +1,36 @@ +#[cxx::bridge(namespace = "workerd::rust::jsg")] +pub mod ffi { + extern "C++" { + include!("workerd/rust/jsg/ffi.h"); + + type LocalValue; + type V8Isolate; + } + + extern "Rust" { + type Isolate; + + unsafe fn isolate_created(isolate: *mut V8Isolate) -> Box; + } +} + +trait IsolateMember: Drop { + /// Initialize the member with the given isolate. + /// Nit: It will call something like "register()" + fn init(&mut self, isolate: &mut Isolate); +} + +/// Represents a V8 isolate. +/// It needs to have the same lifetime as the v8 Isolate. +pub struct Isolate { + ptr: *mut ffi::V8Isolate, + members: Vec>, +} + +/// This method is called whenever a new isolate is created. +pub unsafe fn isolate_created(isolate: *mut ffi::V8Isolate) -> Box { + Box::new(Isolate { + ptr: isolate, + members: Vec::new(), + }) +} diff --git a/src/workerd/api/node/BUILD.bazel b/src/workerd/api/node/BUILD.bazel index 506ae7c3534..c004033a5d5 100644 --- a/src/workerd/api/node/BUILD.bazel +++ b/src/workerd/api/node/BUILD.bazel @@ -46,6 +46,7 @@ wd_cc_library( deps = [ ":node-core", "//src/node", + "//src/rust/api", "//src/workerd/io", "@capnp-cpp//src/kj/compat:kj-brotli", "@ncrypto", @@ -57,7 +58,6 @@ wd_cc_library( name = "node-core", srcs = [ "buffer.c++", - "dns.c++", "i18n.c++", "sqlite.c++", "url.c++", @@ -65,7 +65,6 @@ wd_cc_library( hdrs = [ "buffer.h", "buffer-string-search.h", - "dns.h", "i18n.h", "node-version.h", "sqlite.h", @@ -73,7 +72,6 @@ wd_cc_library( ], implementation_deps = [ "//src/rust/cxx-integration", - "//src/rust/dns", "//src/rust/net", "@ada-url", "@nbytes", diff --git a/src/workerd/api/node/dns.c++ b/src/workerd/api/node/dns.c++ deleted file mode 100644 index c329e1f7db5..00000000000 --- a/src/workerd/api/node/dns.c++ +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2017-2022 Cloudflare, Inc. -// Licensed under the Apache 2.0 license found in the LICENSE file or at: -// https://opensource.org/licenses/Apache-2.0 -#include "dns.h" - -#include -#include - -#include - -using namespace kj_rs; - -namespace workerd::api::node { - -DnsUtil::CaaRecord DnsUtil::parseCaaRecord(kj::String record) { - // value comes from js so it is always valid utf-8 - auto parsed = rust::dns::parse_caa_record(record.as()); - return CaaRecord{ - .critical = parsed.critical, .field = kj::str(parsed.field), .value = kj::str(parsed.value)}; -} - -DnsUtil::NaptrRecord DnsUtil::parseNaptrRecord(kj::String record) { - // value comes from js so it is always valid utf-8 - auto parsed = rust::dns::parse_naptr_record(record.as()); - return NaptrRecord{ - .flags = kj::str(parsed.flags), - .service = kj::str(parsed.service), - .regexp = kj::str(parsed.regexp), - .replacement = kj::str(parsed.replacement), - .order = parsed.order, - .preference = parsed.preference, - }; -} - -} // namespace workerd::api::node diff --git a/src/workerd/api/node/dns.h b/src/workerd/api/node/dns.h deleted file mode 100644 index 33207254e65..00000000000 --- a/src/workerd/api/node/dns.h +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2017-2022 Cloudflare, Inc. -// Licensed under the Apache 2.0 license found in the LICENSE file or at: -// https://opensource.org/licenses/Apache-2.0 -#pragma once - -#include - -#include - -#include - -namespace workerd::api::node { - -class DnsUtil final: public jsg::Object { - public: - DnsUtil() = default; - DnsUtil(jsg::Lock&, const jsg::Url&) {} - - // TODO: Remove this once we can expose Rust structs - struct CaaRecord { - uint8_t critical; - kj::String field; - kj::String value; - - JSG_STRUCT(critical, field, value); - }; - - struct NaptrRecord { - kj::String flags; - kj::String service; - kj::String regexp; - kj::String replacement; - uint32_t order; - uint32_t preference; - - JSG_STRUCT(flags, service, regexp, replacement, order, preference); - }; - - CaaRecord parseCaaRecord(kj::String record); - NaptrRecord parseNaptrRecord(kj::String record); - - JSG_RESOURCE_TYPE(DnsUtil) { - JSG_METHOD(parseCaaRecord); - JSG_METHOD(parseNaptrRecord); - } -}; - -#define EW_NODE_DNS_ISOLATE_TYPES \ - api::node::DnsUtil, api::node::DnsUtil::CaaRecord, api::node::DnsUtil::NaptrRecord - -} // namespace workerd::api::node diff --git a/src/workerd/api/node/node.h b/src/workerd/api/node/node.h index 57b3561f27b..e6215124e8b 100644 --- a/src/workerd/api/node/node.h +++ b/src/workerd/api/node/node.h @@ -6,7 +6,6 @@ #include #include -#include #include #include #include @@ -17,6 +16,7 @@ #include #include #include +#include #include @@ -34,7 +34,6 @@ namespace workerd::api::node { V(DiagnosticsChannelModule, "node-internal:diagnostics_channel") \ V(ZlibUtil, "node-internal:zlib") \ V(UrlUtil, "node-internal:url") \ - V(DnsUtil, "node-internal:dns") \ V(TimersUtil, "node-internal:timers") \ V(SqliteUtil, "node-internal:sqlite") @@ -207,6 +206,9 @@ void registerNodeJsCompatModules(Registry& registry, auto featureFlags) { } } } + + ::workerd::rust::jsg::RustModuleRegistry r(registry); + ::workerd::rust::api::register_nodejs_modules(r); } template @@ -254,5 +256,5 @@ kj::Own getExternalNodeJsCompatModuleBundle(auto fea EW_NODE_BUFFER_ISOLATE_TYPES, EW_NODE_CRYPTO_ISOLATE_TYPES, \ EW_NODE_DIAGNOSTICCHANNEL_ISOLATE_TYPES, EW_NODE_ASYNCHOOKS_ISOLATE_TYPES, \ EW_NODE_UTIL_ISOLATE_TYPES, EW_NODE_PROCESS_ISOLATE_TYPES, EW_NODE_ZLIB_ISOLATE_TYPES, \ - EW_NODE_URL_ISOLATE_TYPES, EW_NODE_MODULE_ISOLATE_TYPES, EW_NODE_DNS_ISOLATE_TYPES, \ - EW_NODE_TIMERS_ISOLATE_TYPES, EW_NODE_SQLITE_ISOLATE_TYPES + EW_NODE_URL_ISOLATE_TYPES, EW_NODE_MODULE_ISOLATE_TYPES, EW_NODE_TIMERS_ISOLATE_TYPES, \ + EW_NODE_SQLITE_ISOLATE_TYPES diff --git a/src/workerd/api/rtti.c++ b/src/workerd/api/rtti.c++ index e3cf3b7d22f..fd5d914a833 100644 --- a/src/workerd/api/rtti.c++ +++ b/src/workerd/api/rtti.c++ @@ -147,6 +147,12 @@ struct EncoderModuleRegistryImpl { modules.add(kj::mv(info)); } + void addBuiltinModule(kj::StringPtr specifier, + jsg::ModuleRegistry::ModuleCallback callback, + jsg::ModuleRegistry::Type type = jsg::ModuleRegistry::Type::BUILTIN) { + // TODO(soon): Implement this function + } + template void addBuiltinModule(kj::StringPtr specifier, jsg::ModuleRegistry::Type type = jsg::ModuleRegistry::Type::BUILTIN) { diff --git a/src/workerd/jsg/BUILD.bazel b/src/workerd/jsg/BUILD.bazel index 39892a715f7..0d38bace59c 100644 --- a/src/workerd/jsg/BUILD.bazel +++ b/src/workerd/jsg/BUILD.bazel @@ -120,6 +120,7 @@ wd_cc_library( ":modules_capnp", ":observer", ":url", + "//src/rust/kj", "//src/workerd/util", "//src/workerd/util:autogate", "//src/workerd/util:sentry", diff --git a/src/workerd/jsg/iterator.h b/src/workerd/jsg/iterator.h index b6c32028017..7b5652dba26 100644 --- a/src/workerd/jsg/iterator.h +++ b/src/workerd/jsg/iterator.h @@ -6,6 +6,7 @@ #include #include +#include #include #include diff --git a/src/workerd/jsg/setup.c++ b/src/workerd/jsg/setup.c++ index 2f2880ebe13..e0e100a1005 100644 --- a/src/workerd/jsg/setup.c++ +++ b/src/workerd/jsg/setup.c++ @@ -377,7 +377,8 @@ IsolateBase::IsolateBase(V8System& system, externalMemoryTarget(kj::arc(ptr)), envAsyncContextKey(kj::refcounted()), heapTracer(ptr), - observer(kj::mv(observer)) { + observer(kj::mv(observer)), + rustIsolate(workerd::rust::jsg::isolate_created(ptr)) { jsg::runInV8Stack([&](jsg::V8StackScope& stackScope) { ptr->SetEmbedderRootsHandler(&heapTracer); diff --git a/src/workerd/jsg/setup.h b/src/workerd/jsg/setup.h index b86a4cf6ea1..50913e8f951 100644 --- a/src/workerd/jsg/setup.h +++ b/src/workerd/jsg/setup.h @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -25,6 +26,14 @@ namespace workerd::jsg { class Deserializer; class Serializer; +} // namespace workerd::jsg + +namespace workerd::rust::jsg { +struct Isolate; +} + +namespace workerd::jsg { + // Construct a default V8 platform, with the given background thread pool size. // // Passing zero for `backgroundThreadCount` causes V8 to ask glibc how many processors there are. @@ -415,6 +424,7 @@ class IsolateBase { HeapTracer heapTracer; kj::Own observer; + ::rust::Box rustIsolate; friend class Data; friend class Wrappable;