From 9586a37b5295d3cde3a4dfe93ddf59e3dd99a42d Mon Sep 17 00:00:00 2001 From: Ryan Kelly Date: Wed, 10 Jun 2020 17:37:58 +1000 Subject: [PATCH] Initial import for extremely work-in-progress code to separate repo --- .gitignore | 3 + Cargo.toml | 13 + README.md | 204 +++++++++ examples/arithmetic/Cargo.toml | 17 + examples/arithmetic/arithmetic.idl | 10 + examples/arithmetic/build.rs | 28 ++ examples/arithmetic/src/lib.rs | 20 + src/kotlin.rs | 407 +++++++++++++++++ src/lib.rs | 83 ++++ src/scaffolding.rs | 336 ++++++++++++++ src/types.rs | 681 +++++++++++++++++++++++++++++ 11 files changed, 1802 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 examples/arithmetic/Cargo.toml create mode 100644 examples/arithmetic/arithmetic.idl create mode 100644 examples/arithmetic/build.rs create mode 100644 examples/arithmetic/src/lib.rs create mode 100644 src/kotlin.rs create mode 100644 src/lib.rs create mode 100644 src/scaffolding.rs create mode 100644 src/types.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..77d5a476cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target +.cargo +.*.swp diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000..3edde99859 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "uniffi" +version = "0.1.0" +authors = ["Firefox Sync Team "] +license = "MPL-2.0" +edition = "2018" + +[dependencies] +weedle = "0.11" +ffi-support = "0.4" +anyhow = "1.0" +askama = "0.9" +heck ="0.3" diff --git a/README.md b/README.md new file mode 100644 index 0000000000..f796a7bded --- /dev/null +++ b/README.md @@ -0,0 +1,204 @@ +# uniffi + +A very hand-wavy idea about autogenerating our FFI code and its bindings. +Don't get your hopes up just yet ;-) + +## What? + +Our current approach to building shared components in rust involves writing a lot +of boilerplate code by hand, manually flattening the rust code into an `extern "C"` +FFI layer and then manually importing and exposing that into each of Kotlin, Swift +and XPCOM+JS. The process is time-consuming, error-prone, and boring. + +What if we didn't have to write all of that by hand? + +In an aspirational world, we could get this kind of easy cross-language interop for +free using [wasm_bindgen](https://rustwasm.github.io/docs/wasm-bindgen/) and +[webassembly interface types](https://hacks.mozilla.org/2019/08/webassembly-interface-types/) - +imagine writing an API in Rust, annotating it with some `#[wasm_bindgen]` macros, +compiling it into a webassembly bundle, and being able to import and use that bundle +from any target language! + +That kind of tooling is not available to shipping applications today, but that doesn't +mean we can't take a small step in that general direction. + +### Key Ideas + +* Specify the component API using an abstract *Interface Definition Language*. +* When implementing the component: + * Process the IDL into some Rust code scaffolding to define the FFI, data classes, etc. + * Have the component crate `!include()` the scaffolding and fill in the implementation. +* When using the component: + * Process the IDL to produce FFI bindings in your language of choice + * Use some runtime helpers to hook it up to the compiled FFI from the component crate. + + +## Component Interface Definition + +We'll abstractly specify the API of a component using the syntax of [WebIDL](https://en.wikipedia.org/wiki/Web_IDL), +but without getting too caught up in matching its precise semantics. This choice is largely driven by +the availability of quality tooling such as the [weedle crate](https://docs.rs/weedle/0.11.0/weedle/), +general familiarity around Mozilla, and the desire to avoid bikeshedding any new syntax. + +We'll model the *semantics* of the component API loosely on the primitives defined by the +[Wasm Interface Types](https://github.com/WebAssembly/interface-types/blob/master/proposals/interface-types/Explainer.md) +proposal (henceforth "WIT"). WIT aims to solve a very similarly-shaped problem to the one we're faced +with here, and by organizing this work around similar concepts, we might make it easier to one day +replace all of this with direct use of WIT tooling. + +In the future, we may be able to generate the Interface Definition from annotations on the rust code (in the style of `#[wasm_bindgen]`) rather than from a separate IDL file. But it's much easier to get +started using a separate file. + +The prototype implementation of this is in [./src/types.rs](./src/types.rs). +See [fxa-client.idl](../../fxa-client/fxa-client.idl) for an example of an interface definition. + +### Primitive Types + +We'd avoid WedIDL's sparse and JS-specific types and aim to provide similar primitive types +to the WIT proposal: strings, bools, integers of various sizes and signedeness. We already know +how to pass these around through the FFI and the details don't seem very remarkable. + +They all pass by value (including strings, which get copied when transiting the FFI). + +### Object Types (a.k.a. Reference Types, Handle Types, Structs, Classes, etc) + +These represent objects that you can instantiate, that have opaque internal state and methods that +operate on that state. They're typically the "interesting" part of a component's API. We currently +implement these by defining a Rust struct, putting instances of it in a `ConcurrentHandleMap`, and +defining a bunch of `extern "C"` functions that can be used to call methods on it. + +In WebIDL these would be an `interface`, like so: + +``` +interface MyObject { + constructor(string foo, bool isBar); + bool checkIfBar(); +} +``` + +I don't think the WIT proposal has an equivalent to these types; they're kind of like an +`anyref` I guess? We should investigate further... + +In the FFI, these are represented by an opaque `u64` handle. + +When generating component scaffolding, we could transparently create the HandleMap and the `extern "C"` functions that operate on it. We'd rely on the component code to provide a corresponding `MyObject` struct, and on Rust's typechecker to complain if the implemented methods on that struct don't match the expectations of the generated scaffolding. + +When generating language-specific bindings, this becomes a `class` or equivalent. + +TODO: +* Can we use member attributes to annotate which methods require mutable vs shared access? +* Can we use member attributes to identify which methods may block, and hence should be turned into a deferred/promise/whatever. + +### Record Types (a.k.a. Value Types, Data Classes, Protobuf Messages, etc) + +These are structural types that are passed around *by value* and are typically only used +for their data. The sorts of things that we currently use JSON or Protobuf for in the FFI. + +In WebIDL this corresponds to the notion of a `dictionary`, which IMHO is not a great +name for them in the context of our work here, but will do the job: + +``` +dictionary MyData { + required string foo; + u64 value = 0; +} +``` + +In the WIT proposal these are "records" and we use the same name here. + +When generating the component scaffolding, we'd do a similar job to what's done with protobuf +today - turn the record description into a rust `struct` with appropriate fields, and helper +methods for serializing/deserializing, accessing data etc. + +When generating language-specific bindings, records become a "data class" or similar construct, +with field access and serialization helpers. Again, much like we currently do with protobufs. + +When passing back and forth over the FFI, records are serialized to a byte buffer and +deserialized on the other side. We could continue to use protobuf for this, but I suspect +we could come up with a simpler scheme given we control both sides of the pipe. Callers +passing a record must keep the buffer alive until the callee returns; callers receiving +a record must call a destructor to free the buffer after hydrating an object on their side. + +### Sequences + +Both WebIDL and WIT have a builtin `sequence` type and we should use it verbatim. + +``` +interface MyObject { + sequence getAllTheFoos(); +} +``` + +We currently use ad-hoc Protobuf messages for this, e.g. the `AccountEvent` and +`AccountEvents` types in fxa-client. But it seems reasonable to support a generic +implementation on both sides of the FFI boundary. + +When traversing the FFI, these would be serialized into a byte buffer and parsed +back out into a Vec or Array or whatever on the other side. Just like Record types. + +### Enums + +WebIDL as simple C-style enums, like this: + +``` +enum AccountEventType { + "INCOMING_DEVICE_COMMAND", + "PROFILE_UPDATED", + "DEVICE_CONNECTED", + "ACCOUNT_AUTH_STATE_CHANGED", + "DEVICE_DISCONNECTED", + "ACCOUNT_DESTROYED", +}; +``` + +These could be encoded into an unsigned integer type for transmission over the FFI, +like the way we currently handle error numbers. + +There is also more sophisticated stuff in there, like union types and nullable +types. I haven't really thought about how to map those on to what we need. + +### Callbacks + +WebIDL has some syntax for them, but I haven't looked at this in any detail +at all. It seems hard, but also extremely valuable because handling callbacks +across the FFI boundary has been a pain point for us in the past. + +## Scaffolding Generation + +Currently a very hacky attempt in [./src/scaffolding.rs](./src/scaffolding.rs), +and a `generate_component_scaffolding(idl_file: &str)` function that's intended +to be used from the component's build file. + +See the [fxa-client crate](../../fxa-client/build.rs) for an example of (attempting to) +use this, although it's in a very incomplete state. + +It doesn't produce working Rust code, but it'll produce Rust-shaped kind-of-code +that gives you a bit of an idea what it's going for. + +Could really benefit from a templating library rather than doing a bunch of +`writeln!()` with raw source code fragements. + +## Language Bindings Generation + +Currently totally unimplemented. + +A great opportunity for anyone interested to +dive in! You could try looking at the hand-written Kotlin or Swift code for +the fxa-client component, and see if you can generate something similar from +`fxa-client.idl`. Take a look at the way the scaffolding generator works to +see how to get started. + +## What could possibly go wrong? + +Lots! + +The complexity of maintaining all this tooling could be a greater burden then maintaining +the manual bindings. We might isolate expertise in a small number of team members. We might +spend more time working on this tooling than we'll ever hope to get back in time savings +from the generated code. + +By trying to define a one-size-fits-all API surface, we might end up with suboptimal +APIs on every platform, and it could be harder to tweak them on a platform-by-platform +basis. + +The resulting autogenerated code might be a lot harder to debug when things go wrong. diff --git a/examples/arithmetic/Cargo.toml b/examples/arithmetic/Cargo.toml new file mode 100644 index 0000000000..f557ed0529 --- /dev/null +++ b/examples/arithmetic/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "uniffi-example-arithmetic" +edition = "2018" +version = "0.1.0" +authors = ["Firefox Sync Team "] +license = "MPL-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1.0" +log = "0.4" +ffi-support = "0.4" + +[build-dependencies] +uniffi = {path = "../../"} diff --git a/examples/arithmetic/arithmetic.idl b/examples/arithmetic/arithmetic.idl new file mode 100644 index 0000000000..35c1afed75 --- /dev/null +++ b/examples/arithmetic/arithmetic.idl @@ -0,0 +1,10 @@ + +enum Overflow { + "WRAPPING", + "SATURATING", +}; + +namespace Arithmetic { + u64 add(u64 a, u64 b, optional Overflow overflow = "WRAPPING"); + u64 sub(u64 a, u64 b, optional Overflow overflow = "WRAPPING"); +}; \ No newline at end of file diff --git a/examples/arithmetic/build.rs b/examples/arithmetic/build.rs new file mode 100644 index 0000000000..95057beac6 --- /dev/null +++ b/examples/arithmetic/build.rs @@ -0,0 +1,28 @@ +/* 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/. */ + +fn main() { + uniffi::generate_component_scaffolding("arithmetic.idl"); + uniffi::generate_kotlin_bindings("arithmetic.idl"); + compile_kotlin_example(); +} + +fn compile_kotlin_example() { + let mut gen_file = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()); + gen_file.push("arithmetic.kt"); + // There's a whole lot of packaging and dependency-management stuff to figure out here. + // For now I just want to hack it into compiling and running some generated kotlin code. + let status = std::process::Command::new("kotlinc") + .arg("-include-runtime") + .arg("-classpath").arg("/Users/rfk/.gradle/caches/modules-2/files-2.1/net.java.dev.jna/jna/5.2.0/ed8b772eb077a9cb50e44e90899c66a9a6c00e67/jna-5.2.0.jar") + .arg("/Users/rfk/repos/mozilla/application-services/components/support/android/src/main/java/mozilla/appservices/support/native/Helpers.kt") + .arg(gen_file) + .arg("/Users/rfk/repos/mozilla/application-services/main.kt") + .arg("-d").arg("/Users/rfk/repos/mozilla/application-services/arithmetic.jar") + .spawn() + .unwrap() + .wait() + .unwrap(); + assert!(status.success()); +} \ No newline at end of file diff --git a/examples/arithmetic/src/lib.rs b/examples/arithmetic/src/lib.rs new file mode 100644 index 0000000000..5e990bc699 --- /dev/null +++ b/examples/arithmetic/src/lib.rs @@ -0,0 +1,20 @@ +/* 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!(concat!(env!("OUT_DIR"), "/arithmetic.uniffi.rs")); + +impl Arithmetic { + pub fn add(a: u64, b: u64, overflow: Overflow) -> u64 { + match overflow { + Overflow::WRAPPING => a.overflowing_add(b).0, + Overflow::SATURATING => a.saturating_add(b), + } + } + pub fn sub(a: u64, b: u64, overflow: Overflow) -> u64 { + match overflow { + Overflow::WRAPPING => a.overflowing_sub(b).0, + Overflow::SATURATING => a.saturating_sub(b), + } + } +} \ No newline at end of file diff --git a/src/kotlin.rs b/src/kotlin.rs new file mode 100644 index 0000000000..c79c9a015a --- /dev/null +++ b/src/kotlin.rs @@ -0,0 +1,407 @@ +/* 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::io::prelude::*; +use std::{ + env, + collections::HashMap, + convert::TryFrom, convert::TryInto, + fs::File, + iter::IntoIterator, + fmt::Display, + path::{Path, PathBuf}, +}; + +use anyhow::bail; +use anyhow::Result; +use askama::Template; + +use super::types; + +#[derive(Template)] +#[template(ext="kt", escape="none", source=r#" +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +// TODO: how to declare package namespace? Probably when calling the generator. +package mozilla.appservices.example; + +import com.sun.jna.Library +import com.sun.jna.Pointer +//import mozilla.appservices.support.native.RustBuffer +import mozilla.appservices.support.native.loadIndirect + +internal typealias Handle = Long +//TODO error type specific to this component. + +@Suppress("FunctionNaming", "FunctionParameterNaming", "LongParameterList", "TooGenericExceptionThrown") +internal interface LibTODOName : Library { + companion object { + internal var INSTANCE: LibTODOName = + loadIndirect(componentName = "name", componentVersion = "TODO") + } + {% for m in self.ffi_function_decls() -%} + {{ m.render().unwrap() }} + {% endfor -%} +} + +{% for m in self.members() -%} +{{ m.render().unwrap() }} +{% endfor -%} +"#)] +pub struct ComponentInterfaceKotlinWrapper<'a> { + ci: &'a types::ComponentInterface, +} +impl<'a> ComponentInterfaceKotlinWrapper<'a> { + pub fn new(ci: &'a types::ComponentInterface) -> Self { + Self { ci } + } + + // XXX TODO: I'd like to make this return `impl Iterator` but then askama takes a reference to it + // and tries to call `into_iter()` and that doesn't work becaue it tries to do a move. Bleh. More to learn... + fn members(&self) -> Vec> { + self.ci.members.iter().map(|m| { + match m { + types::InterfaceMember::Object(obj) => ObjectKotlinWrapper::boxed(self.ci, obj), + types::InterfaceMember::Record(rec) => RecordKotlinWrapper::boxed(self.ci, rec), + types::InterfaceMember::Namespace(n) => NamespaceKotlinWrapper::boxed(self.ci, n), + types::InterfaceMember::Enum(e) => EnumKotlinWrapper::boxed(self.ci, e), + } + }).collect() + } + + // XXX TODO: I'd like to make this return `impl Iterator` but then askama takes a reference to it + // and tries to call `into_iter()` and that doesn't work becaue it tries to do a move. Bleh. More to learn... + fn ffi_function_decls(&self) -> Vec> { + self.ci.members.iter().filter_map(|m| { + match m { + types::InterfaceMember::Object(obj) => Some(ObjectFFIKotlinWrapper::boxed(self.ci, obj)), + types::InterfaceMember::Namespace(n) => Some(NamespaceFFIKotlinWrapper::boxed(self.ci, n)), + _ => None, + } + }).collect() + } +} + +#[derive(Template)] +#[template(ext="kt", escape="none", source=r#" + +fun {{ self.struct_name() }}_free(hande: Handle, err: RustError.ByReference) + +{%- for m in self.members() %} + {{ m.render().unwrap() }} +{% endfor -%} +"#)] +struct ObjectFFIKotlinWrapper<'a> { + ci: &'a types::ComponentInterface, + obj: &'a types::ObjectType, +} +impl<'a> ObjectFFIKotlinWrapper<'a> { + fn boxed(ci: &'a types::ComponentInterface, obj: &'a types::ObjectType) -> Box { + Box::new(Self { ci, obj }) + } + + fn struct_name(&self) -> &'a str { + &self.obj.name + } + + fn members(&self) -> Vec> { + self.obj.members.iter().map(|m|{ + match m { + types::ObjectTypeMember::Constructor(cons) => ObjectConstructorKotlinWrapper::boxed(self.ci, self.obj, cons), + types::ObjectTypeMember::Method(meth) => ObjectMethodKotlinWrapper::boxed(self.ci, self.obj, meth), + } + }).collect() + } +} + +#[derive(Template)] +#[template(ext="kt", escape="none", source=r#" + +fun {{ self.struct_name() }}_free(hande: Handle, err: RustError.ByReference) + +{%- for m in self.members() %} + {{ m.render().unwrap() }} +{% endfor -%} +"#)] +struct ObjectKotlinWrapper<'a> { + ci: &'a types::ComponentInterface, + obj: &'a types::ObjectType, +} +impl<'a> ObjectKotlinWrapper<'a> { + fn boxed(ci: &'a types::ComponentInterface, obj: &'a types::ObjectType) -> Box { + Box::new(Self { ci, obj }) + } + + fn struct_name(&self) -> &'a str { + &self.obj.name + } + + fn members(&self) -> Vec> { + self.obj.members.iter().map(|m|{ + match m { + types::ObjectTypeMember::Constructor(cons) => ObjectConstructorKotlinWrapper::boxed(self.ci, self.obj, cons), + types::ObjectTypeMember::Method(meth) => ObjectMethodKotlinWrapper::boxed(self.ci, self.obj, meth), + } + }).collect() + } +} +#[derive(Template)] +#[template(ext="kt", escape="none", source=r#" + fun {{ self.ffi_name() }}( + {%- for arg in cons.argument_types %} + {{ arg.ffi_name() }}: {{ arg.typ.resolve(ci)|type_decl }}{% if loop.last %},{% endif %} + {%- endfor %} + err: RustError.ByReference, + ): Handle +"#)] +struct ObjectConstructorKotlinWrapper<'a> { + ci: &'a types::ComponentInterface, + obj: &'a types::ObjectType, + cons: &'a types::ObjectTypeConstructor, +} + +impl<'a> ObjectConstructorKotlinWrapper<'a> { + fn boxed(ci: &'a types::ComponentInterface, obj: &'a types::ObjectType, cons: &'a types::ObjectTypeConstructor) -> Box { + Box::new(Self { ci, obj, cons }) + } + + fn ffi_name(&self) -> String { + format!("{}_{}", self.obj.name, self.cons.name) + } +} + +#[derive(Template)] +#[template(ext="rs", escape="none", source=r#" + fun {{ self.ffi_name() }}( + handle: Handle, + {%- for arg in meth.argument_types %} + {{ arg.ffi_name() }}: {{ arg.typ.resolve(ci)|type_decl }}{% if loop.last %},{% endif %} + {%- endfor %} + err: RustError.ByReference, + ) + {%- match meth.return_type -%} + {%- when Some with (typ) %} + : {{ typ.resolve(ci)|type_decl }} + {% when None -%} + {%- endmatch %} +"#)] +struct ObjectMethodKotlinWrapper<'a> { + ci: &'a types::ComponentInterface, + obj: &'a types::ObjectType, + meth: &'a types::ObjectTypeMethod, +} + +impl<'a> ObjectMethodKotlinWrapper<'a> { + fn boxed(ci: &'a types::ComponentInterface, obj: &'a types::ObjectType, meth: &'a types::ObjectTypeMethod) -> Box { + Box::new(Self {ci, obj, meth }) + } + + fn ffi_name(&self) -> String { + format!("{}_{}", self.obj.name, self.meth.name) + } +} + +#[derive(Template)] +#[template(ext="rs", escape="none", source=r#" +// TODO: RECORD {{ rec.name }} +"#)] +struct RecordKotlinWrapper<'a> { + ci: &'a types::ComponentInterface, + rec: &'a types::RecordType, +} + +impl<'a> RecordKotlinWrapper<'a> { + fn boxed(ci: &'a types::ComponentInterface, rec: &'a types::RecordType) -> Box { + Box::new(Self { ci, rec }) + } +} + +#[derive(Template)] +#[template(ext="rs", escape="none", source=r#" +{%- for m in self.members() %} + {{ m.render().unwrap() }} +{% endfor -%} +"#)] +struct NamespaceFFIKotlinWrapper<'a> { + ci: &'a types::ComponentInterface, + ns: &'a types::NamespaceType, +} +impl<'a> NamespaceFFIKotlinWrapper<'a> { + fn boxed(ci: &'a types::ComponentInterface, ns: &'a types::NamespaceType) -> Box { + Box::new(Self { ci, ns }) + } + + fn members(&self) -> Vec> { + self.ns.members.iter().map(|m|{ + match m { + types::NamespaceTypeMember::Function(f) => NamespaceFunctionFFIKotlinWrapper::boxed(self.ci, self.ns, f), + } + }).collect() + } +} + + +#[derive(Template)] +#[template(ext="rs", escape="none", source=r#" + fun {{ f.ffi_name() }}( + {%- for arg in f.argument_types %} + {{ arg.ffi_name() }}: {{ arg.typ.resolve(ci)|type_decl }}{% if loop.last %}{% else %},{% endif %} + {%- endfor %} + // TODO: error param + ) + {%- match f.return_type -%} + {%- when Some with (typ) %} + : {{ typ.resolve(ci)|type_decl }} + {% when None -%} + {%- endmatch %} +"#)] +struct NamespaceFunctionFFIKotlinWrapper<'a> { + ci: &'a types::ComponentInterface, + ns: &'a types::NamespaceType, + f: &'a types::NamespaceTypeFunction, +} + +impl<'a> NamespaceFunctionFFIKotlinWrapper<'a> { + fn boxed(ci: &'a types::ComponentInterface, ns: &'a types::NamespaceType, f: &'a types::NamespaceTypeFunction) -> Box { + Box::new(Self { ci, ns, f }) + } +} + +#[derive(Template)] +#[template(ext="rs", escape="none", source=r#" +// XXX TODO probably this should be like a package or something, +// just going for most direct translation for now. +class {{ns.name}} { + companion object Members { + {%- for m in self.members() %} + {{ m.render().unwrap() }} + {% endfor -%} + } +} +"#)] +struct NamespaceKotlinWrapper<'a> { + ci: &'a types::ComponentInterface, + ns: &'a types::NamespaceType, +} +impl<'a> NamespaceKotlinWrapper<'a> { + fn boxed(ci: &'a types::ComponentInterface, ns: &'a types::NamespaceType) -> Box { + Box::new(Self { ci, ns }) + } + + fn members(&self) -> Vec> { + self.ns.members.iter().map(|m|{ + match m { + types::NamespaceTypeMember::Function(f) => NamespaceFunctionKotlinWrapper::boxed(self.ci, self.ns, f), + } + }).collect() + } +} + +#[derive(Template)] +#[template(ext="rs", escape="none", source=r#" +fun {{ f.name }}( + {%- for arg in f.argument_types %} + {{ arg.name }}: {{ arg.typ.resolve(ci)|type_decl_kt }}{% if loop.last %}{% else %},{% endif %} + {%- endfor %} +) + {%- match f.return_type -%} + {%- when Some with (typ) %} + : {{ typ.resolve(ci)|type_decl_kt }} + {% when None -%} + {%- endmatch %} +{ + // XXX TODO: whole bunch of error-checking wrapper call convention stuff. + return LibTODOName.INSTANCE.{{ f.ffi_name() }}( + {%- for arg in f.argument_types %} + {{ arg.name }}{% if loop.last %}{% else %},{% endif %} + {%- endfor %} + ) +} +"#)] +struct NamespaceFunctionKotlinWrapper<'a> { + ci: &'a types::ComponentInterface, + ns: &'a types::NamespaceType, + f: &'a types::NamespaceTypeFunction, +} + +impl<'a> NamespaceFunctionKotlinWrapper<'a> { + fn boxed(ci: &'a types::ComponentInterface, ns: &'a types::NamespaceType, f: &'a types::NamespaceTypeFunction) -> Box { + Box::new(Self { ci, ns, f }) + } +} + +#[derive(Template)] +#[template(ext="rs", escape="none", source=r#" +// TODO: enum {{e.name}} +"#)] +struct EnumKotlinWrapper<'a> { + ci: &'a types::ComponentInterface, + e: &'a types::EnumType, +} + +impl<'a> EnumKotlinWrapper<'a> { + fn boxed(ci: &'a types::ComponentInterface, e: &'a types::EnumType) -> Box { + Box::new(Self { ci, e }) + } +} + +mod filters { + use std::fmt; + use crate::types; + use anyhow::Error; + + pub fn type_decl(typ: &Result) -> Result { + // TODO: how to return a nice askama::Error here? + let typ = typ.as_ref().unwrap(); + Ok(match typ { + types::TypeReference::Boolean => "Byte".to_string(), + types::TypeReference::U64 => "Long".to_string(), + types::TypeReference::U32 => "Int".to_string(), + types::TypeReference::Enum(_) => "Int".to_string(), + types::TypeReference::String => "String".to_string(), // XXX TODO: not suitable for use in return position? + _ => format!("[TODO: DECL {:?}]", typ), + }) + } + + pub fn type_decl_kt(typ: &Result) -> Result { + // TODO: how to return a nice askama::Error here? + let typ = typ.as_ref().unwrap(); + Ok(match typ { + types::TypeReference::Boolean => "Byte".to_string(), + types::TypeReference::U64 => "Long".to_string(), + types::TypeReference::U32 => "Int".to_string(), + types::TypeReference::Enum(_) => "Int".to_string(), + types::TypeReference::String => "String".to_string(), // XXX TODO: not suitable for use in return position? + _ => format!("[TODO: DECL {:?}]", typ), + }) + } + + pub fn type_lift(nm: &dyn fmt::Display, typ: &Result) -> Result { + let nm = nm.to_string(); + let typ = typ.as_ref().unwrap(); + Ok(match typ { + types::TypeReference::Boolean => format!("({} != 0)", nm), + types::TypeReference::U64 => nm, + types::TypeReference::U32 => nm, + // XXX TODO: error handling if the conversion fails. + types::TypeReference::Enum(_) => format!("({}).try_into().unwrap()", nm), + types::TypeReference::String => format!("({}).as_str()", nm), + _ => format!("[TODO: LIFT {:?}]", typ), + }) + } + + pub fn type_lower( nm: &dyn fmt::Display, typ: &Result) -> Result { + let nm = nm.to_string(); + let typ = typ.as_ref().unwrap(); + Ok(match typ { + types::TypeReference::Boolean => format!("({} as u8)", nm), + types::TypeReference::U64 => nm, + types::TypeReference::U32 => nm, + types::TypeReference::Enum(_) => format!("({} as u32)", nm), + types::TypeReference::String => format!("({}).as_str()", nm), + _ => format!("[TODO: LOWER {:?}]", typ), + }) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000000..5899b67066 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,83 @@ +/* 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::io::prelude::*; +use std::{ + env, + collections::HashMap, + convert::TryFrom, convert::TryInto, + fs::File, + path::{Path, PathBuf}, +}; + +use anyhow::bail; +use anyhow::Result; + +pub mod types; +pub mod scaffolding; +pub mod kotlin; + +use scaffolding::ComponentInterfaceScaffolding; +use kotlin::ComponentInterfaceKotlinWrapper; + +fn slurp_file(file_name: &str) -> Result { + let mut contents = String::new(); + let mut f = File::open(file_name)?; + f.read_to_string(&mut contents)?; + Ok(contents) +} + +// Call this when building the rust crate that implements the specified interface. +// It will generate a bunch of the infrastructural rust code for implementing +// the interface, such as the `extern "C"` function definitions and record data types. +// +pub fn generate_component_scaffolding(idl_file: &str) { + println!("cargo:rerun-if-changed={}", idl_file); + let idl = slurp_file(idl_file).unwrap(); + let component= idl.parse::().unwrap(); + let mut filename = Path::new(idl_file).file_stem().unwrap().to_os_string(); + filename.push(".uniffi.rs"); + let mut out_file = PathBuf::from(env::var("OUT_DIR").unwrap()); + out_file.push(filename); + let mut f = File::create(out_file).unwrap(); + write!(f, "{}", ComponentInterfaceScaffolding::new(&component)).unwrap(); +} + + +// Call this to generate Kotlin bindings to load and call into the specified interface. +// XXX TODO: need to have more params here to control how and where it's generated. +// Writing into the OUT_DIR is probably not the write thing for foreign language bindings. +pub fn generate_kotlin_bindings(idl_file: &str) { + let idl = slurp_file(idl_file).unwrap(); + let component= idl.parse::().unwrap(); + let mut filename = Path::new(idl_file).file_stem().unwrap().to_os_string(); + filename.push(".kt"); + let mut out_file = PathBuf::from(env::var("OUT_DIR").unwrap()); + out_file.push(filename); + let mut f = File::create(out_file).unwrap(); + write!(f, "{}", ComponentInterfaceKotlinWrapper::new(&component)).unwrap(); +} + + +// Call this to generate Swift bindings to load and call into the specified interface. +// XXX TODO: actually, you know, implement it... +pub fn generate_swift_bindings(idl_file: &str) { + panic!("haven't implemented generation of swift bindings yet"); +} + + +// Call this to generate XPCOM JS bindings to load and call into the specified interface. +// XXX TODO: actually, you know, implement it... +pub fn generate_xpcom_js_bindings(idl_file: &str) { + panic!("haven't implemented generation of xpcom js bindings yet"); +} + +// Call this to generate Rust bindings to load and call into the specified interface. +// These bindings are what *external* consumers of the component should call, in order +// to access it through the FFI. For example, we might use it to generate bindings for +// glean so we can use glean from outside the megazord. +// XXX TODO: actually, you know, implement it... +pub fn generate_rust_bindings(idl_file: &str) { + panic!("haven't implemented generation of rust bindings yet"); +} \ No newline at end of file diff --git a/src/scaffolding.rs b/src/scaffolding.rs new file mode 100644 index 0000000000..8d82ba8e2a --- /dev/null +++ b/src/scaffolding.rs @@ -0,0 +1,336 @@ +/* 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::io::prelude::*; +use std::{ + env, + collections::HashMap, + convert::TryFrom, convert::TryInto, + fs::File, + iter::IntoIterator, + fmt::Display, + path::{Path, PathBuf}, +}; + +use anyhow::bail; +use anyhow::Result; +use askama::Template; + +use super::types; + +#[derive(Template)] +#[template(ext="rs", escape="none", source=r#" +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +use anyhow::bail; +use std::convert::TryInto; +use ffi_support::{ + define_bytebuffer_destructor, define_handle_map_deleter, define_string_destructor, ByteBuffer, + ConcurrentHandleMap, ExternError, FfiStr +}; + +{% for m in self.members() -%} + {{ m.render().unwrap() }} +{% endfor -%} + +// XXX TODO: string, bytes etc destructors if necessary +"#)] +pub struct ComponentInterfaceScaffolding<'a> { + ci: &'a types::ComponentInterface, +} +impl<'a> ComponentInterfaceScaffolding<'a> { + pub fn new(ci: &'a types::ComponentInterface) -> Self { + Self { ci } + } + + // XXX TODO: I'd like to make this return `impl Iterator` but then askama takes a reference to it + // and tries to call `into_iter()` and that doesn't work becaue it tries to do a move. Bleh. More to learn... + fn members(&self) -> Vec> { + self.ci.members.iter().map(|m| { + match m { + types::InterfaceMember::Object(obj) => ObjectScaffolding::boxed(self.ci, obj), + types::InterfaceMember::Record(rec) => RecordScaffolding::boxed(self.ci, rec), + types::InterfaceMember::Namespace(n) => NamespaceScaffolding::boxed(self.ci, n), + types::InterfaceMember::Enum(e) => EnumScaffolding::boxed(self.ci, e), + } + }).collect() + } +} + +#[derive(Template)] +#[template(ext="rs", escape="none", source=r#" +lazy_static::lazy_static! { + static ref UNIFFI_HANDLE_MAP_{{ self.struct_name() }}: ConcurrentHandleMap<{{ self.struct_name() }}> = ConcurrentHandleMap::new(); +} +// XXX TODO: interpolate name prefix from containing environment. +define_handle_map_deleter!(UNIFFI_HANDLE_MAP_{{ self.struct_name() }}, {{ self.struct_name() }}_free,); + +{%- for m in self.members() %} + {{ m.render().unwrap() }} +{% endfor -%} +"#)] +struct ObjectScaffolding<'a> { + ci: &'a types::ComponentInterface, + obj: &'a types::ObjectType, +} +impl<'a> ObjectScaffolding<'a> { + fn boxed(ci: &'a types::ComponentInterface, obj: &'a types::ObjectType) -> Box { + Box::new(Self { ci, obj }) + } + + fn struct_name(&self) -> &'a str { + &self.obj.name + } + + fn members(&self) -> Vec> { + self.obj.members.iter().map(|m|{ + match m { + types::ObjectTypeMember::Constructor(cons) => ObjectConstructorScaffolding::boxed(self.ci, self.obj, cons), + types::ObjectTypeMember::Method(meth) => ObjectMethodScaffolding::boxed(self.ci, self.obj, meth), + } + }).collect() + } +} +#[derive(Template)] +#[template(ext="rs", escape="none", source=r#" +#[no_mangle] +pub extern "C" fn {{ self.ffi_name() }}( + {%- for arg in cons.argument_types %} + {{ arg.ffi_name() }}: {{ arg.typ.resolve(ci)|type_decl }}, + {%- endfor %} + err: &mut ExternError, +) -> u64 { + log::debug!("{{ self.ffi_name() }}"); + UNIFFI_HANDLE_MAP_{{ obj.name }}.insert_with_output(err, || { + {{ obj.name }}::{{ cons.name }}( + {%- for arg in cons.argument_types %} + {{ arg.ffi_name()|type_lift(arg.typ.resolve(ci)) }}, + {%- endfor %} + ) + }) +} +"#)] +struct ObjectConstructorScaffolding<'a> { + ci: &'a types::ComponentInterface, + obj: &'a types::ObjectType, + cons: &'a types::ObjectTypeConstructor, +} + +impl<'a> ObjectConstructorScaffolding<'a> { + fn boxed(ci: &'a types::ComponentInterface, obj: &'a types::ObjectType, cons: &'a types::ObjectTypeConstructor) -> Box { + Box::new(Self { ci, obj, cons }) + } + + fn ffi_name(&self) -> String { + format!("{}_{}", self.obj.name, self.cons.name) + } +} + +#[derive(Template)] +#[template(ext="rs", escape="none", source=r#" +#[no_mangle] +pub extern "C" fn {{ self.ffi_name() }}( + handle u64, + {%- for arg in meth.argument_types %} + {{ arg.ffi_name() }}: {{ arg.typ.resolve(ci)|type_decl }}, + {%- endfor %} + err: &mut ExternError, +) + {%- match meth.return_type -%} + {%- when Some with (typ) %} + -> {{ typ.resolve(ci)|type_decl }} + {% when None -%} + {%- endmatch %} +{ + log::debug!("{{ self.ffi_name() }}"); + UNIFFI_HANDLE_MAP_{{ obj.name }}.call_with_result_mut(err, handle, |val| { + let r = val.{{ meth.name }}( + {%- for arg in meth.argument_types %} + {{ arg.ffi_name()|type_lift(arg.typ.resolve(ci)) }}, + {%- endfor %} + ); + {%- match meth.return_type -%} + {%- when Some with (typ) %} + {{ "r"|type_lower(typ.resolve(ci)) }} + {% when None -%} + r + {%- endmatch %} + }) +}"#)] +struct ObjectMethodScaffolding<'a> { + ci: &'a types::ComponentInterface, + obj: &'a types::ObjectType, + meth: &'a types::ObjectTypeMethod, +} + +impl<'a> ObjectMethodScaffolding<'a> { + fn boxed(ci: &'a types::ComponentInterface, obj: &'a types::ObjectType, meth: &'a types::ObjectTypeMethod) -> Box { + Box::new(Self {ci, obj, meth }) + } + + fn ffi_name(&self) -> String { + format!("{}_{}", self.obj.name, self.meth.name) + } +} + +#[derive(Template)] +#[template(ext="rs", escape="none", source=r#" +TODO: RECORD {{ rec.name }} +"#)] +struct RecordScaffolding<'a> { + ci: &'a types::ComponentInterface, + rec: &'a types::RecordType, +} + +impl<'a> RecordScaffolding<'a> { + fn boxed(ci: &'a types::ComponentInterface, rec: &'a types::RecordType) -> Box { + Box::new(Self { ci, rec }) + } +} + + + +#[derive(Template)] +#[template(ext="rs", escape="none", source=r#" +// Namespace { ns.name } +enum {{ ns.struct_name() }}{} +{%- for m in self.members() %} + {{ m.render().unwrap() }} +{% endfor -%} +"#)] +struct NamespaceScaffolding<'a> { + ci: &'a types::ComponentInterface, + ns: &'a types::NamespaceType, +} +impl<'a> NamespaceScaffolding<'a> { + fn boxed(ci: &'a types::ComponentInterface, ns: &'a types::NamespaceType) -> Box { + Box::new(Self { ci, ns }) + } + + fn members(&self) -> Vec> { + self.ns.members.iter().map(|m|{ + match m { + types::NamespaceTypeMember::Function(f) => NamespaceFunctionScaffolding::boxed(self.ci, self.ns, f), + } + }).collect() + } +} + +#[derive(Template)] +#[template(ext="rs", escape="none", source=r#" +#[no_mangle] +pub extern "C" fn {{ f.ffi_name() }}( + {%- for arg in f.argument_types %} + {{ arg.ffi_name() }}: {{ arg.typ.resolve(ci)|type_decl }}, + {%- endfor %} +) + {%- match f.return_type -%} + {%- when Some with (typ) %} + -> {{ typ.resolve(ci)|type_decl }} + {% when None -%} + {%- endmatch %} +{ + log::debug!("{{ f.ffi_name() }}"); + let r = {{ ns.struct_name() }}::{{ f.rust_name() }}( + {%- for arg in f.argument_types %} + {{ arg.ffi_name()|type_lift(arg.typ.resolve(ci)) }}, + {%- endfor %} + ); + {%- match f.return_type -%} + {%- when Some with (typ) %} + {{ "r"|type_lower(typ.resolve(ci)) }} + {% when None -%} + r + {%- endmatch %} +} +"#)] +struct NamespaceFunctionScaffolding<'a> { + ci: &'a types::ComponentInterface, + ns: &'a types::NamespaceType, + f: &'a types::NamespaceTypeFunction, +} + +impl<'a> NamespaceFunctionScaffolding<'a> { + fn boxed(ci: &'a types::ComponentInterface, ns: &'a types::NamespaceType, f: &'a types::NamespaceTypeFunction) -> Box { + Box::new(Self { ci, ns, f }) + } +} + +#[derive(Template)] +#[template(ext="rs", escape="none", source=r#" +enum {{ e.rust_name() }} { +{%- for v in e.values %} + {{ v }} = {{ loop.index -}}, +{%- endfor %} +} + +impl std::convert::TryFrom for {{ e.rust_name() }} { + type Error = anyhow::Error; + fn try_from(v: u32) -> Result<{{ e.rust_name() }}, anyhow::Error> { + Ok(match v { + {%- for v in e.values %} + {{ loop.index }} => {{ e.rust_name() }}::{{ v }}, + {%- endfor %} + _ => bail!("Invalid {{ e.rust_name() }} enum value: {}", v), + }) + } +} +"#)] +struct EnumScaffolding<'a> { + ci: &'a types::ComponentInterface, + e: &'a types::EnumType, +} + +impl<'a> EnumScaffolding<'a> { + fn boxed(ci: &'a types::ComponentInterface, e: &'a types::EnumType) -> Box { + Box::new(Self { ci, e }) + } +} + +mod filters { + use std::fmt; + use crate::types; + use anyhow::Error; + + pub fn type_decl(typ: &Result) -> Result { + // TODO: how to return a nice askama::Error here? + let typ = typ.as_ref().unwrap(); + Ok(match typ { + types::TypeReference::Boolean => "u8".to_string(), + types::TypeReference::U64 => "u64".to_string(), + types::TypeReference::U32 => "u32".to_string(), + types::TypeReference::Enum(_) => "u32".to_string(), + types::TypeReference::String => "FfiStr<'_>".to_string(), // XXX TODO: not suitable for use in return position? + _ => format!("[TODO: DECL {:?}]", typ), + }) + } + + pub fn type_lift(nm: &dyn fmt::Display, typ: &Result) -> Result { + let nm = nm.to_string(); + let typ = typ.as_ref().unwrap(); + Ok(match typ { + types::TypeReference::Boolean => format!("({} != 0)", nm), + types::TypeReference::U64 => nm, + types::TypeReference::U32 => nm, + // XXX TODO: error handling if the conversion fails. + types::TypeReference::Enum(_) => format!("({}).try_into().unwrap()", nm), + types::TypeReference::String => format!("({}).as_str()", nm), + _ => format!("[TODO: LIFT {:?}]", typ), + }) + } + + pub fn type_lower( nm: &dyn fmt::Display, typ: &Result) -> Result { + let nm = nm.to_string(); + let typ = typ.as_ref().unwrap(); + Ok(match typ { + types::TypeReference::Boolean => format!("({} as u8)", nm), + types::TypeReference::U64 => nm, + types::TypeReference::U32 => nm, + types::TypeReference::Enum(_) => format!("({} as u32)", nm), + types::TypeReference::String => format!("({}).as_str()", nm), + _ => format!("[TODO: LOWER {:?}]", typ), + }) + } +} \ No newline at end of file diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000000..4d018ce424 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,681 @@ +/* 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/. */ + +//#![deny(missing_docs)] +#![allow(unknown_lints)] +#![warn(rust_2018_idioms)] + +//! # Component Interface Definition and Types +//! +//! This crate provides an abstract representation of the interface provided by a "rust component", +//! in high-level terms suitable for translation into target consumer languages such as Kotlin +//! and swift. It also provides facilities for parsing a WebIDL interface definition file into such a +//! representation. +//! +//! The entrypoint to this crate is the `ComponentInterface` struct, which holds a complete definition +//! of the interface provided by a component. That's really the key concept of this crate so it's +//! worth repeating: a `ComponentInterface` completely defines the shape and semantics of an interface +//! between the rust-based implementation of a component and the foreign language consumers, including +//! details like: +//! +//! * The names of all symbols in the compiled object file +//! * The type and arity of all exported functions +//! * The layout and conventions used for all arguments and return types +//! +//! If you have a dynamic library compiled from a rust component using this crate, and a foreign +//! language binding generated from the same `ComponentInterface` using the same version of this +//! crate, then there should be no opportunities for them to disagree on how the two sides of the +//! FFI boundary interact. +//! +//! Docs TODOS: +//! * define "rust component" for someone who doesn't already know what it is +//! + +use std::io::prelude::*; +use std::{ + env, + collections::HashMap, + convert::TryFrom, convert::TryInto, + fs::File, + path::{Path, PathBuf}, + str::FromStr, +}; + +use anyhow::anyhow; +use anyhow::bail; +use anyhow::Result; + +// We make extensive use of `TryInto` for converting WebIDL types parsed by weedle into the +// simpler types exported by this create. This is a little helper to simply doing that conversion +// on a list of objects and either collecting the result, or propagating the first error. +macro_rules! try_into_collection { + ($e:expr) => { + ($e).iter().map(TryInto::try_into).collect::>() + } +} + +/// The main public interface for this module, representing the complete details of an FFI that will +/// be exposed by a rust component (or equivalently, that will be consumed by foreign language bindings). +/// +/// We're still figuring out the right shape of the abstraction here; currently it just contains a +/// Vec of individual interface members, but it'll almost certainly need to grow metadata about the +/// interface as a whole. +/// +/// TODOs: +/// * probably we can have the `ComponentInterface` own a bunch of interned strings, and have its +/// members just hold references to them. This could reduce a lot of `clone`ing in the code below. +#[derive(Debug)] +pub struct ComponentInterface { + /// The individual members (objects, records, etc) that make the component interface. + pub members: Vec, + +} + +impl ComponentInterface { + + /// Parse a `ComponentInterface` from a string containing a WebIDL definition. + pub fn from_webidl(idl: &str) -> Result { + // There's some lifetime thing with the errors returned from weedle::parse + // that life is too short to figure out; unwrap and move on. + Ok(Self { members: try_into_collection!(weedle::parse(idl.trim()).unwrap())? }) + } +} + +impl FromStr for ComponentInterface { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + ComponentInterface::from_webidl(s) + } +} + +/// A component interface consists of four different types of definition: +/// +/// - Objects, opaque types with methods that can be instantiated and passed +/// around by reference. +/// - Records, types that hold data and are typically passed aroud by value. +/// - Namespaces, collections of standlone functions. +/// - Enums, simple lists of named options. +/// +/// The list of top-level members will almost certainly grow, but this is what +/// we have for now. +#[derive(Debug)] +pub enum InterfaceMember { + Object(ObjectType), + Record(RecordType), + Namespace(NamespaceType), + Enum(EnumType), +} + +impl InterfaceMember { + fn name(&self) -> &str{ + match self { + InterfaceMember::Object(t) => &t.name, + InterfaceMember::Record(t) => &t.name, + InterfaceMember::Namespace(t) => &t.name, + InterfaceMember::Enum(t) => &t.name, + } + } +} + +impl TryFrom<&weedle::Definition<'_>> for InterfaceMember { + type Error = anyhow::Error; + fn try_from(d: &weedle::Definition) -> Result { + Ok(match d { + weedle::Definition::Interface(d) => InterfaceMember::Object(d.try_into()?), + weedle::Definition::Dictionary(d) => InterfaceMember::Record(d.try_into()?), + weedle::Definition::Namespace(d) => InterfaceMember::Namespace(d.try_into()?), + weedle::Definition::Enum(d) => InterfaceMember::Enum(d.try_into()?), + _ => bail!("don't know how to deal with {:?}", d), + }) + } +} + +/// An "object" is an opaque type that can be instantiated and passed around by reference, +/// have methods called on it, and so on - basically your classic Object Oriented Programming +/// type of deal, except without elaborate inheritence hierarchies. +/// +/// In WebIDL these correspond to the `interface` keyword. +/// +/// At the FFI layer, objects are represented by an opaque integer handle and a set of functions +/// a common prefix. The object's constuctors are functions that return new objects by handle, +/// and its methods are functions that take a handle as first argument. The foreign language +/// binding code is expected to stitch these functions back together into an appropriate class +/// definition (or that language's equivalent thereof). +/// +/// TODO: +/// - maybe "ClassType" would be a better name than "ObjectType" here? +#[derive(Debug)] +pub struct ObjectType { + pub name: String, + pub members: Vec, +} + +impl TryFrom<&weedle::InterfaceDefinition<'_>> for ObjectType { + type Error = anyhow::Error; + fn try_from(d: &weedle::InterfaceDefinition) -> Result { + if d.attributes.is_some() { + bail!("no interface attributes are supported yet"); + } + if d.inheritance.is_some() { + bail!("interface inheritence is not supported"); + } + Ok(ObjectType { + name: d.identifier.0.to_string(), + members: try_into_collection!(d.members.body)? + }) + } +} + +// Represets the different types of members that can be part of an individual object's interface: +// - constructors +// - instance methods +// +// We'll probably grow more here over time, e.g. maybe static methods should be separate? +#[derive(Debug)] +pub enum ObjectTypeMember { + Constructor(ObjectTypeConstructor), + Method(ObjectTypeMethod) +} + +impl TryFrom<&weedle::interface::InterfaceMember<'_>> for ObjectTypeMember { + type Error = anyhow::Error; + fn try_from(m: &weedle::interface::InterfaceMember) -> Result { + Ok(match m { + weedle::interface::InterfaceMember::Constructor(t) => ObjectTypeMember::Constructor(t.try_into()?), + weedle::interface::InterfaceMember::Operation(t) => ObjectTypeMember::Method(t.try_into()?), + _ => bail!("no support for interface member type {:?} yet", m), + }) + } +} + +// Represents a constructor for an object type. +// +// In the FFI, this will be a function that returns a handle for an instance +// of the corresponding object type. +#[derive(Debug)] +pub struct ObjectTypeConstructor { + pub name: String, + pub argument_types: Vec, +} + + +impl ObjectTypeConstructor { + pub fn ffi_name(&self) -> String { + self.name.clone() // XXX TODO: calculate prefix from the containing Object declaration, somehow. + } +} + +impl TryFrom<&weedle::interface::ConstructorInterfaceMember<'_>> for ObjectTypeConstructor { + type Error = anyhow::Error; + fn try_from(m: &weedle::interface::ConstructorInterfaceMember) -> Result { + if m.attributes.is_some() { + bail!("no interface member attributes supported yet"); + } + Ok(ObjectTypeConstructor { + name: String::from("new"), // TODO: get the name from an attribute maybe? + argument_types: try_into_collection!(m.args.body.list)? + }) + } +} + +// Represents an instance method for an object type. +// +// The in FFI, this will be a function whose first argument is a handle for an +// instance of the corresponding object type. +#[derive(Debug)] +pub struct ObjectTypeMethod { + pub name: String, + pub return_type: Option, + pub argument_types: Vec, +} + +impl ObjectTypeMethod { + pub fn ffi_name(&self) -> String { + let mut nm = String::from("fxa_"); // XXX TODO: calculate prefix from the containing Object declaration, somehow. + nm.push_str(&self.name); + nm + } +} + +impl TryFrom<&weedle::interface::OperationInterfaceMember<'_>> for ObjectTypeMethod { + type Error = anyhow::Error; + fn try_from(m: &weedle::interface::OperationInterfaceMember) -> Result { + if m.attributes.is_some() { + bail!("no interface member attributes supported yet"); + } + if m.special.is_some() { + bail!("special operations not supported"); + } + if let Some(weedle::interface::StringifierOrStatic::Stringifier(_)) = m.modifier { + bail!("stringifiers are not supported"); + } + if let None = m.identifier { + bail!("anonymous methods are not supported {:?}", m); + } + Ok(ObjectTypeMethod { + name: m.identifier.unwrap().0.to_string(), + return_type: match &m.return_type { + weedle::types::ReturnType::Void(_) => None, + weedle::types::ReturnType::Type(t) => Some(t.try_into()?) + }, + argument_types: try_into_collection!(m.args.body.list)? + }) + } +} + +// Represents an argument to an object constructor or method call. +// +// Each argument has a name and a type, along with some optional +#[derive(Debug)] +pub struct ObjectTypeArgument { + pub name: String, + pub typ: UnresolvedTypeReference, + pub optional: bool, + pub default: Option, +} + +impl ObjectTypeArgument { + pub fn ffi_name(&self) -> String { + self.name.to_string() + } +} + +impl TryFrom<&weedle::argument::Argument<'_>> for ObjectTypeArgument { + type Error = anyhow::Error; + fn try_from(t: &weedle::argument::Argument) -> Result { + Ok(match t { + weedle::argument::Argument::Single(t) => t.try_into()?, + weedle::argument::Argument::Variadic(_) => bail!("variadic arguments not supported"), + }) + } +} + +impl TryFrom<&weedle::argument::SingleArgument<'_>> for ObjectTypeArgument { + type Error = anyhow::Error; + fn try_from(a: &weedle::argument::SingleArgument) -> Result { + if a.attributes.is_some() { + bail!("no argument attributes supported yet"); + } + Ok(ObjectTypeArgument { + name: a.identifier.0.to_string(), + typ: (&a.type_).try_into()?, + optional: a.optional.is_some(), + default: a.default.map(|v| v.value.try_into().unwrap()) + }) + } +} + + +// Represents a "data class" style object, for passing around complex values. +// In the FFI these are represented as a ByteBuffer, which one side explicitly +// serializes the data into and the other serializes it out of. So I guess they're +// kind of like "pass by clone" values. +#[derive(Debug, Default)] +pub struct RecordType { + pub name: String, + pub fields: Vec, +} + +impl TryFrom<&weedle::DictionaryDefinition<'_>> for RecordType { + type Error = anyhow::Error; + fn try_from(d: &weedle::DictionaryDefinition) -> Result { + if d.attributes.is_some() { + bail!("no dictionary attributes are supported yet"); + } + if d.inheritance.is_some() { + bail!("dictionary inheritence is not support"); + } + Ok(RecordType { + name: d.identifier.0.to_string(), + fields: try_into_collection!(d.members.body)?, + }) + } +} + +// Represents an individual field on a Record. +#[derive(Debug)] +pub struct RecordTypeField { + pub name: String, + pub typ: UnresolvedTypeReference, + pub required: bool, + pub default: Option, +} + +impl TryFrom<&weedle::dictionary::DictionaryMember<'_>> for RecordTypeField { + type Error = anyhow::Error; + fn try_from(d: &weedle::dictionary::DictionaryMember) -> Result { + if d.attributes.is_some() { + bail!("no dictionary member attributes are supported yet"); + } + Ok(Self { + name: d.identifier.0.to_string(), + typ: (&d.type_).try_into()?, + required: d.required.is_some(), + default: match d.default { + None => None, + Some(v) => Some(v.value.try_into()?), + }, + }) + } +} + +/// A namespace is simply a collection of stand-alone functions. It looks similar to +/// an interface but cannot be instantiated. +#[derive(Debug)] +pub struct NamespaceType { + pub name: String, + pub members: Vec, +} + +impl NamespaceType { + pub fn struct_name(&self) -> String { + self.name.to_string() + } +} + +impl TryFrom<&weedle::NamespaceDefinition<'_>> for NamespaceType { + type Error = anyhow::Error; + fn try_from(d: &weedle::NamespaceDefinition) -> Result { + if d.attributes.is_some() { + bail!("no interface attributes are supported yet"); + } + Ok(NamespaceType { + name: d.identifier.0.to_string(), + members: try_into_collection!(d.members.body)? + }) + } +} + +// Represets the different types of members that can be part of a namespace. +// - currently, only functions. +#[derive(Debug)] +pub enum NamespaceTypeMember { + Function(NamespaceTypeFunction) +} + +impl TryFrom<&weedle::namespace::NamespaceMember<'_>> for NamespaceTypeMember { + type Error = anyhow::Error; + fn try_from(m: &weedle::namespace::NamespaceMember) -> Result { + Ok(match m { + weedle::namespace::NamespaceMember::Operation(t) => NamespaceTypeMember::Function(t.try_into()?), + _ => bail!("no support for namespace member type {:?} yet", m), + }) + } +} + + +// Represents an individual function in a namespace. +// +// The in FFI, this will be a standalone function. +#[derive(Debug)] +pub struct NamespaceTypeFunction { + pub name: String, + pub return_type: Option, + pub argument_types: Vec, +} + +impl NamespaceTypeFunction { + pub fn ffi_name(&self) -> String { + let mut nm = String::from("fxa_"); // XXX TODO: calculate prefix from the containing Object declaration, somehow. + nm.push_str(&self.name); + nm + } + + pub fn rust_name(&self) -> String { + self.name.to_string() + } +} + +impl TryFrom<&weedle::namespace::OperationNamespaceMember<'_>> for NamespaceTypeFunction { + type Error = anyhow::Error; + fn try_from(f: &weedle::namespace::OperationNamespaceMember) -> Result { + if f.attributes.is_some() { + bail!("no interface member attributes supported yet"); + } + if let None = f.identifier { + bail!("anonymous functions are not supported {:?}", f); + } + Ok(NamespaceTypeFunction { + name: f.identifier.unwrap().0.to_string(), + return_type: match &f.return_type { + weedle::types::ReturnType::Void(_) => None, + weedle::types::ReturnType::Type(t) => Some(t.try_into()?) + }, + argument_types: try_into_collection!(f.args.body.list)? + }) + } +} + +// Represents a simple C-style enum. +// In the FFI these are turned into an appropriately-sized unsigned integer. +#[derive(Debug, Default)] +pub struct EnumType { + pub name: String, + pub values: Vec, +} + +impl EnumType { + pub fn rust_name(&self) -> String { + self.name.to_string() + } +} + +impl TryFrom<&weedle::EnumDefinition<'_>> for EnumType { + type Error = anyhow::Error; + fn try_from(d: &weedle::EnumDefinition) -> Result { + if d.attributes.is_some() { + bail!("no enum attributes are supported yet"); + } + Ok(EnumType { + name: d.identifier.0.to_string(), + values: d.values.body.list.iter().map(|v| v.0.to_string()).collect(), + }) + } +} + + +// Represents a type, either primitive or compound. +#[derive(Debug)] +pub enum TypeReference<'a> { + Boolean, + String, + U8, + S8, + U16, + S16, + U32, + S32, + U64, + S64, + Object(&'a ObjectType), + Record(&'a RecordType), + Enum(&'a EnumType), + Sequence(Box>), + //Union(Vec>>), +} + +// Represents a to-be-resolved reference to a type, either primitive or compound. +// We can't find the actual type until after the whole file has been parsed, so we +// use `UnresolvedTypeReference` as a placeholder and figure out the concrete type +// in post-processing. +#[derive(Debug)] +pub enum UnresolvedTypeReference { + Boolean, + Sequence(Box), // XXX TODO: these boxes could probably just be references + //Union(Vec>), + ByName(String), +} + +impl UnresolvedTypeReference { + pub fn resolve<'a>(&'a self, ci: &'a ComponentInterface) -> Result> { + Ok(match self { + UnresolvedTypeReference::Boolean => TypeReference::Boolean, + UnresolvedTypeReference::Sequence(t) => TypeReference::Sequence(Box::new(t.resolve(ci)?)), + //UnresolvedTypeReference::Union(ts) => TypeReference::Union(ts.iter().map(|t| t.resolve(ci)).collect::>, anyhow::Error>>()?), + UnresolvedTypeReference::ByName(name) => { + // Hard-code a couple of our own non-WebIDL-standard type names. + match name.as_ref() { + // XXX TODO what if someone typedefs one of these standard names? + // Detect it and throw an error? + "string" => TypeReference::String, + "u8" => TypeReference::U8, + "s8" => TypeReference::S8, + "u16" => TypeReference::U16, + "s16" => TypeReference::S16, + "u32" => TypeReference::U32, + "s32" => TypeReference::S32, + "u64" => TypeReference::U64, + "s64" => TypeReference::S64, + _ => { + self.resolve_by_name(ci, name)? + }, + } + } + }) + } + + fn resolve_by_name<'a>(&'a self, ci: &'a ComponentInterface, name: &'a str) -> Result> { + // XXX TODO: this is dumb, the ComponentInterface should have a HashMap of type names. + ci.members.iter().find_map(|m| { + match m { + InterfaceMember::Object(obj) => { + if obj.name == name { Some(TypeReference::Object(obj)) } else { None } + }, + InterfaceMember::Record(rec) => { + if rec.name == name { Some(TypeReference::Record(rec)) } else { None } + }, + InterfaceMember::Enum(e) => { + if e.name == name { Some(TypeReference::Enum(e)) } else { None } + }, + _ => None + } + }).ok_or_else(|| { anyhow!("unknown type name: {}", &name ) }) + } +} + +impl TryFrom<&weedle::types::Type<'_>> for UnresolvedTypeReference { + type Error = anyhow::Error; + fn try_from(t: &weedle::types::Type) -> Result { + Ok(match t { + weedle::types::Type::Single(t) => { + match t { + weedle::types::SingleType::Any(_) => bail!("no support for `any` types"), + weedle::types::SingleType::NonAny(t) => t.try_into()?, + } + }, + weedle::types::Type::Union(t) => { + bail!("no support for union types yet") +/* if t.q_mark.is_some() { + ; + } + UnresolvedTypeReference::Union(t.type_.body.list.iter().map(|v| Box::new(match v { + weedle::types::UnionMemberType::Single(t) => { + t.try_into().unwrap() + }, + weedle::types::UnionMemberType::Union(t) => panic!("no support for union union member types yet"), + })).collect())*/ + }, + }) + } +} + +impl TryFrom> for UnresolvedTypeReference { + type Error = anyhow::Error; + fn try_from(t: weedle::types::NonAnyType) -> Result { + (&t).try_into() + } +} + +impl TryFrom<&weedle::types::NonAnyType<'_>> for UnresolvedTypeReference { + type Error = anyhow::Error; + fn try_from(t: &weedle::types::NonAnyType) -> Result { + Ok(match t { + weedle::types::NonAnyType::Boolean(t) => t.try_into()?, + weedle::types::NonAnyType::Identifier(t) => t.try_into()?, + weedle::types::NonAnyType::Integer(t) => t.try_into()?, + weedle::types::NonAnyType::Sequence(t) => t.try_into()?, + _ => bail!("no support for type reference {:?}", t), + }) + } +} + +impl TryFrom<&weedle::types::AttributedNonAnyType<'_>> for UnresolvedTypeReference { + type Error = anyhow::Error; + fn try_from(t: &weedle::types::AttributedNonAnyType) -> Result { + if t.attributes.is_some() { + bail!("type attributes no support yet"); + } + (&t.type_).try_into() + } +} + +impl TryFrom<&weedle::types::AttributedType<'_>> for UnresolvedTypeReference { + type Error = anyhow::Error; + fn try_from(t: &weedle::types::AttributedType) -> Result { + if t.attributes.is_some() { + bail!("type attributes no support yet"); + } + (&t.type_).try_into() + } +} + +// The `Clone` bound here is because I don't know enough about the typesystem +// to know of to make this generic over T when T has lifetimes involved. +impl + Clone> TryFrom<&weedle::types::MayBeNull> for UnresolvedTypeReference { + type Error = anyhow::Error; + fn try_from(t: &weedle::types::MayBeNull) -> Result { + if t.q_mark.is_some() { + bail!("no support for nullable types yet"); + } + TryInto::try_into(t.type_.clone()) + } +} + +impl TryFrom for UnresolvedTypeReference { + type Error = anyhow::Error; + fn try_from(t: weedle::types::IntegerType) -> Result { + bail!("integer types not implemented ({:?}); consider using u8, u16, u32 or u64", t) + } +} + +impl TryFrom for UnresolvedTypeReference { + type Error = anyhow::Error; + fn try_from(t: weedle::term::Boolean) -> Result { + Ok(UnresolvedTypeReference::Boolean) + } +} + +impl TryFrom> for UnresolvedTypeReference { + type Error = anyhow::Error; + fn try_from(t: weedle::types::SequenceType) -> Result { + Ok(UnresolvedTypeReference::Sequence(Box::new(t.generics.body.as_ref().try_into()?))) + } +} + +impl TryFrom> for UnresolvedTypeReference { + type Error = anyhow::Error; + fn try_from(t: weedle::common::Identifier) -> Result { + Ok(UnresolvedTypeReference::ByName(t.0.to_string())) + } +} + +// Represents a literal value. +// Used for e.g. default argument values. +#[derive(Debug)] +pub enum Literal { + Boolean(bool), + String(String), + // TODO: more types of literal +} + +impl TryFrom> for Literal { + type Error = anyhow::Error; + fn try_from(v: weedle::literal::DefaultValue) -> Result { + Ok(match v { + weedle::literal::DefaultValue::Boolean(b) => Literal::Boolean(b.0), + weedle::literal::DefaultValue::String(s) => Literal::String(s.0.to_string()), + _ => bail!("no support for {:?} literal yet", v), + }) + } +}