diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
commit | 9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /xpcom/rust | |
parent | Initial commit. (diff) | |
download | thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.tar.xz thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'xpcom/rust')
42 files changed, 6714 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..2f8ca84a97 --- /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.10", default-features = false, features = ["color"]} # 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..5da85b9d89 --- /dev/null +++ b/xpcom/rust/gecko_logger/src/lib.rs @@ -0,0 +1,256 @@ +/* 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 unsafe 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 = 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..e398725260 --- /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>"] +license = "MPL-2.0" + +[dependencies] +semver = "1.0" +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..c519d7e002 --- /dev/null +++ b/xpcom/rust/gkrust_utils/src/lib.rs @@ -0,0 +1,24 @@ +/* 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; +use nsstring::nsACString; + +#[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..41152470c7 --- /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 MOZ_UNANNOTATED; +}; + +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..1aba69f01f --- /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.is_empty()); + } 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..d3457930c8 --- /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/TestMozTask.cpp", + "nsstring/TestnsString.cpp", + "xpcom/TestXpcom.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/TestMozTask.cpp b/xpcom/rust/gtest/moz_task/TestMozTask.cpp new file mode 100644 index 0000000000..6c40f1bc97 --- /dev/null +++ b/xpcom/rust/gtest/moz_task/TestMozTask.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..fa4c0af852 --- /dev/null +++ b/xpcom/rust/gtest/moz_task/test.rs @@ -0,0 +1,78 @@ +/* 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, + task::{Context, Poll, Waker}, +}; + +/// Demo `Future` to demonstrate executing futures to completion via `nsIEventTarget`. +struct MyFuture { + poll_count: u32, + waker: Option<Waker>, + expect_main_thread: bool, +} + +impl MyFuture { + fn new(expect_main_thread: bool) -> Self { + Self { + poll_count: 0, + waker: None, + expect_main_thread, + } + } +} + +impl Future for MyFuture { + type Output = u32; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u32> { + assert_eq!(moz_task::is_main_thread(), self.expect_main_thread); + + 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(self.poll_count) + } 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 future = async move { + assert_eq!(MyFuture::new(true).await, 6); + assert_eq!( + moz_task::spawn_local("Rust_Future inner spawn_local", MyFuture::new(true)).await, + 6 + ); + assert_eq!( + moz_task::spawn("Rust_Future inner spawn", MyFuture::new(false)).await, + 6 + ); + unsafe { + *it_worked = true; + } + }; + unsafe { + moz_task::gtest_only::spin_event_loop_until("Rust_Future", future).unwrap(); + }; +} 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/TestnsString.cpp b/xpcom/rust/gtest/nsstring/TestnsString.cpp new file mode 100644 index 0000000000..4e14c7bca5 --- /dev/null +++ b/xpcom/rust/gtest/nsstring/TestnsString.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/TestXpcom.cpp b/xpcom/rust/gtest/xpcom/TestXpcom.cpp new file mode 100644 index 0000000000..69425274f6 --- /dev/null +++ b/xpcom/rust/gtest/xpcom/TestXpcom.cpp @@ -0,0 +1,66 @@ +/* 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 "nsIObserver.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); +} + +extern "C" void Rust_GetMultipleInterfaces(nsIRunnable** aRunnable, + nsIObserver** aObserver); + +TEST(RustXpcom, DynamicCastVoid) +{ + nsCOMPtr<nsIRunnable> runnable; + nsCOMPtr<nsIObserver> observer; + Rust_GetMultipleInterfaces(getter_AddRefs(runnable), + getter_AddRefs(observer)); + + // They should have different addresses when `static_cast` to void* + EXPECT_NE(static_cast<void*>(runnable.get()), + static_cast<void*>(observer.get())); + + // These should be the same object + nsCOMPtr<nsISupports> runnableSupports = do_QueryInterface(runnable); + nsCOMPtr<nsISupports> observerSupports = do_QueryInterface(observer); + EXPECT_EQ(runnableSupports.get(), observerSupports.get()); + +#ifndef XP_WIN + // They should have the same address when dynamic_cast to void* + // dynamic_cast<void*> is not supported without rtti on windows. + EXPECT_EQ(dynamic_cast<void*>(runnable.get()), + dynamic_cast<void*>(observer.get())); + + // The nsISupports pointer from `do_QueryInterface` should match + // `dynamic_cast<void*>` + EXPECT_EQ(dynamic_cast<void*>(observer.get()), + static_cast<void*>(observerSupports.get())); +#endif +} diff --git a/xpcom/rust/gtest/xpcom/test.rs b/xpcom/rust/gtest/xpcom/test.rs new file mode 100644 index 0000000000..f26a0140f3 --- /dev/null +++ b/xpcom/rust/gtest/xpcom/test.rs @@ -0,0 +1,131 @@ +/* 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, RefPtr}; + +#[no_mangle] +pub unsafe extern "C" fn Rust_ObserveFromRust() -> *const interfaces::nsIObserverService { + let obssvc: RefPtr<interfaces::nsIObserverService> = + xpcom::components::Observer::service().unwrap(); + + // Define an observer + #[xpcom(implement(nsIObserver), nonatomic)] + struct Observer { + run: *mut bool, + } + impl Observer { + unsafe fn Observe( + &self, + _subject: *const interfaces::nsISupports, + topic: *const c_char, + _data: *const u16, + ) -> 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. + #[xpcom(implement(nsIRunnable), atomic)] + struct RunnableFn<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); +} + +#[no_mangle] +pub unsafe extern "C" fn Rust_GetMultipleInterfaces( + runnable: *mut *const interfaces::nsIRunnable, + observer: *mut *const interfaces::nsIObserver, +) { + // Define a type which implements nsIRunnable and nsIObserver in rust, and + // hand both references back to c++ + #[xpcom(implement(nsIRunnable, nsIObserver), atomic)] + struct MultipleInterfaces {} + + impl MultipleInterfaces { + unsafe fn Run(&self) -> nsresult { + NS_OK + } + unsafe fn Observe( + &self, + _subject: *const interfaces::nsISupports, + _topic: *const c_char, + _data: *const u16, + ) -> nsresult { + NS_OK + } + } + + let instance = MultipleInterfaces::allocate(InitMultipleInterfaces {}); + instance + .query_interface::<interfaces::nsIRunnable>() + .unwrap() + .forget(&mut *runnable); + instance + .query_interface::<interfaces::nsIObserver>() + .unwrap() + .forget(&mut *observer); +} diff --git a/xpcom/rust/moz_task/Cargo.toml b/xpcom/rust/moz_task/Cargo.toml new file mode 100644 index 0000000000..2e3e43ef7a --- /dev/null +++ b/xpcom/rust/moz_task/Cargo.toml @@ -0,0 +1,16 @@ +[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" +edition = "2018" + +[dependencies] +log = "0.4" +cstr = "0.2" +libc = "0.2" +async-task = { version = "4.3" } +nserror = { path = "../nserror" } +nsstring = { path = "../nsstring" } +xpcom = { path = "../xpcom" } diff --git a/xpcom/rust/moz_task/src/dispatcher.rs b/xpcom/rust/moz_task/src/dispatcher.rs new file mode 100644 index 0000000000..17ad9ceb81 --- /dev/null +++ b/xpcom/rust/moz_task/src/dispatcher.rs @@ -0,0 +1,153 @@ +/* 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_background_task_runnable, dispatch_runnable, get_current_thread, DispatchOptions, +}; +use nserror::{nsresult, NS_OK}; +use nsstring::nsACString; +use std::sync::Mutex; +use xpcom::interfaces::{nsIEventTarget, nsIRunnablePriority}; +use xpcom::xpcom; + +/// Basic wrapper to convert a FnOnce callback into a `nsIRunnable` to be +/// dispatched using XPCOM. +#[xpcom(implement(nsIRunnable, nsINamed, nsIRunnablePriority), atomic)] +struct RunnableFunction<F: FnOnce() + 'static> { + name: &'static str, + priority: u32, + function: Mutex<Option<F>>, +} + +impl<F: FnOnce() + 'static> RunnableFunction<F> { + #[allow(non_snake_case)] + fn Run(&self) -> nsresult { + let function = self.function.lock().unwrap().take(); + debug_assert!(function.is_some(), "runnable invoked twice?"); + if let Some(function) = function { + function(); + } + NS_OK + } + + #[allow(non_snake_case)] + unsafe fn GetName(&self, result: *mut nsACString) -> nsresult { + (*result).assign(self.name); + NS_OK + } + + #[allow(non_snake_case)] + unsafe fn GetPriority(&self, result: *mut u32) -> nsresult { + *result = self.priority; + NS_OK + } +} + +pub struct RunnableBuilder<F> { + name: &'static str, + function: F, + priority: u32, + options: DispatchOptions, +} + +impl<F> RunnableBuilder<F> { + pub fn new(name: &'static str, function: F) -> Self { + RunnableBuilder { + name, + function, + priority: nsIRunnablePriority::PRIORITY_NORMAL, + options: DispatchOptions::default(), + } + } + + pub fn priority(mut self, priority: u32) -> Self { + self.priority = priority; + self + } + + pub fn options(mut self, options: DispatchOptions) -> Self { + self.options = options; + self + } + + pub fn may_block(mut self, may_block: bool) -> Self { + self.options = self.options.may_block(may_block); + self + } + + pub unsafe fn at_end(mut self, at_end: bool) -> Self { + self.options = self.options.at_end(at_end); + self + } +} + +impl<F> RunnableBuilder<F> +where + F: FnOnce() + Send + 'static, +{ + /// Dispatch this Runnable to the specified EventTarget. The runnable function must be `Send`. + pub fn dispatch(self, target: &nsIEventTarget) -> Result<(), nsresult> { + let runnable = RunnableFunction::allocate(InitRunnableFunction { + name: self.name, + priority: self.priority, + function: Mutex::new(Some(self.function)), + }); + unsafe { dispatch_runnable(runnable.coerce(), target, self.options) } + } + + /// Dispatch this Runnable to the specified EventTarget as a background + /// task. The runnable function must be `Send`. + pub fn dispatch_background_task(self) -> Result<(), nsresult> { + let runnable = RunnableFunction::allocate(InitRunnableFunction { + name: self.name, + priority: self.priority, + function: Mutex::new(Some(self.function)), + }); + unsafe { dispatch_background_task_runnable(runnable.coerce(), self.options) } + } +} + +impl<F> RunnableBuilder<F> +where + F: FnOnce() + 'static, +{ + /// Dispatch this Runnable to the current thread. + /// + /// Unlike `dispatch` and `dispatch_background_task`, the runnable does not + /// need to be `Send` to dispatch to the current thread. + pub fn dispatch_local(self) -> Result<(), nsresult> { + let target = get_current_thread()?; + let runnable = RunnableFunction::allocate(InitRunnableFunction { + name: self.name, + priority: self.priority, + function: Mutex::new(Some(self.function)), + }); + unsafe { dispatch_runnable(runnable.coerce(), target.coerce(), self.options) } + } +} + +pub fn dispatch_onto<F>( + name: &'static str, + target: &nsIEventTarget, + function: F, +) -> Result<(), nsresult> +where + F: FnOnce() + Send + 'static, +{ + RunnableBuilder::new(name, function).dispatch(target) +} + +pub fn dispatch_background_task<F>(name: &'static str, function: F) -> Result<(), nsresult> +where + F: FnOnce() + Send + 'static, +{ + RunnableBuilder::new(name, function).dispatch_background_task() +} + +pub fn dispatch_local<F>(name: &'static str, function: F) -> Result<(), nsresult> +where + F: FnOnce() + 'static, +{ + RunnableBuilder::new(name, function).dispatch_local() +} 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..f8d113ed57 --- /dev/null +++ b/xpcom/rust/moz_task/src/event_loop.rs @@ -0,0 +1,66 @@ +/* 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; + +use cstr::cstr; +use nserror::{nsresult, NS_ERROR_SERVICE_NOT_AVAILABLE, NS_ERROR_UNEXPECTED, NS_OK}; +use nsstring::*; +use std::cell::RefCell; +use std::future::Future; +use xpcom::{interfaces::nsIThreadManager, xpcom, xpcom_method}; + +#[xpcom(implement(nsINestedEventLoopCondition), nonatomic)] +struct FutureCompleteCondition<T: 'static> { + value: RefCell<Option<T>>, +} + +impl<T: 'static> FutureCompleteCondition<T> { + xpcom_method!(is_done => IsDone() -> bool); + fn is_done(&self) -> Result<bool, nsresult> { + Ok(self.value.borrow().is_some()) + } +} + +/// Spin the event loop on the current thread until `future` is resolved. +/// +/// # 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<F>( + reason: &'static str, + future: F, +) -> Result<F::Output, nsresult> +where + F: Future + 'static, + F::Output: 'static, +{ + let thread_manager = + xpcom::get_service::<nsIThreadManager>(cstr!("@mozilla.org/thread-manager;1")) + .ok_or(NS_ERROR_SERVICE_NOT_AVAILABLE)?; + + let cond = FutureCompleteCondition::<F::Output>::allocate(InitFutureCompleteCondition { + value: RefCell::new(None), + }); + + // Spawn our future onto the current thread event loop, and record the + // completed value as it completes. + let cond2 = cond.clone(); + crate::spawn_local(reason, async move { + let rv = future.await; + *cond2.value.borrow_mut() = Some(rv); + }) + .detach(); + + thread_manager + .SpinEventLoopUntil(&*nsCStr::from(reason), cond.coerce()) + .to_result()?; + let rv = cond.value.borrow_mut().take(); + rv.ok_or(NS_ERROR_UNEXPECTED) +} diff --git a/xpcom/rust/moz_task/src/executor.rs b/xpcom/rust/moz_task/src/executor.rs new file mode 100644 index 0000000000..0016839373 --- /dev/null +++ b/xpcom/rust/moz_task/src/executor.rs @@ -0,0 +1,291 @@ +/* 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, DispatchOptions, RunnableBuilder}; +use std::{ + cell::Cell, + fmt::Debug, + future::Future, + pin::Pin, + ptr, + sync::Arc, + task::{Context, Poll}, +}; +use xpcom::interfaces::{nsIEventTarget, nsIRunnablePriority}; +use xpcom::RefPtr; + +/// A spawned task. +/// +/// A [`AsyncTask`] can be awaited to retrieve the output of its future. +/// +/// Dropping an [`AsyncTask`] cancels it, which means its future won't be polled +/// again. To drop the [`AsyncTask`] handle without canceling it, use +/// [`detach()`][`AsyncTask::detach()`] instead. To cancel a task gracefully and +/// wait until it is fully destroyed, use the [`cancel()`][AsyncTask::cancel()] +/// method. +/// +/// A task which is cancelled due to the nsIEventTarget it was dispatched to no +/// longer accepting events will never be resolved. +#[derive(Debug)] +#[must_use = "tasks get canceled when dropped, use `.detach()` to run them in the background"] +pub struct AsyncTask<T> { + task: async_task::FallibleTask<T>, +} + +impl<T> AsyncTask<T> { + fn new(task: async_task::Task<T>) -> Self { + AsyncTask { + task: task.fallible(), + } + } + + /// Detaches the task to let it keep running in the background. + pub fn detach(self) { + self.task.detach() + } + + /// Cancels the task and waits for it to stop running. + /// + /// Returns the task's output if it was completed just before it got canceled, or [`None`] if + /// it didn't complete. + /// + /// While it's possible to simply drop the [`Task`] to cancel it, this is a cleaner way of + /// canceling because it also waits for the task to stop running. + pub async fn cancel(self) -> Option<T> { + self.task.cancel().await + } +} + +impl<T> Future for AsyncTask<T> { + type Output = T; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + // Wrap the future produced by `AsyncTask` to never resolve if the + // Runnable was dropped, and the task was cancelled. + match Pin::new(&mut self.task).poll(cx) { + Poll::Ready(Some(t)) => Poll::Ready(t), + Poll::Ready(None) | Poll::Pending => Poll::Pending, + } + } +} + +enum SpawnTarget { + BackgroundTask, + EventTarget(RefPtr<nsIEventTarget>), +} + +// SAFETY: All XPCOM interfaces are considered !Send + !Sync, however all +// well-behaved nsIEventTarget instances must be threadsafe. +unsafe impl Send for SpawnTarget {} +unsafe impl Sync for SpawnTarget {} + +/// Information used by tasks as they are spawned. Stored in an Arc such that +/// their identity can be used for `POLLING_TASK`. +struct TaskSpawnConfig { + name: &'static str, + priority: u32, + options: DispatchOptions, + target: SpawnTarget, +} + +thread_local! { + /// Raw pointer to the TaskSpawnConfig for the currently polling task. Used + /// to detect scheduling callbacks for a runnable while it is polled, to set + /// `DISPATCH_AT_END` on the notification. + static POLLING_TASK: Cell<*const TaskSpawnConfig> = Cell::new(ptr::null()); +} + +fn schedule(config: Arc<TaskSpawnConfig>, runnable: async_task::Runnable) { + // If we're dispatching this task while it is currently running on the same + // thread, set the `DISPATCH_AT_END` flag in the dispatch options to tell + // our threadpool target to not bother to spin up another thread. + let currently_polling = POLLING_TASK.with(|t| t.get() == Arc::as_ptr(&config)); + + // SAFETY: We use the POLLING_TASK thread local to check if we meet the + // requirements for `at_end`. + let options = unsafe { config.options.at_end(currently_polling) }; + + // Build the RunnableBuilder for our task to be dispatched. + let config2 = config.clone(); + let builder = RunnableBuilder::new(config.name, move || { + // Record the pointer for the currently executing task in the + // POLLING_TASK thread-local so that nested dispatches can detect it. + POLLING_TASK.with(|t| { + let prev = t.get(); + t.set(Arc::as_ptr(&config2)); + runnable.run(); + t.set(prev); + }); + }) + .priority(config.priority) + .options(options); + + let rv = match &config.target { + SpawnTarget::BackgroundTask => builder.dispatch_background_task(), + SpawnTarget::EventTarget(target) => builder.dispatch(&*target), + }; + if let Err(err) = rv { + log::warn!( + "dispatch for spawned task '{}' failed: {:?}", + config.name, + err + ); + } +} + +/// Helper for starting an async task which will run a future to completion. +#[derive(Debug)] +pub struct TaskBuilder<F> { + name: &'static str, + future: F, + priority: u32, + options: DispatchOptions, +} + +impl<F> TaskBuilder<F> { + pub fn new(name: &'static str, future: F) -> TaskBuilder<F> { + TaskBuilder { + name, + future, + priority: nsIRunnablePriority::PRIORITY_NORMAL, + options: DispatchOptions::default(), + } + } + + /// Specify the priority of the task's runnables. + pub fn priority(mut self, priority: u32) -> Self { + self.priority = priority; + self + } + + /// Specify options to use when dispatching the task. + pub fn options(mut self, options: DispatchOptions) -> Self { + self.options = options; + self + } + + /// Set whether or not the event may block, and should be run on the IO + /// thread pool. + pub fn may_block(mut self, may_block: bool) -> Self { + self.options = self.options.may_block(may_block); + self + } +} + +impl<F> TaskBuilder<F> +where + F: Future + Send + 'static, + F::Output: Send + 'static, +{ + /// Run the future on the background task pool. + pub fn spawn(self) -> AsyncTask<F::Output> { + let config = Arc::new(TaskSpawnConfig { + name: self.name, + priority: self.priority, + options: self.options, + target: SpawnTarget::BackgroundTask, + }); + let (runnable, task) = async_task::spawn(self.future, move |runnable| { + schedule(config.clone(), runnable) + }); + runnable.schedule(); + AsyncTask::new(task) + } + + /// Run the future on the specified nsIEventTarget. + pub fn spawn_onto(self, target: &nsIEventTarget) -> AsyncTask<F::Output> { + let config = Arc::new(TaskSpawnConfig { + name: self.name, + priority: self.priority, + options: self.options, + target: SpawnTarget::EventTarget(RefPtr::new(target)), + }); + let (runnable, task) = async_task::spawn(self.future, move |runnable| { + schedule(config.clone(), runnable) + }); + runnable.schedule(); + AsyncTask::new(task) + } +} + +impl<F> TaskBuilder<F> +where + F: Future + 'static, + F::Output: 'static, +{ + /// Run the future on the current thread. + /// + /// Unlike the other `spawn` methods, this method supports non-Send futures. + /// + /// # Panics + /// + /// This method may panic if run on a thread which cannot run local futures + /// (e.g. due to it is not being an XPCOM thread, or if we are very late + /// during shutdown). + pub fn spawn_local(self) -> AsyncTask<F::Output> { + let current_thread = get_current_thread().expect("cannot get current thread"); + let config = Arc::new(TaskSpawnConfig { + name: self.name, + priority: self.priority, + options: self.options, + target: SpawnTarget::EventTarget(RefPtr::new(current_thread.coerce())), + }); + let (runnable, task) = async_task::spawn_local(self.future, move |runnable| { + schedule(config.clone(), runnable) + }); + runnable.schedule(); + AsyncTask::new(task) + } +} + +/// Spawn a future onto the background task pool. The future will not be run on +/// the main thread. +pub fn spawn<F>(name: &'static str, future: F) -> AsyncTask<F::Output> +where + F: Future + Send + 'static, + F::Output: Send + 'static, +{ + TaskBuilder::new(name, future).spawn() +} + +/// Spawn a potentially-blocking future onto the background task pool. The +/// future will not be run on the main thread. +pub fn spawn_blocking<F>(name: &'static str, future: F) -> AsyncTask<F::Output> +where + F: Future + Send + 'static, + F::Output: Send + 'static, +{ + TaskBuilder::new(name, future).may_block(true).spawn() +} + +/// Spawn a local future onto the current thread. +pub fn spawn_local<F>(name: &'static str, future: F) -> AsyncTask<F::Output> +where + F: Future + 'static, + F::Output: 'static, +{ + TaskBuilder::new(name, future).spawn_local() +} + +pub fn spawn_onto<F>(name: &'static str, target: &nsIEventTarget, future: F) -> AsyncTask<F::Output> +where + F: Future + Send + 'static, + F::Output: Send + 'static, +{ + TaskBuilder::new(name, future).spawn_onto(target) +} + +pub fn spawn_onto_blocking<F>( + name: &'static str, + target: &nsIEventTarget, + future: F, +) -> AsyncTask<F::Output> +where + F: Future + Send + 'static, + F::Output: Send + 'static, +{ + TaskBuilder::new(name, future) + .may_block(true) + .spawn_onto(target) +} diff --git a/xpcom/rust/moz_task/src/lib.rs b/xpcom/rust/moz_task/src/lib.rs new file mode 100644 index 0000000000..2f0c0cfd0a --- /dev/null +++ b/xpcom/rust/moz_task/src/lib.rs @@ -0,0 +1,378 @@ +/* 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. + +mod dispatcher; +pub use dispatcher::{dispatch_background_task, dispatch_local, dispatch_onto, RunnableBuilder}; +mod event_loop; +mod executor; +pub use executor::{ + spawn, spawn_blocking, spawn_local, spawn_onto, spawn_onto_blocking, AsyncTask, TaskBuilder, +}; + +// 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 crate::event_loop::spin_event_loop_until; +} + +use nserror::nsresult; +use nsstring::{nsACString, nsCString}; +use std::{ffi::CStr, marker::PhantomData, mem, ptr}; +use xpcom::{ + getter_addrefs, + interfaces::{nsIEventTarget, nsIRunnable, nsISerialEventTarget, nsISupports, nsIThread}, + AtomicRefcnt, RefCounted, RefPtr, XpCom, +}; + +extern "C" { + fn NS_GetCurrentThreadRust(result: *mut *const nsIThread) -> nsresult; + fn NS_GetMainThreadRust(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_IsOnCurrentThread(target: *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_GetCurrentThreadRust(p) }) +} + +pub fn get_main_thread() -> Result<RefPtr<nsIThread>, nsresult> { + getter_addrefs(|p| unsafe { NS_GetMainThreadRust(p) }) +} + +pub fn is_main_thread() -> bool { + unsafe { NS_IsMainThread() } +} + +// There's no OS requirement that thread names be static, but dynamic thread +// names tend to conceal more than they reveal when processing large numbers of +// crash reports. +pub fn create_thread(name: &'static str) -> Result<RefPtr<nsIThread>, nsresult> { + getter_addrefs(|p| unsafe { + NS_NewNamedThreadWithDefaultStackSize(&*nsCString::from(name), p, ptr::null()) + }) +} + +pub fn is_on_current_thread(target: &nsIEventTarget) -> bool { + unsafe { NS_IsOnCurrentThread(target) } +} + +/// 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, 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_runnable( + 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 +/// 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. +/// +/// 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. +/// +/// ### 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_background_task_runnable( + runnable: &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. + NS_DispatchBackgroundTask(runnable, 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) + } +} + +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; + if may_block { + DispatchOptions(self.flags() | FLAG) + } else { + DispatchOptions(self.flags() & !FLAG) + } + } + + /// Specifies that the dispatch is occurring from a running event that was + /// dispatched to the same event target, and that event is about to finish. + /// + /// A thread pool can use this as an optimization hint to not spin up + /// another thread, since the current thread is about to become idle. + /// + /// Setting this flag is unsafe, as it may only be used from the target + /// event target when the event is about to finish. + #[inline] + pub unsafe fn at_end(self, may_block: bool) -> DispatchOptions { + const FLAG: u32 = nsIEventTarget::DISPATCH_AT_END; + 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. +/// +/// # Alternatives +/// +/// This trait is no longer necessary for basic tasks to be dispatched to +/// another thread with a callback on the originating thread. `moz_task` now has +/// a series of more rust-like primitives which can be used instead. For +/// example, it may be preferable to use the async executor over `Task`: +/// +/// ```ignore +/// // Spawn a task onto the background task pool, and capture the result of its +/// // execution. +/// let bg_task = moz_task::spawn("Example", async move { +/// do_background_work(captured_state) +/// }); +/// +/// // Spawn another task on the calling thread which will await on the result +/// // of the async operation, and invoke a non-Send callback. This task won't +/// // be awaited on, so needs to be `detach`-ed. +/// moz_task::spawn_local("Example", async move { +/// callback.completed(bg_task.await); +/// }) +/// .detach(); +/// ``` +/// +/// If no result is needed, the task returned from `spawn` may be also detached +/// directly. +pub trait Task { + // FIXME: These could accept `&mut`. + fn run(&self); + fn done(&self) -> Result<(), nsresult>; +} + +pub struct TaskRunnable { + name: &'static str, + task: Box<dyn Task + Send + Sync>, +} + +impl TaskRunnable { + // XXX: Fixme: clean up this old API. (bug 1744312) + pub fn new( + name: &'static str, + task: Box<dyn Task + Send + Sync>, + ) -> Result<TaskRunnable, nsresult> { + Ok(TaskRunnable { name, task }) + } + + pub fn dispatch(self, target: &nsIEventTarget) -> Result<(), nsresult> { + self.dispatch_with_options(target, DispatchOptions::default()) + } + + pub fn dispatch_with_options( + self, + target: &nsIEventTarget, + options: DispatchOptions, + ) -> Result<(), nsresult> { + // Perform `task.run()` on a background thread. + let task = self.task; + let handle = TaskBuilder::new(self.name, async move { + task.run(); + task + }) + .options(options) + .spawn_onto(target); + + // Run `task.done()` on the starting thread once the background thread + // is done with the task. + spawn_local(self.name, async move { + let task = handle.await; + let _ = task.done(); + }) + .detach(); + Ok(()) + } + + pub fn dispatch_background_task_with_options( + self, + options: DispatchOptions, + ) -> Result<(), nsresult> { + // Perform `task.run()` on a background thread. + let task = self.task; + let handle = TaskBuilder::new(self.name, async move { + task.run(); + task + }) + .options(options) + .spawn(); + + // Run `task.done()` on the starting thread once the background thread + // is done with the task. + spawn_local(self.name, async move { + let task = handle.await; + let _ = task.done(); + }) + .detach(); + Ok(()) + } +} + +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_on_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. + mem::drop(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_on_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..4fca5a4c2c --- /dev/null +++ b/xpcom/rust/nserror/Cargo.toml @@ -0,0 +1,11 @@ +[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" } +mozbuild = "0.1" diff --git a/xpcom/rust/nserror/src/lib.rs b/xpcom/rust/nserror/src/lib.rs new file mode 100644 index 0000000000..e4107d1b57 --- /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!(mozbuild::objdir_path!("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..521c2c8c04 --- /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..9c8a1ec83b --- /dev/null +++ b/xpcom/rust/xpcom/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "xpcom" +version = "0.1.0" +authors = ["Nika Layzell <nika@thelayzells.com>"] +edition = "2018" +license = "MPL-2.0" + +[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"] } +mozbuild = "0.1" + +[features] +thread_sanitizer = [] +gecko_refcount_logging = [] 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/components.rs b/xpcom/rust/xpcom/src/components.rs new file mode 100644 index 0000000000..c83e2df705 --- /dev/null +++ b/xpcom/rust/xpcom/src/components.rs @@ -0,0 +1,23 @@ +/* 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 static XPCOM components. +//! +//! The contents of this file are generated from +//! `xpcom/components/gen_static_components.py`. + +extern "C" { + fn Gecko_GetServiceByModuleID( + id: ModuleID, + iid: &crate::nsIID, + result: *mut *mut libc::c_void, + ) -> nserror::nsresult; + fn Gecko_CreateInstanceByModuleID( + id: ModuleID, + iid: &crate::nsIID, + result: *mut *mut libc::c_void, + ) -> nserror::nsresult; +} + +include!(mozbuild::objdir_path!("xpcom/components/components.rs")); diff --git a/xpcom/rust/xpcom/src/interfaces/idl.rs b/xpcom/rust/xpcom/src/interfaces/idl.rs new file mode 100644 index 0000000000..c8d62ce716 --- /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!(mozbuild::objdir_path!("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..ac039ebe76 --- /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 component getters. +pub mod components; + +// 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..66c0510bd9 --- /dev/null +++ b/xpcom/rust/xpcom/src/method.rs @@ -0,0 +1,241 @@ +/* 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 #[xpcom] to implement it: +/// +/// ```ignore +/// #[xpcom(implement(nsIFooBarBaz), atomic)] +/// struct FooBarBaz { +/// // … +/// } +/// ``` +/// +/// 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..d198899497 --- /dev/null +++ b/xpcom/rust/xpcom/src/reexports.rs @@ -0,0 +1,52 @@ +/* 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) +} + +/// On some ABIs, extra information is included before the vtable's function +/// pointers which are used to implement RTTI. We build Gecko with RTTI +/// disabled, however these fields may still be present to support +/// `dynamic_cast<void*>` on our rust VTables in case they are accessed. +/// +/// Itanium ABI Layout: https://refspecs.linuxbase.org/cxxabi-1.83.html#vtable +#[repr(C)] +pub struct VTableExtra<T> { + #[cfg(not(windows))] + pub offset: isize, + #[cfg(not(windows))] + pub typeinfo: *const libc::c_void, + pub vtable: T, +} diff --git a/xpcom/rust/xpcom/src/refptr.rs b/xpcom/rust/xpcom/src/refptr.rs new file mode 100644 index 0000000000..8549b6d2f0 --- /dev/null +++ b/xpcom/rust/xpcom/src/refptr.rs @@ -0,0 +1,388 @@ +/* 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::nsISupports; +use libc; +use nserror::{nsresult, NS_OK}; +use std::cell::Cell; +use std::convert::TryInto; +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; + +// This should match the definition in mfbt/RefCountType.h, modulo the delicate +// effort at maintaining binary compatibility with Microsoft COM on Windows. +pub type MozExternalRefCountType = u32; + +/// 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. +#[repr(transparent)] +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> {} + +macro_rules! assert_layout_eq { + ($T:ty, $U:ty) => { + const _: [(); std::mem::size_of::<$T>()] = [(); std::mem::size_of::<$U>()]; + const _: [(); std::mem::align_of::<$T>()] = [(); std::mem::align_of::<$U>()]; + }; +} + +// Assert that `RefPtr<nsISupports>` has the correct memory layout. +assert_layout_eq!(RefPtr<nsISupports>, *const nsISupports); +// Assert that the null-pointer optimization applies to `RefPtr<nsISupports>`. +assert_layout_eq!(RefPtr<nsISupports>, Option<RefPtr<nsISupports>>); + +/// 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. +/// +/// `#[xpcom(nonatomic)]` will use this type for the `__refcnt` field. +#[derive(Debug)] +pub struct Refcnt(Cell<usize>); +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) -> MozExternalRefCountType { + // XXX: Checked add? + let new = self.0.get() + 1; + self.0.set(new); + new.try_into().unwrap() + } + + /// 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) -> MozExternalRefCountType { + // XXX: Checked sub? + let new = self.0.get() - 1; + self.0.set(new); + new.try_into().unwrap() + } + + /// Get the current value of the reference count. + pub fn get(&self) -> usize { + self.0.get() + } +} + +/// The type of the atomic reference count used for xpcom structs. +/// +/// `#[xpcom(atomic)]` will use this type for the `__refcnt` field. +/// +/// 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) -> MozExternalRefCountType { + let result = self.0.fetch_add(1, Ordering::Relaxed) + 1; + result.try_into().unwrap() + } + + /// 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) -> MozExternalRefCountType { + let result = self.0.fetch_sub(1, Ordering::Release) - 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.try_into().unwrap() + } + + /// Get the current value of the reference count. + pub fn get(&self) -> usize { + self.0.load(Ordering::Acquire) + } +} + +#[cfg(feature = "gecko_refcount_logging")] +pub mod trace_refcnt { + extern "C" { + pub fn NS_LogCtor(aPtr: *mut libc::c_void, aTypeName: *const libc::c_char, aSize: u32); + pub fn NS_LogDtor(aPtr: *mut libc::c_void, aTypeName: *const libc::c_char, aSize: u32); + pub fn NS_LogAddRef( + aPtr: *mut libc::c_void, + aRefcnt: usize, + aClass: *const libc::c_char, + aClassSize: u32, + ); + pub fn NS_LogRelease( + aPtr: *mut libc::c_void, + aRefcnt: usize, + aClass: *const libc::c_char, + aClassSize: u32, + ); + } +} + +// stub inline methods for the refcount logging functions for when the feature +// is disabled. +#[cfg(not(feature = "gecko_refcount_logging"))] +pub mod trace_refcnt { + #[inline] + #[allow(non_snake_case)] + pub unsafe extern "C" fn NS_LogCtor(_: *mut libc::c_void, _: *const libc::c_char, _: u32) {} + #[inline] + #[allow(non_snake_case)] + pub unsafe extern "C" fn NS_LogDtor(_: *mut libc::c_void, _: *const libc::c_char, _: u32) {} + #[inline] + #[allow(non_snake_case)] + pub unsafe extern "C" fn NS_LogAddRef( + _: *mut libc::c_void, + _: usize, + _: *const libc::c_char, + _: u32, + ) { + } + #[inline] + #[allow(non_snake_case)] + pub unsafe extern "C" fn NS_LogRelease( + _: *mut libc::c_void, + _: usize, + _: *const libc::c_char, + _: u32, + ) { + } +} diff --git a/xpcom/rust/xpcom/src/statics.rs b/xpcom/rust/xpcom/src/statics.rs new file mode 100644 index 0000000000..27f66fdb4b --- /dev/null +++ b/xpcom/rust/xpcom/src/statics.rs @@ -0,0 +1,75 @@ +/* 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; + +/// 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(), &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..4b5bcdc315 --- /dev/null +++ b/xpcom/rust/xpcom/xpcom_macros/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "xpcom_macros" +version = "0.1.0" +authors = ["Nika Layzell <nika@thelayzells.com>"] +edition = "2018" +license = "MPL-2.0" + +[lib] +proc-macro = true + +[dependencies] +syn = "1" +quote = "1" +proc-macro2 = "1" +lazy_static = "1.0" +mozbuild = "0.1" 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..2511ba9103 --- /dev/null +++ b/xpcom/rust/xpcom/xpcom_macros/src/lib.rs @@ -0,0 +1,818 @@ +/* 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 `#[xpcom]` custom attribute. This custom attribute +//! 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 +//! #[xpcom(implement(nsIRunnable), atomic)] +//! struct ImplRunnable { +//! 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. +//! #[xpcom( +//! // The implement argument 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. +//! implement(nsIRunnable), +//! +//! // The refcount kind can be specified as 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++ +//! atomic, +//! )] +//! +//! // It is a compile time error to put the `#[xpcom]` attribute on +//! // an enum, union, or tuple struct. +//! // +//! // The macro will generate both the named struct, as well as a version with +//! // its name prefixed with `Init` which can be used to initialize the type. +//! struct ImplRunnable { +//! i: i32, +//! } +//! ``` +//! +//! The above example will generate `ImplRunnable` and `InitImplRunnable` +//! structs. The `ImplRunnable` struct will implement the [`nsIRunnable`] XPCOM +//! interface, and cannot be constructed directly. +//! +//! The following methods will be automatically implemented on it: +//! +//! ```ignore +//! // Automatic nsISupports implementation +//! unsafe fn AddRef(&self) -> MozExternalRefCountType; +//! unsafe fn Release(&self) -> MozExternalRefCountType; +//! 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 template 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, AttributeArgs, Field, Fields, Ident, ItemStruct, Meta, + MetaList, NestedMeta, Path, 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!(mozbuild::objdir_path!("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), + } + } +} + +/// Extract the fields list from the input struct. +fn get_fields(si: &ItemStruct) -> Result<&Punctuated<Field, Token![,]>, syn::Error> { + match si.fields { + Fields::Named(ref named) => Ok(&named.named), + _ => bail!(@(si), "The initializer struct must be a standard named \ + value struct definition"), + } +} + +/// Takes the template struct in, and generates `ItemStruct` for the "real" and +/// "init" structs. +fn gen_structs( + template: &ItemStruct, + bases: &[&Interface], + refcnt_ty: RefcntKind, +) -> Result<(ItemStruct, ItemStruct), syn::Error> { + let real_ident = &template.ident; + let init_ident = format_ident!("Init{}", real_ident); + let vis = &template.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(template)?; + let (impl_generics, _, where_clause) = template.generics.split_for_impl(); + Ok(( + parse_quote! { + #[repr(C)] + #vis struct #real_ident #impl_generics #where_clause { + #(#bases,)* + __refcnt: #refcnt_ty, + #fields + } + }, + parse_quote! { + #vis struct #init_ident #impl_generics #where_clause { + #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: &ItemStruct, + 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: &ItemStruct, 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: &ItemStruct, + 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 ::xpcom::reexports::VTableExtra<#vtable_ty> #where_clause { + &::xpcom::reexports::VTableExtra { + #[cfg(not(windows))] + offset: { + // NOTE: workaround required to avoid depending on the + // unstable const expression feature `const {}`. + const OFFSET: isize = -((::std::mem::size_of::<usize>() * #idx) as isize); + OFFSET + }, + #[cfg(not(windows))] + typeinfo: 0 as *const _, + vtable: #vtable, + } + } + &get_vtable #turbofish ().vtable + },}) +} + +/// 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: &ItemStruct, + 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 coercions. + 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 use #[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 use #[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(()) +} + +#[derive(Default)] +struct Options { + bases: Vec<&'static Interface>, + refcnt: Option<RefcntKind>, +} + +impl Options { + fn parse_path_arg(&mut self, path: &Path) -> Result<(), syn::Error> { + if path.is_ident("atomic") || path.is_ident("nonatomic") { + if self.refcnt.is_some() { + bail!(@(path), "Duplicate refcnt atomicity specifier"); + } + self.refcnt = Some(if path.is_ident("atomic") { + RefcntKind::Atomic + } else { + RefcntKind::NonAtomic + }); + return Ok(()); + } + bail!(@(path), "Unexpected path argument to #[xpcom]"); + } + + fn parse_list_arg(&mut self, list: &MetaList) -> Result<(), syn::Error> { + if list.path.is_ident("implement") { + for item in list.nested.iter() { + let iface = match *item { + NestedMeta::Meta(syn::Meta::Path(ref iface)) => iface, + _ => bail!(@(item), "Expected interface name to implement"), + }; + let ident = match iface.get_ident() { + Some(ref iface) => iface.to_string(), + _ => bail!(@(iface), "Interface name must be unqualified"), + }; + if let Some(&iface) = IFACES.get(ident.as_str()) { + self.bases.push(iface); + } else { + bail!(@(item), "Invalid base interface `{}`", ident); + } + } + return Ok(()); + } + bail!(@(list), "Unexpected list argument to #[xpcom]"); + } + + fn parse(&mut self, args: &AttributeArgs) -> Result<(), syn::Error> { + for arg in args { + match arg { + NestedMeta::Meta(Meta::Path(path)) => self.parse_path_arg(path)?, + NestedMeta::Meta(Meta::List(list)) => self.parse_list_arg(list)?, + NestedMeta::Meta(Meta::NameValue(name_value)) => { + bail!(@(name_value), "Unexpected name-value argument to #[xpcom]") + } + NestedMeta::Lit(lit) => bail!(@(lit), "Unexpected literal argument to #[xpcom]"), + } + } + + if self.bases.is_empty() { + bail!( + "Types with #[xpcom(..)] must implement at least one \ + interface. Interfaces can be implemented by adding an \ + implements(nsIFoo, nsIBar) parameter to the #[xpcom] attribute" + ); + } + + if self.refcnt.is_none() { + bail!("Must specify refcnt kind in #[xpcom] attribute"); + } + + Ok(()) + } +} + +/// The root xpcom procedural macro definition. +fn xpcom_impl(args: AttributeArgs, template: ItemStruct) -> Result<TokenStream, syn::Error> { + let mut options = Options::default(); + options.parse(&args)?; + + check_generics(&template.generics)?; + + let bases = options.bases; + + // 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 = options.refcnt.unwrap(); + let (real, init) = gen_structs(&template, &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 size_for_logs = if real.generics.params.is_empty() { + quote!(::std::mem::size_of::<Self>() as u32) + } else { + // Refcount logging requires all types with the same name to have the + // same size, and generics aren't taken into account when creating our + // name string, so we need to make sure that all possible instantiations + // report the same size. To do that, we fake a size based on the number + // of vtable pointers and the known refcount field. + let fake_size_npointers = bases.len() + 1; + quote!((::std::mem::size_of::<usize>() * #fake_size_npointers) as u32) + }; + + let (impl_generics, ty_generics, where_clause) = real.generics.split_for_impl(); + let name_for_logs = quote!( + concat!(module_path!(), "::", stringify!(#name #ty_generics), "\0").as_ptr() + as *const ::xpcom::reexports::libc::c_char + ); + Ok(quote! { + #init + + #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::MozExternalRefCountType { + let new = self.__refcnt.inc(); + ::xpcom::trace_refcnt::NS_LogAddRef( + self as *const _ as *mut ::xpcom::reexports::libc::c_void, + new as usize, + #name_for_logs, + #size_for_logs, + ); + new + } + + /// Automatically generated implementation of Release for nsISupports. + #vis unsafe fn Release(&self) -> ::xpcom::MozExternalRefCountType { + let new = self.__refcnt.dec(); + ::xpcom::trace_refcnt::NS_LogRelease( + self as *const _ as *mut ::xpcom::reexports::libc::c_void, + new as usize, + #name_for_logs, + #size_for_logs, + ); + if new == 0 { + // dealloc + ::std::mem::drop(::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 + /// `#[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 `#[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_attribute] +pub fn xpcom( + args: proc_macro::TokenStream, + input: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let args = parse_macro_input!(args as AttributeArgs); + let input = parse_macro_input!(input as ItemStruct); + match xpcom_impl(args, input) { + Ok(ts) => ts.into(), + Err(err) => err.to_compile_error().into(), + } +} |