diff options
Diffstat (limited to 'xpcom/rust')
42 files changed, 6297 insertions, 0 deletions
diff --git a/xpcom/rust/gecko_logger/Cargo.toml b/xpcom/rust/gecko_logger/Cargo.toml new file mode 100644 index 0000000000..73af438679 --- /dev/null +++ b/xpcom/rust/gecko_logger/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "gecko_logger" +version = "0.1.0" +authors = ["nobody@mozilla.com"] +edition = "2018" +license = "MPL-2.0" + +[dependencies] +lazy_static = "1" +log = {version = "0.4", features = ["release_max_level_info"]} +env_logger = {version = "0.8", default-features = false} # disable `regex` to reduce code size +app_services_logger = { path = "../../../services/common/app_services_logger" } diff --git a/xpcom/rust/gecko_logger/src/lib.rs b/xpcom/rust/gecko_logger/src/lib.rs new file mode 100644 index 0000000000..da85ff92e5 --- /dev/null +++ b/xpcom/rust/gecko_logger/src/lib.rs @@ -0,0 +1,258 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! This provides a way to direct rust logging into the gecko logger. + +#[macro_use] +extern crate lazy_static; + +use app_services_logger::{AppServicesLogger, LOGGERS_BY_TARGET}; +use log::Log; +use log::{Level, LevelFilter}; +use std::boxed::Box; +use std::collections::HashMap; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use std::os::raw::c_int; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::RwLock; +use std::{cmp, env}; + +extern "C" { + fn ExternMozLog(tag: *const c_char, prio: c_int, text: *const c_char); + fn gfx_critical_note(msg: *const c_char); + #[cfg(target_os = "android")] + fn __android_log_write(prio: c_int, tag: *const c_char, text: *const c_char) -> c_int; +} + +lazy_static! { + // This could be a proper static once [1] is fixed or parking_lot's const fn + // support is not nightly-only. + // + // [1]: https://github.com/rust-lang/rust/issues/73714 + static ref LOG_MODULE_MAP: RwLock<HashMap<String, (LevelFilter, bool)>> = RwLock::new(HashMap::new()); +} + +/// This tells us whether LOG_MODULE_MAP is possibly non-empty. +static LOGGING_ACTIVE: AtomicBool = AtomicBool::new(false); + +/// This function searches for the module's name in the hashmap. If that is not +/// found, it proceeds to search for the parent modules. +/// It returns a tuple containing the matched string, if the matched module +/// was a pattern match, and the level we found in the hashmap. +/// If none is found, it will return the module's name and LevelFilter::Off +fn get_level_for_module<'a>( + map: &HashMap<String, (LevelFilter, bool)>, + key: &'a str, +) -> (&'a str, bool, LevelFilter) { + if let Some((level, is_pattern_match)) = map.get(key) { + return (key, *is_pattern_match, level.clone()); + } + + let mut mod_name = &key[..]; + while let Some(pos) = mod_name.rfind("::") { + mod_name = &mod_name[..pos]; + if let Some((level, is_pattern_match)) = map.get(mod_name) { + return (mod_name, *is_pattern_match, level.clone()); + } + } + + return (key, false, LevelFilter::Off); +} + +/// This function takes a record to maybe log to Gecko. +/// It returns true if the record was handled by Gecko's logging, and false +/// otherwise. +pub fn log_to_gecko(record: &log::Record) -> bool { + if !LOGGING_ACTIVE.load(Ordering::Relaxed) { + return false; + } + + let key = match record.module_path() { + Some(key) => key, + None => return false, + }; + + let (mod_name, is_pattern_match, level) = { + let map = LOG_MODULE_MAP.read().unwrap(); + get_level_for_module(&map, &key) + }; + + if level == LevelFilter::Off { + return false; + } + + if level < record.metadata().level() { + return false; + } + + // Map the log::Level to mozilla::LogLevel. + let moz_log_level = match record.metadata().level() { + Level::Error => 1, // Error + Level::Warn => 2, // Warning + Level::Info => 3, // Info + Level::Debug => 4, // Debug + Level::Trace => 5, // Verbose + }; + + // If it was a pattern match, we need to append ::* to the matched string. + let (tag, msg) = if is_pattern_match { + ( + CString::new(format!("{}::*", mod_name)).unwrap(), + CString::new(format!("[{}] {}", key, record.args())).unwrap(), + ) + } else { + ( + CString::new(key).unwrap(), + CString::new(format!("{}", record.args())).unwrap(), + ) + }; + + unsafe { + ExternMozLog(tag.as_ptr(), moz_log_level, msg.as_ptr()); + } + + return true; +} + +#[no_mangle] +pub extern "C" fn set_rust_log_level(module: *const c_char, level: u8) { + // Convert the Gecko level to a rust LevelFilter. + let rust_level = match level { + 1 => LevelFilter::Error, + 2 => LevelFilter::Warn, + 3 => LevelFilter::Info, + 4 => LevelFilter::Debug, + 5 => LevelFilter::Trace, + _ => LevelFilter::Off, + }; + + // This is the name of the rust module that we're trying to log in Gecko. + let mut mod_name = unsafe { CStr::from_ptr(module) } + .to_string_lossy() + .into_owned(); + + let is_pattern_match = mod_name.ends_with("::*"); + + // If this is a pattern, remove the last "::*" from it so we can search it + // in the map. + if is_pattern_match { + let len = mod_name.len() - 3; + mod_name.truncate(len); + } + + LOGGING_ACTIVE.store(true, Ordering::Relaxed); + let mut map = LOG_MODULE_MAP.write().unwrap(); + map.insert(mod_name, (rust_level, is_pattern_match)); + + // Figure out the max level of all the modules. + let max = map + .values() + .map(|(lvl, _)| lvl) + .max() + .unwrap_or(&LevelFilter::Off); + log::set_max_level(*max); +} + +pub struct GeckoLogger { + logger: env_logger::Logger, +} + +impl GeckoLogger { + pub fn new() -> GeckoLogger { + let mut builder = env_logger::Builder::new(); + let default_level = if cfg!(debug_assertions) { + "warn" + } else { + "error" + }; + let logger = match env::var("RUST_LOG") { + Ok(v) => builder.parse_filters(&v).build(), + _ => builder.parse_filters(default_level).build(), + }; + + GeckoLogger { logger } + } + + pub fn init() -> Result<(), log::SetLoggerError> { + let gecko_logger = Self::new(); + + // The max level may have already been set by gecko_logger. Don't + // set it to a lower level. + let level = cmp::max(log::max_level(), gecko_logger.logger.filter()); + log::set_max_level(level); + log::set_boxed_logger(Box::new(gecko_logger)) + } + + fn should_log_to_app_services(target: &str) -> bool { + return AppServicesLogger::is_app_services_logger_registered(target.into()); + } + + fn maybe_log_to_app_services(&self, record: &log::Record) { + if Self::should_log_to_app_services(record.target()) { + if let Some(l) = LOGGERS_BY_TARGET.read().unwrap().get(record.target()) { + l.log(record); + } + } + } + + fn should_log_to_gfx_critical_note(record: &log::Record) -> bool { + record.level() == log::Level::Error && record.target().contains("webrender") + } + + fn maybe_log_to_gfx_critical_note(&self, record: &log::Record) { + if Self::should_log_to_gfx_critical_note(record) { + let msg = CString::new(format!("{}", record.args())).unwrap(); + unsafe { + gfx_critical_note(msg.as_ptr()); + } + } + } + + #[cfg(not(target_os = "android"))] + fn log_out(&self, record: &log::Record) { + // If the log wasn't handled by the gecko platform logger, just pass it + // to the env_logger. + if !log_to_gecko(record) { + self.logger.log(record); + } + } + + #[cfg(target_os = "android")] + fn log_out(&self, record: &log::Record) { + if !self.logger.matches(record) { + return; + } + + let msg = CString::new(format!("{}", record.args())).unwrap(); + let tag = CString::new(record.module_path().unwrap()).unwrap(); + let prio = match record.metadata().level() { + Level::Error => 6, /* ERROR */ + Level::Warn => 5, /* WARN */ + Level::Info => 4, /* INFO */ + Level::Debug => 3, /* DEBUG */ + Level::Trace => 2, /* VERBOSE */ + }; + // Output log directly to android log, since env_logger can output log + // only to stderr or stdout. + unsafe { + __android_log_write(prio, tag.as_ptr(), msg.as_ptr()); + } + } +} + +impl log::Log for GeckoLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + self.logger.enabled(metadata) || GeckoLogger::should_log_to_app_services(metadata.target()) + } + + fn log(&self, record: &log::Record) { + // Forward log to gfxCriticalNote, if the log should be in gfx crash log. + self.maybe_log_to_gfx_critical_note(record); + self.maybe_log_to_app_services(record); + self.log_out(record); + } + + fn flush(&self) {} +} diff --git a/xpcom/rust/gkrust_utils/Cargo.toml b/xpcom/rust/gkrust_utils/Cargo.toml new file mode 100644 index 0000000000..3a484b68dd --- /dev/null +++ b/xpcom/rust/gkrust_utils/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "gkrust_utils" +version = "0.1.0" +authors = ["Jonathan Kingston <jkt@mozilla.com>"] + +[dependencies] +uuid = { version = "0.8", features = ["v4"] } +semver = "0.9" +nsstring = { path = "../nsstring" } diff --git a/xpcom/rust/gkrust_utils/cbindgen.toml b/xpcom/rust/gkrust_utils/cbindgen.toml new file mode 100644 index 0000000000..967fbefd47 --- /dev/null +++ b/xpcom/rust/gkrust_utils/cbindgen.toml @@ -0,0 +1,31 @@ +header = """/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */""" +autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. + * To generate this file: + * 1. Get the latest cbindgen using `cargo install --force cbindgen` + * a. Alternatively, you can clone `https://github.com/eqrion/cbindgen` and use a tagged release + * 2. Run `rustup run nightly cbindgen xpcom/rust/gkrust_utils --lockfile Cargo.lock --crate gkrust_utils -o xpcom/base/gk_rust_utils_ffi_generated.h` + */ +#include "nsError.h" +#include "nsString.h" +""" +include_version = true +braces = "SameLine" +line_length = 100 +tab_width = 2 +language = "C++" +namespaces = ["mozilla"] + +[export] +# Skip constants because we don't have any +item_types = ["globals", "enums", "structs", "unions", "typedefs", "opaque", "functions"] + +[enum] +add_sentinel = true +derive_helper_methods = true + +[defines] +"target_os = windows" = "XP_WIN" +"target_os = macos" = "XP_MACOSX" +"target_os = android" = "ANDROID" diff --git a/xpcom/rust/gkrust_utils/src/lib.rs b/xpcom/rust/gkrust_utils/src/lib.rs new file mode 100644 index 0000000000..a733ece897 --- /dev/null +++ b/xpcom/rust/gkrust_utils/src/lib.rs @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate nsstring; +extern crate semver; +extern crate uuid; +use nsstring::nsACString; +use uuid::Uuid; + +use std::fmt::Write; + +#[no_mangle] +pub extern "C" fn GkRustUtils_GenerateUUID(res: &mut nsACString) { + let uuid = Uuid::new_v4(); + write!(res, "{{{}}}", uuid.to_hyphenated_ref()).expect("Unexpected uuid generated"); +} + +#[no_mangle] +pub unsafe extern "C" fn GkRustUtils_ParseSemVer( + ver: &nsACString, + out_major: *mut u64, + out_minor: *mut u64, + out_patch: *mut u64, +) -> bool { + let version = match semver::Version::parse(&ver.to_utf8()) { + Ok(ver) => ver, + Err(_) => return false, + }; + *out_major = version.major; + *out_minor = version.minor; + *out_patch = version.patch; + true +} diff --git a/xpcom/rust/gtest/bench-collections/Bench.cpp b/xpcom/rust/gtest/bench-collections/Bench.cpp new file mode 100644 index 0000000000..5ed4629938 --- /dev/null +++ b/xpcom/rust/gtest/bench-collections/Bench.cpp @@ -0,0 +1,297 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Overview +// -------- +// This file measures the speed of various implementations of C++ and Rust +// collections (hash tables, etc.) used within the codebase. There are a small +// number of benchmarks for each collection type; each benchmark tests certain +// operations (insertion, lookup, iteration, etc.) More benchmarks could easily +// be envisioned, but this small number is enough to characterize the major +// differences between implementations, while keeping the file size and +// complexity low. +// +// Details +// ------- +// The file uses `MOZ_GTEST_BENCH_F` so that results are integrated into +// PerfHerder. It is also designed so that individual test benchmarks can be +// run under a profiler. +// +// The C++ code uses `MOZ_RELEASE_ASSERT` extensively to check values and +// ensure operations aren't optimized away by the compiler. The Rust code uses +// `assert!()`. These should be roughly equivalent, but aren't guaranteed to be +// the same. As a result, the intra-C++ comparisons should be reliable, and the +// intra-Rust comparisons should be reliable, but the C++ vs. Rust comparisons +// may be less reliable. +// +// Note that the Rust implementations run very slowly without --enable-release. +// +// Profiling +// --------- +// If you want to measure a particular implementation under a profiler such as +// Callgrind, do something like this: +// +// MOZ_RUN_GTEST=1 GTEST_FILTER='*BenchCollections*$IMPL*' +// valgrind --tool=callgrind --callgrind-out-file=clgout +// $OBJDIR/dist/bin/firefox -unittest +// callgrind_annotate --auto=yes clgout > clgann +// +// where $IMPL is part of an implementation name in a test (e.g. "PLDHash", +// "MozHash") and $OBJDIR is an objdir containing a --enable-release build. +// +// Note that multiple processes are spawned, so `clgout` gets overwritten +// multiple times, but the last process to write its profiling data to file is +// the one of interest. (Alternatively, use --callgrind-out-file=clgout.%p to +// get separate output files for each process, with a PID suffix.) + +#include "gtest/gtest.h" +#include "gtest/MozGTestBench.h" // For MOZ_GTEST_BENCH +#include "mozilla/AllocPolicy.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/HashTable.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/TimeStamp.h" +#include "PLDHashTable.h" +#include <unordered_set> + +using namespace mozilla; + +// This function gives a pseudo-random sequence with the following properties: +// - Deterministic and platform-independent. +// - No duplicates in the first VALS_LEN results, which is useful for ensuring +// the tables get to a particular size, and also for guaranteeing lookups +// that fail. +static uintptr_t MyRand() { + static uintptr_t s = 0; + s = s * 1103515245 + 12345; + return s; +} + +// Keep this in sync with Params in bench.rs. +struct Params { + const char* mConfigName; + size_t mNumInserts; // Insert this many unique keys + size_t mNumSuccessfulLookups; // Does mNumInserts lookups each time + size_t mNumFailingLookups; // Does mNumInserts lookups each time + size_t mNumIterations; // Iterates the full table each time + bool mRemoveInserts; // Remove all entries at end? +}; + +// We don't use std::unordered_{set,map}, but it's an interesting thing to +// benchmark against. +// +// Keep this in sync with all the other Bench_*() functions. +static void Bench_Cpp_unordered_set(const Params* aParams, void** aVals, + size_t aLen) { + std::unordered_set<void*> hs; + + for (size_t j = 0; j < aParams->mNumInserts; j++) { + hs.insert(aVals[j]); + } + + for (size_t i = 0; i < aParams->mNumSuccessfulLookups; i++) { + for (size_t j = 0; j < aParams->mNumInserts; j++) { + MOZ_RELEASE_ASSERT(hs.find(aVals[j]) != hs.end()); + } + } + + for (size_t i = 0; i < aParams->mNumFailingLookups; i++) { + for (size_t j = aParams->mNumInserts; j < aParams->mNumInserts * 2; j++) { + MOZ_RELEASE_ASSERT(hs.find(aVals[j]) == hs.end()); + } + } + + for (size_t i = 0; i < aParams->mNumIterations; i++) { + size_t n = 0; + for (const auto& elem : hs) { + (void)elem; + n++; + } + MOZ_RELEASE_ASSERT(aParams->mNumInserts == n); + MOZ_RELEASE_ASSERT(hs.size() == n); + } + + if (aParams->mRemoveInserts) { + for (size_t j = 0; j < aParams->mNumInserts; j++) { + MOZ_RELEASE_ASSERT(hs.erase(aVals[j]) == 1); + } + MOZ_RELEASE_ASSERT(hs.size() == 0); + } else { + MOZ_RELEASE_ASSERT(hs.size() == aParams->mNumInserts); + } +} + +// Keep this in sync with all the other Bench_*() functions. +static void Bench_Cpp_PLDHashTable(const Params* aParams, void** aVals, + size_t aLen) { + PLDHashTable hs(PLDHashTable::StubOps(), sizeof(PLDHashEntryStub)); + + for (size_t j = 0; j < aParams->mNumInserts; j++) { + auto entry = static_cast<PLDHashEntryStub*>(hs.Add(aVals[j])); + MOZ_RELEASE_ASSERT(!entry->key); + entry->key = aVals[j]; + } + + for (size_t i = 0; i < aParams->mNumSuccessfulLookups; i++) { + for (size_t j = 0; j < aParams->mNumInserts; j++) { + MOZ_RELEASE_ASSERT(hs.Search(aVals[j])); + } + } + + for (size_t i = 0; i < aParams->mNumFailingLookups; i++) { + for (size_t j = aParams->mNumInserts; j < aParams->mNumInserts * 2; j++) { + MOZ_RELEASE_ASSERT(!hs.Search(aVals[j])); + } + } + + for (size_t i = 0; i < aParams->mNumIterations; i++) { + size_t n = 0; + for (auto iter = hs.Iter(); !iter.Done(); iter.Next()) { + n++; + } + MOZ_RELEASE_ASSERT(aParams->mNumInserts == n); + MOZ_RELEASE_ASSERT(hs.EntryCount() == n); + } + + if (aParams->mRemoveInserts) { + for (size_t j = 0; j < aParams->mNumInserts; j++) { + hs.Remove(aVals[j]); + } + MOZ_RELEASE_ASSERT(hs.EntryCount() == 0); + } else { + MOZ_RELEASE_ASSERT(hs.EntryCount() == aParams->mNumInserts); + } +} + +// Keep this in sync with all the other Bench_*() functions. +static void Bench_Cpp_MozHashSet(const Params* aParams, void** aVals, + size_t aLen) { + mozilla::HashSet<void*, mozilla::DefaultHasher<void*>, MallocAllocPolicy> hs; + + for (size_t j = 0; j < aParams->mNumInserts; j++) { + MOZ_RELEASE_ASSERT(hs.put(aVals[j])); + } + + for (size_t i = 0; i < aParams->mNumSuccessfulLookups; i++) { + for (size_t j = 0; j < aParams->mNumInserts; j++) { + MOZ_RELEASE_ASSERT(hs.has(aVals[j])); + } + } + + for (size_t i = 0; i < aParams->mNumFailingLookups; i++) { + for (size_t j = aParams->mNumInserts; j < aParams->mNumInserts * 2; j++) { + MOZ_RELEASE_ASSERT(!hs.has(aVals[j])); + } + } + + for (size_t i = 0; i < aParams->mNumIterations; i++) { + size_t n = 0; + for (auto iter = hs.iter(); !iter.done(); iter.next()) { + n++; + } + MOZ_RELEASE_ASSERT(aParams->mNumInserts == n); + MOZ_RELEASE_ASSERT(hs.count() == n); + } + + if (aParams->mRemoveInserts) { + for (size_t j = 0; j < aParams->mNumInserts; j++) { + hs.remove(aVals[j]); + } + MOZ_RELEASE_ASSERT(hs.count() == 0); + } else { + MOZ_RELEASE_ASSERT(hs.count() == aParams->mNumInserts); + } +} + +extern "C" { +void Bench_Rust_HashSet(const Params* params, void** aVals, size_t aLen); +void Bench_Rust_FnvHashSet(const Params* params, void** aVals, size_t aLen); +void Bench_Rust_FxHashSet(const Params* params, void** aVals, size_t aLen); +} + +static const size_t VALS_LEN = 131072; + +// Each benchmark measures a different aspect of performance. +// Note that no "Inserts" value can exceed VALS_LEN. +// Also, if any failing lookups are done, Inserts must be <= VALS_LEN/2. +const Params gParamsList[] = { + // clang-format off + // Successful Failing Remove + // Inserts lookups lookups Iterations inserts + { "succ_lookups", 1024, 5000, 0, 0, false }, + { "fail_lookups", 1024, 0, 5000, 0, false }, + { "insert_remove", VALS_LEN, 0, 0, 0, true }, + { "iterate", 1024, 0, 0, 5000, false }, + // clang-format on +}; + +class BenchCollections : public ::testing::Test { + protected: + void SetUp() override { + StaticMutexAutoLock lock(sValsMutex); + + if (!sVals) { + sVals = (void**)malloc(VALS_LEN * sizeof(void*)); + for (size_t i = 0; i < VALS_LEN; i++) { + // This leaves the high 32 bits zero on 64-bit platforms, but that + // should still be enough randomness to get typical behaviour. + sVals[i] = reinterpret_cast<void*>(uintptr_t(MyRand())); + } + } + + printf("\n"); + for (size_t i = 0; i < ArrayLength(gParamsList); i++) { + const Params* params = &gParamsList[i]; + printf("%14s", params->mConfigName); + } + printf("%14s\n", "total"); + } + + public: + void BenchImpl(void (*aBench)(const Params*, void**, size_t)) { + StaticMutexAutoLock lock(sValsMutex); + + double total = 0; + for (size_t i = 0; i < ArrayLength(gParamsList); i++) { + const Params* params = &gParamsList[i]; + TimeStamp t1 = TimeStamp::Now(); + aBench(params, sVals, VALS_LEN); + TimeStamp t2 = TimeStamp::Now(); + double t = (t2 - t1).ToMilliseconds(); + printf("%11.1f ms", t); + total += t; + } + printf("%11.1f ms\n", total); + } + + private: + // Random values used in the benchmarks. + static void** sVals; + + // A mutex that protects all benchmark operations, ensuring that two + // benchmarks never run concurrently. + static StaticMutex sValsMutex; +}; + +void** BenchCollections::sVals; +StaticMutex BenchCollections::sValsMutex; + +MOZ_GTEST_BENCH_F(BenchCollections, unordered_set, + [this] { BenchImpl(Bench_Cpp_unordered_set); }); + +MOZ_GTEST_BENCH_F(BenchCollections, PLDHash, + [this] { BenchImpl(Bench_Cpp_PLDHashTable); }); + +MOZ_GTEST_BENCH_F(BenchCollections, MozHash, + [this] { BenchImpl(Bench_Cpp_MozHashSet); }); + +MOZ_GTEST_BENCH_F(BenchCollections, RustHash, + [this] { BenchImpl(Bench_Rust_HashSet); }); + +MOZ_GTEST_BENCH_F(BenchCollections, RustFnvHash, + [this] { BenchImpl(Bench_Rust_FnvHashSet); }); + +MOZ_GTEST_BENCH_F(BenchCollections, RustFxHash, + [this] { BenchImpl(Bench_Rust_FxHashSet); }); diff --git a/xpcom/rust/gtest/bench-collections/Cargo.toml b/xpcom/rust/gtest/bench-collections/Cargo.toml new file mode 100644 index 0000000000..857d87cffd --- /dev/null +++ b/xpcom/rust/gtest/bench-collections/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "bench-collections-gtest" +version = "0.1.0" +license = "MPL-2.0" +description = "Benchmarks for various collections" + +[dependencies] +fnv = "1.0" +fxhash = "0.2.1" + +[lib] +path = "bench.rs" diff --git a/xpcom/rust/gtest/bench-collections/bench.rs b/xpcom/rust/gtest/bench-collections/bench.rs new file mode 100644 index 0000000000..632cbd63e3 --- /dev/null +++ b/xpcom/rust/gtest/bench-collections/bench.rs @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#![allow(non_snake_case)] + +extern crate fnv; +extern crate fxhash; + +use fnv::FnvHashSet; +use fxhash::FxHashSet; +use std::collections::HashSet; +use std::os::raw::{c_char, c_void}; +use std::slice; + +/// Keep this in sync with Params in Bench.cpp. +#[derive(Debug)] +#[repr(C)] +pub struct Params { + config_name: *const c_char, + num_inserts: usize, + num_successful_lookups: usize, + num_failing_lookups: usize, + num_iterations: usize, + remove_inserts: bool, +} + +#[no_mangle] +pub extern "C" fn Bench_Rust_HashSet( + params: *const Params, + vals: *const *const c_void, + len: usize, +) { + let hs: HashSet<_> = std::collections::HashSet::default(); + Bench_Rust(hs, params, vals, len); +} + +#[no_mangle] +pub extern "C" fn Bench_Rust_FnvHashSet( + params: *const Params, + vals: *const *const c_void, + len: usize, +) { + let hs = FnvHashSet::default(); + Bench_Rust(hs, params, vals, len); +} + +#[no_mangle] +pub extern "C" fn Bench_Rust_FxHashSet( + params: *const Params, + vals: *const *const c_void, + len: usize, +) { + let hs = FxHashSet::default(); + Bench_Rust(hs, params, vals, len); +} + +// Keep this in sync with all the other Bench_*() functions. +fn Bench_Rust<H: std::hash::BuildHasher>( + mut hs: HashSet<*const c_void, H>, + params: *const Params, + vals: *const *const c_void, + len: usize, +) { + let params = unsafe { &*params }; + let vals = unsafe { slice::from_raw_parts(vals, len) }; + + for j in 0..params.num_inserts { + hs.insert(vals[j]); + } + + for _i in 0..params.num_successful_lookups { + for j in 0..params.num_inserts { + assert!(hs.contains(&vals[j])); + } + } + + for _i in 0..params.num_failing_lookups { + for j in params.num_inserts..params.num_inserts * 2 { + assert!(!hs.contains(&vals[j])); + } + } + + for _i in 0..params.num_iterations { + let mut n = 0; + for _ in hs.iter() { + n += 1; + } + assert!(params.num_inserts == n); + assert!(hs.len() == n); + } + + if params.remove_inserts { + for j in 0..params.num_inserts { + assert!(hs.remove(&vals[j])); + } + assert!(hs.len() == 0); + } else { + assert!(hs.len() == params.num_inserts); + } +} diff --git a/xpcom/rust/gtest/moz.build b/xpcom/rust/gtest/moz.build new file mode 100644 index 0000000000..0e64cdc9ca --- /dev/null +++ b/xpcom/rust/gtest/moz.build @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +UNIFIED_SOURCES += [ + "bench-collections/Bench.cpp", + "moz_task/Test.cpp", + "nsstring/Test.cpp", + "xpcom/Test.cpp", +] + +FINAL_LIBRARY = "xul-gtest" diff --git a/xpcom/rust/gtest/moz_task/Cargo.toml b/xpcom/rust/gtest/moz_task/Cargo.toml new file mode 100644 index 0000000000..09d240c309 --- /dev/null +++ b/xpcom/rust/gtest/moz_task/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "moz_task-gtest" +version = "0.1.0" +authors = ["nobody@mozilla.com"] +license = "MPL-2.0" +description = "Tests for rust bindings to xpcom event target types" +edition = "2018" + +[dependencies] +moz_task = { path = "../../moz_task" } + +[lib] +path = "test.rs" diff --git a/xpcom/rust/gtest/moz_task/Test.cpp b/xpcom/rust/gtest/moz_task/Test.cpp new file mode 100644 index 0000000000..6c40f1bc97 --- /dev/null +++ b/xpcom/rust/gtest/moz_task/Test.cpp @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +extern "C" void Rust_Future(bool* aItWorked); + +TEST(RustMozTask, Future) +{ + bool itWorked = false; + Rust_Future(&itWorked); + EXPECT_TRUE(itWorked); +} diff --git a/xpcom/rust/gtest/moz_task/test.rs b/xpcom/rust/gtest/moz_task/test.rs new file mode 100644 index 0000000000..4261030f47 --- /dev/null +++ b/xpcom/rust/gtest/moz_task/test.rs @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use moz_task; +use std::{ + future::Future, + pin::Pin, + sync::atomic::{AtomicBool, Ordering::Relaxed}, + sync::Arc, + task::{Context, Poll, Waker}, +}; + +/// Demo `Future` to demonstrate executing futures to completion via `nsIEventTarget`. +struct MyFuture { + poll_count: u32, + waker: Option<Waker>, +} + +impl Default for MyFuture { + fn default() -> Self { + Self { + poll_count: 0, + waker: None, + } + } +} + +impl Future for MyFuture { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { + self.poll_count += 1; + + if let Some(waker) = &mut self.waker { + if !waker.will_wake(cx.waker()) { + *waker = cx.waker().clone(); + } + } else { + let waker = cx.waker().clone(); + self.waker = Some(waker); + } + + println!("Poll count = {}", self.poll_count); + if self.poll_count > 5 { + Poll::Ready(()) + } else { + // Just notify the task that we need to re-polled. + if let Some(waker) = &self.waker { + waker.wake_by_ref(); + } + Poll::Pending + } + } +} + +#[no_mangle] +pub extern "C" fn Rust_Future(it_worked: *mut bool) { + let done = Arc::new(AtomicBool::new(false)); + let done2 = done.clone(); + + moz_task::spawn_current_thread(async move { + MyFuture::default().await; + done.store(true, Relaxed); + }) + .unwrap(); + + unsafe { + moz_task::gtest_only::spin_event_loop_until(move || done2.load(Relaxed)).unwrap(); + *it_worked = true; + }; +} diff --git a/xpcom/rust/gtest/nsstring/Cargo.toml b/xpcom/rust/gtest/nsstring/Cargo.toml new file mode 100644 index 0000000000..fd997926a7 --- /dev/null +++ b/xpcom/rust/gtest/nsstring/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "nsstring-gtest" +version = "0.1.0" +authors = ["nobody@mozilla.com"] +license = "MPL-2.0" +description = "Tests for rust bindings to xpcom string types" + +[dependencies] +nsstring = { path = "../../nsstring" } + +[lib] +path = "test.rs" diff --git a/xpcom/rust/gtest/nsstring/Test.cpp b/xpcom/rust/gtest/nsstring/Test.cpp new file mode 100644 index 0000000000..4e14c7bca5 --- /dev/null +++ b/xpcom/rust/gtest/nsstring/Test.cpp @@ -0,0 +1,177 @@ +#include "gtest/gtest.h" +#include <stdint.h> +#include <utility> +#include "nsString.h" + +extern "C" { +// This function is called by the rust code in test.rs if a non-fatal test +// failure occurs. +void GTest_ExpectFailure(const char* aMessage) { EXPECT_STREQ(aMessage, ""); } +} + +#define SIZE_ALIGN_CHECK(Clazz) \ + extern "C" void Rust_Test_ReprSizeAlign_##Clazz(size_t* size, \ + size_t* align); \ + TEST(RustNsString, ReprSizeAlign_##Clazz) \ + { \ + size_t size, align; \ + Rust_Test_ReprSizeAlign_##Clazz(&size, &align); \ + EXPECT_EQ(size, sizeof(Clazz)); \ + EXPECT_EQ(align, alignof(Clazz)); \ + } + +SIZE_ALIGN_CHECK(nsString) +SIZE_ALIGN_CHECK(nsCString) + +#define MEMBER_CHECK(Clazz, Member) \ + extern "C" void Rust_Test_Member_##Clazz##_##Member( \ + size_t* size, size_t* align, size_t* offset); \ + TEST(RustNsString, ReprMember_##Clazz##_##Member) \ + { \ + class Hack : public Clazz { \ + public: \ + static void RunTest() { \ + size_t size, align, offset; \ + Rust_Test_Member_##Clazz##_##Member(&size, &align, &offset); \ + EXPECT_EQ(size, sizeof(std::declval<Hack>().Member)); \ + EXPECT_EQ(align, alignof(decltype(std::declval<Hack>().Member))); \ + EXPECT_EQ(offset, offsetof(Hack, Member)); \ + } \ + }; \ + static_assert(sizeof(Clazz) == sizeof(Hack), "Hack matches class"); \ + Hack::RunTest(); \ + } + +MEMBER_CHECK(nsString, mData) +MEMBER_CHECK(nsString, mLength) +MEMBER_CHECK(nsString, mDataFlags) +MEMBER_CHECK(nsString, mClassFlags) +MEMBER_CHECK(nsCString, mData) +MEMBER_CHECK(nsCString, mLength) +MEMBER_CHECK(nsCString, mDataFlags) +MEMBER_CHECK(nsCString, mClassFlags) + +extern "C" void Rust_Test_NsStringFlags( + uint16_t* f_terminated, uint16_t* f_voided, uint16_t* f_refcounted, + uint16_t* f_owned, uint16_t* f_inline, uint16_t* f_literal, + uint16_t* f_class_inline, uint16_t* f_class_null_terminated); +TEST(RustNsString, NsStringFlags) +{ + uint16_t f_terminated, f_voided, f_refcounted, f_owned, f_inline, f_literal, + f_class_inline, f_class_null_terminated; + Rust_Test_NsStringFlags(&f_terminated, &f_voided, &f_refcounted, &f_owned, + &f_inline, &f_literal, &f_class_inline, + &f_class_null_terminated); + EXPECT_EQ(f_terminated, uint16_t(nsAString::DataFlags::TERMINATED)); + EXPECT_EQ(f_terminated, uint16_t(nsACString::DataFlags::TERMINATED)); + EXPECT_EQ(f_voided, uint16_t(nsAString::DataFlags::VOIDED)); + EXPECT_EQ(f_voided, uint16_t(nsACString::DataFlags::VOIDED)); + EXPECT_EQ(f_refcounted, uint16_t(nsAString::DataFlags::REFCOUNTED)); + EXPECT_EQ(f_refcounted, uint16_t(nsACString::DataFlags::REFCOUNTED)); + EXPECT_EQ(f_owned, uint16_t(nsAString::DataFlags::OWNED)); + EXPECT_EQ(f_owned, uint16_t(nsACString::DataFlags::OWNED)); + EXPECT_EQ(f_inline, uint16_t(nsAString::DataFlags::INLINE)); + EXPECT_EQ(f_inline, uint16_t(nsACString::DataFlags::INLINE)); + EXPECT_EQ(f_literal, uint16_t(nsAString::DataFlags::LITERAL)); + EXPECT_EQ(f_literal, uint16_t(nsACString::DataFlags::LITERAL)); + EXPECT_EQ(f_class_inline, uint16_t(nsAString::ClassFlags::INLINE)); + EXPECT_EQ(f_class_inline, uint16_t(nsACString::ClassFlags::INLINE)); + EXPECT_EQ(f_class_null_terminated, + uint16_t(nsAString::ClassFlags::NULL_TERMINATED)); + EXPECT_EQ(f_class_null_terminated, + uint16_t(nsACString::ClassFlags::NULL_TERMINATED)); +} + +extern "C" void Rust_StringFromCpp(const nsACString* aCStr, + const nsAString* aStr); +TEST(RustNsString, StringFromCpp) +{ + nsAutoCString foo; + foo.AssignASCII("Hello, World!"); + + nsAutoString bar; + bar.AssignASCII("Hello, World!"); + + Rust_StringFromCpp(&foo, &bar); +} + +extern "C" void Rust_AssignFromRust(nsACString* aCStr, nsAString* aStr); +TEST(RustNsString, AssignFromRust) +{ + nsAutoCString cs; + nsAutoString s; + Rust_AssignFromRust(&cs, &s); + EXPECT_TRUE(cs.EqualsASCII("Hello, World!")); + EXPECT_TRUE(s.EqualsASCII("Hello, World!")); +} + +extern "C" { +void Cpp_AssignFromCpp(nsACString* aCStr, nsAString* aStr) { + aCStr->AssignASCII("Hello, World!"); + aStr->AssignASCII("Hello, World!"); +} +} +extern "C" void Rust_AssignFromCpp(); +TEST(RustNsString, AssignFromCpp) +{ Rust_AssignFromCpp(); } + +extern "C" void Rust_StringWrite(); +TEST(RustNsString, StringWrite) +{ Rust_StringWrite(); } + +extern "C" void Rust_FromEmptyRustString(); +TEST(RustNsString, FromEmptyRustString) +{ Rust_FromEmptyRustString(); } + +extern "C" void Rust_WriteToBufferFromRust(nsACString* aCStr, nsAString* aStr, + nsACString* aFallibleCStr, + nsAString* aFallibleStr); +TEST(RustNsString, WriteToBufferFromRust) +{ + nsAutoCString cStr; + nsAutoString str; + nsAutoCString fallibleCStr; + nsAutoString fallibleStr; + + cStr.AssignLiteral("abc"); + str.AssignLiteral("abc"); + fallibleCStr.AssignLiteral("abc"); + fallibleStr.AssignLiteral("abc"); + + Rust_WriteToBufferFromRust(&cStr, &str, &fallibleCStr, &fallibleStr); + + EXPECT_TRUE(cStr.EqualsASCII("ABC")); + EXPECT_TRUE(str.EqualsASCII("ABC")); + EXPECT_TRUE(fallibleCStr.EqualsASCII("ABC")); + EXPECT_TRUE(fallibleStr.EqualsASCII("ABC")); +} + +extern "C" void Rust_InlineCapacityFromRust(const nsACString* aCStr, + const nsAString* aStr, + size_t* aCStrCapacity, + size_t* aStrCapacity); +TEST(RustNsString, InlineCapacityFromRust) +{ + size_t cStrCapacity; + size_t strCapacity; + nsAutoCStringN<93> cs; + nsAutoStringN<93> s; + Rust_InlineCapacityFromRust(&cs, &s, &cStrCapacity, &strCapacity); + EXPECT_EQ(cStrCapacity, 92U); + EXPECT_EQ(strCapacity, 92U); +} + +extern "C" void Rust_VoidStringFromRust(nsACString* aCStr, nsAString* aStr); +TEST(RustNsString, VoidStringFromRust) +{ + nsAutoCString cs; + nsAutoString s; + + EXPECT_FALSE(cs.IsVoid()); + EXPECT_FALSE(s.IsVoid()); + + Rust_VoidStringFromRust(&cs, &s); + + EXPECT_TRUE(cs.IsVoid()); + EXPECT_TRUE(s.IsVoid()); +} diff --git a/xpcom/rust/gtest/nsstring/test.rs b/xpcom/rust/gtest/nsstring/test.rs new file mode 100644 index 0000000000..a5d142f2b2 --- /dev/null +++ b/xpcom/rust/gtest/nsstring/test.rs @@ -0,0 +1,131 @@ +#![allow(non_snake_case)] + +extern crate nsstring; + +use nsstring::*; +use std::ffi::CString; +use std::fmt::Write; +use std::os::raw::c_char; + +fn nonfatal_fail(msg: String) { + extern "C" { + fn GTest_ExpectFailure(message: *const c_char); + } + unsafe { + let msg = CString::new(msg).unwrap(); + GTest_ExpectFailure(msg.as_ptr()); + } +} + +/// This macro checks if the two arguments are equal, and causes a non-fatal +/// GTest test failure if they are not. +macro_rules! expect_eq { + ($x:expr, $y:expr) => { + match (&$x, &$y) { + (x, y) => { + if *x != *y { + nonfatal_fail(format!( + "check failed: (`{:?}` == `{:?}`) at {}:{}", + x, + y, + file!(), + line!() + )) + } + } + } + }; +} + +#[no_mangle] +pub extern "C" fn Rust_StringFromCpp(cs: *const nsACString, s: *const nsAString) { + unsafe { + expect_eq!(&*cs, "Hello, World!"); + expect_eq!(&*s, "Hello, World!"); + } +} + +#[no_mangle] +pub extern "C" fn Rust_AssignFromRust(cs: *mut nsACString, s: *mut nsAString) { + unsafe { + (*cs).assign(&nsCString::from("Hello, World!")); + expect_eq!(&*cs, "Hello, World!"); + (*s).assign(&nsString::from("Hello, World!")); + expect_eq!(&*s, "Hello, World!"); + } +} + +extern "C" { + fn Cpp_AssignFromCpp(cs: *mut nsACString, s: *mut nsAString); +} + +#[no_mangle] +pub extern "C" fn Rust_AssignFromCpp() { + let mut cs = nsCString::new(); + let mut s = nsString::new(); + unsafe { + Cpp_AssignFromCpp(&mut *cs, &mut *s); + } + expect_eq!(cs, "Hello, World!"); + expect_eq!(s, "Hello, World!"); +} + +#[no_mangle] +pub extern "C" fn Rust_StringWrite() { + let mut cs = nsCString::new(); + let mut s = nsString::new(); + + write!(s, "a").unwrap(); + write!(cs, "a").unwrap(); + expect_eq!(s, "a"); + expect_eq!(cs, "a"); + write!(s, "bc").unwrap(); + write!(cs, "bc").unwrap(); + expect_eq!(s, "abc"); + expect_eq!(cs, "abc"); + write!(s, "{}", 123).unwrap(); + write!(cs, "{}", 123).unwrap(); + expect_eq!(s, "abc123"); + expect_eq!(cs, "abc123"); +} + +#[no_mangle] +pub extern "C" fn Rust_FromEmptyRustString() { + let mut test = nsString::from("Blah"); + test.assign_utf8(&nsCString::from(String::new())); + assert!(test.is_empty()); +} + +#[no_mangle] +pub extern "C" fn Rust_WriteToBufferFromRust( + cs: *mut nsACString, + s: *mut nsAString, + fallible_cs: *mut nsACString, + fallible_s: *mut nsAString, +) { + unsafe { + let cs_buf = (*cs).to_mut(); + let s_buf = (*s).to_mut(); + let fallible_cs_buf = (*fallible_cs).fallible_to_mut().unwrap(); + let fallible_s_buf = (*fallible_s).fallible_to_mut().unwrap(); + + cs_buf[0] = b'A'; + cs_buf[1] = b'B'; + cs_buf[2] = b'C'; + s_buf[0] = b'A' as u16; + s_buf[1] = b'B' as u16; + s_buf[2] = b'C' as u16; + fallible_cs_buf[0] = b'A'; + fallible_cs_buf[1] = b'B'; + fallible_cs_buf[2] = b'C'; + fallible_s_buf[0] = b'A' as u16; + fallible_s_buf[1] = b'B' as u16; + fallible_s_buf[2] = b'C' as u16; + } +} + +#[no_mangle] +pub extern "C" fn Rust_VoidStringFromRust(cs: &mut nsACString, s: &mut nsAString) { + cs.set_is_void(true); + s.set_is_void(true); +} diff --git a/xpcom/rust/gtest/xpcom/Cargo.toml b/xpcom/rust/gtest/xpcom/Cargo.toml new file mode 100644 index 0000000000..777080b33b --- /dev/null +++ b/xpcom/rust/gtest/xpcom/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "xpcom-gtest" +version = "0.1.0" +authors = ["michael@thelayzells.com"] +license = "MPL-2.0" +description = "Tests for rust bindings to xpcom interfaces" + +[dependencies] +xpcom = { path = "../../xpcom" } +nserror = { path = "../../nserror" } +nsstring = { path = "../../nsstring" } + +[lib] +path = "test.rs" diff --git a/xpcom/rust/gtest/xpcom/Test.cpp b/xpcom/rust/gtest/xpcom/Test.cpp new file mode 100644 index 0000000000..4c2a6fb117 --- /dev/null +++ b/xpcom/rust/gtest/xpcom/Test.cpp @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "nsCOMPtr.h" +#include "nsIRunnable.h" +#include "mozilla/Services.h" +#include "nsIObserverService.h" + +extern "C" nsIObserverService* Rust_ObserveFromRust(); + +TEST(RustXpcom, ObserverFromRust) +{ + nsCOMPtr<nsIObserverService> rust = Rust_ObserveFromRust(); + nsCOMPtr<nsIObserverService> cpp = mozilla::services::GetObserverService(); + EXPECT_EQ(rust, cpp); +} + +extern "C" void Rust_ImplementRunnableInRust(bool* aItWorked, + nsIRunnable** aRunnable); + +TEST(RustXpcom, ImplementRunnableInRust) +{ + bool itWorked = false; + nsCOMPtr<nsIRunnable> runnable; + Rust_ImplementRunnableInRust(&itWorked, getter_AddRefs(runnable)); + + EXPECT_TRUE(runnable); + EXPECT_FALSE(itWorked); + runnable->Run(); + EXPECT_TRUE(itWorked); +} diff --git a/xpcom/rust/gtest/xpcom/test.rs b/xpcom/rust/gtest/xpcom/test.rs new file mode 100644 index 0000000000..8e3e08701c --- /dev/null +++ b/xpcom/rust/gtest/xpcom/test.rs @@ -0,0 +1,99 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#![allow(non_snake_case)] + +#[macro_use] +extern crate xpcom; + +extern crate nserror; + +use nserror::{nsresult, NS_OK}; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use std::ptr; +use xpcom::interfaces; + +#[no_mangle] +pub unsafe extern "C" fn Rust_ObserveFromRust() -> *const interfaces::nsIObserverService { + let obssvc = xpcom::services::get_ObserverService().unwrap(); + + // Define an observer + #[derive(xpcom)] + #[xpimplements(nsIObserver)] + #[refcnt = "nonatomic"] + struct InitObserver { + run: *mut bool, + } + impl Observer { + unsafe fn Observe( + &self, + _subject: *const interfaces::nsISupports, + topic: *const c_char, + _data: *const i16, + ) -> nsresult { + *self.run = true; + assert!(CStr::from_ptr(topic).to_str() == Ok("test-rust-observe")); + NS_OK + } + } + + let topic = CString::new("test-rust-observe").unwrap(); + + let mut run = false; + let observer = Observer::allocate(InitObserver { run: &mut run }); + let rv = obssvc.AddObserver( + observer.coerce::<interfaces::nsIObserver>(), + topic.as_ptr(), + false, + ); + assert!(rv.succeeded()); + + let rv = obssvc.NotifyObservers(ptr::null(), topic.as_ptr(), ptr::null()); + assert!(rv.succeeded()); + assert!(run, "The observer should have been run!"); + + let rv = obssvc.RemoveObserver(observer.coerce::<interfaces::nsIObserver>(), topic.as_ptr()); + assert!(rv.succeeded()); + + assert!( + observer.coerce::<interfaces::nsISupports>() as *const _ + == &*observer + .query_interface::<interfaces::nsISupports>() + .unwrap() as *const _ + ); + + &*obssvc +} + +#[no_mangle] +pub unsafe extern "C" fn Rust_ImplementRunnableInRust( + it_worked: *mut bool, + runnable: *mut *const interfaces::nsIRunnable, +) { + // Define a type which implements nsIRunnable in rust. + #[derive(xpcom)] + #[xpimplements(nsIRunnable)] + #[refcnt = "atomic"] + struct InitRunnableFn<F: Fn() + 'static> { + run: F, + } + + impl<F: Fn() + 'static> RunnableFn<F> { + unsafe fn Run(&self) -> nsresult { + (self.run)(); + NS_OK + } + } + + let my_runnable = RunnableFn::allocate(InitRunnableFn { + run: move || { + *it_worked = true; + }, + }); + my_runnable + .query_interface::<interfaces::nsIRunnable>() + .unwrap() + .forget(&mut *runnable); +} diff --git a/xpcom/rust/moz_task/Cargo.toml b/xpcom/rust/moz_task/Cargo.toml new file mode 100644 index 0000000000..943275a0c1 --- /dev/null +++ b/xpcom/rust/moz_task/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "moz_task" +version = "0.1.0" +authors = ["Myk Melez <myk@mykzilla.org>"] +license = "MPL-2.0" +description = "Rust wrappers around XPCOM threading functions" + +[dependencies] +cstr = "0.2" +libc = "0.2" +futures-task = { version = "0.3" } +nserror = { path = "../nserror" } +nsstring = { path = "../nsstring" } +thiserror = "1.0" +xpcom = { path = "../xpcom" } diff --git a/xpcom/rust/moz_task/src/event_loop.rs b/xpcom/rust/moz_task/src/event_loop.rs new file mode 100644 index 0000000000..b07cb5412f --- /dev/null +++ b/xpcom/rust/moz_task/src/event_loop.rs @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use cstr::cstr; +use nserror::{nsresult, NS_ERROR_SERVICE_NOT_AVAILABLE, NS_OK}; +use std::cell::UnsafeCell; +use xpcom::{interfaces::nsIThreadManager, xpcom, xpcom_method}; + +type IsDoneClosure = dyn FnMut() -> bool + 'static; + +#[derive(xpcom)] +#[xpimplements(nsINestedEventLoopCondition)] +#[refcnt = "atomic"] +struct InitEventLoopCondition { + closure: UnsafeCell<Box<IsDoneClosure>>, +} + +impl EventLoopCondition { + xpcom_method!(is_done => IsDone() -> bool); + fn is_done(&self) -> Result<bool, nsresult> { + unsafe { Ok((&mut *self.closure.get())()) } + } +} + +/// Spin the event loop on the current thread until `pred` returns true. +/// +/// # Safety +/// +/// Spinning a nested event loop should always be avoided when possible, as it +/// can cause hangs, break JS run-to-completion guarantees, and break other C++ +/// code currently on the stack relying on heap invariants. While in a pure-rust +/// codebase this method would only be ill-advised and not technically "unsafe", +/// it is marked as unsafe due to the potential for triggering unsafety in +/// unrelated C++ code. +pub unsafe fn spin_event_loop_until<P>(pred: P) -> Result<(), nsresult> +where + P: FnMut() -> bool + 'static, +{ + let closure = Box::new(pred) as Box<IsDoneClosure>; + let cond = EventLoopCondition::allocate(InitEventLoopCondition { + closure: UnsafeCell::new(closure), + }); + let thread_manager = + xpcom::get_service::<nsIThreadManager>(cstr!("@mozilla.org/thread-manager;1")) + .ok_or(NS_ERROR_SERVICE_NOT_AVAILABLE)?; + + thread_manager.SpinEventLoopUntil(cond.coerce()).to_result() +} diff --git a/xpcom/rust/moz_task/src/executor/future_task.rs b/xpcom/rust/moz_task/src/executor/future_task.rs new file mode 100644 index 0000000000..53f00e8d68 --- /dev/null +++ b/xpcom/rust/moz_task/src/executor/future_task.rs @@ -0,0 +1,230 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::dispatch; +use futures_task::{waker, ArcWake}; +use nserror::{nsresult, NS_OK}; +use std::{ + future::Future, + pin::Pin, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, Mutex, + }, + task::{Context, Poll}, +}; +use xpcom::{interfaces::nsIEventTarget, xpcom, xpcom_method, RefPtr, ThreadBoundRefPtr}; + +#[derive(xpcom)] +#[xpimplements(nsIRunnable)] +#[refcnt = "atomic"] +struct InitLocalTask<F: Future<Output = ()> + 'static> { + future: Mutex<F>, + event_target: RefPtr<nsIEventTarget>, + state: TaskState, +} + +impl<T> InitLocalTask<T> +where + T: Future<Output = ()> + 'static, +{ + fn new(future: T, event_target: RefPtr<nsIEventTarget>) -> Self { + InitLocalTask { + future: Mutex::new(future), + event_target, + state: TaskState::default(), + } + } +} + +impl<T> LocalTask<T> +where + T: Future<Output = ()> + 'static, +{ + /// Runs a closure from the context of the task. + /// + /// Any wake notifications resulting from the execution of the closure are + /// tracked. + fn enter<F, R>(&self, f: F) -> R + where + F: FnOnce(&mut Context<'_>) -> R, + { + let task = ThreadBoundRefPtr::new(RefPtr::new(self)); + let wake_handle = Arc::new(LocalWakeHandle { task }); + let waker = waker(wake_handle); + let mut cx = Context::from_waker(&waker); + + f(&mut cx) + } + + xpcom_method!(run => Run()); + fn run(&self) -> Result<(), nsresult> { + // # Safety + // + // Mutex ensures that future is polled serially. + self.enter(|cx| { + // The only way to have this `LocalTask` dispatched to the named + // event target is for it to be dispatched by the Waker, which will + // put the state into POLL before dispatching the runnable. + assert!(self.state.is(POLL)); + loop { + // # Safety + // + // LocalTask is a heap allocation due to being an XPCOM object, + // so `fut` is effectively `Box`ed. + // + // Also the value is never moved value out of its owning `Mutex`. + let mut lock = self.future.lock().expect("Failed to lock future"); + let fut = unsafe { Pin::new_unchecked(&mut *lock) }; + let res = fut.poll(cx); + match res { + Poll::Pending => {} + Poll::Ready(()) => return unsafe { self.state.complete() }, + } + if unsafe { !self.state.wait() } { + break; + } + } + }); + Ok(()) + } + + fn wake_up(&self) { + if self.state.wake_up() { + unsafe { dispatch(self.coerce(), &self.event_target) }.unwrap() + } + } +} + +// Task State Machine - This was heavily cribbed from futures-executor::ThreadPool +struct TaskState { + state: AtomicUsize, +} + +// There are four possible task states, listed below with their possible +// transitions: + +// The task is blocked, waiting on an event +const IDLE: usize = 0; // --> POLL + +// The task is actively being polled by a thread; arrival of additional events +// of interest should move it to the REPOLL state +const POLL: usize = 1; // --> IDLE, REPOLL, or COMPLETE + +// The task is actively being polled, but will need to be re-polled upon +// completion to ensure that all events were observed. +const REPOLL: usize = 2; // --> POLL + +// The task has finished executing (either successfully or with an error/panic) +const COMPLETE: usize = 3; // No transitions out + +impl Default for TaskState { + fn default() -> Self { + Self { + state: AtomicUsize::new(IDLE), + } + } +} + +impl TaskState { + fn is(&self, state: usize) -> bool { + self.state.load(SeqCst) == state + } + + /// Attempt to "wake up" the task and poll the future. + /// + /// A `true` result indicates that the `POLL` state has been entered, and + /// the caller can proceed to poll the future. A `false` result indicates + /// that polling is not necessary (because the task is finished or the + /// polling has been delegated). + fn wake_up(&self) -> bool { + let mut state = self.state.load(SeqCst); + loop { + match state { + // The task is idle, so try to run it immediately. + IDLE => match self.state.compare_exchange(IDLE, POLL, SeqCst, SeqCst) { + Ok(_) => { + return true; + } + Err(cur) => state = cur, + }, + + // The task is being polled, so we need to record that it should + // be *repolled* when complete. + POLL => match self.state.compare_exchange(POLL, REPOLL, SeqCst, SeqCst) { + Ok(_) => return false, + Err(cur) => state = cur, + }, + + // The task is already scheduled for polling, or is complete, so + // we've got nothing to do. + _ => return false, + } + } + } + + /// Alert the Task that polling completed with `Pending`. + /// + /// Returns true if a `REPOLL` is pending. + /// + /// # Safety + /// + /// Callable only from the `POLL`/`REPOLL` states, i.e. between + /// successful calls to `notify` and `wait`/`complete`. + unsafe fn wait(&self) -> bool { + debug_assert!(matches!(self.state.load(SeqCst), POLL | REPOLL)); + match self.state.compare_exchange(POLL, IDLE, SeqCst, SeqCst) { + // no wakeups came in while we were running + Ok(_) => false, + + // guaranteed to be in REPOLL state; just clobber the + // state and run again. + Err(state) => { + assert_eq!(state, REPOLL); + self.state.store(POLL, SeqCst); + true + } + } + } + + /// Alert the Task that it has completed execution and should not be + /// notified again. + /// + /// # Safety + /// + /// Callable only from the `POLL`/`REPOLL` states, i.e. between + /// successful calls to `wake_up` and `wait`/`complete`. + unsafe fn complete(&self) { + debug_assert!(matches!(self.state.load(SeqCst), POLL | REPOLL)); + self.state.store(COMPLETE, SeqCst); + } +} + +struct LocalWakeHandle<F: Future<Output = ()> + 'static> { + task: ThreadBoundRefPtr<LocalTask<F>>, +} + +impl<F> ArcWake for LocalWakeHandle<F> +where + F: Future<Output = ()> + 'static, +{ + fn wake_by_ref(arc_self: &Arc<Self>) { + if let Some(task) = arc_self.task.get_ref() { + task.wake_up(); + } else { + panic!("Attempting to wake task from the wrong thread!"); + } + } +} + +/// # Safety +/// +/// There is no guarantee that `current_thread` is acutally the current thread. +pub unsafe fn local_task<T>(future: T, current_thread: &nsIEventTarget) +where + T: Future<Output = ()> + 'static, +{ + let task = LocalTask::allocate(InitLocalTask::new(future, RefPtr::new(current_thread))); + task.wake_up(); +} diff --git a/xpcom/rust/moz_task/src/executor/mod.rs b/xpcom/rust/moz_task/src/executor/mod.rs new file mode 100644 index 0000000000..5e7d1ca81f --- /dev/null +++ b/xpcom/rust/moz_task/src/executor/mod.rs @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::get_current_thread; +use std::{fmt::Debug, future::Future}; +use thiserror::Error; + +mod future_task; + +pub fn spawn_current_thread<Fut>(future: Fut) -> Result<(), Shutdown> +where + Fut: Future<Output = ()> + 'static, +{ + let current_thread = get_current_thread().map_err(|_| Shutdown { _priv: () })?; + // # Safety + // + // It's safe to use `local_task` since `future` is dispatched `current_thread`. + unsafe { + future_task::local_task(future, current_thread.coerce()); + } + Ok(()) +} + +/// An error that occurred during spawning on a shutdown event queue +#[derive(Error, Debug)] +#[error("Event target is shutdown")] +pub struct Shutdown { + _priv: (), +} diff --git a/xpcom/rust/moz_task/src/lib.rs b/xpcom/rust/moz_task/src/lib.rs new file mode 100644 index 0000000000..d6db10ef38 --- /dev/null +++ b/xpcom/rust/moz_task/src/lib.rs @@ -0,0 +1,365 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! This module wraps XPCOM threading functions with Rust functions +//! to make it safer and more convenient to call the XPCOM functions. +//! It also provides the Task trait and TaskRunnable struct, +//! which make it easier to dispatch tasks to threads. + +extern crate cstr; +extern crate futures_task; +extern crate libc; +extern crate nserror; +extern crate nsstring; +extern crate thiserror; +extern crate xpcom; + +mod event_loop; +mod executor; +pub use executor::spawn_current_thread; + +// Expose functions intended to be used only in gtest via this module. +// We don't use a feature gate here to stop the need to compile all crates that +// depend upon `moz_task` twice. +pub mod gtest_only { + pub use event_loop::spin_event_loop_until; +} + +use nserror::{nsresult, NS_OK}; +use nsstring::{nsACString, nsCString}; +use std::{ + ffi::CStr, + marker::PhantomData, + mem, ptr, + sync::atomic::{AtomicBool, Ordering}, +}; +use xpcom::{ + getter_addrefs, + interfaces::{nsIEventTarget, nsIRunnable, nsISerialEventTarget, nsISupports, nsIThread}, + xpcom, xpcom_method, AtomicRefcnt, RefCounted, RefPtr, XpCom, +}; + +extern "C" { + fn NS_GetCurrentThreadEventTarget(result: *mut *const nsIThread) -> nsresult; + fn NS_GetMainThreadEventTarget(result: *mut *const nsIThread) -> nsresult; + fn NS_IsMainThread() -> bool; + fn NS_NewNamedThreadWithDefaultStackSize( + name: *const nsACString, + result: *mut *const nsIThread, + event: *const nsIRunnable, + ) -> nsresult; + fn NS_IsCurrentThread(thread: *const nsIEventTarget) -> bool; + fn NS_ProxyReleaseISupports( + name: *const libc::c_char, + target: *const nsIEventTarget, + doomed: *const nsISupports, + always_proxy: bool, + ); + fn NS_CreateBackgroundTaskQueue( + name: *const libc::c_char, + target: *mut *const nsISerialEventTarget, + ) -> nsresult; + fn NS_DispatchBackgroundTask(event: *const nsIRunnable, flags: u32) -> nsresult; +} + +pub fn get_current_thread() -> Result<RefPtr<nsIThread>, nsresult> { + getter_addrefs(|p| unsafe { NS_GetCurrentThreadEventTarget(p) }) +} + +pub fn get_main_thread() -> Result<RefPtr<nsIThread>, nsresult> { + getter_addrefs(|p| unsafe { NS_GetMainThreadEventTarget(p) }) +} + +pub fn is_main_thread() -> bool { + unsafe { NS_IsMainThread() } +} + +pub fn create_thread(name: &str) -> Result<RefPtr<nsIThread>, nsresult> { + getter_addrefs(|p| unsafe { + NS_NewNamedThreadWithDefaultStackSize(&*nsCString::from(name), p, ptr::null()) + }) +} + +pub fn is_current_thread(thread: &nsIThread) -> bool { + unsafe { NS_IsCurrentThread(thread.coerce()) } +} + +/// Creates a queue that runs tasks on the background thread pool. The tasks +/// will run in the order they're dispatched, one after the other. +pub fn create_background_task_queue( + name: &'static CStr, +) -> Result<RefPtr<nsISerialEventTarget>, nsresult> { + getter_addrefs(|p| unsafe { NS_CreateBackgroundTaskQueue(name.as_ptr(), p) }) +} + +/// Dispatches a one-shot runnable to an event target with the default options. +/// +/// # Safety +/// +/// As there is no guarantee that the runnable is actually `Send + Sync`, we +/// can't know that it's safe to dispatch an `nsIRunnable` to any +/// `nsIEventTarget`. +#[inline] +pub unsafe fn dispatch(runnable: &nsIRunnable, target: &nsIEventTarget) -> Result<(), nsresult> { + dispatch_with_options(runnable, target, DispatchOptions::default()) +} + +/// Dispatches a one-shot runnable to an event target, like a thread or a +/// task queue, with the given options. +/// +/// This function leaks the runnable if dispatch fails. +/// +/// # Safety +/// +/// As there is no guarantee that the runnable is actually `Send + Sync`, we +/// can't know that it's safe to dispatch an `nsIRunnable` to any +/// `nsIEventTarget`. +pub unsafe fn dispatch_with_options( + runnable: &nsIRunnable, + target: &nsIEventTarget, + options: DispatchOptions, +) -> Result<(), nsresult> { + // NOTE: DispatchFromScript performs an AddRef on `runnable` which is + // why this function leaks on failure. + target + .DispatchFromScript(runnable, options.flags()) + .to_result() +} + +/// Dispatches a one-shot task runnable to the background thread pool with the +/// default options. +#[inline] +pub fn dispatch_background_task(runnable: RefPtr<nsIRunnable>) -> Result<(), nsresult> { + dispatch_background_task_with_options(runnable, DispatchOptions::default()) +} + +/// Dispatches a one-shot task runnable to the background thread pool with the +/// given options. The task may run concurrently with other background tasks. +/// If you need tasks to run in a specific order, please create a background +/// task queue using `create_background_task_queue`, and dispatch tasks to it +/// instead. +/// +/// ### Safety +/// +/// This function leaks the runnable if dispatch fails. This avoids a race where +/// a runnable can be destroyed on either the original or target thread, which +/// is important if the runnable holds thread-unsafe members. +pub fn dispatch_background_task_with_options( + runnable: RefPtr<nsIRunnable>, + options: DispatchOptions, +) -> Result<(), nsresult> { + // This eventually calls the non-`already_AddRefed<nsIRunnable>` overload of + // `nsIEventTarget::Dispatch` (see xpcom/threads/nsIEventTarget.idl#20-25), + // which adds an owning reference and leaks if dispatch fails. + unsafe { NS_DispatchBackgroundTask(runnable.coerce(), options.flags()) }.to_result() +} + +/// Options to control how task runnables are dispatched. +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] +pub struct DispatchOptions(u32); + +impl Default for DispatchOptions { + #[inline] + fn default() -> Self { + DispatchOptions(nsIEventTarget::DISPATCH_NORMAL as u32) + } +} + +impl DispatchOptions { + /// Creates a blank set of options. The runnable will be dispatched using + /// the default mode. + #[inline] + pub fn new() -> Self { + DispatchOptions::default() + } + + /// Indicates whether or not the dispatched runnable may block its target + /// thread by waiting on I/O. If `true`, the runnable may be dispatched to a + /// dedicated thread pool, leaving the main pool free for CPU-bound tasks. + #[inline] + pub fn may_block(self, may_block: bool) -> DispatchOptions { + const FLAG: u32 = nsIEventTarget::DISPATCH_EVENT_MAY_BLOCK as u32; + if may_block { + DispatchOptions(self.flags() | FLAG) + } else { + DispatchOptions(self.flags() & !FLAG) + } + } + + /// Returns the set of bitflags to pass to `DispatchFromScript`. + #[inline] + fn flags(self) -> u32 { + self.0 + } +} + +/// A task represents an operation that asynchronously executes on a target +/// thread, and returns its result to the original thread. +pub trait Task { + fn run(&self); + fn done(&self) -> Result<(), nsresult>; +} + +/// The struct responsible for dispatching a Task by calling its run() method +/// on the target thread and returning its result by calling its done() method +/// on the original thread. +/// +/// The struct uses its has_run field to determine whether it should call +/// run() or done(). It could instead check if task.result is Some or None, +/// but if run() failed to set task.result, then it would loop infinitely. +#[derive(xpcom)] +#[xpimplements(nsIRunnable, nsINamed)] +#[refcnt = "atomic"] +pub struct InitTaskRunnable { + name: &'static str, + original_thread: RefPtr<nsIThread>, + task: Box<dyn Task + Send + Sync>, + has_run: AtomicBool, +} + +impl TaskRunnable { + pub fn new( + name: &'static str, + task: Box<dyn Task + Send + Sync>, + ) -> Result<RefPtr<TaskRunnable>, nsresult> { + Ok(TaskRunnable::allocate(InitTaskRunnable { + name, + original_thread: get_current_thread()?, + task, + has_run: AtomicBool::new(false), + })) + } + + /// Dispatches this task runnable to an event target with the default + /// options. + #[inline] + pub fn dispatch(this: RefPtr<Self>, target: &nsIEventTarget) -> Result<(), nsresult> { + Self::dispatch_with_options(this, target, DispatchOptions::default()) + } + + /// Dispatches this task runnable to an event target, like a thread or a + /// task queue, with the given options. + /// + /// Note that this is an associated function, not a method, because it takes + /// an owned reference to the runnable, and must be called like + /// `TaskRunnable::dispatch_with_options(runnable, options)` and *not* + /// `runnable.dispatch_with_options(options)`. + /// + /// This function leaks the runnable if dispatch fails. + pub fn dispatch_with_options( + this: RefPtr<Self>, + target: &nsIEventTarget, + options: DispatchOptions, + ) -> Result<(), nsresult> { + unsafe { target.DispatchFromScript(this.coerce(), options.flags()) }.to_result() + } + + xpcom_method!(run => Run()); + fn run(&self) -> Result<(), nsresult> { + match self + .has_run + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + { + Ok(_) => { + self.task.run(); + Self::dispatch(RefPtr::new(self), &self.original_thread) + } + Err(_) => { + assert!(is_current_thread(&self.original_thread)); + self.task.done() + } + } + } + + xpcom_method!(get_name => GetName() -> nsACString); + fn get_name(&self) -> Result<nsCString, nsresult> { + Ok(nsCString::from(self.name)) + } +} + +pub type ThreadPtrHandle<T> = RefPtr<ThreadPtrHolder<T>>; + +/// A Rust analog to `nsMainThreadPtrHolder` that wraps an `nsISupports` object +/// with thread-safe refcounting. The holder keeps one reference to the wrapped +/// object that's released when the holder's refcount reaches zero. +pub struct ThreadPtrHolder<T: XpCom + 'static> { + ptr: *const T, + marker: PhantomData<T>, + name: &'static CStr, + owning_thread: RefPtr<nsIThread>, + refcnt: AtomicRefcnt, +} + +unsafe impl<T: XpCom + 'static> Send for ThreadPtrHolder<T> {} +unsafe impl<T: XpCom + 'static> Sync for ThreadPtrHolder<T> {} + +unsafe impl<T: XpCom + 'static> RefCounted for ThreadPtrHolder<T> { + unsafe fn addref(&self) { + self.refcnt.inc(); + } + + unsafe fn release(&self) { + let rc = self.refcnt.dec(); + if rc == 0 { + // Once the holder's count reaches zero, release the wrapped + // object... + if !self.ptr.is_null() { + // The holder can be released on any thread. If we're on the + // owning thread, we can release the object directly. Otherwise, + // we need to post a proxy release event to release the object + // on the owning thread. + if is_current_thread(&self.owning_thread) { + (*self.ptr).release() + } else { + NS_ProxyReleaseISupports( + self.name.as_ptr(), + self.owning_thread.coerce(), + self.ptr as *const T as *const nsISupports, + false, + ); + } + } + // ...And deallocate the holder. + Box::from_raw(self as *const Self as *mut Self); + } + } +} + +impl<T: XpCom + 'static> ThreadPtrHolder<T> { + /// Creates a new owning thread pointer holder. Returns an error if the + /// thread manager has shut down. Panics if `name` isn't a valid C string. + pub fn new(name: &'static CStr, ptr: RefPtr<T>) -> Result<RefPtr<Self>, nsresult> { + let owning_thread = get_current_thread()?; + // Take ownership of the `RefPtr`. This does _not_ decrement its + // refcount, which is what we want. Once we've released all references + // to the holder, we'll release the wrapped `RefPtr`. + let raw: *const T = &*ptr; + mem::forget(ptr); + unsafe { + let boxed = Box::new(ThreadPtrHolder { + name, + ptr: raw, + marker: PhantomData, + owning_thread, + refcnt: AtomicRefcnt::new(), + }); + Ok(RefPtr::from_raw(Box::into_raw(boxed)).unwrap()) + } + } + + /// Returns the wrapped object's owning thread. + pub fn owning_thread(&self) -> &nsIThread { + &self.owning_thread + } + + /// Returns the wrapped object if called from the owning thread, or + /// `None` if called from any other thread. + pub fn get(&self) -> Option<&T> { + if is_current_thread(&self.owning_thread) && !self.ptr.is_null() { + unsafe { Some(&*self.ptr) } + } else { + None + } + } +} diff --git a/xpcom/rust/nserror/Cargo.toml b/xpcom/rust/nserror/Cargo.toml new file mode 100644 index 0000000000..5c793f9cec --- /dev/null +++ b/xpcom/rust/nserror/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "nserror" +version = "0.1.0" +authors = ["Nika Layzell <nika@thelayzells.com>"] +license = "MPL-2.0" +description = "Rust bindings to xpcom nsresult and NS_ERROR_ values" +edition = "2018" + +[dependencies] +nsstring = { path = "../nsstring" } diff --git a/xpcom/rust/nserror/src/lib.rs b/xpcom/rust/nserror/src/lib.rs new file mode 100644 index 0000000000..c6b06a6a44 --- /dev/null +++ b/xpcom/rust/nserror/src/lib.rs @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use nsstring::{nsACString, nsCString}; +use std::error::Error; +use std::fmt; + +/// The type of errors in gecko. Uses a newtype to provide additional type +/// safety in Rust and #[repr(transparent)] to ensure the same representation +/// as the C++ equivalent. +#[repr(transparent)] +#[allow(non_camel_case_types)] +#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct nsresult(pub u32); + +impl nsresult { + pub fn failed(self) -> bool { + (self.0 >> 31) != 0 + } + + pub fn succeeded(self) -> bool { + !self.failed() + } + + pub fn to_result(self) -> Result<(), nsresult> { + if self.failed() { + Err(self) + } else { + Ok(()) + } + } + + /// Get a printable name for the nsresult error code. This function returns + /// a nsCString<'static>, which implements `Display`. + pub fn error_name(self) -> nsCString { + let mut cstr = nsCString::new(); + unsafe { + Gecko_GetErrorName(self, &mut *cstr); + } + cstr + } +} + +impl fmt::Display for nsresult { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.error_name()) + } +} + +impl fmt::Debug for nsresult { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.error_name()) + } +} + +impl<T, E> From<Result<T, E>> for nsresult +where + E: Into<nsresult>, +{ + fn from(result: Result<T, E>) -> nsresult { + match result { + Ok(_) => NS_OK, + Err(e) => e.into(), + } + } +} + +impl Error for nsresult {} + +extern "C" { + fn Gecko_GetErrorName(rv: nsresult, cstr: *mut nsACString); +} + +mod error_list { + include!(concat!(env!("MOZ_TOPOBJDIR"), "/xpcom/base/error_list.rs")); +} + +pub use error_list::*; diff --git a/xpcom/rust/nsstring/Cargo.toml b/xpcom/rust/nsstring/Cargo.toml new file mode 100644 index 0000000000..c2f8e34b45 --- /dev/null +++ b/xpcom/rust/nsstring/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "nsstring" +version = "0.1.0" +authors = ["nobody@mozilla.com"] +license = "MPL-2.0" +description = "Rust bindings to xpcom string types" +edition = "2018" + +[features] +gecko_debug = [] + +[dependencies] +bitflags = "1.0" +encoding_rs = "0.8.0" diff --git a/xpcom/rust/nsstring/src/conversions.rs b/xpcom/rust/nsstring/src/conversions.rs new file mode 100644 index 0000000000..c72c195c08 --- /dev/null +++ b/xpcom/rust/nsstring/src/conversions.rs @@ -0,0 +1,751 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::{ + nsACString, nsAString, nsCStringLike, BulkWriteOk, Gecko_FallibleAssignCString, + Latin1StringLike, +}; +use encoding_rs::mem::*; +use encoding_rs::Encoding; +use std::slice; + +/// Required math stated in the docs of +/// `convert_utf16_to_utf8()`. +#[inline(always)] +fn times_three(a: usize) -> Option<usize> { + a.checked_mul(3) +} + +#[inline(always)] +fn identity(a: usize) -> Option<usize> { + Some(a) +} + +#[inline(always)] +fn plus_one(a: usize) -> Option<usize> { + a.checked_add(1) +} + +/// Typical cache line size per +/// https://stackoverflow.com/questions/14707803/line-size-of-l1-and-l2-caches +/// +/// For consistent behavior, not trying to use 128 on aarch64 +/// or other fanciness like that. +const CACHE_LINE: usize = 64; + +const CACHE_LINE_MASK: usize = CACHE_LINE - 1; + +/// Returns true if the string is both longer than a cache line +/// and the first cache line is ASCII. +#[inline(always)] +fn long_string_starts_with_ascii(buffer: &[u8]) -> bool { + // We examine data only up to the end of the cache line + // to make this check minimally disruptive. + if buffer.len() <= CACHE_LINE { + return false; + } + let bound = CACHE_LINE - ((buffer.as_ptr() as usize) & CACHE_LINE_MASK); + is_ascii(&buffer[..bound]) +} + +/// Returns true if the string is both longer than two cache lines +/// and the first two cache lines are Basic Latin. +#[inline(always)] +fn long_string_stars_with_basic_latin(buffer: &[u16]) -> bool { + // We look at two cache lines with code unit size of two. There is need + // to look at more than one cache line in the UTF-16 case, because looking + // at just one cache line wouldn't catch non-ASCII Latin with high enough + // probability with Latin-script languages that have relatively infrequent + // non-ASCII characters. + if buffer.len() <= CACHE_LINE { + return false; + } + let bound = (CACHE_LINE * 2 - ((buffer.as_ptr() as usize) & CACHE_LINE_MASK)) / 2; + is_basic_latin(&buffer[..bound]) +} + +// Ignoring the copy avoidance complications of conversions between Latin1 and +// UTF-8, a conversion function has the outward form of +// `fn F(&mut self, other: &[T], old_len: usize) -> Result<BulkWriteOk, ()>`, +// where `T` is either `u8` or `u16`. `other` is the slice whose converted +// content are to be appended to `self` and `old_len` indicates how many +// code unit of `self` are to be preserved (0 for the assignment case and +// `self.len()` for the appending case). +// +// As implementation parameters a conversion function needs to know the +// math for computing the worst case conversion length in code units given +// the input length in code units. For a _constant conversion_ the number +// of code units the conversion produces equals the number of code units +// in the input. For a _shinking conversion_ the maximum number of code +// units the conversion can produce equals the number of code units in +// the input, but the conversion can produce fewer code units. Still, due +// to implementation details, the function might want _one_ unit more of +// output space. For an _expanding conversion_ (no need for macro), the +// minimum number of code units produced by the conversion is the number +// of code units in the input, but the conversion can produce more. +// +// Copy avoidance conversions avoid copying a refcounted buffer when it's +// ASCII-only. +// +// Internally, a conversion function needs to know the underlying +// encoding_rs conversion function, the math for computing the required +// output buffer size and, depending on the case, the underlying +// encoding_rs ASCII prefix handling function. + +/// A conversion where the number of code units in the output is potentially +/// smaller than the number of code units in the input. +/// +/// Takes the name of the method to be generated, the name of the conversion +/// function and the type of the input slice. +/// +/// `$name` is the name of the function to generate +/// `$convert` is the underlying `encoding_rs::mem` function to use +/// `$other_ty` is the type of the input slice +/// `$math` is the worst-case length math that `$convert` expects +macro_rules! shrinking_conversion { + (name = $name:ident, + convert = $convert:ident, + other_ty = $other_ty:ty, + math = $math:ident) => { + fn $name(&mut self, other: $other_ty, old_len: usize) -> Result<BulkWriteOk, ()> { + let needed = $math(other.len()).ok_or(())?; + let mut handle = + unsafe { self.bulk_write(old_len.checked_add(needed).ok_or(())?, old_len, false)? }; + let written = $convert(other, &mut handle.as_mut_slice()[old_len..]); + let new_len = old_len + written; + Ok(handle.finish(new_len, new_len > CACHE_LINE)) + } + }; +} + +/// A conversion where the number of code units in the output is always equal +/// to the number of code units in the input. +/// +/// Takes the name of the method to be generated, the name of the conversion +/// function and the type of the input slice. +/// +/// `$name` is the name of the function to generate +/// `$convert` is the underlying `encoding_rs::mem` function to use +/// `$other_ty` is the type of the input slice +macro_rules! constant_conversion { + (name = $name:ident, + convert = $convert:ident, + other_ty = $other_ty:ty) => { + fn $name( + &mut self, + other: $other_ty, + old_len: usize, + allow_shrinking: bool, + ) -> Result<BulkWriteOk, ()> { + let new_len = old_len.checked_add(other.len()).ok_or(())?; + let mut handle = unsafe { self.bulk_write(new_len, old_len, allow_shrinking)? }; + $convert(other, &mut handle.as_mut_slice()[old_len..]); + Ok(handle.finish(new_len, false)) + } + }; +} + +/// An intermediate check for avoiding a copy and having an `nsStringBuffer` +/// refcount increment instead when both `self` and `other` are `nsACString`s, +/// `other` is entirely ASCII and all old data in `self` is discarded. +/// +/// `$name` is the name of the function to generate +/// `$impl` is the underlying conversion that takes a slice and that is used +/// when we can't just adopt the incoming buffer as-is +/// `$string_like` is the kind of input taken +macro_rules! ascii_copy_avoidance { + (name = $name:ident, + implementation = $implementation:ident, + string_like = $string_like:ident) => { + fn $name<T: $string_like + ?Sized>( + &mut self, + other: &T, + old_len: usize, + ) -> Result<BulkWriteOk, ()> { + let adapter = other.adapt(); + let other_slice = adapter.as_ref(); + let num_ascii = if adapter.is_abstract() && old_len == 0 { + let up_to = Encoding::ascii_valid_up_to(other_slice); + if up_to == other_slice.len() { + // Calling something whose argument can be obtained from + // the adapter rather than an nsStringLike avoids a huge + // lifetime mess by keeping nsStringLike and + // Latin1StringLike free of lifetime interdependencies. + if unsafe { Gecko_FallibleAssignCString(self, other.adapt().as_ptr()) } { + return Ok(BulkWriteOk {}); + } else { + return Err(()); + } + } + Some(up_to) + } else { + None + }; + self.$implementation(other_slice, old_len, num_ascii) + } + }; +} + +impl nsAString { + // Valid UTF-8 to UTF-16 + + // Documentation says the destination buffer needs to have + // as many code units as the input. + shrinking_conversion!( + name = fallible_append_str_impl, + convert = convert_str_to_utf16, + other_ty = &str, + math = identity + ); + + /// Convert a valid UTF-8 string into valid UTF-16 and replace the content + /// of this string with the conversion result. + pub fn assign_str(&mut self, other: &str) { + self.fallible_append_str_impl(other, 0) + .expect("Out of memory"); + } + + /// Convert a valid UTF-8 string into valid UTF-16 and fallibly replace the + /// content of this string with the conversion result. + pub fn fallible_assign_str(&mut self, other: &str) -> Result<(), ()> { + self.fallible_append_str_impl(other, 0).map(|_| ()) + } + + /// Convert a valid UTF-8 string into valid UTF-16 and append the conversion + /// to this string. + pub fn append_str(&mut self, other: &str) { + let len = self.len(); + self.fallible_append_str_impl(other, len) + .expect("Out of memory"); + } + + /// Convert a valid UTF-8 string into valid UTF-16 and fallibly append the + /// conversion to this string. + pub fn fallible_append_str(&mut self, other: &str) -> Result<(), ()> { + let len = self.len(); + self.fallible_append_str_impl(other, len).map(|_| ()) + } + + // Potentially-invalid UTF-8 to UTF-16 + + // Documentation says the destination buffer needs to have + // one more code unit than the input. + shrinking_conversion!( + name = fallible_append_utf8_impl, + convert = convert_utf8_to_utf16, + other_ty = &[u8], + math = plus_one + ); + + /// Convert a potentially-invalid UTF-8 string into valid UTF-16 + /// (replacing invalid sequences with the REPLACEMENT CHARACTER) and + /// replace the content of this string with the conversion result. + pub fn assign_utf8(&mut self, other: &[u8]) { + self.fallible_append_utf8_impl(other, 0) + .expect("Out of memory"); + } + + /// Convert a potentially-invalid UTF-8 string into valid UTF-16 + /// (replacing invalid sequences with the REPLACEMENT CHARACTER) and + /// fallibly replace the content of this string with the conversion result. + pub fn fallible_assign_utf8(&mut self, other: &[u8]) -> Result<(), ()> { + self.fallible_append_utf8_impl(other, 0).map(|_| ()) + } + + /// Convert a potentially-invalid UTF-8 string into valid UTF-16 + /// (replacing invalid sequences with the REPLACEMENT CHARACTER) and + /// append the conversion result to this string. + pub fn append_utf8(&mut self, other: &[u8]) { + let len = self.len(); + self.fallible_append_utf8_impl(other, len) + .expect("Out of memory"); + } + + /// Convert a potentially-invalid UTF-8 string into valid UTF-16 + /// (replacing invalid sequences with the REPLACEMENT CHARACTER) and + /// fallibly append the conversion result to this string. + pub fn fallible_append_utf8(&mut self, other: &[u8]) -> Result<(), ()> { + let len = self.len(); + self.fallible_append_utf8_impl(other, len).map(|_| ()) + } + + // Latin1 to UTF-16 + + constant_conversion!( + name = fallible_append_latin1_impl, + convert = convert_latin1_to_utf16, + other_ty = &[u8] + ); + + /// Convert a Latin1 (i.e. byte value equals scalar value; not windows-1252!) + /// into UTF-16 and replace the content of this string with the conversion result. + pub fn assign_latin1(&mut self, other: &[u8]) { + self.fallible_append_latin1_impl(other, 0, true) + .expect("Out of memory"); + } + + /// Convert a Latin1 (i.e. byte value equals scalar value; not windows-1252!) + /// into UTF-16 and fallibly replace the content of this string with the + /// conversion result. + pub fn fallible_assign_latin1(&mut self, other: &[u8]) -> Result<(), ()> { + self.fallible_append_latin1_impl(other, 0, true).map(|_| ()) + } + + /// Convert a Latin1 (i.e. byte value equals scalar value; not windows-1252!) + /// into UTF-16 and append the conversion result to this string. + pub fn append_latin1(&mut self, other: &[u8]) { + let len = self.len(); + self.fallible_append_latin1_impl(other, len, false) + .expect("Out of memory"); + } + + /// Convert a Latin1 (i.e. byte value equals scalar value; not windows-1252!) + /// into UTF-16 and fallibly append the conversion result to this string. + pub fn fallible_append_latin1(&mut self, other: &[u8]) -> Result<(), ()> { + let len = self.len(); + self.fallible_append_latin1_impl(other, len, false) + .map(|_| ()) + } +} + +impl nsACString { + // UTF-16 to UTF-8 + + fn fallible_append_utf16_to_utf8_impl( + &mut self, + other: &[u16], + old_len: usize, + ) -> Result<BulkWriteOk, ()> { + // We first size the buffer for ASCII if the first two cache lines are ASCII. If that turns out + // not to be enough, we size for the worst case given the length of the remaining input at that + // point. BUT if the worst case fits inside the inline capacity of an autostring, we skip + // the ASCII stuff. + let worst_case_needed = if let Some(inline_capacity) = self.inline_capacity() { + let worst_case = times_three(other.len()).ok_or(())?; + if worst_case <= inline_capacity { + Some(worst_case) + } else { + None + } + } else { + None + }; + let (filled, read, mut handle) = + if worst_case_needed.is_none() && long_string_stars_with_basic_latin(other) { + let new_len_with_ascii = old_len.checked_add(other.len()).ok_or(())?; + let mut handle = unsafe { self.bulk_write(new_len_with_ascii, old_len, false)? }; + let (read, written) = + convert_utf16_to_utf8_partial(other, &mut handle.as_mut_slice()[old_len..]); + let left = other.len() - read; + if left == 0 { + return Ok(handle.finish(old_len + written, true)); + } + let filled = old_len + written; + let needed = times_three(left).ok_or(())?; + let new_len = filled.checked_add(needed).ok_or(())?; + unsafe { + handle.restart_bulk_write(new_len, filled, false)?; + } + (filled, read, handle) + } else { + // Started with non-ASCII. Compute worst case + let needed = if let Some(n) = worst_case_needed { + n + } else { + times_three(other.len()).ok_or(())? + }; + let new_len = old_len.checked_add(needed).ok_or(())?; + let handle = unsafe { self.bulk_write(new_len, old_len, false)? }; + (old_len, 0, handle) + }; + let written = convert_utf16_to_utf8(&other[read..], &mut handle.as_mut_slice()[filled..]); + Ok(handle.finish(filled + written, true)) + } + + /// Convert a potentially-invalid UTF-16 string into valid UTF-8 + /// (replacing invalid sequences with the REPLACEMENT CHARACTER) and + /// replace the content of this string with the conversion result. + pub fn assign_utf16_to_utf8(&mut self, other: &[u16]) { + self.fallible_append_utf16_to_utf8_impl(other, 0) + .expect("Out of memory"); + } + + /// Convert a potentially-invalid UTF-16 string into valid UTF-8 + /// (replacing invalid sequences with the REPLACEMENT CHARACTER) and + /// fallibly replace the content of this string with the conversion result. + pub fn fallible_assign_utf16_to_utf8(&mut self, other: &[u16]) -> Result<(), ()> { + self.fallible_append_utf16_to_utf8_impl(other, 0) + .map(|_| ()) + } + + /// Convert a potentially-invalid UTF-16 string into valid UTF-8 + /// (replacing invalid sequences with the REPLACEMENT CHARACTER) and + /// append the conversion result to this string. + pub fn append_utf16_to_utf8(&mut self, other: &[u16]) { + let len = self.len(); + self.fallible_append_utf16_to_utf8_impl(other, len) + .expect("Out of memory"); + } + + /// Convert a potentially-invalid UTF-16 string into valid UTF-8 + /// (replacing invalid sequences with the REPLACEMENT CHARACTER) and + /// fallibly append the conversion result to this string. + pub fn fallible_append_utf16_to_utf8(&mut self, other: &[u16]) -> Result<(), ()> { + let len = self.len(); + self.fallible_append_utf16_to_utf8_impl(other, len) + .map(|_| ()) + } + + // UTF-16 to Latin1 + + constant_conversion!( + name = fallible_append_utf16_to_latin1_lossy_impl, + convert = convert_utf16_to_latin1_lossy, + other_ty = &[u16] + ); + + /// Convert a UTF-16 string whose all code points are below U+0100 into + /// a Latin1 (scalar value is byte value; not windows-1252!) string and + /// replace the content of this string with the conversion result. + /// + /// # Panics + /// + /// If the input contains code points above U+00FF or is not valid UTF-16, + /// panics in debug mode and produces garbage in a memory-safe way in + /// release builds. The nature of the garbage may differ based on CPU + /// architecture and must not be relied upon. + pub fn assign_utf16_to_latin1_lossy(&mut self, other: &[u16]) { + self.fallible_append_utf16_to_latin1_lossy_impl(other, 0, true) + .expect("Out of memory"); + } + + /// Convert a UTF-16 string whose all code points are below U+0100 into + /// a Latin1 (scalar value is byte value; not windows-1252!) string and + /// fallibly replace the content of this string with the conversion result. + /// + /// # Panics + /// + /// If the input contains code points above U+00FF or is not valid UTF-16, + /// panics in debug mode and produces garbage in a memory-safe way in + /// release builds. The nature of the garbage may differ based on CPU + /// architecture and must not be relied upon. + pub fn fallible_assign_utf16_to_latin1_lossy(&mut self, other: &[u16]) -> Result<(), ()> { + self.fallible_append_utf16_to_latin1_lossy_impl(other, 0, true) + .map(|_| ()) + } + + /// Convert a UTF-16 string whose all code points are below U+0100 into + /// a Latin1 (scalar value is byte value; not windows-1252!) string and + /// append the conversion result to this string. + /// + /// # Panics + /// + /// If the input contains code points above U+00FF or is not valid UTF-16, + /// panics in debug mode and produces garbage in a memory-safe way in + /// release builds. The nature of the garbage may differ based on CPU + /// architecture and must not be relied upon. + pub fn append_utf16_to_latin1_lossy(&mut self, other: &[u16]) { + let len = self.len(); + self.fallible_append_utf16_to_latin1_lossy_impl(other, len, false) + .expect("Out of memory"); + } + + /// Convert a UTF-16 string whose all code points are below U+0100 into + /// a Latin1 (scalar value is byte value; not windows-1252!) string and + /// fallibly append the conversion result to this string. + /// + /// # Panics + /// + /// If the input contains code points above U+00FF or is not valid UTF-16, + /// panics in debug mode and produces garbage in a memory-safe way in + /// release builds. The nature of the garbage may differ based on CPU + /// architecture and must not be relied upon. + pub fn fallible_append_utf16_to_latin1_lossy(&mut self, other: &[u16]) -> Result<(), ()> { + let len = self.len(); + self.fallible_append_utf16_to_latin1_lossy_impl(other, len, false) + .map(|_| ()) + } + + // UTF-8 to Latin1 + + ascii_copy_avoidance!( + name = fallible_append_utf8_to_latin1_lossy_check, + implementation = fallible_append_utf8_to_latin1_lossy_impl, + string_like = nsCStringLike + ); + + fn fallible_append_utf8_to_latin1_lossy_impl( + &mut self, + other: &[u8], + old_len: usize, + maybe_num_ascii: Option<usize>, + ) -> Result<BulkWriteOk, ()> { + let new_len = old_len.checked_add(other.len()).ok_or(())?; + let num_ascii = maybe_num_ascii.unwrap_or(0); + // Already checked for overflow above, so this can't overflow. + let old_len_plus_num_ascii = old_len + num_ascii; + let mut handle = unsafe { self.bulk_write(new_len, old_len, false)? }; + let written = { + let buffer = handle.as_mut_slice(); + if num_ascii != 0 { + (&mut buffer[old_len..old_len_plus_num_ascii]).copy_from_slice(&other[..num_ascii]); + } + convert_utf8_to_latin1_lossy(&other[num_ascii..], &mut buffer[old_len_plus_num_ascii..]) + }; + Ok(handle.finish(old_len_plus_num_ascii + written, true)) + } + + /// Convert a UTF-8 string whose all code points are below U+0100 into + /// a Latin1 (scalar value is byte value; not windows-1252!) string and + /// replace the content of this string with the conversion result. + /// + /// # Panics + /// + /// If the input contains code points above U+00FF or is not valid UTF-8, + /// panics in debug mode and produces garbage in a memory-safe way in + /// release builds. The nature of the garbage may differ based on CPU + /// architecture and must not be relied upon. + pub fn assign_utf8_to_latin1_lossy<T: nsCStringLike + ?Sized>(&mut self, other: &T) { + self.fallible_append_utf8_to_latin1_lossy_check(other, 0) + .expect("Out of memory"); + } + + /// Convert a UTF-8 string whose all code points are below U+0100 into + /// a Latin1 (scalar value is byte value; not windows-1252!) string and + /// fallibly replace the content of this string with the conversion result. + /// + /// # Panics + /// + /// If the input contains code points above U+00FF or is not valid UTF-8, + /// panics in debug mode and produces garbage in a memory-safe way in + /// release builds. The nature of the garbage may differ based on CPU + /// architecture and must not be relied upon. + pub fn fallible_assign_utf8_to_latin1_lossy<T: nsCStringLike + ?Sized>( + &mut self, + other: &T, + ) -> Result<(), ()> { + self.fallible_append_utf8_to_latin1_lossy_check(other, 0) + .map(|_| ()) + } + + /// Convert a UTF-8 string whose all code points are below U+0100 into + /// a Latin1 (scalar value is byte value; not windows-1252!) string and + /// append the conversion result to this string. + /// + /// # Panics + /// + /// If the input contains code points above U+00FF or is not valid UTF-8, + /// panics in debug mode and produces garbage in a memory-safe way in + /// release builds. The nature of the garbage may differ based on CPU + /// architecture and must not be relied upon. + pub fn append_utf8_to_latin1_lossy<T: nsCStringLike + ?Sized>(&mut self, other: &T) { + let len = self.len(); + self.fallible_append_utf8_to_latin1_lossy_check(other, len) + .expect("Out of memory"); + } + + /// Convert a UTF-8 string whose all code points are below U+0100 into + /// a Latin1 (scalar value is byte value; not windows-1252!) string and + /// fallibly append the conversion result to this string. + /// + /// # Panics + /// + /// If the input contains code points above U+00FF or is not valid UTF-8, + /// panics in debug mode and produces garbage in a memory-safe way in + /// release builds. The nature of the garbage may differ based on CPU + /// architecture and must not be relied upon. + pub fn fallible_append_utf8_to_latin1_lossy<T: nsCStringLike + ?Sized>( + &mut self, + other: &T, + ) -> Result<(), ()> { + let len = self.len(); + self.fallible_append_utf8_to_latin1_lossy_check(other, len) + .map(|_| ()) + } + + // Latin1 to UTF-8 CString + + ascii_copy_avoidance!( + name = fallible_append_latin1_to_utf8_check, + implementation = fallible_append_latin1_to_utf8_impl, + string_like = Latin1StringLike + ); + + fn fallible_append_latin1_to_utf8_impl( + &mut self, + other: &[u8], + old_len: usize, + maybe_num_ascii: Option<usize>, + ) -> Result<BulkWriteOk, ()> { + let (filled, read, mut handle) = if let Some(num_ascii) = maybe_num_ascii { + // Wrapper checked for ASCII + let left = other.len() - num_ascii; + let filled = old_len + num_ascii; + let needed = left.checked_mul(2).ok_or(())?; + let new_len = filled.checked_add(needed).ok_or(())?; + let mut handle = unsafe { self.bulk_write(new_len, old_len, false)? }; + if num_ascii != 0 { + (&mut handle.as_mut_slice()[old_len..filled]).copy_from_slice(&other[..num_ascii]); + } + (filled, num_ascii, handle) + } else { + let worst_case_needed = if let Some(inline_capacity) = self.inline_capacity() { + let worst_case = other.len().checked_mul(2).ok_or(())?; + if worst_case <= inline_capacity { + Some(worst_case) + } else { + None + } + } else { + None + }; + if worst_case_needed.is_none() && long_string_starts_with_ascii(other) { + // Wrapper didn't check for ASCII, so let's see if `other` starts with ASCII + // `other` starts with ASCII, so let's first size the buffer + // with optimism that it's ASCII-only. + let new_len_with_ascii = old_len.checked_add(other.len()).ok_or(())?; + let mut handle = unsafe { self.bulk_write(new_len_with_ascii, old_len, false)? }; + let (read, written) = + convert_latin1_to_utf8_partial(other, &mut handle.as_mut_slice()[old_len..]); + let left = other.len() - read; + let filled = old_len + written; + if left == 0 { + // `other` fit in the initial allocation + return Ok(handle.finish(filled, true)); + } + let needed = left.checked_mul(2).ok_or(())?; + let new_len = filled.checked_add(needed).ok_or(())?; + unsafe { + handle.restart_bulk_write(new_len, filled, false)?; + } + (filled, read, handle) + } else { + // Started with non-ASCII. Assume worst case. + let needed = if let Some(n) = worst_case_needed { + n + } else { + other.len().checked_mul(2).ok_or(())? + }; + let new_len = old_len.checked_add(needed).ok_or(())?; + let handle = unsafe { self.bulk_write(new_len, old_len, false)? }; + (old_len, 0, handle) + } + }; + let written = convert_latin1_to_utf8(&other[read..], &mut handle.as_mut_slice()[filled..]); + Ok(handle.finish(filled + written, true)) + } + + /// Convert a Latin1 (i.e. byte value equals scalar value; not windows-1252!) + /// into UTF-8 and replace the content of this string with the conversion result. + pub fn assign_latin1_to_utf8<T: Latin1StringLike + ?Sized>(&mut self, other: &T) { + self.fallible_append_latin1_to_utf8_check(other, 0) + .expect("Out of memory"); + } + + /// Convert a Latin1 (i.e. byte value equals scalar value; not windows-1252!) + /// into UTF-8 and fallibly replace the content of this string with the + /// conversion result. + pub fn fallible_assign_latin1_to_utf8<T: Latin1StringLike + ?Sized>( + &mut self, + other: &T, + ) -> Result<(), ()> { + self.fallible_append_latin1_to_utf8_check(other, 0) + .map(|_| ()) + } + + /// Convert a Latin1 (i.e. byte value equals scalar value; not windows-1252!) + /// into UTF-8 and append the conversion result to this string. + pub fn append_latin1_to_utf8<T: Latin1StringLike + ?Sized>(&mut self, other: &T) { + let len = self.len(); + self.fallible_append_latin1_to_utf8_check(other, len) + .expect("Out of memory"); + } + + /// Convert a Latin1 (i.e. byte value equals scalar value; not windows-1252!) + /// into UTF-8 and fallibly append the conversion result to this string. + pub fn fallible_append_latin1_to_utf8<T: Latin1StringLike + ?Sized>( + &mut self, + other: &T, + ) -> Result<(), ()> { + let len = self.len(); + self.fallible_append_latin1_to_utf8_check(other, len) + .map(|_| ()) + } +} + +#[no_mangle] +pub unsafe extern "C" fn nsstring_fallible_append_utf8_impl( + this: *mut nsAString, + other: *const u8, + other_len: usize, + old_len: usize, +) -> bool { + let other_slice = slice::from_raw_parts(other, other_len); + (*this) + .fallible_append_utf8_impl(other_slice, old_len) + .is_ok() +} + +#[no_mangle] +pub unsafe extern "C" fn nsstring_fallible_append_latin1_impl( + this: *mut nsAString, + other: *const u8, + other_len: usize, + old_len: usize, + allow_shrinking: bool, +) -> bool { + let other_slice = slice::from_raw_parts(other, other_len); + (*this) + .fallible_append_latin1_impl(other_slice, old_len, allow_shrinking) + .is_ok() +} + +#[no_mangle] +pub unsafe extern "C" fn nscstring_fallible_append_utf16_to_utf8_impl( + this: *mut nsACString, + other: *const u16, + other_len: usize, + old_len: usize, +) -> bool { + let other_slice = slice::from_raw_parts(other, other_len); + (*this) + .fallible_append_utf16_to_utf8_impl(other_slice, old_len) + .is_ok() +} + +#[no_mangle] +pub unsafe extern "C" fn nscstring_fallible_append_utf16_to_latin1_lossy_impl( + this: *mut nsACString, + other: *const u16, + other_len: usize, + old_len: usize, + allow_shrinking: bool, +) -> bool { + let other_slice = slice::from_raw_parts(other, other_len); + (*this) + .fallible_append_utf16_to_latin1_lossy_impl(other_slice, old_len, allow_shrinking) + .is_ok() +} + +#[no_mangle] +pub unsafe extern "C" fn nscstring_fallible_append_utf8_to_latin1_lossy_check( + this: *mut nsACString, + other: *const nsACString, + old_len: usize, +) -> bool { + (*this) + .fallible_append_utf8_to_latin1_lossy_check(&*other, old_len) + .is_ok() +} + +#[no_mangle] +pub unsafe extern "C" fn nscstring_fallible_append_latin1_to_utf8_check( + this: *mut nsACString, + other: *const nsACString, + old_len: usize, +) -> bool { + (*this) + .fallible_append_latin1_to_utf8_check(&*other, old_len) + .is_ok() +} diff --git a/xpcom/rust/nsstring/src/lib.rs b/xpcom/rust/nsstring/src/lib.rs new file mode 100644 index 0000000000..78eda1f3b7 --- /dev/null +++ b/xpcom/rust/nsstring/src/lib.rs @@ -0,0 +1,1543 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! This module provides rust bindings for the XPCOM string types. +//! +//! # TL;DR (what types should I use) +//! +//! Use `&{mut,} nsA[C]String` for functions in rust which wish to take or +//! mutate XPCOM strings. The other string types `Deref` to this type. +//! +//! Use `ns[C]String` (`ns[C]String` in C++) for string struct members, and as +//! an intermediate between rust string data structures (such as `String` or +//! `Vec<u16>`) and `&{mut,} nsA[C]String` (using `ns[C]String::from(value)`). +//! These conversions will attempt to re-use the passed-in buffer, appending a +//! null. +//! +//! Use `ns[C]Str` (`nsDependent[C]String` in C++) as an intermediate between +//! borrowed rust data structures (such as `&str` and `&[u16]`) and `&{mut,} +//! nsA[C]String` (using `ns[C]Str::from(value)`). These conversions should not +//! perform any allocations. This type is not safe to share with `C++` as a +//! struct field, but passing the borrowed `&{mut,} nsA[C]String` over FFI is +//! safe. +//! +//! Use `*{const,mut} nsA[C]String` (`{const,} nsA[C]String*` in C++) for +//! function arguments passed across the rust/C++ language boundary. +//! +//! There is currently no Rust equivalent to nsAuto[C]String. Implementing a +//! type that contains a pointer to an inline buffer is difficult in Rust due +//! to its move semantics, which require that it be safe to move a value by +//! copying its bits. If such a type is genuinely needed at some point, +//! https://bugzilla.mozilla.org/show_bug.cgi?id=1403506#c6 has a sketch of how +//! to emulate it via macros. +//! +//! # String Types +//! +//! ## `nsA[C]String` +//! +//! The core types in this module are `nsAString` and `nsACString`. These types +//! are zero-sized as far as rust is concerned, and are safe to pass around +//! behind both references (in rust code), and pointers (in C++ code). They +//! represent a handle to a XPCOM string which holds either `u16` or `u8` +//! characters respectively. The backing character buffer is guaranteed to live +//! as long as the reference to the `nsAString` or `nsACString`. +//! +//! These types in rust are simply used as dummy types. References to them +//! represent a pointer to the beginning of a variable-sized `#[repr(C)]` struct +//! which is common between both C++ and Rust implementations. In C++, their +//! corresponding types are also named `nsAString` or `nsACString`, and they are +//! defined within the `nsTSubstring.{cpp,h}` file. +//! +//! ### Valid Operations +//! +//! An `&nsA[C]String` acts like rust's `&str`, in that it is a borrowed +//! reference to the backing data. When used as an argument to other functions +//! on `&mut nsA[C]String`, optimizations can be performed to avoid copying +//! buffers, as information about the backing storage is preserved. +//! +//! An `&mut nsA[C]String` acts like rust's `&mut Cow<str>`, in that it is a +//! mutable reference to a potentially borrowed string, which when modified will +//! ensure that it owns its own backing storage. This type can be appended to +//! with the methods `.append`, `.append_utf{8,16}`, and with the `write!` +//! macro, and can be assigned to with `.assign`. +//! +//! ## `ns[C]Str<'a>` +//! +//! This type is an maybe-owned string type. It acts similarially to a +//! `Cow<[{u8,u16}]>`. This type provides `Deref` and `DerefMut` implementations +//! to `nsA[C]String`, which provides the methods for manipulating this type. +//! This type's lifetime parameter, `'a`, represents the lifetime of the backing +//! storage. When modified this type may re-allocate in order to ensure that it +//! does not mutate its backing storage. +//! +//! `ns[C]Str`s can be constructed either with `ns[C]Str::new()`, which creates +//! an empty `ns[C]Str<'static>`, or through one of the provided `From` +//! implementations. Only `nsCStr` can be constructed `From<'a str>`, as +//! constructing a `nsStr` would require transcoding. Use `ns[C]String` instead. +//! +//! When passing this type by reference, prefer passing a `&nsA[C]String` or +//! `&mut nsA[C]String`. to passing this type. +//! +//! When passing this type across the language boundary, pass it as `*const +//! nsA[C]String` for an immutable reference, or `*mut nsA[C]String` for a +//! mutable reference. +//! +//! ## `ns[C]String` +//! +//! This type is an owned, null-terminated string type. This type provides +//! `Deref` and `DerefMut` implementations to `nsA[C]String`, which provides the +//! methods for manipulating this type. +//! +//! `ns[C]String`s can be constructed either with `ns[C]String::new()`, which +//! creates an empty `ns[C]String`, or through one of the provided `From` +//! implementations, which will try to avoid reallocating when possible, +//! although a terminating `null` will be added. +//! +//! When passing this type by reference, prefer passing a `&nsA[C]String` or +//! `&mut nsA[C]String`. to passing this type. +//! +//! When passing this type across the language boundary, pass it as `*const +//! nsA[C]String` for an immutable reference, or `*mut nsA[C]String` for a +//! mutable reference. This struct may also be included in `#[repr(C)]` structs +//! shared with C++. +//! +//! ## `ns[C]StringRepr` +//! +//! This crate also provides the type `ns[C]StringRepr` which acts conceptually +//! similar to an `ns[C]String`, however, it does not have a `Drop` +//! implementation. +//! +//! If this type is dropped in rust, it will not free its backing storage. This +//! can be useful when implementing FFI types which contain `ns[C]String` members +//! which invoke their member's destructors through C++ code. + +#![allow(non_camel_case_types)] +#![allow(clippy::missing_safety_doc)] +#![allow(clippy::new_without_default)] +#![allow(clippy::result_unit_err)] + +use bitflags::bitflags; +use std::borrow; +use std::cmp; +use std::fmt; +use std::marker::PhantomData; +use std::mem; +use std::ops::{Deref, DerefMut}; +use std::os::raw::c_void; +use std::ptr; +use std::slice; +use std::str; + +mod conversions; + +pub use self::conversions::nscstring_fallible_append_latin1_to_utf8_check; +pub use self::conversions::nscstring_fallible_append_utf16_to_latin1_lossy_impl; +pub use self::conversions::nscstring_fallible_append_utf16_to_utf8_impl; +pub use self::conversions::nscstring_fallible_append_utf8_to_latin1_lossy_check; +pub use self::conversions::nsstring_fallible_append_latin1_impl; +pub use self::conversions::nsstring_fallible_append_utf8_impl; + +/// A type for showing that `finish()` was called on a `BulkWriteHandle`. +/// Instantiating this type from elsewhere is basically an assertion that +/// there is no `BulkWriteHandle` around, so be very careful with instantiating +/// this type! +pub struct BulkWriteOk; + +/// Semi-arbitrary threshold below which we don't care about shrinking +/// buffers to size. Currently matches `CACHE_LINE` in the `conversions` +/// module. +const SHRINKING_THRESHOLD: usize = 64; + +/////////////////////////////////// +// Internal Implementation Flags // +/////////////////////////////////// + +bitflags! { + // While this has the same layout as u16, it cannot be passed + // over FFI safely as a u16. + #[repr(C)] + struct DataFlags: u16 { + const TERMINATED = 1 << 0; // IsTerminated returns true + const VOIDED = 1 << 1; // IsVoid returns true + const REFCOUNTED = 1 << 2; // mData points to a heap-allocated, shareable, refcounted + // buffer + const OWNED = 1 << 3; // mData points to a heap-allocated, raw buffer + const INLINE = 1 << 4; // mData points to a writable, inline buffer + const LITERAL = 1 << 5; // mData points to a string literal; TERMINATED will also be set + } +} + +bitflags! { + // While this has the same layout as u16, it cannot be passed + // over FFI safely as a u16. + #[repr(C)] + struct ClassFlags: u16 { + const INLINE = 1 << 0; // |this|'s buffer is inline + const NULL_TERMINATED = 1 << 1; // |this| requires its buffer is null-terminated + } +} + +//////////////////////////////////// +// Generic String Bindings Macros // +//////////////////////////////////// + +macro_rules! string_like { + { + char_t = $char_t: ty; + + AString = $AString: ident; + String = $String: ident; + Str = $Str: ident; + + StringLike = $StringLike: ident; + StringAdapter = $StringAdapter: ident; + } => { + /// This trait is implemented on types which are `ns[C]String`-like, in + /// that they can at very low cost be converted to a borrowed + /// `&nsA[C]String`. Unfortunately, the intermediate type + /// `ns[C]StringAdapter` is required as well due to types like `&[u8]` + /// needing to be (cheaply) wrapped in a `nsCString` on the stack to + /// create the `&nsACString`. + /// + /// This trait is used to DWIM when calling the methods on + /// `nsA[C]String`. + pub trait $StringLike { + fn adapt(&self) -> $StringAdapter; + } + + impl<'a, T: $StringLike + ?Sized> $StringLike for &'a T { + fn adapt(&self) -> $StringAdapter { + <T as $StringLike>::adapt(*self) + } + } + + impl<'a, T> $StringLike for borrow::Cow<'a, T> + where T: $StringLike + borrow::ToOwned + ?Sized { + fn adapt(&self) -> $StringAdapter { + <T as $StringLike>::adapt(self.as_ref()) + } + } + + impl $StringLike for $AString { + fn adapt(&self) -> $StringAdapter { + $StringAdapter::Abstract(self) + } + } + + impl<'a> $StringLike for $Str<'a> { + fn adapt(&self) -> $StringAdapter { + $StringAdapter::Abstract(self) + } + } + + impl $StringLike for $String { + fn adapt(&self) -> $StringAdapter { + $StringAdapter::Abstract(self) + } + } + + impl $StringLike for [$char_t] { + fn adapt(&self) -> $StringAdapter { + $StringAdapter::Borrowed($Str::from(self)) + } + } + + impl $StringLike for Vec<$char_t> { + fn adapt(&self) -> $StringAdapter { + $StringAdapter::Borrowed($Str::from(&self[..])) + } + } + + impl $StringLike for Box<[$char_t]> { + fn adapt(&self) -> $StringAdapter { + $StringAdapter::Borrowed($Str::from(&self[..])) + } + } + } +} + +impl<'a> Drop for nsAStringBulkWriteHandle<'a> { + /// This only runs in error cases. In success cases, `finish()` + /// calls `forget(self)`. + fn drop(&mut self) { + if self.capacity == 0 { + // If capacity is 0, the string is a zero-length + // string, so we have nothing to do. + return; + } + // The old zero terminator may be gone by now, so we need + // to write a new one somewhere and make length match. + // We can use a length between 1 and self.capacity. + // Seems prudent to overwrite the uninitialized memory. + // Using the length 1 leaves the shortest memory to overwrite. + // U+FFFD is the safest placeholder. Merely truncating the + // string to a zero-length string might be dangerous in some + // scenarios. See + // https://www.unicode.org/reports/tr36/#Substituting_for_Ill_Formed_Subsequences + // for closely related scenario. + unsafe { + let mut this = self.string.as_repr_mut(); + this.as_mut().length = 1u32; + *(this.as_mut().data.as_mut()) = 0xFFFDu16; + *(this.as_mut().data.as_ptr().add(1)) = 0; + } + } +} + +impl<'a> Drop for nsACStringBulkWriteHandle<'a> { + /// This only runs in error cases. In success cases, `finish()` + /// calls `forget(self)`. + fn drop(&mut self) { + if self.capacity == 0 { + // If capacity is 0, the string is a zero-length + // string, so we have nothing to do. + return; + } + // The old zero terminator may be gone by now, so we need + // to write a new one somewhere and make length match. + // We can use a length between 1 and self.capacity. + // Seems prudent to overwrite the uninitialized memory. + // Using the length 1 leaves the shortest memory to overwrite. + // U+FFFD is the safest placeholder, but when it doesn't fit, + // let's use ASCII substitute. Merely truncating the + // string to a zero-length string might be dangerous in some + // scenarios. See + // https://www.unicode.org/reports/tr36/#Substituting_for_Ill_Formed_Subsequences + // for closely related scenario. + unsafe { + let mut this = self.string.as_repr_mut(); + if self.capacity >= 3 { + this.as_mut().length = 3u32; + *(this.as_mut().data.as_mut()) = 0xEFu8; + *(this.as_mut().data.as_ptr().add(1)) = 0xBFu8; + *(this.as_mut().data.as_ptr().add(2)) = 0xBDu8; + *(this.as_mut().data.as_ptr().add(3)) = 0; + } else { + this.as_mut().length = 1u32; + *(this.as_mut().data.as_mut()) = 0x1Au8; // U+FFFD doesn't fit + *(this.as_mut().data.as_ptr().add(1)) = 0; + } + } + } +} + +macro_rules! define_string_types { + { + char_t = $char_t: ty; + + AString = $AString: ident; + String = $String: ident; + Str = $Str: ident; + + StringLike = $StringLike: ident; + StringAdapter = $StringAdapter: ident; + + StringRepr = $StringRepr: ident; + AutoStringRepr = $AutoStringRepr: ident; + + BulkWriteHandle = $BulkWriteHandle: ident; + + drop = $drop: ident; + assign = $assign: ident, $fallible_assign: ident; + take_from = $take_from: ident, $fallible_take_from: ident; + append = $append: ident, $fallible_append: ident; + set_length = $set_length: ident, $fallible_set_length: ident; + begin_writing = $begin_writing: ident, $fallible_begin_writing: ident; + start_bulk_write = $start_bulk_write: ident; + } => { + /// The representation of a ns[C]String type in C++. This type is + /// used internally by our definition of ns[C]String to ensure layout + /// compatibility with the C++ ns[C]String type. + /// + /// This type may also be used in place of a C++ ns[C]String inside of + /// struct definitions which are shared with C++, as it has identical + /// layout to our ns[C]String type. + /// + /// This struct will leak its data if dropped from rust. See the module + /// documentation for more information on this type. + #[repr(C)] + #[derive(Debug)] + pub struct $StringRepr { + data: ptr::NonNull<$char_t>, + length: u32, + dataflags: DataFlags, + classflags: ClassFlags, + } + + impl $StringRepr { + fn new(classflags: ClassFlags) -> $StringRepr { + static NUL: $char_t = 0; + $StringRepr { + data: unsafe { ptr::NonNull::new_unchecked(&NUL as *const _ as *mut _) }, + length: 0, + dataflags: DataFlags::TERMINATED | DataFlags::LITERAL, + classflags, + } + } + } + + impl Deref for $StringRepr { + type Target = $AString; + fn deref(&self) -> &$AString { + unsafe { + &*(self as *const _ as *const $AString) + } + } + } + + impl DerefMut for $StringRepr { + fn deref_mut(&mut self) -> &mut $AString { + unsafe { + &mut *(self as *mut _ as *mut $AString) + } + } + } + + #[repr(C)] + #[derive(Debug)] + pub struct $AutoStringRepr { + super_repr: $StringRepr, + inline_capacity: u32, + } + + pub struct $BulkWriteHandle<'a> { + string: &'a mut $AString, + capacity: usize, + } + + impl<'a> $BulkWriteHandle<'a> { + fn new(string: &'a mut $AString, capacity: usize) -> Self { + $BulkWriteHandle{ string, capacity } + } + + pub unsafe fn restart_bulk_write(&mut self, + capacity: usize, + units_to_preserve: usize, + allow_shrinking: bool) -> Result<(), ()> { + self.capacity = + self.string.start_bulk_write_impl(capacity, + units_to_preserve, + allow_shrinking)?; + Ok(()) + } + + pub fn finish(mut self, length: usize, allow_shrinking: bool) -> BulkWriteOk { + // NOTE: Drop is implemented outside the macro earlier in this file, + // because it needs to deal with different code unit representations + // for the REPLACEMENT CHARACTER in the UTF-16 and UTF-8 cases and + // needs to deal with a REPLACEMENT CHARACTER not fitting in the + // buffer in the UTF-8 case. + assert!(length <= self.capacity); + if length == 0 { + // `truncate()` is OK even when the string + // is in invalid state. + self.string.truncate(); + mem::forget(self); // Don't run the failure path in drop() + return BulkWriteOk{}; + } + if allow_shrinking && length > SHRINKING_THRESHOLD { + unsafe { + let _ = self.restart_bulk_write(length, length, true); + } + } + unsafe { + let mut this = self.string.as_repr_mut(); + this.as_mut().length = length as u32; + *(this.as_mut().data.as_ptr().add(length)) = 0; + if cfg!(debug_assertions) { + // Overwrite the unused part in debug builds. Note + // that capacity doesn't include space for the zero + // terminator, so starting after the zero-terminator + // we wrote ends up overwriting the terminator space + // not reflected in the capacity number. + // write_bytes() takes care of multiplying the length + // by the size of T. + ptr::write_bytes(this.as_mut().data.as_ptr().add(length + 1), + 0xE4u8, + self.capacity - length); + } + // We don't have a Rust interface for mozilla/MemoryChecking.h, + // so let's just not communicate with MSan/Valgrind here. + } + mem::forget(self); // Don't run the failure path in drop() + BulkWriteOk{} + } + + pub fn as_mut_slice(&mut self) -> &mut [$char_t] { + unsafe { + let mut this = self.string.as_repr_mut(); + slice::from_raw_parts_mut(this.as_mut().data.as_ptr(), self.capacity) + } + } + } + + /// This type is the abstract type which is used for interacting with + /// strings in rust. Each string type can derefence to an instance of + /// this type, which provides the useful operations on strings. + /// + /// NOTE: Rust thinks this type has a size of 0, because the data + /// associated with it is not necessarially safe to move. It is not safe + /// to construct a nsAString yourself, unless it is received by + /// dereferencing one of these types. + /// + /// NOTE: The `[u8; 0]` member is zero sized, and only exists to prevent + /// the construction by code outside of this module. It is used instead + /// of a private `()` member because the `improper_ctypes` lint complains + /// about some ZST members in `extern "C"` function declarations. + #[repr(C)] + pub struct $AString { + _prohibit_constructor: [u8; 0], + } + + impl $AString { + /// Assign the value of `other` into self, overwriting any value + /// currently stored. Performs an optimized assignment when possible + /// if `other` is a `nsA[C]String`. + pub fn assign<T: $StringLike + ?Sized>(&mut self, other: &T) { + unsafe { $assign(self, other.adapt().as_ptr()) }; + } + + /// Assign the value of `other` into self, overwriting any value + /// currently stored. Performs an optimized assignment when possible + /// if `other` is a `nsA[C]String`. + /// + /// Returns Ok(()) on success, and Err(()) if the allocation failed. + pub fn fallible_assign<T: $StringLike + ?Sized>(&mut self, other: &T) -> Result<(), ()> { + if unsafe { $fallible_assign(self, other.adapt().as_ptr()) } { + Ok(()) + } else { + Err(()) + } + } + + /// Take the value of `other` and set `self`, overwriting any value + /// currently stored. The passed-in string will be truncated. + pub fn take_from(&mut self, other: &mut $AString) { + unsafe { $take_from(self, other) }; + } + + /// Take the value of `other` and set `self`, overwriting any value + /// currently stored. If this function fails, the source string will + /// be left untouched, otherwise it will be truncated. + /// + /// Returns Ok(()) on success, and Err(()) if the allocation failed. + pub fn fallible_take_from(&mut self, other: &mut $AString) -> Result<(), ()> { + if unsafe { $fallible_take_from(self, other) } { + Ok(()) + } else { + Err(()) + } + } + + /// Append the value of `other` into self. + pub fn append<T: $StringLike + ?Sized>(&mut self, other: &T) { + unsafe { $append(self, other.adapt().as_ptr()) }; + } + + /// Append the value of `other` into self. + /// + /// Returns Ok(()) on success, and Err(()) if the allocation failed. + pub fn fallible_append<T: $StringLike + ?Sized>(&mut self, other: &T) -> Result<(), ()> { + if unsafe { $fallible_append(self, other.adapt().as_ptr()) } { + Ok(()) + } else { + Err(()) + } + } + + /// Mark the string's data as void. If `true`, the string will be truncated. + /// + /// A void string is generally converted to a `null` JS value by bindings code. + pub fn set_is_void(&mut self, is_void: bool) { + if is_void { + self.truncate(); + } + unsafe { + self.as_repr_mut().as_mut().dataflags.set(DataFlags::VOIDED, is_void); + } + } + + /// Returns whether the string's data is voided. + pub fn is_void(&self) -> bool { + self.as_repr().dataflags.contains(DataFlags::VOIDED) + } + + /// Set the length of the string to the passed-in length, and expand + /// the backing capacity to match. This method is unsafe as it can + /// expose uninitialized memory when len is greater than the current + /// length of the string. + pub unsafe fn set_length(&mut self, len: u32) { + $set_length(self, len); + } + + /// Set the length of the string to the passed-in length, and expand + /// the backing capacity to match. This method is unsafe as it can + /// expose uninitialized memory when len is greater than the current + /// length of the string. + /// + /// Returns Ok(()) on success, and Err(()) if the allocation failed. + pub unsafe fn fallible_set_length(&mut self, len: u32) -> Result<(), ()> { + if $fallible_set_length(self, len) { + Ok(()) + } else { + Err(()) + } + } + + pub fn truncate(&mut self) { + unsafe { + self.set_length(0); + } + } + + /// Get a `&mut` reference to the backing data for this string. + /// This method will allocate and copy if the current backing buffer + /// is immutable or shared. + pub fn to_mut(&mut self) -> &mut [$char_t] { + unsafe { + let len = self.len(); + if len == 0 { + // Use an arbitrary but aligned non-null value as the pointer + slice::from_raw_parts_mut(ptr::NonNull::<$char_t>::dangling().as_ptr(), 0) + } else { + slice::from_raw_parts_mut($begin_writing(self), len) + } + } + } + + /// Get a `&mut` reference to the backing data for this string. + /// This method will allocate and copy if the current backing buffer + /// is immutable or shared. + /// + /// Returns `Ok(&mut [T])` on success, and `Err(())` if the + /// allocation failed. + pub fn fallible_to_mut(&mut self) -> Result<&mut [$char_t], ()> { + unsafe { + let len = self.len(); + if len == 0 { + // Use an arbitrary but aligned non-null value as the pointer + Ok(slice::from_raw_parts_mut( + ptr::NonNull::<$char_t>::dangling().as_ptr() as *mut $char_t, 0)) + } else { + let ptr = $fallible_begin_writing(self); + if ptr.is_null() { + Err(()) + } else { + Ok(slice::from_raw_parts_mut(ptr, len)) + } + } + } + } + + /// Unshares the buffer of the string and returns a handle + /// from which a writable slice whose length is the rounded-up + /// capacity can be obtained. + /// + /// Fails also if the new length doesn't fit in 32 bits. + /// + /// # Safety + /// + /// Unsafe because of exposure of uninitialized memory. + pub unsafe fn bulk_write(&mut self, + capacity: usize, + units_to_preserve: usize, + allow_shrinking: bool) -> Result<$BulkWriteHandle, ()> { + let capacity = + self.start_bulk_write_impl(capacity, units_to_preserve, allow_shrinking)?; + Ok($BulkWriteHandle::new(self, capacity)) + } + + unsafe fn start_bulk_write_impl(&mut self, + capacity: usize, + units_to_preserve: usize, + allow_shrinking: bool) -> Result<usize, ()> { + if capacity > u32::MAX as usize { + Err(()) + } else { + let capacity32 = capacity as u32; + let rounded = $start_bulk_write(self, + capacity32, + units_to_preserve as u32, + allow_shrinking && capacity > SHRINKING_THRESHOLD); + if rounded == u32::MAX { + return Err(()) + } + Ok(rounded as usize) + } + } + + fn as_repr(&self) -> &$StringRepr { + // All $AString values point to a struct prefix which is + // identical to $StringRepr, thus we can cast `self` + // into *const $StringRepr to get the reference to the + // underlying data. + unsafe { + &*(self as *const _ as *const $StringRepr) + } + } + + fn as_repr_mut(&mut self) -> ptr::NonNull<$StringRepr> { + unsafe { ptr::NonNull::new_unchecked(self as *mut _ as *mut $StringRepr)} + } + + fn as_auto_string_repr(&self) -> Option<&$AutoStringRepr> { + if !self.as_repr().classflags.contains(ClassFlags::INLINE) { + return None; + } + + unsafe { + Some(&*(self as *const _ as *const $AutoStringRepr)) + } + } + + /// If this is an autostring, returns the capacity (excluding the + /// zero terminator) of the inline buffer within `Some()`. Otherwise + /// returns `None`. + pub fn inline_capacity(&self) -> Option<usize> { + Some(self.as_auto_string_repr()?.inline_capacity as usize) + } + } + + impl Deref for $AString { + type Target = [$char_t]; + fn deref(&self) -> &[$char_t] { + unsafe { + // All $AString values point to a struct prefix which is + // identical to $StringRepr, thus we can cast `self` + // into *const $StringRepr to get the reference to the + // underlying data. + let this = &*(self as *const _ as *const $StringRepr); + slice::from_raw_parts(this.data.as_ptr(), this.length as usize) + } + } + } + + impl AsRef<[$char_t]> for $AString { + fn as_ref(&self) -> &[$char_t] { + self + } + } + + impl cmp::PartialEq for $AString { + fn eq(&self, other: &$AString) -> bool { + &self[..] == &other[..] + } + } + + impl cmp::PartialEq<[$char_t]> for $AString { + fn eq(&self, other: &[$char_t]) -> bool { + &self[..] == other + } + } + + impl cmp::PartialEq<$String> for $AString { + fn eq(&self, other: &$String) -> bool { + self.eq(&**other) + } + } + + impl<'a> cmp::PartialEq<$Str<'a>> for $AString { + fn eq(&self, other: &$Str<'a>) -> bool { + self.eq(&**other) + } + } + + #[repr(C)] + pub struct $Str<'a> { + hdr: $StringRepr, + _marker: PhantomData<&'a [$char_t]>, + } + + impl $Str<'static> { + pub fn new() -> $Str<'static> { + $Str { + hdr: $StringRepr::new(ClassFlags::empty()), + _marker: PhantomData, + } + } + } + + impl<'a> Drop for $Str<'a> { + fn drop(&mut self) { + unsafe { + $drop(&mut **self); + } + } + } + + impl<'a> Deref for $Str<'a> { + type Target = $AString; + fn deref(&self) -> &$AString { + &self.hdr + } + } + + impl<'a> DerefMut for $Str<'a> { + fn deref_mut(&mut self) -> &mut $AString { + &mut self.hdr + } + } + + impl<'a> AsRef<[$char_t]> for $Str<'a> { + fn as_ref(&self) -> &[$char_t] { + &self + } + } + + impl<'a> From<&'a [$char_t]> for $Str<'a> { + fn from(s: &'a [$char_t]) -> $Str<'a> { + assert!(s.len() < (u32::MAX as usize)); + if s.is_empty() { + return $Str::new(); + } + $Str { + hdr: $StringRepr { + data: unsafe { ptr::NonNull::new_unchecked(s.as_ptr() as *mut _) }, + length: s.len() as u32, + dataflags: DataFlags::empty(), + classflags: ClassFlags::empty(), + }, + _marker: PhantomData, + } + } + } + + impl<'a> From<&'a Vec<$char_t>> for $Str<'a> { + fn from(s: &'a Vec<$char_t>) -> $Str<'a> { + $Str::from(&s[..]) + } + } + + impl<'a> From<&'a $AString> for $Str<'a> { + fn from(s: &'a $AString) -> $Str<'a> { + $Str::from(&s[..]) + } + } + + impl<'a> fmt::Write for $Str<'a> { + fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> { + $AString::write_str(self, s) + } + } + + impl<'a> fmt::Display for $Str<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + <$AString as fmt::Display>::fmt(self, f) + } + } + + impl<'a> fmt::Debug for $Str<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + <$AString as fmt::Debug>::fmt(self, f) + } + } + + impl<'a> cmp::PartialEq for $Str<'a> { + fn eq(&self, other: &$Str<'a>) -> bool { + $AString::eq(self, other) + } + } + + impl<'a> cmp::PartialEq<[$char_t]> for $Str<'a> { + fn eq(&self, other: &[$char_t]) -> bool { + $AString::eq(self, other) + } + } + + impl<'a, 'b> cmp::PartialEq<&'b [$char_t]> for $Str<'a> { + fn eq(&self, other: &&'b [$char_t]) -> bool { + $AString::eq(self, *other) + } + } + + impl<'a> cmp::PartialEq<str> for $Str<'a> { + fn eq(&self, other: &str) -> bool { + $AString::eq(self, other) + } + } + + impl<'a, 'b> cmp::PartialEq<&'b str> for $Str<'a> { + fn eq(&self, other: &&'b str) -> bool { + $AString::eq(self, *other) + } + } + + #[repr(C)] + pub struct $String { + hdr: $StringRepr, + } + + unsafe impl Send for $String {} + unsafe impl Sync for $String {} + + impl $String { + pub fn new() -> $String { + $String { + hdr: $StringRepr::new(ClassFlags::NULL_TERMINATED), + } + } + + /// Converts this String into a StringRepr, which will leak if the + /// repr is not passed to something that knows how to free it. + pub fn into_repr(mut self) -> $StringRepr { + mem::replace(&mut self.hdr, $StringRepr::new(ClassFlags::NULL_TERMINATED)) + } + } + + impl Drop for $String { + fn drop(&mut self) { + unsafe { + $drop(&mut **self); + } + } + } + + impl Deref for $String { + type Target = $AString; + fn deref(&self) -> &$AString { + &self.hdr + } + } + + impl DerefMut for $String { + fn deref_mut(&mut self) -> &mut $AString { + &mut self.hdr + } + } + + impl Clone for $String { + fn clone(&self) -> Self { + let mut copy = $String::new(); + copy.assign(self); + copy + } + } + + impl AsRef<[$char_t]> for $String { + fn as_ref(&self) -> &[$char_t] { + &self + } + } + + impl<'a> From<&'a [$char_t]> for $String { + fn from(s: &'a [$char_t]) -> $String { + let mut res = $String::new(); + res.assign(&$Str::from(&s[..])); + res + } + } + + impl<'a> From<&'a Vec<$char_t>> for $String { + fn from(s: &'a Vec<$char_t>) -> $String { + $String::from(&s[..]) + } + } + + impl<'a> From<&'a $AString> for $String { + fn from(s: &'a $AString) -> $String { + $String::from(&s[..]) + } + } + + impl From<Box<[$char_t]>> for $String { + fn from(s: Box<[$char_t]>) -> $String { + s.into_vec().into() + } + } + + impl From<Vec<$char_t>> for $String { + fn from(mut s: Vec<$char_t>) -> $String { + assert!(s.len() < (u32::MAX as usize)); + if s.is_empty() { + return $String::new(); + } + + let length = s.len() as u32; + s.push(0); // null terminator + + // SAFETY NOTE: This method produces an data_flags::OWNED + // ns[C]String from a Box<[$char_t]>. this is only safe + // because in the Gecko tree, we use the same allocator for + // Rust code as for C++ code, meaning that our box can be + // legally freed with libc::free(). + let ptr = s.as_mut_ptr(); + mem::forget(s); + unsafe { + Gecko_IncrementStringAdoptCount(ptr as *mut _); + } + $String { + hdr: $StringRepr { + data: unsafe { ptr::NonNull::new_unchecked(ptr) }, + length, + dataflags: DataFlags::OWNED | DataFlags::TERMINATED, + classflags: ClassFlags::NULL_TERMINATED, + } + } + } + } + + impl fmt::Write for $String { + fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> { + $AString::write_str(self, s) + } + } + + impl fmt::Display for $String { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + <$AString as fmt::Display>::fmt(self, f) + } + } + + impl fmt::Debug for $String { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + <$AString as fmt::Debug>::fmt(self, f) + } + } + + impl cmp::PartialEq for $String { + fn eq(&self, other: &$String) -> bool { + $AString::eq(self, other) + } + } + + impl cmp::PartialEq<[$char_t]> for $String { + fn eq(&self, other: &[$char_t]) -> bool { + $AString::eq(self, other) + } + } + + impl<'a> cmp::PartialEq<&'a [$char_t]> for $String { + fn eq(&self, other: &&'a [$char_t]) -> bool { + $AString::eq(self, *other) + } + } + + impl cmp::PartialEq<str> for $String { + fn eq(&self, other: &str) -> bool { + $AString::eq(self, other) + } + } + + impl<'a> cmp::PartialEq<&'a str> for $String { + fn eq(&self, other: &&'a str) -> bool { + $AString::eq(self, *other) + } + } + + /// An adapter type to allow for passing both types which coerce to + /// &[$char_type], and &$AString to a function, while still performing + /// optimized operations when passed the $AString. + pub enum $StringAdapter<'a> { + Borrowed($Str<'a>), + Abstract(&'a $AString), + } + + impl<'a> $StringAdapter<'a> { + fn as_ptr(&self) -> *const $AString { + &**self + } + } + + impl<'a> Deref for $StringAdapter<'a> { + type Target = $AString; + + fn deref(&self) -> &$AString { + match *self { + $StringAdapter::Borrowed(ref s) => s, + $StringAdapter::Abstract(ref s) => s, + } + } + } + + impl<'a> $StringAdapter<'a> { + #[allow(dead_code)] + fn is_abstract(&self) -> bool { + match *self { + $StringAdapter::Borrowed(_) => false, + $StringAdapter::Abstract(_) => true, + } + } + } + + string_like! { + char_t = $char_t; + + AString = $AString; + String = $String; + Str = $Str; + + StringLike = $StringLike; + StringAdapter = $StringAdapter; + } + } +} + +/////////////////////////////////////////// +// Bindings for nsCString (u8 char type) // +/////////////////////////////////////////// + +define_string_types! { + char_t = u8; + + AString = nsACString; + String = nsCString; + Str = nsCStr; + + StringLike = nsCStringLike; + StringAdapter = nsCStringAdapter; + + StringRepr = nsCStringRepr; + AutoStringRepr = nsAutoCStringRepr; + + BulkWriteHandle = nsACStringBulkWriteHandle; + + drop = Gecko_FinalizeCString; + assign = Gecko_AssignCString, Gecko_FallibleAssignCString; + take_from = Gecko_TakeFromCString, Gecko_FallibleTakeFromCString; + append = Gecko_AppendCString, Gecko_FallibleAppendCString; + set_length = Gecko_SetLengthCString, Gecko_FallibleSetLengthCString; + begin_writing = Gecko_BeginWritingCString, Gecko_FallibleBeginWritingCString; + start_bulk_write = Gecko_StartBulkWriteCString; +} + +impl nsACString { + /// Gets a CString as an utf-8 str or a String, trying to avoid copies, and + /// replacing invalid unicode sequences with replacement characters. + #[inline] + pub fn to_utf8(&self) -> borrow::Cow<str> { + String::from_utf8_lossy(&self[..]) + } + + #[inline] + pub unsafe fn as_str_unchecked(&self) -> &str { + if cfg!(debug_assertions) { + str::from_utf8(self).expect("Should be utf-8") + } else { + str::from_utf8_unchecked(self) + } + } +} + +impl<'a> From<&'a str> for nsCStr<'a> { + fn from(s: &'a str) -> nsCStr<'a> { + s.as_bytes().into() + } +} + +impl<'a> From<&'a String> for nsCStr<'a> { + fn from(s: &'a String) -> nsCStr<'a> { + nsCStr::from(&s[..]) + } +} + +impl<'a> From<&'a str> for nsCString { + fn from(s: &'a str) -> nsCString { + s.as_bytes().into() + } +} + +impl<'a> From<&'a String> for nsCString { + fn from(s: &'a String) -> nsCString { + nsCString::from(&s[..]) + } +} + +impl From<Box<str>> for nsCString { + fn from(s: Box<str>) -> nsCString { + s.into_string().into() + } +} + +impl From<String> for nsCString { + fn from(s: String) -> nsCString { + s.into_bytes().into() + } +} + +// Support for the write!() macro for appending to nsACStrings +impl fmt::Write for nsACString { + fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> { + self.append(s); + Ok(()) + } +} + +impl fmt::Display for nsACString { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + fmt::Display::fmt(&self.to_utf8(), f) + } +} + +impl fmt::Debug for nsACString { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + fmt::Debug::fmt(&self.to_utf8(), f) + } +} + +impl cmp::PartialEq<str> for nsACString { + fn eq(&self, other: &str) -> bool { + &self[..] == other.as_bytes() + } +} + +impl nsCStringLike for str { + fn adapt(&self) -> nsCStringAdapter { + nsCStringAdapter::Borrowed(nsCStr::from(self)) + } +} + +impl nsCStringLike for String { + fn adapt(&self) -> nsCStringAdapter { + nsCStringAdapter::Borrowed(nsCStr::from(&self[..])) + } +} + +impl nsCStringLike for Box<str> { + fn adapt(&self) -> nsCStringAdapter { + nsCStringAdapter::Borrowed(nsCStr::from(&self[..])) + } +} + +// This trait is implemented on types which are Latin1 `nsCString`-like, +// in that they can at very low cost be converted to a borrowed +// `&nsACString` and do not denote UTF-8ness in the Rust type system. +// +// This trait is used to DWIM when calling the methods on +// `nsACString`. +string_like! { + char_t = u8; + + AString = nsACString; + String = nsCString; + Str = nsCStr; + + StringLike = Latin1StringLike; + StringAdapter = nsCStringAdapter; +} + +/////////////////////////////////////////// +// Bindings for nsString (u16 char type) // +/////////////////////////////////////////// + +define_string_types! { + char_t = u16; + + AString = nsAString; + String = nsString; + Str = nsStr; + + StringLike = nsStringLike; + StringAdapter = nsStringAdapter; + + StringRepr = nsStringRepr; + AutoStringRepr = nsAutoStringRepr; + + BulkWriteHandle = nsAStringBulkWriteHandle; + + drop = Gecko_FinalizeString; + assign = Gecko_AssignString, Gecko_FallibleAssignString; + take_from = Gecko_TakeFromString, Gecko_FallibleTakeFromString; + append = Gecko_AppendString, Gecko_FallibleAppendString; + set_length = Gecko_SetLengthString, Gecko_FallibleSetLengthString; + begin_writing = Gecko_BeginWritingString, Gecko_FallibleBeginWritingString; + start_bulk_write = Gecko_StartBulkWriteString; +} + +// NOTE: The From impl for a string slice for nsString produces a <'static> +// lifetime, as it allocates. +impl<'a> From<&'a str> for nsString { + fn from(s: &'a str) -> nsString { + s.encode_utf16().collect::<Vec<u16>>().into() + } +} + +impl<'a> From<&'a String> for nsString { + fn from(s: &'a String) -> nsString { + nsString::from(&s[..]) + } +} + +// Support for the write!() macro for writing to nsStrings +impl fmt::Write for nsAString { + fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> { + // Directly invoke gecko's routines for appending utf8 strings to + // nsAString values, to avoid as much overhead as possible + self.append_str(s); + Ok(()) + } +} + +impl nsAString { + /// Turns this utf-16 string into a string, replacing invalid unicode + /// sequences with replacement characters. + /// + /// This is needed because the default ToString implementation goes through + /// fmt::Display, and thus allocates the string twice. + #[allow(clippy::inherent_to_string_shadow_display)] + pub fn to_string(&self) -> String { + String::from_utf16_lossy(&self[..]) + } +} + +impl fmt::Display for nsAString { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + fmt::Display::fmt(&self.to_string(), f) + } +} + +impl fmt::Debug for nsAString { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + fmt::Debug::fmt(&self.to_string(), f) + } +} + +impl cmp::PartialEq<str> for nsAString { + fn eq(&self, other: &str) -> bool { + other.encode_utf16().eq(self.iter().cloned()) + } +} + +#[cfg(not(feature = "gecko_debug"))] +#[allow(non_snake_case)] +unsafe fn Gecko_IncrementStringAdoptCount(_: *mut c_void) {} + +extern "C" { + #[cfg(feature = "gecko_debug")] + fn Gecko_IncrementStringAdoptCount(data: *mut c_void); + + // Gecko implementation in nsSubstring.cpp + fn Gecko_FinalizeCString(this: *mut nsACString); + + fn Gecko_AssignCString(this: *mut nsACString, other: *const nsACString); + fn Gecko_TakeFromCString(this: *mut nsACString, other: *mut nsACString); + fn Gecko_AppendCString(this: *mut nsACString, other: *const nsACString); + fn Gecko_SetLengthCString(this: *mut nsACString, length: u32); + fn Gecko_BeginWritingCString(this: *mut nsACString) -> *mut u8; + fn Gecko_FallibleAssignCString(this: *mut nsACString, other: *const nsACString) -> bool; + fn Gecko_FallibleTakeFromCString(this: *mut nsACString, other: *mut nsACString) -> bool; + fn Gecko_FallibleAppendCString(this: *mut nsACString, other: *const nsACString) -> bool; + fn Gecko_FallibleSetLengthCString(this: *mut nsACString, length: u32) -> bool; + fn Gecko_FallibleBeginWritingCString(this: *mut nsACString) -> *mut u8; + fn Gecko_StartBulkWriteCString( + this: *mut nsACString, + capacity: u32, + units_to_preserve: u32, + allow_shrinking: bool, + ) -> u32; + + fn Gecko_FinalizeString(this: *mut nsAString); + + fn Gecko_AssignString(this: *mut nsAString, other: *const nsAString); + fn Gecko_TakeFromString(this: *mut nsAString, other: *mut nsAString); + fn Gecko_AppendString(this: *mut nsAString, other: *const nsAString); + fn Gecko_SetLengthString(this: *mut nsAString, length: u32); + fn Gecko_BeginWritingString(this: *mut nsAString) -> *mut u16; + fn Gecko_FallibleAssignString(this: *mut nsAString, other: *const nsAString) -> bool; + fn Gecko_FallibleTakeFromString(this: *mut nsAString, other: *mut nsAString) -> bool; + fn Gecko_FallibleAppendString(this: *mut nsAString, other: *const nsAString) -> bool; + fn Gecko_FallibleSetLengthString(this: *mut nsAString, length: u32) -> bool; + fn Gecko_FallibleBeginWritingString(this: *mut nsAString) -> *mut u16; + fn Gecko_StartBulkWriteString( + this: *mut nsAString, + capacity: u32, + units_to_preserve: u32, + allow_shrinking: bool, + ) -> u32; +} + +////////////////////////////////////// +// Repr Validation Helper Functions // +////////////////////////////////////// + +pub mod test_helpers { + //! This module only exists to help with ensuring that the layout of the + //! structs inside of rust and C++ are identical. + //! + //! It is public to ensure that these testing functions are avaliable to + //! gtest code. + + use super::{nsACString, nsAString}; + use super::{nsCStr, nsCString, nsCStringRepr}; + use super::{nsStr, nsString, nsStringRepr}; + use super::{ClassFlags, DataFlags}; + use std::mem; + + /// Generates an #[no_mangle] extern "C" function which returns the size and + /// alignment of the given type with the given name. + macro_rules! size_align_check { + ($T:ty, $fname:ident) => { + #[no_mangle] + #[allow(non_snake_case)] + pub unsafe extern "C" fn $fname(size: *mut usize, align: *mut usize) { + *size = mem::size_of::<$T>(); + *align = mem::align_of::<$T>(); + } + }; + ($T:ty, $U:ty, $V:ty, $fname:ident) => { + #[no_mangle] + #[allow(non_snake_case)] + pub unsafe extern "C" fn $fname(size: *mut usize, align: *mut usize) { + *size = mem::size_of::<$T>(); + *align = mem::align_of::<$T>(); + + assert_eq!(*size, mem::size_of::<$U>()); + assert_eq!(*align, mem::align_of::<$U>()); + assert_eq!(*size, mem::size_of::<$V>()); + assert_eq!(*align, mem::align_of::<$V>()); + } + }; + } + + size_align_check!( + nsStringRepr, + nsString, + nsStr<'static>, + Rust_Test_ReprSizeAlign_nsString + ); + size_align_check!( + nsCStringRepr, + nsCString, + nsCStr<'static>, + Rust_Test_ReprSizeAlign_nsCString + ); + + /// Generates a $[no_mangle] extern "C" function which returns the size, + /// alignment and offset in the parent struct of a given member, with the + /// given name. + /// + /// This method can trigger Undefined Behavior if the accessing the member + /// $member on a given type would use that type's `Deref` implementation. + macro_rules! member_check { + ($T:ty, $U:ty, $V:ty, $member:ident, $method:ident) => { + #[no_mangle] + #[allow(non_snake_case)] + pub unsafe extern "C" fn $method( + size: *mut usize, + align: *mut usize, + offset: *mut usize, + ) { + // Create a temporary value of type T to get offsets, sizes + // and alignments from. + let tmp: mem::MaybeUninit<$T> = mem::MaybeUninit::uninit(); + // FIXME: This should use &raw references when available, + // this is technically UB as it creates a reference to + // uninitialized memory, but there's no better way to do + // this right now. + let tmp = &*tmp.as_ptr(); + *size = mem::size_of_val(&tmp.$member); + *align = mem::align_of_val(&tmp.$member); + *offset = (&tmp.$member as *const _ as usize) - (tmp as *const $T as usize); + + let tmp: mem::MaybeUninit<$U> = mem::MaybeUninit::uninit(); + let tmp = &*tmp.as_ptr(); + assert_eq!(*size, mem::size_of_val(&tmp.hdr.$member)); + assert_eq!(*align, mem::align_of_val(&tmp.hdr.$member)); + assert_eq!( + *offset, + (&tmp.hdr.$member as *const _ as usize) - (tmp as *const $U as usize) + ); + + let tmp: mem::MaybeUninit<$V> = mem::MaybeUninit::uninit(); + let tmp = &*tmp.as_ptr(); + assert_eq!(*size, mem::size_of_val(&tmp.hdr.$member)); + assert_eq!(*align, mem::align_of_val(&tmp.hdr.$member)); + assert_eq!( + *offset, + (&tmp.hdr.$member as *const _ as usize) - (tmp as *const $V as usize) + ); + } + }; + } + + member_check!( + nsStringRepr, + nsString, + nsStr<'static>, + data, + Rust_Test_Member_nsString_mData + ); + member_check!( + nsStringRepr, + nsString, + nsStr<'static>, + length, + Rust_Test_Member_nsString_mLength + ); + member_check!( + nsStringRepr, + nsString, + nsStr<'static>, + dataflags, + Rust_Test_Member_nsString_mDataFlags + ); + member_check!( + nsStringRepr, + nsString, + nsStr<'static>, + classflags, + Rust_Test_Member_nsString_mClassFlags + ); + member_check!( + nsCStringRepr, + nsCString, + nsCStr<'static>, + data, + Rust_Test_Member_nsCString_mData + ); + member_check!( + nsCStringRepr, + nsCString, + nsCStr<'static>, + length, + Rust_Test_Member_nsCString_mLength + ); + member_check!( + nsCStringRepr, + nsCString, + nsCStr<'static>, + dataflags, + Rust_Test_Member_nsCString_mDataFlags + ); + member_check!( + nsCStringRepr, + nsCString, + nsCStr<'static>, + classflags, + Rust_Test_Member_nsCString_mClassFlags + ); + + #[no_mangle] + #[allow(non_snake_case)] + pub unsafe extern "C" fn Rust_Test_NsStringFlags( + f_terminated: *mut u16, + f_voided: *mut u16, + f_refcounted: *mut u16, + f_owned: *mut u16, + f_inline: *mut u16, + f_literal: *mut u16, + f_class_inline: *mut u16, + f_class_null_terminated: *mut u16, + ) { + *f_terminated = DataFlags::TERMINATED.bits(); + *f_voided = DataFlags::VOIDED.bits(); + *f_refcounted = DataFlags::REFCOUNTED.bits(); + *f_owned = DataFlags::OWNED.bits(); + *f_inline = DataFlags::INLINE.bits(); + *f_literal = DataFlags::LITERAL.bits(); + *f_class_inline = ClassFlags::INLINE.bits(); + *f_class_null_terminated = ClassFlags::NULL_TERMINATED.bits(); + } + + #[no_mangle] + #[allow(non_snake_case)] + pub unsafe extern "C" fn Rust_InlineCapacityFromRust( + cstring: *const nsACString, + string: *const nsAString, + cstring_capacity: *mut usize, + string_capacity: *mut usize, + ) { + *cstring_capacity = (*cstring).inline_capacity().unwrap(); + *string_capacity = (*string).inline_capacity().unwrap(); + } +} diff --git a/xpcom/rust/xpcom/Cargo.toml b/xpcom/rust/xpcom/Cargo.toml new file mode 100644 index 0000000000..8de17167b0 --- /dev/null +++ b/xpcom/rust/xpcom/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "xpcom" +version = "0.1.0" +authors = ["Nika Layzell <nika@thelayzells.com>"] +edition = "2018" + +[dependencies] +cstr = "0.2" +libc = "0.2" +nsstring = { path = "../nsstring" } +nserror = { path = "../nserror" } +threadbound = "0.1" +xpcom_macros = { path = "xpcom_macros" } +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } + +[features] +thread_sanitizer = [] diff --git a/xpcom/rust/xpcom/src/base.rs b/xpcom/rust/xpcom/src/base.rs new file mode 100644 index 0000000000..556768a179 --- /dev/null +++ b/xpcom/rust/xpcom/src/base.rs @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::interfaces::{nsIInterfaceRequestor, nsISupports}; +use crate::{GetterAddrefs, RefCounted, RefPtr}; + +#[repr(C)] +#[derive(Copy, Clone, Eq, PartialEq)] +/// A "unique identifier". This is modeled after OSF DCE UUIDs. +pub struct nsID(pub u32, pub u16, pub u16, pub [u8; 8]); + +/// Interface IDs +pub type nsIID = nsID; +/// Class IDs +pub type nsCID = nsID; + +/// A type which implements XpCom must follow the following rules: +/// +/// * It must be a legal XPCOM interface. +/// * The result of a QueryInterface or similar call, passing IID, must return a +/// valid reference to an object of the given type. +/// * It must be valid to cast a &self reference to a &nsISupports reference. +pub unsafe trait XpCom: RefCounted { + const IID: nsIID; + + /// Perform a QueryInterface call on this object, attempting to dynamically + /// cast it to the requested interface type. Returns Some(RefPtr<T>) if the + /// cast succeeded, and None otherwise. + fn query_interface<T: XpCom>(&self) -> Option<RefPtr<T>> { + let mut ga = GetterAddrefs::<T>::new(); + unsafe { + if (*(self as *const Self as *const nsISupports)) + .QueryInterface(&T::IID, ga.void_ptr()) + .succeeded() + { + ga.refptr() + } else { + None + } + } + } + + /// Perform a `GetInterface` call on this object, returning `None` if the + /// object doesn't implement `nsIInterfaceRequestor`, or can't access the + /// interface `T`. + fn get_interface<T: XpCom>(&self) -> Option<RefPtr<T>> { + let ireq = self.query_interface::<nsIInterfaceRequestor>()?; + + let mut ga = GetterAddrefs::<T>::new(); + unsafe { + if ireq.GetInterface(&T::IID, ga.void_ptr()).succeeded() { + ga.refptr() + } else { + None + } + } + } +} diff --git a/xpcom/rust/xpcom/src/interfaces/idl.rs b/xpcom/rust/xpcom/src/interfaces/idl.rs new file mode 100644 index 0000000000..37b963519d --- /dev/null +++ b/xpcom/rust/xpcom/src/interfaces/idl.rs @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#![allow(bad_style)] + +use crate::interfaces::*; +use crate::*; + +// NOTE: This file contains a series of `include!()` invocations, defining all +// idl interfaces directly within this module. +include!(concat!(env!("MOZ_TOPOBJDIR"), "/dist/xpcrs/rt/all.rs")); diff --git a/xpcom/rust/xpcom/src/interfaces/mod.rs b/xpcom/rust/xpcom/src/interfaces/mod.rs new file mode 100644 index 0000000000..5b9de5cef5 --- /dev/null +++ b/xpcom/rust/xpcom/src/interfaces/mod.rs @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! This module contains the xpcom interfaces exposed to rust code. +//! +//! The items in this module come in a few flavours: +//! +//! 1. `nsI*`: These are the types for XPCOM interfaces. They should always be +//! passed behind a reference, pointer, or `RefPtr`. They may be coerced to +//! their base interfaces using the `coerce` method. +//! +//! 2. `nsI*Coerce`: These traits provide the implementation mechanics for the +//! `coerce` method, and can usually be ignored. *These traits are hidden in +//! rustdoc* +//! +//! 3. `nsI*VTable`: These structs are the vtable definitions for each type. +//! They contain the base interface's vtable, followed by pointers for each +//! of the vtable's methods. If direct access is needed, a `*const nsI*` can +//! be safely transmuted to a `*const nsI*VTable`. *These structs are hidden +//! in rustdoc* +//! +//! 4. Typedefs used in idl file definitions. + +// Interfaces defined in .idl files +mod idl; +pub use self::idl::*; + +// Other interfaces which are needed to compile +mod nonidl; +pub use self::nonidl::*; diff --git a/xpcom/rust/xpcom/src/interfaces/nonidl.rs b/xpcom/rust/xpcom/src/interfaces/nonidl.rs new file mode 100644 index 0000000000..b9e3f1abe2 --- /dev/null +++ b/xpcom/rust/xpcom/src/interfaces/nonidl.rs @@ -0,0 +1,180 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! This module contains definitions of interfaces which are used in idl files +//! as forward declarations, but are not actually defined in an idl file. +//! +//! NOTE: The IIDs in these files must be kept in sync with the IDL definitions +//! in the corresponding C++ files. + +use crate::nsID; + +// XXX: This macro should have an option for a custom base interface instead of +// nsISupports, such that Document can have nsINode as a base, etc. For now, +// query_interface should be sufficient. +macro_rules! nonidl { + ($name:ident, $iid:expr) => { + /// This interface is referenced from idl files, but not defined in + /// them. It exports no methods to rust code. + #[repr(C)] + pub struct $name { + _vtable: *const $crate::interfaces::nsISupportsVTable, + } + + unsafe impl $crate::XpCom for $name { + const IID: $crate::nsIID = $iid; + } + + unsafe impl $crate::RefCounted for $name { + #[inline] + unsafe fn addref(&self) { + self.AddRef(); + } + #[inline] + unsafe fn release(&self) { + self.Release(); + } + } + + impl ::std::ops::Deref for $name { + type Target = $crate::interfaces::nsISupports; + #[inline] + fn deref(&self) -> &$crate::interfaces::nsISupports { + unsafe { ::std::mem::transmute(self) } + } + } + }; +} + +// Must be kept in sync with Document.h +nonidl!( + Document, + nsID( + 0xce1f7627, + 0x7109, + 0x4977, + [0xba, 0x77, 0x49, 0x0f, 0xfd, 0xe0, 0x7a, 0xaa] + ) +); + +// Must be kept in sync with nsINode.h +nonidl!( + nsINode, + nsID( + 0x70ba4547, + 0x7699, + 0x44fc, + [0xb3, 0x20, 0x52, 0xdb, 0xe3, 0xd1, 0xf9, 0x0a] + ) +); + +// Must be kept in sync with nsIContent.h +nonidl!( + nsIContent, + nsID( + 0x8e1bab9d, + 0x8815, + 0x4d2c, + [0xa2, 0x4d, 0x7a, 0xba, 0x52, 0x39, 0xdc, 0x22] + ) +); + +// Must be kept in sync with nsIConsoleReportCollector.h +nonidl!( + nsIConsoleReportCollector, + nsID( + 0xdd98a481, + 0xd2c4, + 0x4203, + [0x8d, 0xfa, 0x85, 0xbf, 0xd7, 0xdc, 0xd7, 0x05] + ) +); + +// Must be kept in sync with nsIGlobalObject.h +nonidl!( + nsIGlobalObject, + nsID( + 0x11afa8be, + 0xd997, + 0x4e07, + [0xa6, 0xa3, 0x6f, 0x87, 0x2e, 0xc3, 0xee, 0x7f] + ) +); + +// Must be kept in sync with nsIScriptElement.h +nonidl!( + nsIScriptElement, + nsID( + 0xe60fca9b, + 0x1b96, + 0x4e4e, + [0xa9, 0xb4, 0xdc, 0x98, 0x4f, 0x88, 0x3f, 0x9c] + ) +); + +// Must be kept in sync with nsPIDOMWindow.h +nonidl!( + nsPIDOMWindowOuter, + nsID( + 0x769693d4, + 0xb009, + 0x4fe2, + [0xaf, 0x18, 0x7d, 0xc8, 0xdf, 0x74, 0x96, 0xdf] + ) +); + +// Must be kept in sync with nsPIDOMWindow.h +nonidl!( + nsPIDOMWindowInner, + nsID( + 0x775dabc9, + 0x8f43, + 0x4277, + [0x9a, 0xdb, 0xf1, 0x99, 0x0d, 0x77, 0xcf, 0xfb] + ) +); + +// Must be kept in sync with nsIScriptContext.h +nonidl!( + nsIScriptContext, + nsID( + 0x54cbe9cf, + 0x7282, + 0x421a, + [0x91, 0x6f, 0xd0, 0x70, 0x73, 0xde, 0xb8, 0xc0] + ) +); + +// Must be kept in sync with nsIScriptGlobalObject.h +nonidl!( + nsIScriptGlobalObject, + nsID( + 0x876f83bd, + 0x6314, + 0x460a, + [0xa0, 0x45, 0x1c, 0x8f, 0x46, 0x2f, 0xb8, 0xe1] + ) +); + +// Must be kept in sync with nsIScrollObserver.h +nonidl!( + nsIScrollObserver, + nsID( + 0xaa5026eb, + 0x2f88, + 0x4026, + [0xa4, 0x6b, 0xf4, 0x59, 0x6b, 0x4e, 0xdf, 0x00] + ) +); + +// Must be kept in sync with nsIWidget.h +nonidl!( + nsIWidget, + nsID( + 0x06396bf6, + 0x2dd8, + 0x45e5, + [0xac, 0x45, 0x75, 0x26, 0x53, 0xb1, 0xc9, 0x80] + ) +); diff --git a/xpcom/rust/xpcom/src/lib.rs b/xpcom/rust/xpcom/src/lib.rs new file mode 100644 index 0000000000..fbd5f916d1 --- /dev/null +++ b/xpcom/rust/xpcom/src/lib.rs @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! This crate contains the functionality required in order to both implement +//! and call XPCOM methods from rust code. +//! +//! For documentation on how to implement XPCOM methods, see the documentation +//! for the [`xpcom_macros`](../xpcom_macros/index.html) crate. + +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] + +// re-export the xpcom_macros macro +pub use xpcom_macros::xpcom; + +// Helper functions and data structures are exported in the root of the crate. +mod base; +pub use base::*; + +// Declarative macro to generate XPCOM method stubs. +mod method; +pub use method::*; + +// dom::Promise resolving. +mod promise; +pub use promise::*; + +mod refptr; +pub use refptr::*; + +mod statics; +pub use statics::*; + +// XPCOM interface definitions. +pub mod interfaces; + +// XPCOM service getters. +pub mod services; + +// Implementation details of the xpcom_macros crate. +#[doc(hidden)] +pub mod reexports; diff --git a/xpcom/rust/xpcom/src/method.rs b/xpcom/rust/xpcom/src/method.rs new file mode 100644 index 0000000000..2ed828093f --- /dev/null +++ b/xpcom/rust/xpcom/src/method.rs @@ -0,0 +1,243 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use nserror::{nsresult, NS_ERROR_NULL_POINTER}; + +/// The xpcom_method macro generates a Rust XPCOM method stub that converts +/// raw pointer arguments to references, calls a Rustic implementation +/// of the method, writes its return value into the XPCOM method's outparameter, +/// and returns an nsresult. +/// +/// In other words, given an XPCOM method like: +/// +/// ```ignore +/// interface nsIFooBarBaz : nsISupports { +/// nsIVariant foo(in AUTF8String bar, [optional] in bool baz); +/// } +/// ``` +/// +/// And a Rust implementation that uses #[derive(xpcom)] to implement it: +/// +/// ```ignore +/// #[derive(xpcom)] +/// #[xpimplements(nsIFooBarBaz)] +/// #[refcnt = "atomic"] +/// struct InitFooBarBaz { +/// // … +/// } +/// ``` +/// +/// With the appropriate extern crate and use declarations +/// +/// ```ignore +/// extern crate xpcom; +/// use xpcom::xpcom_method; +/// ``` +/// +/// Invoking the macro with the name of the XPCOM method, the name of its +/// Rustic implementation, the set of its arguments, and its return value: +/// +/// ```ignore +/// impl FooBarBaz { +/// xpcom_method!( +/// foo => Foo(bar: *const nsACString, baz: bool) -> *const nsIVariant +/// ); +/// } +/// ``` +/// +/// Results in the macro generating an XPCOM stub like the following: +/// +/// ```ignore +/// unsafe fn Foo(&self, bar: *const nsACString, baz: bool, retval: *mut *const nsIVariant) -> nsresult { +/// let bar = match Ensure::ensure(bar) { +/// Ok(val) => val, +/// Err(result) => return result, +/// }; +/// let baz = match Ensure::ensure(baz) { +/// Ok(val) => val, +/// Err(result) => return result, +/// }; +/// +/// match self.foo(bar, baz) { +/// Ok(val) => { +/// val.forget(&mut *retval); +/// NS_OK +/// } +/// Err(error) => { +/// error!("{}", error); +/// error.into() +/// } +/// } +/// } +/// ``` +/// +/// Which calls a Rustic implementation (that you implement) like the following: +/// +/// ```ignore +/// impl FooBarBaz { +/// fn foo(&self, bar: &nsACString, baz: bool) -> Result<RefPtr<nsIVariant>, nsresult> { +/// // … +/// } +/// } +/// ``` +/// +/// Notes: +/// +/// On error, the Rustic implementation can return an Err(nsresult) or any +/// other type that implements Into<nsresult>. So you can define and return +/// a custom error type, which the XPCOM stub will convert to nsresult. +/// +/// This macro assumes that all non-null pointer arguments are valid! +/// It does ensure that they aren't null, using the `ensure_param` macro. +/// But it doesn't otherwise check their validity. That makes the function +/// unsafe, so callers must ensure that they only call it with valid pointer +/// arguments. +#[macro_export] +macro_rules! xpcom_method { + // This rule is provided to ensure external modules don't need to import + // internal implementation details of xpcom_method. + // The @ensure_param rule converts raw pointer arguments to references, + // returning NS_ERROR_NULL_POINTER if the argument is_null(). + // + // Notes: + // + // This rule can be called on a non-pointer copy parameter, but there's no + // benefit to doing so. The macro will just set the value of the parameter + // to itself. (This macro does this anyway due to limitations in declarative + // macros; it isn't currently possible to distinguish between pointer and + // copy types when processing a set of parameters.) + // + // The macro currently supports only in-parameters (*const nsIFoo); It + // doesn't (yet?) support out-parameters (*mut nsIFoo). The xpcom_method + // macro itself does, however, support the return value out-parameter. + (@ensure_param $name:ident) => { + let $name = match $crate::Ensure::ensure($name) { + Ok(val) => val, + Err(result) => return result, + }; + }; + + // `#[allow(non_snake_case)]` is used for each method because `$xpcom_name` + // is almost always UpperCamelCase, and Rust gives a warning that it should + // be snake_case. It isn't reasonable to rename the XPCOM methods, so + // silence the warning. + + // A method whose return value is a *mut *const nsISomething type. + // Example: foo => Foo(bar: *const nsACString, baz: bool) -> *const nsIVariant + ($rust_name:ident => $xpcom_name:ident($($param_name:ident: $param_type:ty),*) -> *const $retval:ty) => { + #[allow(non_snake_case)] + unsafe fn $xpcom_name(&self, $($param_name: $param_type,)* retval: *mut *const $retval) -> nsresult { + $(xpcom_method!(@ensure_param $param_name);)* + match self.$rust_name($($param_name, )*) { + Ok(val) => { + val.forget(&mut *retval); + NS_OK + } + Err(error) => { + error.into() + } + } + } + }; + + // A method whose return value is a *mut nsAString type. + // Example: foo => Foo(bar: *const nsACString, baz: bool) -> nsAString + ($rust_name:ident => $xpcom_name:ident($($param_name:ident: $param_type:ty),*) -> nsAString) => { + #[allow(non_snake_case)] + unsafe fn $xpcom_name(&self, $($param_name: $param_type,)* retval: *mut nsAString) -> nsresult { + $(xpcom_method!(@ensure_param $param_name);)* + match self.$rust_name($($param_name, )*) { + Ok(val) => { + (*retval).assign(&val); + NS_OK + } + Err(error) => { + error.into() + } + } + } + }; + + // A method whose return value is a *mut nsACString type. + // Example: foo => Foo(bar: *const nsACString, baz: bool) -> nsACString + ($rust_name:ident => $xpcom_name:ident($($param_name:ident: $param_type:ty),*) -> nsACString) => { + #[allow(non_snake_case)] + unsafe fn $xpcom_name(&self, $($param_name: $param_type,)* retval: *mut nsACString) -> nsresult { + $(xpcom_method!(@ensure_param $param_name);)* + match self.$rust_name($($param_name, )*) { + Ok(val) => { + (*retval).assign(&val); + NS_OK + } + Err(error) => { + error.into() + } + } + } + }; + + // A method whose return value is a non-nsA[C]String *mut type. + // Example: foo => Foo(bar: *const nsACString, baz: bool) -> bool + ($rust_name:ident => $xpcom_name:ident($($param_name:ident: $param_type:ty),*) -> $retval:ty) => { + #[allow(non_snake_case)] + unsafe fn $xpcom_name(&self, $($param_name: $param_type,)* retval: *mut $retval) -> nsresult { + $(xpcom_method!(@ensure_param $param_name);)* + match self.$rust_name($($param_name, )*) { + Ok(val) => { + *retval = val; + NS_OK + } + Err(error) => { + error.into() + } + } + } + }; + + // A method that doesn't have a return value. + // Example: foo => Foo(bar: *const nsACString, baz: bool) + ($rust_name:ident => $xpcom_name:ident($($param_name:ident: $param_type:ty),*)) => { + #[allow(non_snake_case)] + unsafe fn $xpcom_name(&self, $($param_name: $param_type,)*) -> nsresult { + $(xpcom_method!(@ensure_param $param_name);)* + match self.$rust_name($($param_name, )*) { + Ok(_) => NS_OK, + Err(error) => { + error.into() + } + } + } + }; +} + +/// A trait that ensures that a raw pointer isn't null and converts it to +/// a reference. Because of limitations in declarative macros, this includes an +/// implementation for types that are Copy, which simply returns the value +/// itself. +#[doc(hidden)] +pub trait Ensure<T> { + unsafe fn ensure(value: T) -> Self; +} + +impl<'a, T: 'a> Ensure<*const T> for Result<&'a T, nsresult> { + unsafe fn ensure(ptr: *const T) -> Result<&'a T, nsresult> { + if ptr.is_null() { + Err(NS_ERROR_NULL_POINTER) + } else { + Ok(&*ptr) + } + } +} + +impl<'a, T: 'a> Ensure<*const T> for Result<Option<&'a T>, nsresult> { + unsafe fn ensure(ptr: *const T) -> Result<Option<&'a T>, nsresult> { + Ok(if ptr.is_null() { None } else { Some(&*ptr) }) + } +} + +impl<T: Copy> Ensure<T> for Result<T, nsresult> { + unsafe fn ensure(copyable: T) -> Result<T, nsresult> { + Ok(copyable) + } +} diff --git a/xpcom/rust/xpcom/src/promise.rs b/xpcom/rust/xpcom/src/promise.rs new file mode 100644 index 0000000000..0fdab9b6aa --- /dev/null +++ b/xpcom/rust/xpcom/src/promise.rs @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::{ + create_instance, + interfaces::{nsIVariant, nsIWritableVariant}, + RefCounted, +}; + +use cstr::*; + +mod ffi { + use super::*; + + extern "C" { + // These are implemented in dom/promise/Promise.cpp + pub fn DomPromise_AddRef(promise: *const Promise); + pub fn DomPromise_Release(promise: *const Promise); + pub fn DomPromise_RejectWithVariant(promise: *const Promise, variant: *const nsIVariant); + pub fn DomPromise_ResolveWithVariant(promise: *const Promise, variant: *const nsIVariant); + } +} + +#[repr(C)] +pub struct Promise { + private: [u8; 0], + + /// This field is a phantomdata to ensure that the Promise type and any + /// struct containing it is not safe to send across threads, as DOM is + /// generally not threadsafe. + __nosync: ::std::marker::PhantomData<::std::rc::Rc<u8>>, +} + +impl Promise { + pub fn reject_with_undefined(&self) { + let variant = create_instance::<nsIWritableVariant>(cstr!("@mozilla.org/variant;1")) + .expect("Failed to create writable variant"); + unsafe { + variant.SetAsVoid(); + } + self.reject_with_variant(&variant); + } + + pub fn reject_with_variant(&self, variant: &nsIVariant) { + unsafe { ffi::DomPromise_RejectWithVariant(self, variant) } + } + + pub fn resolve_with_variant(&self, variant: &nsIVariant) { + unsafe { ffi::DomPromise_ResolveWithVariant(self, variant) } + } +} + +unsafe impl RefCounted for Promise { + unsafe fn addref(&self) { + ffi::DomPromise_AddRef(self) + } + + unsafe fn release(&self) { + ffi::DomPromise_Release(self) + } +} diff --git a/xpcom/rust/xpcom/src/reexports.rs b/xpcom/rust/xpcom/src/reexports.rs new file mode 100644 index 0000000000..147cd93e87 --- /dev/null +++ b/xpcom/rust/xpcom/src/reexports.rs @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! The automatically generated code from `xpcom_macros` depends on some types +//! which are defined in other libraries which `xpcom` depends on, but which may +//! not be `extern crate`-ed into the crate the macros are expanded into. This +//! module re-exports those types from `xpcom` so that they can be used from the +//! macro. +// re-export libc so it can be used by the procedural macro. +pub extern crate libc; + +pub use nsstring::{nsACString, nsAString, nsCString, nsString}; + +pub use nserror::{nsresult, NS_ERROR_NO_INTERFACE, NS_OK}; + +pub use std::ops::Deref; + +/// Helper method used by the xpcom codegen, it is not public API or meant for +/// calling outside of that context. +/// +/// Takes a reference to the `this` pointer received from XPIDL, and offsets and +/// casts it to a reference to the concrete rust `struct` type, `U`. +/// +/// `vtable_index` is the index, and therefore the offset in pointers, of the +/// vtable for `T` in `U`. +/// +/// A reference to `this` is taken, instead of taking `*const T` by value, to use +/// as a lifetime bound, such that the returned `&U` reference has a bounded +/// lifetime when used to call the implementation method. +#[inline] +pub unsafe fn transmute_from_vtable_ptr<'a, T, U>( + this: &'a *const T, + vtable_index: usize, +) -> &'a U { + &*((*this as *const *const ()).sub(vtable_index) as *const U) +} diff --git a/xpcom/rust/xpcom/src/refptr.rs b/xpcom/rust/xpcom/src/refptr.rs new file mode 100644 index 0000000000..52bbe08216 --- /dev/null +++ b/xpcom/rust/xpcom/src/refptr.rs @@ -0,0 +1,321 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::interfaces::nsrefcnt; +use libc; +use nserror::{nsresult, NS_OK}; +use std::cell::Cell; +use std::fmt; +use std::marker::PhantomData; +use std::mem; +use std::ops::Deref; +use std::ptr::{self, NonNull}; +use std::sync::atomic::{self, AtomicUsize, Ordering}; +use threadbound::ThreadBound; + +/// A trait representing a type which can be reference counted invasively. +/// The object is responsible for freeing its backing memory when its +/// reference count reaches 0. +pub unsafe trait RefCounted { + /// Increment the reference count. + unsafe fn addref(&self); + /// Decrement the reference count, potentially freeing backing memory. + unsafe fn release(&self); +} + +/// A smart pointer holding a RefCounted object. The object itself manages its +/// own memory. RefPtr will invoke the addref and release methods at the +/// appropriate times to facilitate the bookkeeping. +pub struct RefPtr<T: RefCounted + 'static> { + _ptr: NonNull<T>, + // Tell dropck that we own an instance of T. + _marker: PhantomData<T>, +} + +impl<T: RefCounted + 'static> RefPtr<T> { + /// Construct a new RefPtr from a reference to the refcounted object. + #[inline] + pub fn new(p: &T) -> RefPtr<T> { + unsafe { + p.addref(); + } + RefPtr { + _ptr: p.into(), + _marker: PhantomData, + } + } + + /// Construct a RefPtr from a raw pointer, addrefing it. + #[inline] + pub unsafe fn from_raw(p: *const T) -> Option<RefPtr<T>> { + let ptr = NonNull::new(p as *mut T)?; + ptr.as_ref().addref(); + Some(RefPtr { + _ptr: ptr, + _marker: PhantomData, + }) + } + + /// Construct a RefPtr from a raw pointer, without addrefing it. + #[inline] + pub unsafe fn from_raw_dont_addref(p: *const T) -> Option<RefPtr<T>> { + Some(RefPtr { + _ptr: NonNull::new(p as *mut T)?, + _marker: PhantomData, + }) + } + + /// Write this RefPtr's value into an outparameter. + #[inline] + pub fn forget(self, into: &mut *const T) { + *into = Self::forget_into_raw(self); + } + + #[inline] + pub fn forget_into_raw(this: RefPtr<T>) -> *const T { + let into = &*this as *const T; + mem::forget(this); + into + } +} + +impl<T: RefCounted + 'static> Deref for RefPtr<T> { + type Target = T; + #[inline] + fn deref(&self) -> &T { + unsafe { self._ptr.as_ref() } + } +} + +impl<T: RefCounted + 'static> Drop for RefPtr<T> { + #[inline] + fn drop(&mut self) { + unsafe { + self._ptr.as_ref().release(); + } + } +} + +impl<T: RefCounted + 'static> Clone for RefPtr<T> { + #[inline] + fn clone(&self) -> RefPtr<T> { + RefPtr::new(self) + } +} + +impl<T: RefCounted + 'static + fmt::Debug> fmt::Debug for RefPtr<T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "RefPtr<{:?}>", self.deref()) + } +} + +// Both `Send` and `Sync` bounds are required for `RefPtr<T>` to implement +// either, as sharing a `RefPtr<T>` also allows transferring ownership, and +// vice-versa. +unsafe impl<T: RefCounted + 'static + Send + Sync> Send for RefPtr<T> {} +unsafe impl<T: RefCounted + 'static + Send + Sync> Sync for RefPtr<T> {} + +/// A wrapper that binds a RefCounted value to its original thread, +/// preventing retrieval from other threads and panicking if the value +/// is dropped on a different thread. +/// +/// These limitations enable values of this type to be Send + Sync, which is +/// useful when creating a struct that holds a RefPtr<T> type while being +/// Send + Sync. Such a struct can hold a ThreadBoundRefPtr<T> type instead. +pub struct ThreadBoundRefPtr<T: RefCounted + 'static>(ThreadBound<*const T>); + +impl<T: RefCounted + 'static> ThreadBoundRefPtr<T> { + pub fn new(ptr: RefPtr<T>) -> Self { + let raw: *const T = &*ptr; + mem::forget(ptr); + ThreadBoundRefPtr(ThreadBound::new(raw)) + } + + pub fn get_ref(&self) -> Option<&T> { + self.0.get_ref().map(|raw| unsafe { &**raw }) + } +} + +impl<T: RefCounted + 'static> Drop for ThreadBoundRefPtr<T> { + fn drop(&mut self) { + unsafe { + RefPtr::from_raw_dont_addref(self.get_ref().expect("drop() called on wrong thread!")); + } + } +} + +/// A helper struct for constructing `RefPtr<T>` from raw pointer outparameters. +/// Holds a `*const T` internally which will be released if non null when +/// destructed, and can be easily transformed into an `Option<RefPtr<T>>`. +/// +/// It many cases it may be easier to use the `getter_addrefs` method. +pub struct GetterAddrefs<T: RefCounted + 'static> { + _ptr: *const T, + _marker: PhantomData<T>, +} + +impl<T: RefCounted + 'static> GetterAddrefs<T> { + /// Create a `GetterAddrefs`, initializing it with the null pointer. + #[inline] + pub fn new() -> GetterAddrefs<T> { + GetterAddrefs { + _ptr: ptr::null(), + _marker: PhantomData, + } + } + + /// Get a reference to the internal `*const T`. This method is unsafe, + /// as the destructor of this class depends on the internal `*const T` + /// being either a valid reference to a value of type `T`, or null. + #[inline] + pub unsafe fn ptr(&mut self) -> &mut *const T { + &mut self._ptr + } + + /// Get a reference to the internal `*const T` as a `*mut libc::c_void`. + /// This is useful to pass to functions like `GetInterface` which take a + /// void pointer outparameter. + #[inline] + pub unsafe fn void_ptr(&mut self) -> *mut *mut libc::c_void { + &mut self._ptr as *mut *const T as *mut *mut libc::c_void + } + + /// Transform this `GetterAddrefs` into an `Option<RefPtr<T>>`, without + /// performing any addrefs or releases. + #[inline] + pub fn refptr(self) -> Option<RefPtr<T>> { + let p = self._ptr; + // Don't run the destructor because we don't want to release the stored + // pointer. + mem::forget(self); + unsafe { RefPtr::from_raw_dont_addref(p) } + } +} + +impl<T: RefCounted + 'static> Drop for GetterAddrefs<T> { + #[inline] + fn drop(&mut self) { + if !self._ptr.is_null() { + unsafe { + (*self._ptr).release(); + } + } + } +} + +/// Helper method for calling XPCOM methods which return a reference counted +/// value through an outparameter. Takes a lambda, which is called with a valid +/// outparameter argument (`*mut *const T`), and returns a `nsresult`. Returns +/// either a `RefPtr<T>` with the value returned from the outparameter, or a +/// `nsresult`. +/// +/// # NOTE: +/// +/// Can return `Err(NS_OK)` if the call succeeded, but the outparameter was set +/// to NULL. +/// +/// # Usage +/// +/// ``` +/// let x: Result<RefPtr<T>, nsresult> = +/// getter_addrefs(|p| iosvc.NewURI(uri, ptr::null(), ptr::null(), p)); +/// ``` +#[inline] +pub fn getter_addrefs<T: RefCounted, F>(f: F) -> Result<RefPtr<T>, nsresult> +where + F: FnOnce(*mut *const T) -> nsresult, +{ + let mut ga = GetterAddrefs::<T>::new(); + let rv = f(unsafe { ga.ptr() }); + if rv.failed() { + return Err(rv); + } + ga.refptr().ok_or(NS_OK) +} + +/// The type of the reference count type for xpcom structs. +/// +/// `#[derive(xpcom)]` will use this type for the `__refcnt` field when +/// `#[refcnt = "nonatomic"]` is used. +#[derive(Debug)] +pub struct Refcnt(Cell<nsrefcnt>); +impl Refcnt { + /// Create a new reference count value. This is unsafe as manipulating + /// Refcnt values is an easy footgun. + pub unsafe fn new() -> Self { + Refcnt(Cell::new(0)) + } + + /// Increment the reference count. Returns the new reference count. This is + /// unsafe as modifying this value can cause a use-after-free. + pub unsafe fn inc(&self) -> nsrefcnt { + // XXX: Checked add? + let new = self.0.get() + 1; + self.0.set(new); + new + } + + /// Decrement the reference count. Returns the new reference count. This is + /// unsafe as modifying this value can cause a use-after-free. + pub unsafe fn dec(&self) -> nsrefcnt { + // XXX: Checked sub? + let new = self.0.get() - 1; + self.0.set(new); + new + } + + /// Get the current value of the reference count. + pub fn get(&self) -> nsrefcnt { + self.0.get() + } +} + +/// The type of the atomic reference count used for xpcom structs. +/// +/// `#[derive(xpcom)]` will use this type for the `__refcnt` field when +/// `#[refcnt = "atomic"]` is used. +/// +/// See `nsISupportsImpl.h`'s `ThreadSafeAutoRefCnt` class for reasoning behind +/// memory ordering decisions. +#[derive(Debug)] +pub struct AtomicRefcnt(AtomicUsize); +impl AtomicRefcnt { + /// Create a new reference count value. This is unsafe as manipulating + /// Refcnt values is an easy footgun. + pub unsafe fn new() -> Self { + AtomicRefcnt(AtomicUsize::new(0)) + } + + /// Increment the reference count. Returns the new reference count. This is + /// unsafe as modifying this value can cause a use-after-free. + pub unsafe fn inc(&self) -> nsrefcnt { + self.0.fetch_add(1, Ordering::Relaxed) as nsrefcnt + 1 + } + + /// Decrement the reference count. Returns the new reference count. This is + /// unsafe as modifying this value can cause a use-after-free. + pub unsafe fn dec(&self) -> nsrefcnt { + let result = self.0.fetch_sub(1, Ordering::Release) as nsrefcnt - 1; + if result == 0 { + // We're going to destroy the object on this thread, so we need + // acquire semantics to synchronize with the memory released by + // the last release on other threads, that is, to ensure that + // writes prior to that release are now visible on this thread. + if cfg!(feature = "thread_sanitizer") { + // TSan doesn't understand atomic::fence, so in order to avoid + // a false positive for every time a refcounted object is + // deleted, we replace the fence with an atomic operation. + self.0.load(Ordering::Acquire); + } else { + atomic::fence(Ordering::Acquire); + } + } + result + } + + /// Get the current value of the reference count. + pub fn get(&self) -> nsrefcnt { + self.0.load(Ordering::Acquire) as nsrefcnt + } +} diff --git a/xpcom/rust/xpcom/src/services.rs b/xpcom/rust/xpcom/src/services.rs new file mode 100644 index 0000000000..ef780da911 --- /dev/null +++ b/xpcom/rust/xpcom/src/services.rs @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! This module contains convenient accessors for XPCOM services. +//! +//! The contents of this file are generated from `xpcom/build/Services.py`. + +include!(concat!(env!("MOZ_TOPOBJDIR"), "/xpcom/build/services.rs")); diff --git a/xpcom/rust/xpcom/src/statics.rs b/xpcom/rust/xpcom/src/statics.rs new file mode 100644 index 0000000000..06c2adc560 --- /dev/null +++ b/xpcom/rust/xpcom/src/statics.rs @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::interfaces::{nsIComponentManager, nsIComponentRegistrar, nsIServiceManager}; +use crate::{GetterAddrefs, RefPtr, XpCom}; +use std::ffi::CStr; +use std::ptr; + +/// Get a reference to the global `nsIComponentManager`. +/// +/// Can return `None` during shutdown. +#[inline] +pub fn component_manager() -> Option<RefPtr<nsIComponentManager>> { + unsafe { RefPtr::from_raw(Gecko_GetComponentManager()) } +} + +/// Get a reference to the global `nsIServiceManager`. +/// +/// Can return `None` during shutdown. +#[inline] +pub fn service_manager() -> Option<RefPtr<nsIServiceManager>> { + unsafe { RefPtr::from_raw(Gecko_GetServiceManager()) } +} + +/// Get a reference to the global `nsIComponentRegistrar` +/// +/// Can return `None` during shutdown. +#[inline] +pub fn component_registrar() -> Option<RefPtr<nsIComponentRegistrar>> { + unsafe { RefPtr::from_raw(Gecko_GetComponentRegistrar()) } +} + +/// Helper for calling `nsIComponentManager::CreateInstanceByContractID` on the +/// global `nsIComponentRegistrar`. +/// +/// This method is similar to `do_CreateInstance` in C++. +#[inline] +pub fn create_instance<T: XpCom>(id: &CStr) -> Option<RefPtr<T>> { + unsafe { + let mut ga = GetterAddrefs::<T>::new(); + if component_manager()? + .CreateInstanceByContractID(id.as_ptr(), ptr::null(), &T::IID, ga.void_ptr()) + .succeeded() + { + ga.refptr() + } else { + None + } + } +} + +/// Helper for calling `nsIServiceManager::GetServiceByContractID` on the global +/// `nsIServiceManager`. +/// +/// This method is similar to `do_GetService` in C++. +#[inline] +pub fn get_service<T: XpCom>(id: &CStr) -> Option<RefPtr<T>> { + unsafe { + let mut ga = GetterAddrefs::<T>::new(); + if service_manager()? + .GetServiceByContractID(id.as_ptr(), &T::IID, ga.void_ptr()) + .succeeded() + { + ga.refptr() + } else { + None + } + } +} + +extern "C" { + fn Gecko_GetComponentManager() -> *const nsIComponentManager; + fn Gecko_GetServiceManager() -> *const nsIServiceManager; + fn Gecko_GetComponentRegistrar() -> *const nsIComponentRegistrar; +} diff --git a/xpcom/rust/xpcom/xpcom_macros/Cargo.toml b/xpcom/rust/xpcom/xpcom_macros/Cargo.toml new file mode 100644 index 0000000000..48ccb830de --- /dev/null +++ b/xpcom/rust/xpcom/xpcom_macros/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "xpcom_macros" +version = "0.1.0" +authors = ["Nika Layzell <nika@thelayzells.com>"] +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +syn = "1" +quote = "1" +proc-macro2 = "1" +lazy_static = "1.0" diff --git a/xpcom/rust/xpcom/xpcom_macros/src/lib.rs b/xpcom/rust/xpcom/xpcom_macros/src/lib.rs new file mode 100644 index 0000000000..e2ef2033c9 --- /dev/null +++ b/xpcom/rust/xpcom/xpcom_macros/src/lib.rs @@ -0,0 +1,774 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! This crate provides the `#[derive(xpcom)]` custom derive. This custom derive +//! is used in order to implement [`xpcom`] interfaces. +//! +//! # Usage +//! +//! The easiest way to explain this crate is probably with a usage example. I'll +//! show you the example, and then we'll destructure it and walk through what +//! each component is doing. +//! +//! ```ignore +//! // Declaring an XPCOM Struct +//! #[derive(xpcom)] +//! #[xpimplements(nsIRunnable)] +//! #[refcnt = "atomic"] +//! struct InitImplRunnable { +//! i: i32, +//! } +//! +//! // Implementing methods on an XPCOM Struct +//! impl ImplRunnable { +//! unsafe fn Run(&self) -> nsresult { +//! println!("{}", self.i); +//! NS_OK +//! } +//! } +//! ``` +//! +//! ## Declaring an XPCOM Struct +//! +//! ```ignore +//! // This derive should be placed on the initialization struct in order to +//! // trigger the procedural macro. +//! #[derive(xpcom)] +//! +//! // The xpimplements attribute should be passed the names of the IDL +//! // interfaces which you want to implement. These can be separated by commas +//! // if you want to implement multiple interfaces. +//! // +//! // Some methods use types which we cannot bind to in rust. Interfaces +//! // like those cannot be implemented, and a compile-time error will occur +//! // if they are listed in this attribute. +//! #[xpimplements(nsIRunnable)] +//! +//! // The refcnt attribute can have one of the following values: +//! // * "atomic" == atomic reference count +//! // ~= NS_DECL_THREADSAFE_ISUPPORTS in C++ +//! // * "nonatomic" == non atomic reference count +//! // ~= NS_DECL_ISUPPORTS in C++ +//! #[refcnt = "atomic"] +//! +//! // The struct with the attribute on its name must start with `Init`. +//! // The custom derive will define the actual underlying struct. For +//! // example, placing the derive on a struct named `InitFoo` will cause +//! // an underlying `Foo` struct to be generated. +//! // +//! // It is a compile time error to put the `#[derive(xpcom)]` derive on +//! // an enum, union, or tuple struct. +//! struct InitImplRunnable { +//! // Fields in the `Init` struct will also be in the underlying struct. +//! i: i32, +//! } +//! ``` +//! +//! The above example will generate an underlying `ImplRunnable` struct, which will implement +//! the [`nsIRunnable`] XPCOM interface. The following methods will be +//! automatically implemented on it: +//! +//! ```ignore +//! // Automatic nsISupports implementation +//! unsafe fn AddRef(&self) -> nsrefcnt; +//! unsafe fn Release(&self) -> nsrefcnt; +//! unsafe fn QueryInterface(&self, uuid: &nsIID, result: *mut *mut libc::c_void) -> nsresult; +//! +//! // Allocates and initializes a new instance of this type. The values will +//! // be moved from the `Init` struct which is passed in. +//! fn allocate(init: InitImplRunnable) -> RefPtr<Self>; +//! +//! // Helper for performing the `query_interface` operation to case to a +//! // specific interface. +//! fn query_interface<T: XpCom>(&self) -> Option<RefPtr<T>>; +//! +//! // Coerce function for cheaply casting to our base interfaces. +//! fn coerce<T: ImplRunnableCoerce>(&self) -> &T; +//! ``` +//! +//! The [`RefCounted`] interface will also be implemented, so that the type can +//! be used within the [`RefPtr`] type. +//! +//! The `coerce` and `query_interface` methods are backed by the generated +//! `*Coerce` trait. This trait is impl-ed for every interface implemented by +//! the trait. For example: +//! +//! ```ignore +//! pub trait ImplRunnableCoerce { +//! fn coerce_from(x: &ImplRunnable) -> &Self; +//! } +//! impl ImplRunnableCoerce for nsIRunnable { .. } +//! impl ImplRunnableCoerce for nsISupports { .. } +//! ``` +//! +//! ## Implementing methods on an XPCOM Struct +//! +//! ```ignore +//! // Methods should be implemented directly on the generated struct. All +//! // methods other than `AddRef`, `Release`, and `QueryInterface` must be +//! // implemented manually. +//! impl ImplRunnable { +//! // The method should have the same name as the corresponding C++ method. +//! unsafe fn Run(&self) -> nsresult { +//! // Fields defined on the `Init` struct will be directly on the +//! // generated struct. +//! println!("{}", self.i); +//! NS_OK +//! } +//! } +//! ``` +//! +//! XPCOM methods implemented in Rust have signatures similar to methods +//! implemented in C++. +//! +//! ```ignore +//! // nsISupports foo(in long long bar, in AString baz); +//! unsafe fn Foo(&self, bar: i64, baz: *const nsAString, +//! _retval: *mut *const nsISupports) -> nsresult; +//! +//! // AString qux(in nsISupports ham); +//! unsafe fn Qux(&self, ham: *const nsISupports, +//! _retval: *mut nsAString) -> nsresult; +//! ``` +//! +//! This is a little tedious, so the `xpcom_method!` macro provides a convenient +//! way to generate wrappers around more idiomatic Rust methods. +//! +//! [`xpcom`]: ../xpcom/index.html +//! [`nsIRunnable`]: ../xpcom/struct.nsIRunnable.html +//! [`RefCounted`]: ../xpcom/struct.RefCounted.html +//! [`RefPtr`]: ../xpcom/struct.RefPtr.html + +use lazy_static::lazy_static; +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote, ToTokens}; +use std::collections::{HashMap, HashSet}; +use syn::punctuated::Punctuated; +use syn::{ + parse_macro_input, parse_quote, Attribute, Data, DataStruct, DeriveInput, Field, Fields, Ident, + Lit, Meta, NestedMeta, Token, Type, +}; + +macro_rules! bail { + (@($t:expr), $s:expr) => { + return Err(syn::Error::new_spanned(&$t, &$s[..])); + }; + (@($t:expr), $f:expr, $($e:expr),*) => { + return Err(syn::Error::new_spanned(&$t, &format!($f, $($e),*)[..])); + }; + ($s:expr) => { + return Err(syn::Error::new(Span::call_site(), &$s[..])); + }; + ($f:expr, $($e:expr),*) => { + return Err(syn::Error::new(Span::call_site(), &format!($f, $($e),*)[..])); + }; +} + +/* These are the structs generated by the rust_macros.py script */ + +/// A single parameter to an XPCOM method. +#[derive(Debug)] +struct Param { + name: &'static str, + ty: &'static str, +} + +/// A single method on an XPCOM interface. +#[derive(Debug)] +struct Method { + name: &'static str, + params: &'static [Param], + ret: &'static str, +} + +/// An XPCOM interface. `methods` will be `Err("reason")` if the interface +/// cannot be implemented in rust code. +#[derive(Debug)] +struct Interface { + name: &'static str, + base: Option<&'static str>, + methods: Result<&'static [Method], &'static str>, +} + +impl Interface { + fn base(&self) -> Option<&'static Interface> { + Some(IFACES[self.base?]) + } + + fn methods(&self) -> Result<&'static [Method], syn::Error> { + match self.methods { + Ok(methods) => Ok(methods), + Err(reason) => Err(syn::Error::new( + Span::call_site(), + &format!( + "Interface {} cannot be implemented in rust \ + because {} is not supported yet", + self.name, reason + ), + )), + } + } +} + +lazy_static! { + /// This item contains the information generated by the procedural macro in + /// the form of a `HashMap` from interface names to their descriptions. + static ref IFACES: HashMap<&'static str, &'static Interface> = { + let lists: &[&[Interface]] = + include!(concat!(env!("MOZ_TOPOBJDIR"), "/dist/xpcrs/bt/all.rs")); + + let mut hm = HashMap::new(); + for &list in lists { + for iface in list { + hm.insert(iface.name, iface); + } + } + hm + }; +} + +/// The type of the reference count to use for the struct. +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +enum RefcntKind { + Atomic, + NonAtomic, +} + +/// Produces the tokens for the type representation. +impl ToTokens for RefcntKind { + fn to_tokens(&self, tokens: &mut TokenStream) { + match *self { + RefcntKind::NonAtomic => quote!(xpcom::Refcnt).to_tokens(tokens), + RefcntKind::Atomic => quote!(xpcom::AtomicRefcnt).to_tokens(tokens), + } + } +} + +/// Scans through the attributes on a struct, and extracts the type of the refcount to use. +fn get_refcnt_kind(attrs: &[Attribute]) -> Result<RefcntKind, syn::Error> { + for attr in attrs { + if let Meta::NameValue(syn::MetaNameValue { + ref path, ref lit, .. + }) = attr.parse_meta()? + { + if !path.is_ident("refcnt") { + continue; + } + + let value = if let Lit::Str(ref s) = lit { + s.value() + } else { + bail!(@(attr), "Unexpected non-string value in #[refcnt]"); + }; + + return if value == "nonatomic" { + Ok(RefcntKind::NonAtomic) + } else if value == "atomic" { + Ok(RefcntKind::Atomic) + } else { + bail!(@(attr), "Unexpected value in #[refcnt]. \ + Expected `nonatomic`, or `atomic`"); + }; + } + } + + bail!("Expected #[refcnt] attribute") +} + +/// Scan the attributes looking for an #[xpimplements] attribute. The identifier +/// arguments passed to this attribute are the interfaces which the type wants to +/// directly implement. +fn get_bases(attrs: &[Attribute]) -> Result<Vec<&'static Interface>, syn::Error> { + let mut inherits = Vec::new(); + for attr in attrs { + if let Meta::List(syn::MetaList { + ref path, + ref nested, + .. + }) = attr.parse_meta()? + { + if !path.is_ident("xpimplements") { + continue; + } + + for item in nested.iter() { + let iface = match *item { + NestedMeta::Meta(syn::Meta::Path(ref iface)) => iface, + _ => bail!(@(attr), "Unexpected non-identifier in #[xpimplements(..)]"), + }; + let ident = match iface.get_ident() { + Some(ref iface) => iface.to_string(), + _ => bail!(@(attr), "Too many components in xpimplements path"), + }; + if let Some(&iface) = IFACES.get(ident.as_str()) { + inherits.push(iface); + } else { + bail!(@(attr), "Unexpected invalid base interface `{}` in #[xpimplements(..)]", ident); + } + } + } + } + Ok(inherits) +} + +/// Extract the fields list from the input struct. +fn get_fields(di: &DeriveInput) -> Result<&Punctuated<Field, Token![,]>, syn::Error> { + match di.data { + Data::Struct(DataStruct { + fields: Fields::Named(ref named), + .. + }) => Ok(&named.named), + _ => bail!(@(di), "The initializer struct must be a standard named \ + value struct definition"), + } +} + +/// Takes the `Init*` struct in, and generates a `DeriveInput` for the "real" struct. +fn gen_real_struct( + init: &DeriveInput, + bases: &[&Interface], + refcnt_ty: RefcntKind, +) -> Result<DeriveInput, syn::Error> { + // Determine the name for the real struct based on the name of the + // initializer struct's name. + if !init.ident.to_string().starts_with("Init") { + bail!(@(init.ident), "The target struct's name must begin with Init"); + } + let name = Ident::new(&init.ident.to_string()[4..], init.ident.span()); + let vis = &init.vis; + + let bases = bases.iter().map(|base| { + let ident = format_ident!("__base_{}", base.name); + let vtable = format_ident!("{}VTable", base.name); + quote!(#ident : *const xpcom::interfaces::#vtable) + }); + + let fields = get_fields(init)?; + let (impl_generics, _, where_clause) = init.generics.split_for_impl(); + Ok(parse_quote! { + #[repr(C)] + #vis struct #name #impl_generics #where_clause { + #(#bases,)* + __refcnt: #refcnt_ty, + #fields + } + }) +} + +/// Generates the `extern "system"` methods which are actually included in the +/// VTable for the given interface. +/// +/// `idx` must be the offset in pointers of the pointer to this vtable in the +/// struct `real`. This is soundness-critical, as it will be used to offset +/// pointers received from xpcom back to the concrete implementation. +fn gen_vtable_methods( + real: &DeriveInput, + iface: &Interface, + vtable_index: usize, +) -> Result<TokenStream, syn::Error> { + let base_ty = format_ident!("{}", iface.name); + + let base_methods = if let Some(base) = iface.base() { + gen_vtable_methods(real, base, vtable_index)? + } else { + quote! {} + }; + + let ty_name = &real.ident; + let (impl_generics, ty_generics, where_clause) = real.generics.split_for_impl(); + + let mut method_defs = Vec::new(); + for method in iface.methods()? { + let ret = syn::parse_str::<Type>(method.ret)?; + + let mut params = Vec::new(); + let mut args = Vec::new(); + for param in method.params { + let name = format_ident!("{}", param.name); + let ty = syn::parse_str::<Type>(param.ty)?; + + params.push(quote! {#name : #ty,}); + args.push(quote! {#name,}); + } + + let name = format_ident!("{}", method.name); + method_defs.push(quote! { + unsafe extern "system" fn #name #impl_generics ( + this: *const #base_ty, #(#params)* + ) -> #ret #where_clause { + let this: &#ty_name #ty_generics = + ::xpcom::reexports::transmute_from_vtable_ptr(&this, #vtable_index); + this.#name(#(#args)*) + } + }); + } + + Ok(quote! { + #base_methods + #(#method_defs)* + }) +} + +/// Generates the VTable for a given base interface. This assumes that the +/// implementations of each of the `extern "system"` methods are in scope. +fn gen_inner_vtable(real: &DeriveInput, iface: &Interface) -> Result<TokenStream, syn::Error> { + let vtable_ty = format_ident!("{}VTable", iface.name); + + // Generate the vtable for the base interface. + let base_vtable = if let Some(base) = iface.base() { + let vt = gen_inner_vtable(real, base)?; + quote! {__base: #vt,} + } else { + quote! {} + }; + + // Include each of the method definitions for this interface. + let (_, ty_generics, _) = real.generics.split_for_impl(); + let turbofish = ty_generics.as_turbofish(); + let vtable_init = iface + .methods()? + .into_iter() + .map(|method| { + let name = format_ident!("{}", method.name); + quote! { #name : #name #turbofish, } + }) + .collect::<Vec<_>>(); + + Ok(quote!(#vtable_ty { + #base_vtable + #(#vtable_init)* + })) +} + +fn gen_root_vtable( + real: &DeriveInput, + base: &Interface, + idx: usize, +) -> Result<TokenStream, syn::Error> { + let field = format_ident!("__base_{}", base.name); + let vtable_ty = format_ident!("{}VTable", base.name); + + let (impl_generics, ty_generics, where_clause) = real.generics.split_for_impl(); + let turbofish = ty_generics.as_turbofish(); + + let methods = gen_vtable_methods(real, base, idx)?; + let vtable = gen_inner_vtable(real, base)?; + + // Define the `recover_self` method. This performs an offset calculation to + // recover a pointer to the original struct from a pointer to the given + // VTable field. + Ok(quote! {#field: { + // The method implementations which will be used to build the vtable. + #methods + + // The actual VTable definition. This is in a separate method in order + // to allow it to be generic. + #[inline] + fn get_vtable #impl_generics () -> &'static #vtable_ty #where_clause { + &#vtable + } + get_vtable #turbofish () + },}) +} + +/// Generate the cast implementations. This generates the implementation details +/// for the `Coerce` trait, and the `QueryInterface` method. The first return +/// value is the `QueryInterface` implementation, and the second is the `Coerce` +/// implementation. +fn gen_casts( + seen: &mut HashSet<&'static str>, + iface: &Interface, + real: &DeriveInput, + coerce_name: &Ident, + vtable_field: &Ident, +) -> Result<(TokenStream, TokenStream), syn::Error> { + if !seen.insert(iface.name) { + return Ok((quote! {}, quote! {})); + } + + // Generate the cast implementations for the base interfaces. + let (base_qi, base_coerce) = if let Some(base) = iface.base() { + gen_casts(seen, base, real, coerce_name, vtable_field)? + } else { + (quote! {}, quote! {}) + }; + + // Add the if statment to QueryInterface for the base class. + let base_name = format_ident!("{}", iface.name); + + let qi = quote! { + #base_qi + if *uuid == #base_name::IID { + // Implement QueryInterface in terms of coersions. + self.addref(); + *result = self.coerce::<#base_name>() + as *const #base_name + as *const ::xpcom::reexports::libc::c_void + as *mut ::xpcom::reexports::libc::c_void; + return ::xpcom::reexports::NS_OK; + } + }; + + // Add an implementation of the `*Coerce` trait for the base interface. + let name = &real.ident; + let (impl_generics, ty_generics, where_clause) = real.generics.split_for_impl(); + let coerce = quote! { + #base_coerce + + impl #impl_generics #coerce_name #ty_generics for ::xpcom::interfaces::#base_name #where_clause { + fn coerce_from(v: &#name #ty_generics) -> &Self { + unsafe { + // Get the address of the VTable field. This should be a + // pointer to a pointer to a vtable, which we can then cast + // into a pointer to our interface. + &*(&(v.#vtable_field) + as *const *const _ + as *const ::xpcom::interfaces::#base_name) + } + } + } + }; + + Ok((qi, coerce)) +} + +fn check_generics(generics: &syn::Generics) -> Result<(), syn::Error> { + for param in &generics.params { + let tp = match param { + syn::GenericParam::Type(tp) => tp, + syn::GenericParam::Lifetime(lp) => bail!( + @(lp), + "Cannot #[derive(xpcom)] on types with lifetime parameters. \ + Implementors of XPCOM interfaces must not contain non-'static \ + lifetimes.", + ), + // XXX: Once const generics become stable, it may be as simple as + // removing this bail! to support them. + syn::GenericParam::Const(cp) => { + bail!(@(cp), "Cannot #[derive(xpcom)] on types with const generics.") + } + }; + + let mut static_lt = false; + for bound in &tp.bounds { + match bound { + syn::TypeParamBound::Lifetime(lt) if lt.ident == "static" => { + static_lt = true; + break; + } + _ => {} + } + } + + if !static_lt { + bail!( + @(param), + "Every generic parameter for xpcom implementation must have a \ + 'static lifetime bound declared in the generics. Implicit \ + lifetime bounds or lifetime bounds in where clauses are not \ + detected by the macro and will be ignored. \ + Implementors of XPCOM interfaces must not contain non-'static \ + lifetimes.", + ); + } + } + Ok(()) +} + +/// The root xpcom procedural macro definition. +fn xpcom(init: DeriveInput) -> Result<TokenStream, syn::Error> { + check_generics(&init.generics)?; + + let bases = get_bases(&init.attrs)?; + if bases.is_empty() { + bail!( + "Types with #[derive(xpcom)] must implement at least one \ + interface. Interfaces can be implemented by adding the \ + #[xpimplements(nsIFoo, nsIBar)] attribute to the struct \ + declaration." + ); + } + + // Ensure that all our base interface methods have unique names. + let mut method_names = HashMap::new(); + for base in &bases { + for method in base.methods()? { + if let Some(existing) = method_names.insert(method.name, base.name) { + bail!( + "The method `{0}` is declared on both `{1}` and `{2}`, + but a Rust type cannot implement two methods with the \ + same name. You can add the `[binaryname(Renamed{0})]` \ + XPIDL attribute to one of the declarations to rename it.", + method.name, + existing, + base.name + ); + } + } + } + + // Determine what reference count type to use, and generate the real struct. + let refcnt_ty = get_refcnt_kind(&init.attrs)?; + let real = gen_real_struct(&init, &bases, refcnt_ty)?; + + let name_init = &init.ident; + let name = &real.ident; + let coerce_name = format_ident!("{}Coerce", name); + + // Generate a VTable for each of the base interfaces. + let mut vtables = Vec::new(); + for (idx, base) in bases.iter().enumerate() { + vtables.push(gen_root_vtable(&real, base, idx)?); + } + + // Generate the field initializers for the final struct, moving each field + // out of the original __init struct. + let inits = get_fields(&init)?.iter().map(|field| { + let id = &field.ident; + quote! { #id : __init.#id, } + }); + + let vis = &real.vis; + + // Generate the implementation for QueryInterface and Coerce. + let mut seen = HashSet::new(); + let mut qi_impl = Vec::new(); + let mut coerce_impl = Vec::new(); + for base in &bases { + let (qi, coerce) = gen_casts( + &mut seen, + base, + &real, + &coerce_name, + &format_ident!("__base_{}", base.name), + )?; + qi_impl.push(qi); + coerce_impl.push(coerce); + } + + let (impl_generics, ty_generics, where_clause) = real.generics.split_for_impl(); + Ok(quote! { + #real + + impl #impl_generics #name #ty_generics #where_clause { + /// This method is used for + fn allocate(__init: #name_init #ty_generics) -> ::xpcom::RefPtr<Self> { + #[allow(unused_imports)] + use ::xpcom::*; + #[allow(unused_imports)] + use ::xpcom::interfaces::*; + #[allow(unused_imports)] + use ::xpcom::reexports::{ + libc, nsACString, nsAString, nsCString, nsString, nsresult + }; + + // Helper for asserting that for all instantiations, this + // object has the 'static lifetime. + fn xpcom_types_must_be_static<T: 'static>(t: &T) {} + + unsafe { + // NOTE: This is split into multiple lines to make the + // output more readable. + let value = #name { + #(#vtables)* + __refcnt: #refcnt_ty::new(), + #(#inits)* + }; + let boxed = ::std::boxed::Box::new(value); + xpcom_types_must_be_static(&*boxed); + let raw = ::std::boxed::Box::into_raw(boxed); + ::xpcom::RefPtr::from_raw(raw).unwrap() + } + } + + /// Automatically generated implementation of AddRef for nsISupports. + #vis unsafe fn AddRef(&self) -> ::xpcom::interfaces::nsrefcnt { + self.__refcnt.inc() + } + + /// Automatically generated implementation of Release for nsISupports. + #vis unsafe fn Release(&self) -> ::xpcom::interfaces::nsrefcnt { + let new = self.__refcnt.dec(); + if new == 0 { + // dealloc + ::std::boxed::Box::from_raw(self as *const Self as *mut Self); + } + new + } + + /// Automatically generated implementation of QueryInterface for + /// nsISupports. + #vis unsafe fn QueryInterface(&self, + uuid: *const ::xpcom::nsIID, + result: *mut *mut ::xpcom::reexports::libc::c_void) + -> ::xpcom::reexports::nsresult { + #[allow(unused_imports)] + use ::xpcom::*; + #[allow(unused_imports)] + use ::xpcom::interfaces::*; + + #(#qi_impl)* + + ::xpcom::reexports::NS_ERROR_NO_INTERFACE + } + + /// Perform a QueryInterface call on this object, attempting to + /// dynamically cast it to the requested interface type. Returns + /// Some(RefPtr<T>) if the cast succeeded, and None otherwise. + #vis fn query_interface<XPCOM_InterfaceType: ::xpcom::XpCom>(&self) + -> ::std::option::Option<::xpcom::RefPtr<XPCOM_InterfaceType>> + { + let mut ga = ::xpcom::GetterAddrefs::<XPCOM_InterfaceType>::new(); + unsafe { + if self.QueryInterface(&XPCOM_InterfaceType::IID, ga.void_ptr()).succeeded() { + ga.refptr() + } else { + None + } + } + } + + /// Coerce this type safely to any of the interfaces which it + /// implements without `AddRef`ing it. + #vis fn coerce<XPCOM_InterfaceType: #coerce_name #ty_generics>(&self) -> &XPCOM_InterfaceType { + XPCOM_InterfaceType::coerce_from(self) + } + } + + /// This trait is implemented on the interface types which this + /// `#[derive(xpcom)]` type can be safely ane cheaply coerced to using + /// the `coerce` method. + /// + /// The trait and its method should usually not be used directly, but + /// rather acts as a trait bound and implementation for the `coerce` + /// methods. + #[doc(hidden)] + #vis trait #coerce_name #impl_generics #where_clause { + /// Convert a value of the `#[derive(xpcom)]` type into the + /// implementing interface type. + fn coerce_from(v: &#name #ty_generics) -> &Self; + } + + #(#coerce_impl)* + + unsafe impl #impl_generics ::xpcom::RefCounted for #name #ty_generics #where_clause { + unsafe fn addref(&self) { + self.AddRef(); + } + + unsafe fn release(&self) { + self.Release(); + } + } + }) +} + +#[proc_macro_derive(xpcom, attributes(xpimplements, refcnt))] +pub fn xpcom_internal(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + match xpcom(input) { + Ok(ts) => ts.into(), + Err(err) => err.to_compile_error().into(), + } +} |