diff --git a/examples/arithmetic/Cargo.toml b/examples/arithmetic/Cargo.toml index ad0bc26e3b..a9e9a2ad47 100644 --- a/examples/arithmetic/Cargo.toml +++ b/examples/arithmetic/Cargo.toml @@ -7,7 +7,7 @@ license = "MPL-2.0" publish = false [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "lib"] name = "uniffi_arithmetic" [dependencies] diff --git a/examples/rondpoint/Cargo.toml b/examples/rondpoint/Cargo.toml index 8cf415b3a6..d56e67ab06 100644 --- a/examples/rondpoint/Cargo.toml +++ b/examples/rondpoint/Cargo.toml @@ -7,7 +7,7 @@ license = "MPL-2.0" publish = false [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "lib"] name = "uniffi_rondpoint" [dependencies] diff --git a/examples/rondpoint/tests/gecko_js/test_rondpoint.js b/examples/rondpoint/tests/gecko_js/test_rondpoint.js new file mode 100644 index 0000000000..2bbcf99b80 --- /dev/null +++ b/examples/rondpoint/tests/gecko_js/test_rondpoint.js @@ -0,0 +1,257 @@ +/* + * This file is an xpcshell test that exercises the Rondpoint binding in + * Firefox. Non-Gecko JS consumers can safely ignore it. + * + * If you're working on the Gecko JS bindings, you'll want to either copy or + * symlink this folder into m-c, and add the `xpcshell.ini` manifest in this + * folder to an `XPCSHELL_TESTS_MANIFESTS` section in the `moz.build` file + * that includes the generated bindings. + * + * Currently, this must be done manually, though we're looking at ways to + * run `uniffi-bindgen` as part of the Firefox build, and keep the UniFFI + * bindings tests in the tree. https://github.com/mozilla/uniffi-rs/issues/272 + * has more details. + */ + +add_task(async function test_rondpoint() { + deepEqual( + Rondpoint.copieDictionnaire({ + un: "deux", + deux: true, + petitNombre: 0, + grosNombre: 123456789, + }), + { + un: "deux", + deux: true, + petitNombre: 0, + grosNombre: 123456789, + } + ); + equal(Rondpoint.copieEnumeration("deux"), "deux"); + deepEqual(Rondpoint.copieEnumerations(["un", "deux"]), ["un", "deux"]); + deepEqual( + Rondpoint.copieCarte({ + 1: "un", + 2: "deux", + }), + { + 1: "un", + 2: "deux", + } + ); + ok(Rondpoint.switcheroo(false)); +}); + +add_task(async function test_retourneur() { + let rt = new Retourneur(); + + // Booleans. + [true, false].forEach(v => strictEqual(rt.identiqueBoolean(v), v)); + + // Bytes. + [-128, 127].forEach(v => equal(rt.identiqueI8(v), v)); + [0x00, 0xff].forEach(v => equal(rt.identiqueU8(v), v)); + + // Shorts. + [-Math.pow(2, 15), Math.pow(2, 15) - 1].forEach(v => + equal(rt.identiqueI16(v), v) + ); + [0, 0xffff].forEach(v => equal(rt.identiqueU16(v), v)); + + // Ints. + [0, 1, -1, -Math.pow(2, 31), Math.pow(2, 31) - 1].forEach(v => + equal(rt.identiqueI32(v), v) + ); + [0, Math.pow(2, 32) - 1].forEach(v => equal(rt.identiqueU32(v), v)); + + // Longs. + [0, 1, -1, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER].forEach(v => + equal(rt.identiqueI64(v), v) + ); + [0, 1, Number.MAX_SAFE_INTEGER].forEach(v => equal(rt.identiqueU64(v), v)); + + // Floats. + [0, 1, 0.25].forEach(v => equal(rt.identiqueFloat(v), v)); + + // Doubles. + [0, 1, 0.25].forEach(v => equal(rt.identiqueDouble(v), v)); + + // Strings. + [ + "", + "abc", + "null\0byte", + "été", + "ښي لاس ته لوستلو لوستل", + "😻emoji 👨‍👧‍👦multi-emoji, 🇨🇭a flag, a canal, panama", + ].forEach(v => equal(rt.identiqueString(v), v)); + + [-1, 0, 1].forEach(v => { + let dict = { + petitNombre: v, + courtNombre: v, + nombreSimple: v, + grosNombre: v, + }; + deepEqual(rt.identiqueNombresSignes(dict), dict); + }); + + [0, 1].forEach(v => { + let dict = { + petitNombre: v, + courtNombre: v, + nombreSimple: v, + grosNombre: v, + }; + deepEqual(rt.identiqueNombres(dict), dict); + }); +}); + +add_task(async function test_stringifier() { + let st = new Stringifier(); + + let wellKnown = st.wellKnownString("firefox"); + equal(wellKnown, "uniffi 💚 firefox!"); + + let table = { + toStringBoolean: [ + [true, "true"], + [false, "false"], + ], + toStringI8: [ + [-128, "-128"], + [127, "127"], + ], + toStringU8: [ + [0x00, "0"], + [0xff, "255"], + ], + toStringI16: [ + [-Math.pow(2, 15), "-32768"], + [Math.pow(2, 15) - 1, "32767"], + ], + toStringU16: [ + [0, "0"], + [0xffff, "65535"], + ], + toStringI32: [ + [0, "0"], + [1, "1"], + [-1, "-1"], + [-Math.pow(2, 31), "-2147483648"], + [Math.pow(2, 31) - 1, "2147483647"], + ], + toStringU32: [ + [0, "0"], + [Math.pow(2, 32) - 1, "4294967295"], + ], + toStringI64: [ + [0, "0"], + [1, "1"], + [-1, "-1"], + [Number.MIN_SAFE_INTEGER, "-9007199254740991"], + [Number.MAX_SAFE_INTEGER, "9007199254740991"], + ], + toStringU64: [ + [0, "0"], + [1, "1"], + [Number.MAX_SAFE_INTEGER, "9007199254740991"], + ], + toStringFloat: [ + [0, "0"], + [1, "1"], + [0.25, "0.25"], + ], + toStringDouble: [ + [0, "0"], + [1, "1"], + [0.25, "0.25"], + ], + }; + for (let method in table) { + for (let [v, expected] of table[method]) { + strictEqual(st[method](v), expected); + } + } +}); + +add_task(async function test_optionneur() { + // Step 1: call the methods without arguments, and check against the IDL. + + let op = new Optionneur(); + + equal(op.sinonString(), "default"); + strictEqual(op.sinonBoolean(), false); + deepEqual(op.sinonSequence(), []); + + // Nullables. + strictEqual(op.sinonNull(), null); + strictEqual(op.sinonZero(), 0); + + // Decimal integers. + equal(op.sinonI8Dec(), -42); + equal(op.sinonU8Dec(), 42); + equal(op.sinonI16Dec(), 42); + equal(op.sinonU16Dec(), 42); + equal(op.sinonI32Dec(), 42); + equal(op.sinonU32Dec(), 42); + equal(op.sinonI64Dec(), 42); + equal(op.sinonU64Dec(), 42); + + // Hexadecimal integers. + equal(op.sinonI8Hex(), -0x7f); + equal(op.sinonU8Hex(), 0xff); + equal(op.sinonI16Hex(), 0x7f); + equal(op.sinonU16Hex(), 0xffff); + equal(op.sinonI32Hex(), 0x7fffffff); + equal(op.sinonU32Hex(), 0xffffffff); + + // Octal integers. + equal(op.sinonU32Oct(), 493); + + // Floats. + equal(op.sinonF32(), 42); + equal(op.sinonF64(), 42.1); + + // Enums. + equal(op.sinonEnum(), "trois"); + + // Step 2. Convince ourselves that if we pass something else, then that + // changes the output. + + let table = { + sinonString: ["foo", "bar"], + sinonBoolean: [true, false], + sinonNull: ["0", "1"], + sinonZero: [0, 1], + sinonU8Dec: [0, 1], + sinonI8Dec: [0, 1], + sinonU16Dec: [0, 1], + sinonI16Dec: [0, 1], + sinonU32Dec: [0, 1], + sinonI32Dec: [0, 1], + sinonU64Dec: [0, 1], + sinonI64Dec: [0, 1], + sinonU8Hex: [0, 1], + sinonI8Hex: [0, 1], + sinonU16Hex: [0, 1], + sinonI16Hex: [0, 1], + sinonU32Hex: [0, 1], + sinonI32Hex: [0, 1], + sinonU64Hex: [0, 1], + sinonI64Hex: [0, 1], + sinonU32Oct: [0, 1], + sinonF32: [0, 1], + sinonF64: [0, 1], + sinonEnum: ["un", "deux", "trois"], + }; + for (let method in table) { + for (let v of table[method]) { + strictEqual(op[method](v), v); + } + } + [["a", "b"], []].forEach(v => { + deepEqual(op.sinonSequence(v), v); + }); +}); diff --git a/examples/rondpoint/tests/gecko_js/xpcshell.ini b/examples/rondpoint/tests/gecko_js/xpcshell.ini new file mode 100644 index 0000000000..6fc0739ca4 --- /dev/null +++ b/examples/rondpoint/tests/gecko_js/xpcshell.ini @@ -0,0 +1 @@ +[test_rondpoint.js] diff --git a/uniffi/Cargo.toml b/uniffi/Cargo.toml index 6a753a2de2..310433c23e 100644 --- a/uniffi/Cargo.toml +++ b/uniffi/Cargo.toml @@ -18,7 +18,7 @@ ffi-support = "~0.4.2" lazy_static = "1.4" log = "0.4" # Regular dependencies -cargo_metadata = "0.11" +cargo_metadata = "0.11.3" paste = "1.0" uniffi_bindgen = { path = "../uniffi_bindgen", optional = true, version = "0.2.0" } diff --git a/uniffi_bindgen/Cargo.toml b/uniffi_bindgen/Cargo.toml index 01a2b946dc..92157e8e2b 100644 --- a/uniffi_bindgen/Cargo.toml +++ b/uniffi_bindgen/Cargo.toml @@ -15,7 +15,7 @@ name = "uniffi-bindgen" path = "src/main.rs" [dependencies] -cargo_metadata = "0.11" +cargo_metadata = "0.11.3" weedle = "0.11" anyhow = "1" askama = "0.10" diff --git a/uniffi_bindgen/askama.toml b/uniffi_bindgen/askama.toml index 53357fbe6f..66b45e67b7 100644 --- a/uniffi_bindgen/askama.toml +++ b/uniffi_bindgen/askama.toml @@ -1,6 +1,6 @@ [general] # Directories to search for templates, relative to the crate root. -dirs = [ "src/templates", "src/bindings/kotlin/templates", "src/bindings/python/templates", "src/bindings/swift/templates" ] +dirs = [ "src/templates", "src/bindings/kotlin/templates", "src/bindings/python/templates", "src/bindings/swift/templates", "src/bindings/gecko_js/templates" ] [[syntax]] name = "kt" @@ -15,4 +15,13 @@ name = "swift" name = "c" [[syntax]] -name = "rs" \ No newline at end of file +name = "rs" + +[[syntax]] +name = "webidl" + +[[syntax]] +name = "xpidl" + +[[syntax]] +name = "cpp" \ No newline at end of file diff --git a/uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs b/uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs new file mode 100644 index 0000000000..ae5f52841c --- /dev/null +++ b/uniffi_bindgen/src/bindings/gecko_js/gen_gecko_js.rs @@ -0,0 +1,580 @@ +/* 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 std::borrow::Cow; + +use anyhow::Result; +use askama::Template; +use heck::{CamelCase, MixedCase}; +use serde::{Deserialize, Serialize}; + +use crate::interface::*; +use crate::MergeWith; + +use super::webidl::{ + BindingArgument, BindingFunction, ReturnBy, ReturningBindingFunction, ThrowBy, +}; + +/// Config options for the generated Firefox front-end bindings. Note that this +/// can only be used to control details *that do not affect the underlying +/// component*, since the details of the underlying component are entirely +/// determined by the `ComponentInterface`. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Config { + /// Specifies an optional prefix to use for all definitions (interfaces, + /// dictionaries, enums, and namespaces) in the generated Firefox WebIDL + /// binding. If a prefix is not specified, the Firefox WebIDL definitions + /// will use the same names as the UDL. + /// + /// For example, if the prefix is `Hola`, and the UDL for the component + /// declares `namespace foo`, `dictionary Bar`, and `interface Baz`, the + /// definitions will be exposed in Firefox WebIDL as `HolaFoo`, `HolaBar`, + /// and `HolaBaz`. + /// + /// This option exists because definition names all share a global + /// namespace (further, all WebIDL namespaces, interfaces, and enums are + /// exposed on `window`), so they must be unique. Firefox will fail to + /// compile if two different WebIDL files declare interfaces, dictionaries, + /// enums, or namespaces with the same name. + /// + /// For this reason, web standards often prefix their definitions: for + /// example, the dictionary to create a `PushSubscription` is called + /// `PushSubscriptionOptionsInit`, not just `Init`. For UniFFI components, + /// prefixing definitions in UDL would make it awkward to consume from other + /// languages that _do_ have namespaces. + /// + /// So we expose the prefix as an option for just Gecko JS bindings. + pub definition_prefix: Option, +} + +impl From<&ComponentInterface> for Config { + fn from(_ci: &ComponentInterface) -> Self { + Config::default() + } +} + +impl MergeWith for Config { + fn merge_with(&self, other: &Self) -> Self { + Config { + definition_prefix: self.definition_prefix.merge_with(&other.definition_prefix), + } + } +} + +/// A context associates config options with a component interface, and provides +/// helper methods that are shared between all templates and filters in this +/// module. +#[derive(Clone, Copy)] +pub struct Context<'config, 'ci> { + config: &'config Config, + ci: &'ci ComponentInterface, +} + +impl<'config, 'ci> Context<'config, 'ci> { + /// Creates a new context with options for the given component interface. + pub fn new(config: &'config Config, ci: &'ci ComponentInterface) -> Self { + Context { config, ci } + } + + /// Returns the `RustBuffer` type name. + /// + /// A `RustBuffer` is a Plain Old Data struct that holds a pointer to a + /// Rust byte buffer, along with its length and capacity. Because the + /// generated binding for each component declares its own FFI symbols in an + /// `extern "C"` block, the `RustBuffer` type name must be unique for each + /// component. + /// + /// Declaring multiple types with the same name in an `extern "C"` block, + /// even if they're in different header files, will fail the build because + /// it violates the One Definition Rule. + pub fn ffi_rustbuffer_type(&self) -> String { + format!("{}_RustBuffer", self.ci.ffi_namespace()) + } + + /// Returns the `ForeignBytes` type name. + /// + /// `ForeignBytes` is a Plain Old Data struct that holds a pointer to some + /// memory allocated by C++, along with its length. See the docs for + /// `ffi_rustbuffer_type` about why this type name must be unique for each + /// component. + pub fn ffi_foreignbytes_type(&self) -> String { + format!("{}_ForeignBytes", self.ci.ffi_namespace()) + } + + /// Returns the `RustError` type name. + /// + /// A `RustError` is a Plain Old Data struct that holds an error code and + /// a message string. See the docs for `ffi_rustbuffer_type` about why this + /// type name must be unique for each component. + pub fn ffi_rusterror_type(&self) -> String { + format!("{}_RustError", self.ci.ffi_namespace()) + } + + /// Returns the name to use for the `detail` C++ namespace, which contains + /// the serialization helpers and other internal types. This name must be + /// unique for each component. + pub fn detail_name(&self) -> String { + format!("{}_detail", self.ci.namespace()) + } + + /// Returns the unprefixed, unmodified component namespace name. This is + /// exposed for convenience, where a template has access to the context but + /// not the component interface. + pub fn namespace(&self) -> &'ci str { + self.ci.namespace() + } + + /// Returns the type name to use for an interface, dictionary, enum, or + /// namespace with the given `ident` in the generated WebIDL and C++ code. + pub fn type_name<'a>(&self, ident: &'a str) -> Cow<'a, str> { + // Prepend the definition prefix if there is one; otherwise, just pass + // the name back as-is. + match self.config.definition_prefix.as_ref() { + Some(prefix) => Cow::Owned(format!("{}{}", prefix, ident)), + None => Cow::Borrowed(ident), + } + } + + /// Returns the C++ header or source file name to use for the given + /// WebIDL interface or namespace name. + pub fn header_name(&self, ident: &str) -> String { + self.type_name(ident).to_camel_case() + } +} + +/// A template for a Firefox WebIDL file. We only generate one of these per +/// component. +#[derive(Template)] +#[template(syntax = "webidl", escape = "none", path = "WebIDLTemplate.webidl")] +pub struct WebIdl<'config, 'ci> { + context: Context<'config, 'ci>, + ci: &'ci ComponentInterface, +} + +impl<'config, 'ci> WebIdl<'config, 'ci> { + pub fn new(config: &'config Config, ci: &'ci ComponentInterface) -> Self { + let context = Context::new(config, ci); + Self { context, ci } + } +} + +/// A shared header file that's included by all our bindings. This defines +/// common serialization logic and `extern` declarations for the FFI. These +/// namespace and interface source files `#include` this file. +#[derive(Template)] +#[template(syntax = "c", escape = "none", path = "SharedHeaderTemplate.h")] +pub struct SharedHeader<'config, 'ci> { + context: Context<'config, 'ci>, + ci: &'ci ComponentInterface, +} + +impl<'config, 'ci> SharedHeader<'config, 'ci> { + pub fn new(config: &'config Config, ci: &'ci ComponentInterface) -> Self { + let context = Context::new(config, ci); + Self { context, ci } + } +} + +/// A header file generated for a namespace containing top-level functions. If +/// the namespace in the UniFFI IDL file is empty, this file isn't generated. +#[derive(Template)] +#[template(syntax = "c", escape = "none", path = "NamespaceHeaderTemplate.h")] +pub struct NamespaceHeader<'config, 'ci> { + context: Context<'config, 'ci>, + functions: &'ci [Function], +} + +impl<'config, 'ci> NamespaceHeader<'config, 'ci> { + pub fn new(context: Context<'config, 'ci>, functions: &'ci [Function]) -> Self { + Self { context, functions } + } +} + +/// An implementation file for a namespace with top-level functions. If the +/// namespace in the UniFFI IDL is empty, this isn't generated. +#[derive(Template)] +#[template(syntax = "cpp", escape = "none", path = "NamespaceTemplate.cpp")] +pub struct Namespace<'config, 'ci> { + context: Context<'config, 'ci>, + functions: &'ci [Function], +} + +impl<'config, 'ci> Namespace<'config, 'ci> { + pub fn new(context: Context<'config, 'ci>, functions: &'ci [Function]) -> Self { + Self { context, functions } + } +} + +/// A header file generated for each interface in the UniFFI IDL. +#[derive(Template)] +#[template(syntax = "c", escape = "none", path = "InterfaceHeaderTemplate.h")] +pub struct InterfaceHeader<'config, 'ci> { + context: Context<'config, 'ci>, + obj: &'ci Object, +} + +impl<'config, 'ci> InterfaceHeader<'config, 'ci> { + pub fn new(context: Context<'config, 'ci>, obj: &'ci Object) -> Self { + Self { context, obj } + } +} + +/// An implementation file generated for each interface in the UniFFI IDL. +#[derive(Template)] +#[template(syntax = "cpp", escape = "none", path = "InterfaceTemplate.cpp")] +pub struct Interface<'config, 'ci> { + context: Context<'config, 'ci>, + obj: &'ci Object, +} + +impl<'config, 'ci> Interface<'config, 'ci> { + pub fn new(context: Context<'config, 'ci>, obj: &'ci Object) -> Self { + Self { context, obj } + } +} + +/// Filters for our Askama templates above. These output C++ and WebIDL. +mod filters { + use super::*; + + /// Declares a WebIDL type. + /// + /// Terminology clarification: UniFFI IDL, the `ComponentInterface`, + /// and Firefox's WebIDL use different but overlapping names for + /// the same types. + /// + /// * `Type::Record` is called a "dictionary" in Firefox WebIDL. It's + /// represented as `dictionary T` in UniFFI IDL and WebIDL. + /// * `Type::Object` is called an "interface" in Firefox WebIDL. It's + /// represented as `interface T` in UniFFI IDL and WebIDL. + /// * `Type::Optional` is called "nullable" in Firefox WebIDL. It's + /// represented as `T?` in UniFFI IDL and WebIDL. + /// * `Type::Map` is called a "record" in Firefox WebIDL. It's represented + /// as `record` in UniFFI IDL, and `record` in + /// WebIDL. + /// + /// There are also semantic differences: + /// + /// * In UniFFI IDL, all `dictionary` members are required by default; in + /// WebIDL, they're all optional. The generated WebIDL file adds a + /// `required` keyword to each member. + /// * In UniFFI IDL, an argument can specify a default value directly. + /// In WebIDL, arguments with default values must have the `optional` + /// keyword. + pub fn type_webidl(type_: &Type, context: &Context<'_, '_>) -> Result { + Ok(match type_ { + Type::Int8 => "byte".into(), + Type::UInt8 => "octet".into(), + Type::Int16 => "short".into(), + Type::UInt16 => "unsigned short".into(), + Type::Int32 => "long".into(), + Type::UInt32 => "unsigned long".into(), + Type::Int64 => "long long".into(), + Type::UInt64 => "unsigned long long".into(), + Type::Float32 => "float".into(), + // Note: Not `unrestricted double`; we don't want to allow NaNs + // and infinity. + Type::Float64 => "double".into(), + Type::Boolean => "boolean".into(), + Type::String => "DOMString".into(), + Type::Enum(name) | Type::Record(name) | Type::Object(name) => { + class_name_webidl(name, context)? + } + Type::Error(_name) => { + // TODO: We don't currently throw typed errors; see + // https://github.com/mozilla/uniffi-rs/issues/295. + panic!("[TODO: type_webidl({:?})]", type_) + } + Type::Optional(inner) => format!("{}?", type_webidl(inner, context)?), + Type::Sequence(inner) => format!("sequence<{}>", type_webidl(inner, context)?), + Type::Map(inner) => format!("record", type_webidl(inner, context)?), + }) + } + + /// Emits a literal default value for WebIDL. + pub fn literal_webidl(literal: &Literal) -> Result { + Ok(match literal { + Literal::Boolean(v) => format!("{}", v), + Literal::String(s) => format!("\"{}\"", s), + Literal::Null => "null".into(), + Literal::EmptySequence => "[]".into(), + Literal::EmptyMap => "{}".into(), + Literal::Enum(v, _) => format!("\"{}\"", enum_variant_webidl(v)?), + Literal::Int(i, radix, _) => match radix { + Radix::Octal => format!("0{:o}", i), + Radix::Decimal => format!("{}", i), + Radix::Hexadecimal => format!("{:#x}", i), + }, + Literal::UInt(i, radix, _) => match radix { + Radix::Octal => format!("0{:o}", i), + Radix::Decimal => format!("{}", i), + Radix::Hexadecimal => format!("{:#x}", i), + }, + Literal::Float(string, _) => string.into(), + }) + } + + /// Declares a C type in the `extern` declarations. + pub fn type_ffi(type_: &FFIType, context: &Context<'_, '_>) -> Result { + Ok(match type_ { + FFIType::Int8 => "int8_t".into(), + FFIType::UInt8 => "uint8_t".into(), + FFIType::Int16 => "int16_t".into(), + FFIType::UInt16 => "uint16_t".into(), + FFIType::Int32 => "int32_t".into(), + FFIType::UInt32 => "uint32_t".into(), + FFIType::Int64 => "int64_t".into(), + FFIType::UInt64 => "uint64_t".into(), + FFIType::Float32 => "float".into(), + FFIType::Float64 => "double".into(), + FFIType::RustCString => "const char*".into(), + FFIType::RustBuffer => context.ffi_rustbuffer_type(), + FFIType::RustError => context.ffi_rusterror_type(), + FFIType::ForeignBytes => context.ffi_foreignbytes_type(), + }) + } + + /// Declares a C++ type. + pub fn type_cpp(type_: &Type, context: &Context<'_, '_>) -> Result { + Ok(match type_ { + Type::Int8 => "int8_t".into(), + Type::UInt8 => "uint8_t".into(), + Type::Int16 => "int16_t".into(), + Type::UInt16 => "uint16_t".into(), + Type::Int32 => "int32_t".into(), + Type::UInt32 => "uint32_t".into(), + Type::Int64 => "int64_t".into(), + Type::UInt64 => "uint64_t".into(), + Type::Float32 => "float".into(), + Type::Float64 => "double".into(), + Type::Boolean => "bool".into(), + Type::String => "nsString".into(), + Type::Enum(name) | Type::Record(name) => class_name_cpp(name, context)?, + Type::Object(name) => format!("OwningNonNull<{}>", class_name_cpp(name, context)?), + Type::Optional(inner) => { + // Nullable objects become `RefPtr` (instead of + // `OwningNonNull`); all others become `Nullable`. + match inner.as_ref() { + Type::Object(name) => format!("RefPtr<{}>", class_name_cpp(name, context)?), + Type::String => "nsString".into(), + _ => format!("Nullable<{}>", type_cpp(inner, context)?), + } + } + Type::Sequence(inner) => format!("nsTArray<{}>", type_cpp(inner, context)?), + Type::Map(inner) => format!("Record", type_cpp(inner, context)?), + Type::Error(_name) => { + // TODO: We don't currently throw typed errors; see + // https://github.com/mozilla/uniffi-rs/issues/295. + panic!("[TODO: type_cpp({:?})]", type_) + } + }) + } + + fn in_arg_type_cpp(type_: &Type, context: &Context<'_, '_>) -> Result { + Ok(match type_ { + Type::Optional(inner) => match inner.as_ref() { + Type::Object(_) | Type::String => type_cpp(type_, context)?, + _ => format!("Nullable<{}>", in_arg_type_cpp(inner, context)?), + }, + Type::Sequence(inner) => format!("Sequence<{}>", in_arg_type_cpp(&inner, context)?), + _ => type_cpp(type_, context)?, + }) + } + + /// Declares a C++ in or out argument type. + pub fn arg_type_cpp( + arg: &BindingArgument<'_>, + context: &Context<'_, '_>, + ) -> Result { + Ok(match arg { + BindingArgument::GlobalObject => "GlobalObject&".into(), + BindingArgument::ErrorResult => "ErrorResult&".into(), + BindingArgument::In(arg) => { + // In arguments are usually passed by `const` reference for + // object types, and by value for primitives. As an exception, + // `nsString` becomes `nsAString` when passed as an argument, + // and nullable objects are passed as pointers. Sequences map + // to the `Sequence` type, not `nsTArray`. + match arg.type_() { + Type::String => "const nsAString&".into(), + Type::Object(name) => format!("{}&", class_name_cpp(&name, context)?), + Type::Optional(inner) => match inner.as_ref() { + Type::String => "const nsAString&".into(), + Type::Object(name) => format!("{}*", class_name_cpp(&name, context)?), + _ => format!("const {}&", in_arg_type_cpp(&arg.type_(), context)?), + }, + Type::Record(_) | Type::Map(_) | Type::Sequence(_) => { + format!("const {}&", in_arg_type_cpp(&arg.type_(), context)?) + } + _ => in_arg_type_cpp(&arg.type_(), context)?, + } + } + BindingArgument::Out(type_) => { + // Out arguments are usually passed by reference. `nsString` + // becomes `nsAString`. + match type_ { + Type::String => "nsAString&".into(), + Type::Optional(inner) => match inner.as_ref() { + Type::String => "nsAString&".into(), + _ => format!("{}&", type_cpp(type_, context)?), + }, + _ => format!("{}&", type_cpp(type_, context)?), + } + } + }) + } + + /// Declares a C++ return type. + pub fn ret_type_cpp(type_: &Type, context: &Context<'_, '_>) -> Result { + Ok(match type_ { + Type::Object(name) => format!("already_AddRefed<{}>", class_name_cpp(name, context)?), + Type::Optional(inner) => match inner.as_ref() { + Type::Object(name) => { + format!("already_AddRefed<{}>", class_name_cpp(name, context)?) + } + _ => type_cpp(type_, context)?, + }, + _ => type_cpp(type_, context)?, + }) + } + + /// Generates a dummy value for a given return type. A C++ function that + /// declares a return type must return some value of that type, even if it + /// throws a DOM exception via the `ErrorResult`. + pub fn dummy_ret_value_cpp( + return_type: &Type, + context: &Context<'_, '_>, + ) -> Result { + Ok(match return_type { + Type::Int8 + | Type::UInt8 + | Type::Int16 + | Type::UInt16 + | Type::Int32 + | Type::UInt32 + | Type::Int64 + | Type::UInt64 => "0".into(), + Type::Float32 => "0.0f".into(), + Type::Float64 => "0.0".into(), + Type::Boolean => "false".into(), + Type::Enum(name) => format!("{}::EndGuard_", class_name_cpp(name, context)?), + Type::Object(_) => "nullptr".into(), + Type::String => "EmptyString()".into(), + Type::Optional(_) | Type::Record(_) | Type::Map(_) | Type::Sequence(_) => { + format!("{}()", type_cpp(return_type, context)?) + } + Type::Error(_) => { + // TODO: We don't currently throw typed errors; see + // https://github.com/mozilla/uniffi-rs/issues/295. + panic!("[TODO: dummy_ret_value_cpp({:?})]", return_type) + } + }) + } + + /// Generates an expression for lowering a C++ type into a C type when + /// calling an FFI function. + pub fn lower_cpp( + type_: &Type, + from: &str, + context: &Context<'_, '_>, + ) -> Result { + let (lifted, nullable) = match type_ { + // Since our in argument type is `nsAString`, we need to use that + // to instantiate `ViaFfi`, not `nsString`. + Type::String => ("nsAString".into(), false), + Type::Optional(inner) => match inner.as_ref() { + Type::String => ("nsAString".into(), true), + _ => (in_arg_type_cpp(type_, context)?, false), + }, + _ => (in_arg_type_cpp(type_, context)?, false), + }; + Ok(format!( + "{}::ViaFfi<{}, {}, {}>::Lower({})", + context.detail_name(), + lifted, + type_ffi(&FFIType::from(type_), context)?, + nullable, + from + )) + } + + /// Generates an expression for lifting a C return type from the FFI into a + /// C++ out parameter. + pub fn lift_cpp( + type_: &Type, + from: &str, + into: &str, + context: &Context<'_, '_>, + ) -> Result { + let (lifted, nullable) = match type_ { + // Out arguments are also `nsAString`, so we need to use it for the + // instantiation. + Type::String => ("nsAString".into(), false), + Type::Optional(inner) => match inner.as_ref() { + Type::String => ("nsAString".into(), true), + _ => (type_cpp(type_, context)?, false), + }, + _ => (type_cpp(type_, context)?, false), + }; + Ok(format!( + "{}::ViaFfi<{}, {}, {}>::Lift({}, {})", + context.detail_name(), + lifted, + type_ffi(&FFIType::from(type_), context)?, + nullable, + from, + into, + )) + } + + pub fn var_name_webidl(nm: &str) -> Result { + Ok(nm.to_mixed_case()) + } + + pub fn enum_variant_webidl(nm: &str) -> Result { + Ok(nm.to_mixed_case()) + } + + pub fn header_name_cpp(nm: &str, context: &Context<'_, '_>) -> Result { + Ok(context.header_name(nm)) + } + + /// Declares an interface, dictionary, enum, or namespace name in WebIDL. + pub fn class_name_webidl(nm: &str, context: &Context<'_, '_>) -> Result { + Ok(context.type_name(nm).to_camel_case()) + } + + /// Declares a class name in C++. + pub fn class_name_cpp(nm: &str, context: &Context<'_, '_>) -> Result { + Ok(context.type_name(nm).to_camel_case()) + } + + /// Declares a method name in WebIDL. + pub fn fn_name_webidl(nm: &str) -> Result { + Ok(nm.to_string().to_mixed_case()) + } + + /// Declares a class or instance method name in C++. Function and methods + /// names are UpperCamelCase in C++, even though they're mixedCamelCase in + /// WebIDL. + pub fn fn_name_cpp(nm: &str) -> Result { + Ok(nm.to_string().to_camel_case()) + } + + /// `Codegen.py` emits field names as `mFieldName`. The `m` prefix is Gecko + /// style for struct members. + pub fn field_name_cpp(nm: &str) -> Result { + Ok(format!("m{}", nm.to_camel_case())) + } + + pub fn enum_variant_cpp(nm: &str) -> Result { + // TODO: Make sure this does the right thing for hyphenated variants + // (https://github.com/mozilla/uniffi-rs/issues/294), or the generated + // code won't compile. + // + // Example: "bookmark-added" should become `Bookmark_added`, because + // that's what Firefox's `Codegen.py` spits out. + Ok(nm.to_camel_case()) + } +} diff --git a/uniffi_bindgen/src/bindings/gecko_js/mod.rs b/uniffi_bindgen/src/bindings/gecko_js/mod.rs new file mode 100644 index 0000000000..c982534015 --- /dev/null +++ b/uniffi_bindgen/src/bindings/gecko_js/mod.rs @@ -0,0 +1,136 @@ +/* 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 std::{ + fs::File, + io::Write, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, Result}; + +pub mod gen_gecko_js; +mod webidl; +pub use gen_gecko_js::{ + Config, Interface, InterfaceHeader, Namespace, NamespaceHeader, SharedHeader, WebIdl, +}; + +use super::super::interface::ComponentInterface; + +pub struct Binding { + name: String, + contents: String, +} + +/// Generate uniffi component bindings for Firefox. +/// +/// Firefox's WebIDL binding declarations, generated by `Codegen.py` in m-c, +/// expect to find a `.h`/`.cpp` pair per interface, even if those interfaces +/// are declared in a single WebIDL file. Dictionaries and enums are +/// autogenerated by `Codegen.py`, so we don't need to worry about them...but +/// we do need to emit serialization code for them, plus the actual interface +/// and top-level function implementations, in the UniFFI bindings. +/// +/// So the Gecko backend generates: +/// +/// * A single WebIDL file with the component interface. This is similar to the +/// UniFFI IDL format, but the names of some types are different. +/// * A shared C++ header, with serialization helpers for all built-in and +/// interface types. +/// * A header and source file for the namespace, if the component defines any +/// top-level functions. +/// * A header and source file for each `interface` declaration in the UniFFI. +/// IDL. +/// +/// These files should be checked in to the Firefox source tree. The WebIDL +/// file goes in `dom/chrome-webidl`, and the header and source files can be +/// added to any directory and referenced in `moz.build`. The Rust component +/// library must also be added as a dependency to `gkrust-shared` (in +/// `toolkit/library/rust/shared`), so that the FFI symbols are linked into +/// libxul. +pub fn write_bindings( + config: &Config, + ci: &ComponentInterface, + out_dir: &Path, + _try_format_code: bool, +) -> Result<()> { + let out_path = PathBuf::from(out_dir); + let bindings = generate_bindings(config, ci)?; + for binding in bindings { + let mut file = out_path.clone(); + file.push(&binding.name); + let mut f = File::create(&file) + .with_context(|| format!("Failed to create file `{}`", binding.name))?; + write!(f, "{}", binding.contents)?; + } + Ok(()) +} + +/// Generate Gecko bindings for the given ComponentInterface, as a string. +pub fn generate_bindings(config: &Config, ci: &ComponentInterface) -> Result> { + use askama::Template; + + let mut bindings = Vec::new(); + + let context = gen_gecko_js::Context::new(config, ci); + + let webidl = WebIdl::new(config, ci) + .render() + .context("Failed to render WebIDL bindings")?; + bindings.push(Binding { + name: format!("{}.webidl", context.header_name(context.namespace())), + contents: webidl, + }); + + let shared_header = SharedHeader::new(config, ci) + .render() + .context("Failed to render shared header")?; + bindings.push(Binding { + name: format!("{}Shared.h", context.header_name(context.namespace())), + contents: shared_header, + }); + + // Top-level functions go in one namespace, which needs its own header and + // source file. + let functions = ci.iter_function_definitions(); + if !functions.is_empty() { + let header = NamespaceHeader::new(context, functions.as_slice()) + .render() + .context("Failed to render top-level namespace header")?; + bindings.push(Binding { + name: format!("{}.h", context.header_name(context.namespace())), + contents: header, + }); + + let source = Namespace::new(context, functions.as_slice()) + .render() + .context("Failed to render top-level namespace binding")?; + bindings.push(Binding { + name: format!("{}.cpp", context.header_name(context.namespace())), + contents: source, + }); + } + + // Now generate one header/source pair for each interface. + let objects = ci.iter_object_definitions(); + for obj in objects { + let header = InterfaceHeader::new(context, &obj) + .render() + .with_context(|| format!("Failed to render {} header", obj.name()))?; + bindings.push(Binding { + name: format!("{}.h", context.header_name(obj.name())), + contents: header, + }); + + let source = Interface::new(context, &obj) + .render() + .with_context(|| format!("Failed to render {} binding", obj.name()))?; + bindings.push(Binding { + name: format!("{}.cpp", context.header_name(obj.name())), + contents: source, + }) + } + + Ok(bindings) +} diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/FFIDeclarationsTemplate.h b/uniffi_bindgen/src/bindings/gecko_js/templates/FFIDeclarationsTemplate.h new file mode 100644 index 0000000000..e707943e9b --- /dev/null +++ b/uniffi_bindgen/src/bindings/gecko_js/templates/FFIDeclarationsTemplate.h @@ -0,0 +1,36 @@ +extern "C" { + +struct {{ context.ffi_rustbuffer_type() }} { + int32_t mCapacity; + int32_t mLen; + uint8_t* mData; +}; + +struct {{ context.ffi_foreignbytes_type() }} { + int32_t mLen; + const uint8_t* mData; +}; + +struct {{ context.ffi_rusterror_type() }} { + int32_t mCode; + char* mMessage; +}; + +{% for func in ci.iter_ffi_function_definitions() -%} +{%- match func.return_type() -%} +{%- when Some with (type_) %} +{{ type_|type_ffi(context) }} +{% when None %} +void +{%- endmatch %} +{{ func.name() }}( + {%- for arg in func.arguments() %} + {{ arg.type_()|type_ffi(context) }} {{ arg.name() -}}{%- if loop.last -%}{%- else -%},{%- endif -%} + {%- endfor %} + {%- if func.arguments().len() > 0 %},{% endif %} + {{ context.ffi_rusterror_type() }}* uniffi_out_err +); + +{% endfor -%} + +} // extern "C" diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceHeaderTemplate.h b/uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceHeaderTemplate.h new file mode 100644 index 0000000000..96b85c73ca --- /dev/null +++ b/uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceHeaderTemplate.h @@ -0,0 +1,61 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +{% import "macros.cpp" as cpp %} + +#ifndef mozilla_dom_{{ obj.name()|header_name_cpp(context) }} +#define mozilla_dom_{{ obj.name()|header_name_cpp(context) }} + +#include "jsapi.h" +#include "nsCOMPtr.h" +#include "nsIGlobalObject.h" +#include "nsWrapperCache.h" + +#include "mozilla/RefPtr.h" + +#include "mozilla/dom/{{ context.namespace()|header_name_cpp(context) }}Binding.h" + +namespace mozilla { +namespace dom { + +class {{ obj.name()|class_name_cpp(context) }} final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS({{ obj.name()|class_name_cpp(context) }}) + + {{ obj.name()|class_name_cpp(context) }}(nsIGlobalObject* aGlobal, uint64_t aHandle); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + nsIGlobalObject* GetParentObject() const { return mGlobal; } + + {%- for cons in obj.constructors() %} + + static already_AddRefed<{{ obj.name()|class_name_cpp(context) }}> Constructor( + {%- for arg in cons.binding_arguments() %} + {{ arg|arg_type_cpp(context) }} {{ arg.name() }}{%- if !loop.last %},{% endif %} + {%- endfor %} + ); + {%- endfor %} + + {%- for meth in obj.methods() %} + + {% match meth.binding_return_type() %}{% when Some with (type_) %}{{ type_|ret_type_cpp(context) }}{% else %}void{% endmatch %} {{ meth.name()|fn_name_cpp }}( + {%- for arg in meth.binding_arguments() %} + {{ arg|arg_type_cpp(context) }} {{ arg.name() }}{%- if !loop.last %},{% endif %} + {%- endfor %} + ); + {%- endfor %} + + private: + ~{{ obj.name()|class_name_cpp(context) }}(); + + nsCOMPtr mGlobal; + uint64_t mHandle; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_{{ obj.name()|header_name_cpp(context) }} diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceTemplate.cpp b/uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceTemplate.cpp new file mode 100644 index 0000000000..53fd5da482 --- /dev/null +++ b/uniffi_bindgen/src/bindings/gecko_js/templates/InterfaceTemplate.cpp @@ -0,0 +1,78 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +{% import "macros.cpp" as cpp %} + +#include "mozilla/dom/{{ obj.name()|header_name_cpp(context) }}.h" +#include "mozilla/dom/{{ context.namespace()|header_name_cpp(context) }}Shared.h" + +namespace mozilla { +namespace dom { + +// Cycle collection boilerplate for our interface implementation. `mGlobal` is +// the only member that needs to be cycle-collected; if we ever add any JS +// object members or other interfaces to the class, those should be collected, +// too. +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE({{ obj.name()|class_name_cpp(context) }}, mGlobal) +NS_IMPL_CYCLE_COLLECTING_ADDREF({{ obj.name()|class_name_cpp(context) }}) +NS_IMPL_CYCLE_COLLECTING_RELEASE({{ obj.name()|class_name_cpp(context) }}) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION({{ obj.name()|class_name_cpp(context) }}) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +{{ obj.name()|class_name_cpp(context) }}::{{ obj.name()|class_name_cpp(context) }}( + nsIGlobalObject* aGlobal, + uint64_t aHandle +) : mGlobal(aGlobal), mHandle(aHandle) {} + +{{ obj.name()|class_name_cpp(context) }}::~{{ obj.name()|class_name_cpp(context) }}() { + {{ context.ffi_rusterror_type() }} err = {0, nullptr}; + {{ obj.ffi_object_free().name() }}(mHandle, &err); + MOZ_ASSERT(!err.mCode); +} + +JSObject* {{ obj.name()|class_name_cpp(context) }}::WrapObject( + JSContext* aCx, + JS::Handle aGivenProto +) { + return dom::{{ obj.name()|class_name_cpp(context) }}_Binding::Wrap(aCx, this, aGivenProto); +} + +{%- for cons in obj.constructors() %} + +/* static */ +already_AddRefed<{{ obj.name()|class_name_cpp(context) }}> {{ obj.name()|class_name_cpp(context) }}::Constructor( + {%- for arg in cons.binding_arguments() %} + {{ arg|arg_type_cpp(context) }} {{ arg.name() }}{%- if !loop.last %},{% endif %} + {%- endfor %} +) { + {%- call cpp::to_ffi_call_head(context, cons, "err", "handle") %} + if (err.mCode) { + {%- match cons.throw_by() %} + {%- when ThrowBy::ErrorResult with (rv) %} + {{ rv }}.ThrowOperationError(err.mMessage); + {%- when ThrowBy::Assert %} + MOZ_ASSERT(false); + {%- endmatch %} + return nullptr; + } + nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); + auto result = MakeRefPtr<{{ obj.name()|class_name_cpp(context) }}>(global, handle); + return result.forget(); +} +{%- endfor %} + +{%- for meth in obj.methods() %} + +{% match meth.binding_return_type() %}{% when Some with (type_) %}{{ type_|ret_type_cpp(context) }}{% else %}void{% endmatch %} {{ obj.name()|class_name_cpp(context) }}::{{ meth.name()|fn_name_cpp }}( + {%- for arg in meth.binding_arguments() %} + {{ arg|arg_type_cpp(context) }} {{ arg.name() }}{%- if !loop.last %},{% endif %} + {%- endfor %} +) { + {%- call cpp::to_ffi_call_with_prefix(context, "mHandle", meth) %} +} +{%- endfor %} + +} // namespace dom +} // namespace mozilla diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/NamespaceHeaderTemplate.h b/uniffi_bindgen/src/bindings/gecko_js/templates/NamespaceHeaderTemplate.h new file mode 100644 index 0000000000..51f18975ee --- /dev/null +++ b/uniffi_bindgen/src/bindings/gecko_js/templates/NamespaceHeaderTemplate.h @@ -0,0 +1,33 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +{% import "macros.cpp" as cpp %} + +#ifndef mozilla_dom_{{ context.namespace()|header_name_cpp(context) }} +#define mozilla_dom_{{ context.namespace()|header_name_cpp(context) }} + +#include "mozilla/dom/{{ context.namespace()|header_name_cpp(context) }}Binding.h" + +namespace mozilla { +namespace dom { + +class {{ context.namespace()|class_name_cpp(context) }} final { + public: + {{ context.namespace()|class_name_cpp(context) }}() = default; + ~{{ context.namespace()|class_name_cpp(context) }}() = default; + + {%- for func in functions %} + + static {% match func.binding_return_type() %}{% when Some with (type_) %}{{ type_|ret_type_cpp(context) }}{% else %}void{% endmatch %} {{ func.name()|fn_name_cpp }}( + {%- for arg in func.binding_arguments() %} + {{ arg|arg_type_cpp(context) }} {{ arg.name() }}{%- if !loop.last %},{% endif %} + {%- endfor %} + ); + + {%- endfor %} +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_{{ context.namespace()|header_name_cpp(context) }} diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/NamespaceTemplate.cpp b/uniffi_bindgen/src/bindings/gecko_js/templates/NamespaceTemplate.cpp new file mode 100644 index 0000000000..51d3c9be4f --- /dev/null +++ b/uniffi_bindgen/src/bindings/gecko_js/templates/NamespaceTemplate.cpp @@ -0,0 +1,25 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +{% import "macros.cpp" as cpp %} + +#include "mozilla/dom/{{ context.namespace()|header_name_cpp(context) }}.h" +#include "mozilla/dom/{{ context.namespace()|header_name_cpp(context) }}Shared.h" + +namespace mozilla { +namespace dom { + +{%- for func in functions %} + +/* static */ +{% match func.binding_return_type() %}{% when Some with (type_) %}{{ type_|ret_type_cpp(context) }}{% else %}void{% endmatch %} {{ context.namespace()|class_name_cpp(context) }}::{{ func.name()|fn_name_cpp }}( + {%- for arg in func.binding_arguments() %} + {{ arg|arg_type_cpp(context) }} {{ arg.name() }}{%- if !loop.last %},{% endif %} + {%- endfor %} +) { + {%- call cpp::to_ffi_call(context, func) %} +} +{%- endfor %} + +} // namespace dom +} // namespace mozilla diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/RustBufferHelper.h b/uniffi_bindgen/src/bindings/gecko_js/templates/RustBufferHelper.h new file mode 100644 index 0000000000..bae00889e0 --- /dev/null +++ b/uniffi_bindgen/src/bindings/gecko_js/templates/RustBufferHelper.h @@ -0,0 +1,701 @@ +namespace {{ context.detail_name() }} { + +/// Estimates the worst-case UTF-8 encoded length for a UTF-16 string. +CheckedInt EstimateUTF8Length(size_t aUTF16Length) { + // `ConvertUtf16toUtf8` expects the destination to have at least three times + // as much space as the source string, even if it doesn't use the excess + // capacity. + CheckedInt length(aUTF16Length); + length *= 3; + return length; +} + +/// Reads values out of a byte buffer received from Rust. +class MOZ_STACK_CLASS Reader final { + public: + explicit Reader(const {{ context.ffi_rustbuffer_type() }}& aBuffer) : mBuffer(aBuffer), mOffset(0) {} + + /// Returns `true` if there are unread bytes in the buffer, or `false` if the + /// current position has reached the end of the buffer. If `HasRemaining()` + /// returns `false`, attempting to read any more values from the buffer will + /// assert. + bool HasRemaining() { return mOffset.value() < mBuffer.mLen; } + + /// `Read{U}Int{8, 16, 32, 64}` read fixed-width integers from the buffer at + /// the current position. + + uint8_t ReadUInt8() { + return ReadAt( + [this](size_t aOffset) { return mBuffer.mData[aOffset]; }); + } + + int8_t ReadInt8() { return BitwiseCast(ReadUInt8()); } + + uint16_t ReadUInt16() { + return ReadAt([this](size_t aOffset) { + uint16_t value; + memcpy(&value, &mBuffer.mData[aOffset], sizeof(uint16_t)); + // `PR_ntohs` ("network to host, short") because the UniFFI serialization + // format encodes integers in big-endian order (also called + // "network byte order"). + return PR_ntohs(value); + }); + } + + int16_t ReadInt16() { return BitwiseCast(ReadUInt16()); } + + uint32_t ReadUInt32() { + return ReadAt([this](size_t aOffset) { + uint32_t value; + memcpy(&value, &mBuffer.mData[aOffset], sizeof(uint32_t)); + return PR_ntohl(value); + }); + } + + int32_t ReadInt32() { return BitwiseCast(ReadUInt32()); } + + uint64_t ReadUInt64() { + return ReadAt([this](size_t aOffset) { + uint64_t value; + memcpy(&value, &mBuffer.mData[aOffset], sizeof(uint64_t)); + return PR_ntohll(value); + }); + } + + int64_t ReadInt64() { return BitwiseCast(ReadUInt64()); } + + /// `Read{Float, Double}` reads a floating-point number from the buffer at + /// the current position. + + float ReadFloat() { return BitwiseCast(ReadUInt32()); } + + double ReadDouble() { return BitwiseCast(ReadUInt64()); } + + /// Reads a sequence or record length from the buffer at the current position. + size_t ReadLength() { + // The UniFFI serialization format uses signed integers for lengths. + auto length = ReadInt32(); + MOZ_RELEASE_ASSERT(length >= 0); + return static_cast(length); + } + + /// Reads a UTF-8 encoded string at the current position. + void ReadCString(nsACString& aValue) { + auto length = ReadInt32(); + CheckedInt newOffset = mOffset; + newOffset += length; + AssertInBounds(newOffset); + aValue.Append(AsChars(Span(&mBuffer.mData[mOffset.value()], length))); + mOffset = newOffset; + } + + /// Reads a UTF-16 encoded string at the current position. + void ReadString(nsAString& aValue) { + auto length = ReadInt32(); + CheckedInt newOffset = mOffset; + newOffset += length; + AssertInBounds(newOffset); + AppendUTF8toUTF16(AsChars(Span(&mBuffer.mData[mOffset.value()], length)), + aValue); + mOffset = newOffset; + } + + private: + void AssertInBounds(const CheckedInt& aNewOffset) const { + MOZ_RELEASE_ASSERT(aNewOffset.isValid() && + aNewOffset.value() <= mBuffer.mLen); + } + + template + T ReadAt(const std::function& aClosure) { + CheckedInt newOffset = mOffset; + newOffset += sizeof(T); + AssertInBounds(newOffset); + T result = aClosure(mOffset.value()); + mOffset = newOffset; + return result; + } + + const {{ context.ffi_rustbuffer_type() }}& mBuffer; + CheckedInt mOffset; +}; + +/// Writes values into a Rust buffer. +class MOZ_STACK_CLASS Writer final { + public: + Writer() { + {{ context.ffi_rusterror_type() }} err = {0, nullptr}; + mBuffer = {{ ci.ffi_rustbuffer_alloc().name() }}(0, &err); + if (err.mCode) { + MOZ_ASSERT(false, "Failed to allocate empty Rust buffer"); + } + } + + /// `Write{U}Int{8, 16, 32, 64}` write fixed-width integers into the buffer at + /// the current position. + + void WriteUInt8(const uint8_t& aValue) { + WriteAt(aValue, [](uint8_t* aBuffer, const uint8_t& aValue) { + *aBuffer = aValue; + }); + } + + void WriteInt8(const int8_t& aValue) { + WriteUInt8(BitwiseCast(aValue)); + } + + void WriteUInt16(const uint16_t& aValue) { + WriteAt(aValue, [](uint8_t* aBuffer, const uint16_t& aValue) { + // `PR_htons` ("host to network, short") because, as mentioned above, the + // UniFFI serialization format encodes integers in big-endian (network + // byte) order. + uint16_t value = PR_htons(aValue); + memcpy(aBuffer, &value, sizeof(uint16_t)); + }); + } + + void WriteInt16(const int16_t& aValue) { + WriteUInt16(BitwiseCast(aValue)); + } + + void WriteUInt32(const uint32_t& aValue) { + WriteAt(aValue, [](uint8_t* aBuffer, const uint32_t& aValue) { + uint32_t value = PR_htonl(aValue); + memcpy(aBuffer, &value, sizeof(uint32_t)); + }); + } + + void WriteInt32(const int32_t& aValue) { + WriteUInt32(BitwiseCast(aValue)); + } + + void WriteUInt64(const uint64_t& aValue) { + WriteAt(aValue, [](uint8_t* aBuffer, const uint64_t& aValue) { + uint64_t value = PR_htonll(aValue); + memcpy(aBuffer, &value, sizeof(uint64_t)); + }); + } + + void WriteInt64(const int64_t& aValue) { + WriteUInt64(BitwiseCast(aValue)); + } + + /// `Write{Float, Double}` writes a floating-point number into the buffer at + /// the current position. + + void WriteFloat(const float& aValue) { + WriteUInt32(BitwiseCast(aValue)); + } + + void WriteDouble(const double& aValue) { + WriteUInt64(BitwiseCast(aValue)); + } + + /// Writes a sequence or record length into the buffer at the current + /// position. + void WriteLength(size_t aValue) { + MOZ_RELEASE_ASSERT( + aValue <= static_cast(std::numeric_limits::max())); + WriteInt32(static_cast(aValue)); + } + + /// Writes a UTF-8 encoded string at the current offset. + void WriteCString(const nsACString& aValue) { + CheckedInt size(aValue.Length()); + size += sizeof(uint32_t); // For the length prefix. + MOZ_RELEASE_ASSERT(size.isValid()); + Reserve(size.value()); + + // Write the length prefix first... + uint32_t lengthPrefix = PR_htonl(aValue.Length()); + memcpy(&mBuffer.mData[mBuffer.mLen], &lengthPrefix, sizeof(uint32_t)); + + // ...Then the string. We just copy the string byte-for-byte into the + // buffer here; the Rust side of the FFI will ensure it's valid UTF-8. + memcpy(&mBuffer.mData[mBuffer.mLen + sizeof(uint32_t)], + aValue.BeginReading(), aValue.Length()); + + mBuffer.mLen += static_cast(size.value()); + } + + /// Writes a UTF-16 encoded string at the current offset. + void WriteString(const nsAString& aValue) { + auto maxSize = EstimateUTF8Length(aValue.Length()); + maxSize += sizeof(uint32_t); // For the length prefix. + MOZ_RELEASE_ASSERT(maxSize.isValid()); + Reserve(maxSize.value()); + + // Convert the string to UTF-8 first... + auto span = AsWritableChars(Span( + &mBuffer.mData[mBuffer.mLen + sizeof(uint32_t)], aValue.Length() * 3)); + size_t bytesWritten = ConvertUtf16toUtf8(aValue, span); + + // And then write the length prefix, with the actual number of bytes + // written. + uint32_t lengthPrefix = PR_htonl(bytesWritten); + memcpy(&mBuffer.mData[mBuffer.mLen], &lengthPrefix, sizeof(uint32_t)); + + mBuffer.mLen += static_cast(bytesWritten) + sizeof(uint32_t); + } + + /// Returns the buffer. + {{ context.ffi_rustbuffer_type() }} Buffer() { return mBuffer; } + + private: + /// Reserves the requested number of bytes in the Rust buffer, aborting on + /// allocation failure. + void Reserve(size_t aBytes) { + if (aBytes >= static_cast(std::numeric_limits::max())) { + NS_ABORT_OOM(aBytes); + } + {{ context.ffi_rusterror_type() }} err = {0, nullptr}; + {{ context.ffi_rustbuffer_type() }} newBuffer = {{ ci.ffi_rustbuffer_reserve().name() }}( + mBuffer, static_cast(aBytes), &err); + if (err.mCode) { + NS_ABORT_OOM(aBytes); + } + mBuffer = newBuffer; + } + + template + void WriteAt(const T& aValue, + const std::function& aClosure) { + Reserve(sizeof(T)); + aClosure(&mBuffer.mData[mBuffer.mLen], aValue); + mBuffer.mLen += sizeof(T); + } + + {{ context.ffi_rustbuffer_type() }} mBuffer; +}; + +/// A "trait" struct with specializations for types that can be read and +/// written into a byte buffer. This struct is specialized for all serializable +/// types. +template +struct Serializable { + /// Reads a value of type `T` from a byte buffer. + static bool ReadFrom(Reader& aReader, T& aValue) = delete; + + /// Writes a value of type `T` into a byte buffer. + static void WriteInto(Writer& aWriter, const T& aValue) = delete; +}; + +/// A "trait" with specializations for types that can be transferred back and +/// forth over the FFI. This is analogous to the Rust trait of the same name. +/// As above, this gives us compile-time type checking for type pairs. If +/// `ViaFfi::Lift(U, T)` compiles, we know that a value of type `U` from +/// the FFI can be lifted into a value of type `T`. +/// +/// The `Nullable` parameter is used to specialize nullable and non-null +/// strings, which have the same `T` and `FfiType`, but are represented +/// differently over the FFI. +template +struct ViaFfi { + /// Converts a low-level `FfiType`, which is a POD (Plain Old Data) type that + /// can be passed over the FFI, into a high-level type `T`. + /// + /// `T` is passed as an "out" parameter because some high-level types, like + /// dictionaries, can't be returned by value. + static bool Lift(const FfiType& aLowered, T& aLifted) = delete; + + /// Converts a high-level type `T` into a low-level `FfiType`. `FfiType` is + /// returned by value because it's guaranteed to be a POD, and because it + /// simplifies the `ViaFfi::Lower` calls that are generated for each argument + /// to an FFI function. + static FfiType Lower(const T& aLifted) = delete; +}; + +// This macro generates boilerplate specializations for primitive numeric types +// that are passed directly over the FFI without conversion. +#define UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(Type, readFunc, writeFunc) \ + template <> \ + struct Serializable { \ + [[nodiscard]] static bool ReadFrom(Reader& aReader, Type& aValue) { \ + aValue = aReader.readFunc(); \ + return true; \ + } \ + static void WriteInto(Writer& aWriter, const Type& aValue) { \ + aWriter.writeFunc(aValue); \ + } \ + }; \ + template <> \ + struct ViaFfi { \ + [[nodiscard]] static bool Lift(const Type& aLowered, Type& aLifted) { \ + aLifted = aLowered; \ + return true; \ + } \ + [[nodiscard]] static Type Lower(const Type& aLifted) { return aLifted; } \ + } + +UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(uint8_t, ReadUInt8, WriteUInt8); +UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(int8_t, ReadInt8, WriteInt8); +UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(uint16_t, ReadUInt16, WriteUInt16); +UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(int16_t, ReadInt16, WriteInt16); +UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(uint32_t, ReadUInt32, WriteUInt32); +UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(int32_t, ReadInt32, WriteInt32); +UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(uint64_t, ReadUInt64, WriteUInt64); +UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(int64_t, ReadInt64, WriteInt64); +UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(float, ReadFloat, WriteFloat); +UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE(double, ReadDouble, WriteDouble); + +#undef UNIFFI_SPECIALIZE_SERIALIZABLE_PRIMITIVE + +/// In the UniFFI serialization format, Booleans are passed as `int8_t`s over +/// the FFI. + +template <> +struct Serializable { + [[nodiscard]] static bool ReadFrom(Reader& aReader, bool& aValue) { + aValue = aReader.ReadInt8() != 0; + return true; + } + static void WriteInto(Writer& aWriter, const bool& aValue) { + aWriter.WriteInt8(aValue ? 1 : 0); + } +}; + +template <> +struct ViaFfi { + [[nodiscard]] static bool Lift(const int8_t& aLowered, bool& aLifted) { + aLifted = aLowered != 0; + return true; + } + [[nodiscard]] static int8_t Lower(const bool& aLifted) { + return aLifted ? 1 : 0; + } +}; + +/// Strings are length-prefixed and UTF-8 encoded when serialized +/// into Rust buffers, and are passed as UTF-8 encoded `RustBuffer`s over +/// the FFI. + +/// `ns{A}CString` is Gecko's "narrow" (8 bits per character) string type. +/// These don't have a fixed encoding: they can be ASCII, Latin-1, or UTF-8. +/// They're called `ByteString`s in WebIDL, and they're pretty uncommon compared +/// to `ns{A}String`. + +template <> +struct Serializable { + [[nodiscard]] static bool ReadFrom(Reader& aReader, nsACString& aValue) { + aReader.ReadCString(aValue); + return true; + } + static void WriteInto(Writer& aWriter, const nsACString& aValue) { + aWriter.WriteCString(aValue); + } +}; + +template <> +struct ViaFfi { + [[nodiscard]] static bool Lift(const {{ context.ffi_rustbuffer_type() }}& aLowered, + nsACString& aLifted) { + if (aLowered.mData) { + aLifted.Append(AsChars(Span(aLowered.mData, aLowered.mLen))); + {{ context.ffi_rusterror_type() }} err = {0, nullptr}; + {{ ci.ffi_rustbuffer_free().name() }}(aLowered, &err); + if (err.mCode) { + MOZ_ASSERT(false, "Failed to lift `nsACString` from Rust buffer"); + return false; + } + } + return true; + } + + [[nodiscard]] static {{ context.ffi_rustbuffer_type() }} Lower(const nsACString& aLifted) { + MOZ_RELEASE_ASSERT( + aLifted.Length() <= + static_cast(std::numeric_limits::max())); + {{ context.ffi_rusterror_type() }} err = {0, nullptr}; + {{ context.ffi_foreignbytes_type() }} bytes = { + static_cast(aLifted.Length()), + reinterpret_cast(aLifted.BeginReading())}; + {{ context.ffi_rustbuffer_type() }} lowered = {{ ci.ffi_rustbuffer_from_bytes().name() }}(bytes, &err); + if (err.mCode) { + MOZ_ASSERT(false, "Failed to lower `nsACString` into Rust string"); + } + return lowered; + } +}; + +template <> +struct Serializable { + [[nodiscard]] static bool ReadFrom(Reader& aReader, nsCString& aValue) { + aReader.ReadCString(aValue); + return true; + } + static void WriteInto(Writer& aWriter, const nsCString& aValue) { + aWriter.WriteCString(aValue); + } +}; + +/// `ns{A}String` is Gecko's "wide" (16 bits per character) string type. +/// These are always UTF-16, so we need to convert them to UTF-8 before +/// passing them to Rust. WebIDL calls these `DOMString`s, and they're +/// ubiquitous. + +template <> +struct Serializable { + [[nodiscard]] static bool ReadFrom(Reader& aReader, nsAString& aValue) { + aReader.ReadString(aValue); + return true; + } + static void WriteInto(Writer& aWriter, const nsAString& aValue) { + aWriter.WriteString(aValue); + } +}; + +template <> +struct ViaFfi { + [[nodiscard]] static bool Lift(const {{ context.ffi_rustbuffer_type() }}& aLowered, + nsAString& aLifted) { + if (aLowered.mData) { + CopyUTF8toUTF16(AsChars(Span(aLowered.mData, aLowered.mLen)), aLifted); + {{ context.ffi_rusterror_type() }} err = {0, nullptr}; + {{ ci.ffi_rustbuffer_free().name() }}(aLowered, &err); + if (err.mCode) { + MOZ_ASSERT(false, "Failed to lift `nsAString` from Rust buffer"); + return false; + } + } + return true; + } + + [[nodiscard]] static {{ context.ffi_rustbuffer_type() }} Lower(const nsAString& aLifted) { + auto maxSize = EstimateUTF8Length(aLifted.Length()); + MOZ_RELEASE_ASSERT( + maxSize.isValid() && + maxSize.value() <= + static_cast(std::numeric_limits::max())); + + {{ context.ffi_rusterror_type() }} err = {0, nullptr}; + {{ context.ffi_rustbuffer_type() }} lowered = {{ ci.ffi_rustbuffer_alloc().name() }}( + static_cast(maxSize.value()), &err); + if (err.mCode) { + MOZ_ASSERT(false, "Failed to lower `nsAString` into Rust string"); + } + + auto span = AsWritableChars(Span(lowered.mData, aLifted.Length() * 3)); + size_t bytesWritten = ConvertUtf16toUtf8(aLifted, span); + lowered.mLen = static_cast(bytesWritten); + + return lowered; + } +}; + +template <> +struct Serializable { + [[nodiscard]] static bool ReadFrom(Reader& aReader, nsString& aValue) { + aReader.ReadString(aValue); + return true; + } + static void WriteInto(Writer& aWriter, const nsString& aValue) { + aWriter.WriteString(aValue); + } +}; + +/// Nullable values are prefixed by a tag: 0 if none; 1 followed by the +/// serialized value if some. These are turned into Rust `Option`s. +/// +/// These are always serialized, never passed directly over the FFI. + +template +struct Serializable> { + [[nodiscard]] static bool ReadFrom(Reader& aReader, + dom::Nullable& aValue) { + auto hasValue = aReader.ReadInt8(); + if (hasValue != 0 && hasValue != 1) { + MOZ_ASSERT(false); + return false; + } + if (!hasValue) { + aValue = dom::Nullable(); + return true; + } + T value; + if (!Serializable::ReadFrom(aReader, value)) { + return false; + } + aValue = dom::Nullable(std::move(value)); + return true; + }; + + static void WriteInto(Writer& aWriter, const dom::Nullable& aValue) { + if (aValue.IsNull()) { + aWriter.WriteInt8(0); + } else { + aWriter.WriteInt8(1); + Serializable::WriteInto(aWriter, aValue.Value()); + } + } +}; + +/// Sequences are length-prefixed, followed by the serialization of each +/// element. They're always serialized, and never passed directly over the +/// FFI. +/// +/// WebIDL has two different representations for sequences, though they both +/// use `nsTArray` under the hood. `dom::Sequence` is for sequence +/// arguments; `nsTArray` is for sequence return values and dictionary +/// members. + +template +struct Serializable> { + // We leave `ReadFrom` unimplemented because sequences should only be + // lowered from the C++ WebIDL binding to the FFI. If the FFI function + // returns a sequence, it'll be lifted into an `nsTArray`, not a + // `dom::Sequence`. See the note about sequences above. + [[nodiscard]] static bool ReadFrom(Reader& aReader, + dom::Sequence& aValue) = delete; + + static void WriteInto(Writer& aWriter, const dom::Sequence& aValue) { + aWriter.WriteLength(aValue.Length()); + for (const T& element : aValue) { + Serializable::WriteInto(aWriter, element); + } + } +}; + +template +struct Serializable> { + [[nodiscard]] static bool ReadFrom(Reader& aReader, nsTArray& aValue) { + auto length = aReader.ReadLength(); + aValue.SetCapacity(length); + for (size_t i = 0; i < length; ++i) { + if (!Serializable::ReadFrom(aReader, *aValue.AppendElement())) { + return false; + } + } + return true; + }; + + static void WriteInto(Writer& aWriter, const nsTArray& aValue) { + aWriter.WriteLength(aValue.Length()); + for (const T& element : aValue) { + Serializable::WriteInto(aWriter, element); + } + } +}; + +/// Records are length-prefixed, followed by the serialization of each +/// key and value. They're always serialized, and never passed directly over the +/// FFI. + +template +struct Serializable> { + [[nodiscard]] static bool ReadFrom(Reader& aReader, Record& aValue) { + auto length = aReader.ReadLength(); + aValue.Entries().SetCapacity(length); + for (size_t i = 0; i < length; ++i) { + typename Record::EntryType* entry = + aValue.Entries().AppendElement(); + if (!Serializable::ReadFrom(aReader, entry->mKey)) { + return false; + } + if (!Serializable::ReadFrom(aReader, entry->mValue)) { + return false; + } + } + return true; + }; + + static void WriteInto(Writer& aWriter, const Record& aValue) { + aWriter.WriteLength(aValue.Entries().Length()); + for (const typename Record::EntryType& entry : aValue.Entries()) { + Serializable::WriteInto(aWriter, entry.mKey); + Serializable::WriteInto(aWriter, entry.mValue); + } + } +}; + +/// Partial specialization for all types that can be serialized into a byte +/// buffer. This is analogous to the `ViaFfiUsingByteBuffer` trait in Rust. + +template +struct ViaFfi { + [[nodiscard]] static bool Lift(const {{ context.ffi_rustbuffer_type() }}& aLowered, T& aLifted) { + auto reader = Reader(aLowered); + if (!Serializable::ReadFrom(reader, aLifted)) { + return false; + } + if (reader.HasRemaining()) { + MOZ_ASSERT(false); + return false; + } + {{ context.ffi_rusterror_type() }} err = {0, nullptr}; + {{ ci.ffi_rustbuffer_free().name() }}(aLowered, &err); + if (err.mCode) { + MOZ_ASSERT(false, "Failed to free Rust buffer after lifting contents"); + return false; + } + return true; + } + + [[nodiscard]] static {{ context.ffi_rustbuffer_type() }} Lower(const T& aLifted) { + auto writer = Writer(); + Serializable::WriteInto(writer, aLifted); + return writer.Buffer(); + } +}; + +/// Nullable strings are a special case. In Gecko C++, there's no type-level +/// way to distinguish between nullable and non-null strings: the WebIDL +/// bindings layer passes `nsAString` for both `DOMString` and `DOMString?`. +/// But the Rust side of the FFI expects nullable strings to be serialized as +/// `Nullable`, not `nsA{C}String`. +/// +/// These specializations serialize nullable strings as if they were +/// `Nullable`. + +template <> +struct ViaFfi { + [[nodiscard]] static bool Lift(const {{ context.ffi_rustbuffer_type() }}& aLowered, + nsACString& aLifted) { + auto value = dom::Nullable(); + if (!ViaFfi, {{ context.ffi_rustbuffer_type() }}>::Lift(aLowered, value)) { + return false; + } + if (value.IsNull()) { + // `SetIsVoid` marks the string as "voided". The JS engine will reflect + // voided strings as `null`, not `""`. + aLifted.SetIsVoid(true); + } else { + aLifted = value.Value(); + } + return true; + } + + [[nodiscard]] static {{ context.ffi_rustbuffer_type() }} Lower(const nsACString& aLifted) { + auto value = dom::Nullable(); + if (!aLifted.IsVoid()) { + value.SetValue() = aLifted; + } + return ViaFfi, {{ context.ffi_rustbuffer_type() }}>::Lower(value); + } +}; + +template <> +struct ViaFfi { + [[nodiscard]] static bool Lift(const {{ context.ffi_rustbuffer_type() }}& aLowered, + nsAString& aLifted) { + auto value = dom::Nullable(); + if (!ViaFfi, {{ context.ffi_rustbuffer_type() }}>::Lift(aLowered, value)) { + return false; + } + if (value.IsNull()) { + aLifted.SetIsVoid(true); + } else { + aLifted = value.Value(); + } + return true; + } + + [[nodiscard]] static {{ context.ffi_rustbuffer_type() }} Lower(const nsAString& aLifted) { + auto value = dom::Nullable(); + if (!aLifted.IsVoid()) { + value.SetValue() = aLifted; + } + return ViaFfi, {{ context.ffi_rustbuffer_type() }}>::Lower(value); + } +}; + +} // namespace {{ context.detail_name() }} diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/SharedHeaderTemplate.h b/uniffi_bindgen/src/bindings/gecko_js/templates/SharedHeaderTemplate.h new file mode 100644 index 0000000000..8e2eef24e9 --- /dev/null +++ b/uniffi_bindgen/src/bindings/gecko_js/templates/SharedHeaderTemplate.h @@ -0,0 +1,99 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +#ifndef mozilla_dom_{{ context.namespace()|header_name_cpp(context) }}_Shared +#define mozilla_dom_{{ context.namespace()|header_name_cpp(context) }}_Shared + +#include + +#include "nsDebug.h" +#include "nsTArray.h" +#include "prnetdb.h" + +#include "mozilla/Casting.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/Utf8.h" + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/Record.h" +#include "mozilla/dom/{{ context.namespace()|header_name_cpp(context) }}Binding.h" + +{% include "FFIDeclarationsTemplate.h" %} + +namespace mozilla { +namespace dom { + +{% include "RustBufferHelper.h" %} + +namespace {{ context.detail_name() }} { + +{% for e in ci.iter_enum_definitions() %} +template <> +struct ViaFfi<{{ e.name()|class_name_cpp(context) }}, uint32_t> { + [[nodiscard]] static bool Lift(const uint32_t& aLowered, {{ e.name()|class_name_cpp(context) }}& aLifted) { + switch (aLowered) { + {% for variant in e.variants() -%} + case {{ loop.index }}: + aLifted = {{ e.name()|class_name_cpp(context) }}::{{ variant|enum_variant_cpp }}; + break; + {% endfor -%} + default: + MOZ_ASSERT(false, "Unexpected enum case"); + return false; + } + return true; + } + + [[nodiscard]] static uint32_t Lower(const {{ e.name()|class_name_cpp(context) }}& aLifted) { + switch (aLifted) { + {% for variant in e.variants() -%} + case {{ e.name()|class_name_cpp(context) }}::{{ variant|enum_variant_cpp }}: + return {{ loop.index }}; + {% endfor -%} + default: + MOZ_ASSERT(false, "Unknown raw enum value"); + } + return 0; + } +}; + +template <> +struct Serializable<{{ e.name()|class_name_cpp(context) }}> { + [[nodiscard]] static bool ReadFrom(Reader& aReader, {{ e.name()|class_name_cpp(context) }}& aValue) { + auto rawValue = aReader.ReadUInt32(); + return ViaFfi<{{ e.name()|class_name_cpp(context) }}, uint32_t>::Lift(rawValue, aValue); + } + + static void WriteInto(Writer& aWriter, const {{ e.name()|class_name_cpp(context) }}& aValue) { + aWriter.WriteUInt32(ViaFfi<{{ e.name()|class_name_cpp(context) }}, uint32_t>::Lower(aValue)); + } +}; +{% endfor %} + +{% for rec in ci.iter_record_definitions() -%} +template <> +struct Serializable<{{ rec.name()|class_name_cpp(context) }}> { + [[nodiscard]] static bool ReadFrom(Reader& aReader, {{ rec.name()|class_name_cpp(context) }}& aValue) { + {%- for field in rec.fields() %} + if (!Serializable<{{ field.type_()|type_cpp(context) }}>::ReadFrom(aReader, aValue.{{ field.name()|field_name_cpp }})) { + return false; + } + {%- endfor %} + return true; + } + + static void WriteInto(Writer& aWriter, const {{ rec.name()|class_name_cpp(context) }}& aValue) { + {%- for field in rec.fields() %} + Serializable<{{ field.type_()|type_cpp(context) }}>::WriteInto(aWriter, aValue.{{ field.name()|field_name_cpp }}); + {%- endfor %} + } +}; +{% endfor %} + +} // namespace {{ context.detail_name() }} + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_{{ context.namespace()|header_name_cpp(context) }}_Shared diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/WebIDLTemplate.webidl b/uniffi_bindgen/src/bindings/gecko_js/templates/WebIDLTemplate.webidl new file mode 100644 index 0000000000..cd6ffd6839 --- /dev/null +++ b/uniffi_bindgen/src/bindings/gecko_js/templates/WebIDLTemplate.webidl @@ -0,0 +1,77 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +{%- for rec in ci.iter_record_definitions() %} +dictionary {{ rec.name()|class_name_webidl(context) }} { + {%- for field in rec.fields() %} + required {{ field.type_()|type_webidl(context) }} {{ field.name()|var_name_webidl }}; + {%- endfor %} +}; +{% endfor %} + +{%- for e in ci.iter_enum_definitions() %} +enum {{ e.name()|class_name_webidl(context) }} { + {% for variant in e.variants() %} + "{{ variant|enum_variant_webidl }}"{%- if !loop.last %}, {% endif %} + {% endfor %} +}; +{% endfor %} + +{%- let functions = ci.iter_function_definitions() %} +{%- if !functions.is_empty() %} +[ChromeOnly, Exposed=Window] +namespace {{ context.namespace()|class_name_webidl(context) }} { + {% for func in functions %} + {%- if func.throws().is_some() %} + [Throws] + {% endif %} + {%- match func.return_type() -%}{%- when Some with (type_) %}{{ type_|type_webidl(context) }}{% when None %}void{% endmatch %} {{ func.name()|fn_name_webidl }}( + {%- for arg in func.arguments() %} + {% if arg.default_value().is_some() -%}optional{%- else -%}{%- endif %} {{ arg.type_()|type_webidl(context) }} {{ arg.name() }} + {%- match arg.default_value() %} + {%- when Some with (literal) %} = {{ literal|literal_webidl }} + {%- else %} + {%- endmatch %} + {%- if !loop.last %}, {% endif %} + {%- endfor %} + ); + {% endfor %} +}; +{% endif -%} + +{%- for obj in ci.iter_object_definitions() %} +[ChromeOnly, Exposed=Window] +interface {{ obj.name()|class_name_webidl(context) }} { + {%- for cons in obj.constructors() %} + {%- if cons.throws().is_some() %} + [Throws] + {% endif %} + constructor( + {%- for arg in cons.arguments() %} + {% if arg.default_value().is_some() -%}optional{%- else -%}{%- endif %} {{ arg.type_()|type_webidl(context) }} {{ arg.name() }} + {%- match arg.default_value() %} + {%- when Some with (literal) %} = {{ literal|literal_webidl }} + {%- else %} + {%- endmatch %} + {%- if !loop.last %}, {% endif %} + {%- endfor %} + ); + {%- endfor %} + + {% for meth in obj.methods() -%} + {%- if meth.throws().is_some() %} + [Throws] + {% endif %} + {%- match meth.return_type() -%}{%- when Some with (type_) %}{{ type_|type_webidl(context) }}{% when None %}void{% endmatch %} {{ meth.name()|fn_name_webidl }}( + {%- for arg in meth.arguments() %} + {% if arg.default_value().is_some() -%}optional{%- else -%}{%- endif %} {{ arg.type_()|type_webidl(context) }} {{ arg.name() }} + {%- match arg.default_value() %} + {%- when Some with (literal) %} = {{ literal|literal_webidl }} + {%- else %} + {%- endmatch %} + {%- if !loop.last %}, {% endif %} + {%- endfor %} + ); + {% endfor %} +}; +{% endfor %} diff --git a/uniffi_bindgen/src/bindings/gecko_js/templates/macros.cpp b/uniffi_bindgen/src/bindings/gecko_js/templates/macros.cpp new file mode 100644 index 0000000000..fe5ef0e432 --- /dev/null +++ b/uniffi_bindgen/src/bindings/gecko_js/templates/macros.cpp @@ -0,0 +1,59 @@ +{# /* Calls an FFI function. */ #} +{%- macro to_ffi_call(context, func) -%} + {%- call to_ffi_call_head(context, func, "err", "loweredRetVal_") -%} + {%- call _to_ffi_call_tail(context, func, "err", "loweredRetVal_") -%} +{%- endmacro -%} + +{# /* Calls an FFI function with an initial argument. */ #} +{%- macro to_ffi_call_with_prefix(context, prefix, func) %} + {{ context.ffi_rusterror_type() }} err = {0, nullptr}; + {% match func.ffi_func().return_type() %}{% when Some with (type_) %}const {{ type_|type_ffi(context) }} loweredRetVal_ ={% else %}{% endmatch %}{{ func.ffi_func().name() }}( + {{ prefix }} + {%- let args = func.arguments() -%} + {%- if !args.is_empty() %},{% endif -%} + {%- for arg in args %} + {{ arg.type_()|lower_cpp(arg.name(), context) }}{%- if !loop.last %},{% endif -%} + {%- endfor %} + , &err + ); + {%- call _to_ffi_call_tail(context, func, "err", "loweredRetVal_") -%} +{%- endmacro -%} + +{# /* Calls an FFI function without handling errors or lifting the return + value. Used in the implementation of `to_ffi_call`, and for + constructors. */ #} +{%- macro to_ffi_call_head(context, func, error, result) %} + {{ context.ffi_rusterror_type() }} {{ error }} = {0, nullptr}; + {% match func.ffi_func().return_type() %}{% when Some with (type_) %}const {{ type_|type_ffi(context) }} {{ result }} ={% else %}{% endmatch %}{{ func.ffi_func().name() }}( + {%- let args = func.arguments() -%} + {%- for arg in args %} + {{ arg.type_()|lower_cpp(arg.name(), context) }}{%- if !loop.last %},{% endif -%} + {%- endfor %} + {% if !args.is_empty() %}, {% endif %}&{{ error }} + ); +{%- endmacro -%} + +{# /* Handles errors and lifts the return value from an FFI function. */ #} +{%- macro _to_ffi_call_tail(context, func, err, result) %} + if ({{ err }}.mCode) { + {%- match func.throw_by() %} + {%- when ThrowBy::ErrorResult with (rv) %} + {# /* TODO: Improve error throwing. See https://github.com/mozilla/uniffi-rs/issues/295 + for details. */ -#} + {{ rv }}.ThrowOperationError({{ err }}.mMessage); + {%- when ThrowBy::Assert %} + MOZ_ASSERT(false); + {%- endmatch %} + return {% match func.binding_return_type() %}{% when Some with (type_) %}{{ type_|dummy_ret_value_cpp(context) }}{% else %}{% endmatch %}; + } + {%- match func.return_by() %} + {%- when ReturnBy::OutParam with (name, type_) %} + DebugOnly ok_ = {{ type_|lift_cpp(result, name, context) }}; + MOZ_RELEASE_ASSERT(ok_); + {%- when ReturnBy::Value with (type_) %} + {{ type_|type_cpp(context) }} retVal_; + DebugOnly ok_ = {{ type_|lift_cpp(result, "retVal_", context) }}; + MOZ_RELEASE_ASSERT(ok_); + return retVal_; + {%- when ReturnBy::Void %}{%- endmatch %} +{%- endmacro -%} diff --git a/uniffi_bindgen/src/bindings/gecko_js/webidl.rs b/uniffi_bindgen/src/bindings/gecko_js/webidl.rs new file mode 100644 index 0000000000..eedfc7000b --- /dev/null +++ b/uniffi_bindgen/src/bindings/gecko_js/webidl.rs @@ -0,0 +1,210 @@ +/* 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/. */ + +//! Helpers for generating C++ WebIDL bindings from a UniFFI component +//! interface. +//! +//! The C++ bindings that we generate for Firefox are a little peculiar. +//! Depending on how they're declared in WebIDL, some methods take extra +//! arguments. For example, static methods take a `GlobalObject`, methods that +//! return an `ArrayBuffer` also take a `JSContext`, some return values are +//! passed via out parameters while others are returned directly, some +//! arguments map to different C++ types depending on whether they're in or out +//! parameters, and throwing functions also take an `ErrorResult`. +//! +//! These conditions and combinations are tricky to express in Askama, so we +//! handle them in extension traits on the UniFFI types, and keep our templates +//! clean. + +use crate::interface::{Argument, Constructor, Function, Method, Type}; + +/// Extension methods for all functions: top-level functions, constructors, and +/// methods. +pub trait BindingFunction { + /// Returns a list of arguments to declare for this function, including any + /// extras and out parameters. + fn binding_arguments(&self) -> Vec>; + + /// Indicates how errors should be thrown from this function, either by an + /// `ErrorResult` parameter, or by a fatal assertion. + fn throw_by(&self) -> ThrowBy; +} + +/// Extension methods for functions that return a value of any type. Excludes +/// constructors, which must return an instance of their type. +pub trait ReturningBindingFunction: BindingFunction { + /// Returns the return type for this function, or `None` if the function + /// doesn't return a value, or returns it via an out parameter. + fn binding_return_type(&self) -> Option<&Type>; + + /// Indicates how results should be returned, either by value or via an out + /// parameter. + fn return_by(&self) -> ReturnBy<'_>; +} + +impl BindingFunction for Function { + fn binding_arguments(&self) -> Vec> { + let args = self.arguments(); + let mut result = Vec::with_capacity(args.len() + 3); + result.push(BindingArgument::GlobalObject); + result.extend(args.into_iter().map(|arg| BindingArgument::In(arg))); + if let Some(type_) = self.return_type().filter(|type_| is_out_param_type(type_)) { + result.push(BindingArgument::Out(type_)); + } + if self.throws().is_some() { + result.push(BindingArgument::ErrorResult); + } + result + } + + fn throw_by(&self) -> ThrowBy { + if self.throws().is_some() { + ThrowBy::ErrorResult("aRv") + } else { + ThrowBy::Assert + } + } +} + +impl ReturningBindingFunction for Function { + fn binding_return_type(&self) -> Option<&Type> { + self.return_type().filter(|type_| !is_out_param_type(type_)) + } + + fn return_by(&self) -> ReturnBy<'_> { + self.return_type() + .map(ReturnBy::from_return_type) + .unwrap_or(ReturnBy::Void) + } +} + +impl BindingFunction for Constructor { + fn binding_arguments(&self) -> Vec> { + let args = self.arguments(); + let mut result = Vec::with_capacity(args.len() + 2); + result.push(BindingArgument::GlobalObject); + result.extend(args.into_iter().map(|arg| BindingArgument::In(arg))); + // Constructors never take out params, since they must return an + // instance of the object. + if self.throws().is_some() { + result.push(BindingArgument::ErrorResult); + } + result + } + + fn throw_by(&self) -> ThrowBy { + if self.throws().is_some() { + ThrowBy::ErrorResult("aRv") + } else { + ThrowBy::Assert + } + } +} + +impl BindingFunction for Method { + fn binding_arguments(&self) -> Vec> { + let args = self.arguments(); + let mut result = Vec::with_capacity(args.len() + 2); + // Methods don't take a `GlobalObject` as their first argument. + result.extend(args.into_iter().map(|arg| BindingArgument::In(arg))); + if let Some(type_) = self.return_type().filter(|type_| is_out_param_type(type_)) { + result.push(BindingArgument::Out(type_)); + } + if self.throws().is_some() { + result.push(BindingArgument::ErrorResult); + } + result + } + + fn throw_by(&self) -> ThrowBy { + if self.throws().is_some() { + ThrowBy::ErrorResult("aRv") + } else { + ThrowBy::Assert + } + } +} + +impl ReturningBindingFunction for Method { + fn binding_return_type(&self) -> Option<&Type> { + self.return_type().filter(|type_| !is_out_param_type(type_)) + } + + fn return_by(&self) -> ReturnBy<'_> { + self.return_type() + .map(ReturnBy::from_return_type) + .unwrap_or(ReturnBy::Void) + } +} + +/// Returns `true` if a type is returned via an out parameter; `false` if +/// by value. +fn is_out_param_type(type_: &Type) -> bool { + match type_ { + Type::String | Type::Record(_) | Type::Map(_) | Type::Sequence(_) => true, + Type::Optional(inner) => is_out_param_type(inner), + _ => false, + } +} + +/// Describes how a function returns its result. +pub enum ReturnBy<'a> { + /// The function returns its result in an out parameter with the given + /// name and type. + OutParam(&'static str, &'a Type), + + /// The function returns its result by value. + Value(&'a Type), + + /// The function doesn't declare a return type. + Void, +} + +impl<'a> ReturnBy<'a> { + fn from_return_type(type_: &'a Type) -> Self { + if is_out_param_type(type_) { + ReturnBy::OutParam("aRetVal", type_) + } else { + ReturnBy::Value(type_) + } + } +} + +/// Describes how a function throws errors. +pub enum ThrowBy { + /// Errors should be set on the `ErrorResult` parameter with the given + /// name. + ErrorResult(&'static str), + + /// Errors should fatally assert. + Assert, +} + +/// Describes a function argument. +pub enum BindingArgument<'a> { + /// The argument is a `GlobalObject`, passed to constructors, static, and + /// namespace methods. + GlobalObject, + + /// The argument is an `ErrorResult`, passed to throwing functions. + ErrorResult, + + /// The argument is a normal input parameter. + In(&'a Argument), + + /// The argument is an out parameter used to return results by reference. + Out(&'a Type), +} + +impl<'a> BindingArgument<'a> { + /// Returns the argument name. + pub fn name(&self) -> &'a str { + match self { + BindingArgument::GlobalObject => "aGlobal", + BindingArgument::ErrorResult => "aRv", + BindingArgument::In(arg) => arg.name(), + BindingArgument::Out(_) => "aRetVal", + } + } +} diff --git a/uniffi_bindgen/src/bindings/mod.rs b/uniffi_bindgen/src/bindings/mod.rs index 83ca7c2193..8fe70d41f2 100644 --- a/uniffi_bindgen/src/bindings/mod.rs +++ b/uniffi_bindgen/src/bindings/mod.rs @@ -15,6 +15,7 @@ use std::path::Path; use crate::interface::ComponentInterface; use crate::MergeWith; +pub mod gecko_js; pub mod kotlin; pub mod python; pub mod swift; @@ -30,6 +31,7 @@ pub enum TargetLanguage { Kotlin, Swift, Python, + GeckoJs, } impl TryFrom<&str> for TargetLanguage { @@ -39,6 +41,7 @@ impl TryFrom<&str> for TargetLanguage { "kotlin" | "kt" | "kts" => TargetLanguage::Kotlin, "swift" => TargetLanguage::Swift, "python" | "py" => TargetLanguage::Python, + "gecko_js" => TargetLanguage::GeckoJs, _ => bail!("Unknown or unsupported target language: \"{}\"", value), }) } @@ -69,6 +72,8 @@ pub struct Config { swift: swift::Config, #[serde(default)] python: python::Config, + #[serde(default)] + gecko_js: gecko_js::Config, } impl From<&ComponentInterface> for Config { @@ -77,6 +82,7 @@ impl From<&ComponentInterface> for Config { kotlin: ci.into(), swift: ci.into(), python: ci.into(), + gecko_js: ci.into(), } } } @@ -87,6 +93,7 @@ impl MergeWith for Config { kotlin: self.kotlin.merge_with(&other.kotlin), swift: self.swift.merge_with(&other.swift), python: self.python.merge_with(&other.python), + gecko_js: self.gecko_js.merge_with(&other.gecko_js), } } } @@ -113,6 +120,9 @@ where TargetLanguage::Python => { python::write_bindings(&config.python, &ci, out_dir, try_format_code)? } + TargetLanguage::GeckoJs => { + gecko_js::write_bindings(&config.gecko_js, &ci, out_dir, try_format_code)? + } } Ok(()) } @@ -132,6 +142,7 @@ where TargetLanguage::Kotlin => kotlin::compile_bindings(&config.kotlin, &ci, out_dir)?, TargetLanguage::Swift => swift::compile_bindings(&config.swift, &ci, out_dir)?, TargetLanguage::Python => (), + TargetLanguage::GeckoJs => (), } Ok(()) } @@ -148,6 +159,7 @@ where TargetLanguage::Kotlin => kotlin::run_script(out_dir, script_file)?, TargetLanguage::Swift => swift::run_script(out_dir, script_file)?, TargetLanguage::Python => python::run_script(out_dir, script_file)?, + TargetLanguage::GeckoJs => bail!("Can't run Gecko code standalone"), } Ok(()) } diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift.rs b/uniffi_bindgen/src/bindings/swift/gen_swift.rs index 4a9dd10ed6..6d839b7de8 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift.rs @@ -12,9 +12,9 @@ use serde::{Deserialize, Serialize}; use crate::interface::*; use crate::MergeWith; -// Some config options for it the caller wants to customize the generated python. -// Note that this can only be used to control details of the python *that do not affect the underlying component*, -// sine the details of the underlying component are entirely determined by the `ComponentInterface`. +// Some config options for the caller to customize the generated Swift. +// Note that this can only be used to control details of the Swift *that do not affect the underlying component*, +// since the details of the underlying component are entirely determined by the `ComponentInterface`. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Config { diff --git a/uniffi_bindgen/src/interface/types.rs b/uniffi_bindgen/src/interface/types.rs index 694317699b..ed21b8f5df 100644 --- a/uniffi_bindgen/src/interface/types.rs +++ b/uniffi_bindgen/src/interface/types.rs @@ -97,6 +97,12 @@ pub enum Type { Map(/* String, */ Box), } +impl Type { + pub fn to_ffi(&self) -> FFIType { + FFIType::from(self) + } +} + /// When passing data across the FFI, each `Type` value will be lowered into a corresponding /// `FFIType` value. This conversion tells you which one. impl From<&Type> for FFIType { diff --git a/uniffi_bindgen/src/main.rs b/uniffi_bindgen/src/main.rs index ec9234e41a..24dafeae60 100644 --- a/uniffi_bindgen/src/main.rs +++ b/uniffi_bindgen/src/main.rs @@ -4,7 +4,7 @@ use anyhow::{bail, Result}; -const POSSIBLE_LANGUAGES: &[&str] = &["kotlin", "python", "swift"]; +const POSSIBLE_LANGUAGES: &[&str] = &["kotlin", "python", "swift", "gecko_js"]; fn main() -> Result<()> { let matches = clap::App::new("uniffi-bindgen") diff --git a/uniffi_build/Cargo.toml b/uniffi_build/Cargo.toml index 2c6e365c7c..d17882e033 100644 --- a/uniffi_build/Cargo.toml +++ b/uniffi_build/Cargo.toml @@ -11,7 +11,7 @@ edition = "2018" keywords = ["ffi", "bindgen"] [dependencies] -cargo_metadata = "0.11" +cargo_metadata = "0.11.3" anyhow = "1" uniffi_bindgen = { path = "../uniffi_bindgen", optional = true, version = "0.2.0" }