Skip to content

Commit

Permalink
Bug 1766045 - Initial work for UniFFI JS bindings r=nika
Browse files Browse the repository at this point in the history
Generate the C++ and JS code to handle UniFFI bindings.  The WebIDL code
is completely static and doesn't need to be generated.

There's support for both synchronus and async functions, but we haven't
decided the how we want this to be configured.  In practice, almost all
functions will need to be async, so for now we're just forcing all
functions to be.

The `uniffi-bindgen-gecko-js` crate builds the binary that generates the
bindings.  This binary needs to be fed a list of UDL files, the path of
the .cpp file to generate, and the directory to generate .jsm files in
(and also all of those arguments again, but for the test fixtures).
This is quiet a horrible UI, but it's going to be wrapped in a mach
command.

The `uniffi-js` directory contains shared C++ code for
`uniffi-bindgen-gecko-js`.  As much as possible we tried to put the
functionality here and have the generated code simply forward function
calls here.

Still Todo:
  - CallbackInterfaces
  - Custom and external types
  - Datetime and TimeInterval

Differential Revision: https://phabricator.services.mozilla.com/D144472
  • Loading branch information
bendk committed Aug 2, 2022
1 parent 42ed8c3 commit c1816ba
Show file tree
Hide file tree
Showing 56 changed files with 4,101 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .clang-format-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ widget/gtk/MPRISInterfaceDescription.h
xpcom/reflect/xptcall/md/win32/.*
xpcom/reflect/xptcall/md/unix/.*

# Askama template code, which isn't valid C++ in its original form
toolkit/components/uniffi-bindgen-gecko-js/src/templates/.*
# Generated from that template code
toolkit/components/uniffi-js/UniFFIGeneratedScaffolding.cpp
toolkit/components/uniffi-js/UniFFIFixtureScaffolding.cpp

# Generated from ./tools/rewriting/ThirdPartyPaths.txt
# awk '{print ""$1".*"}' ./tools/rewriting/ThirdPartyPaths.txt
browser/components/translation/cld2/.*
Expand Down
3 changes: 3 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,6 @@ toolkit/components/backgroundtasks/defaults

# Ignore pre-generated webpack and typescript transpiled files for translations
browser/extensions/translations/extension/

# "scaffolding" used by uniffi which isn't valid JS in its original form.
toolkit/components/uniffi-bindgen-gecko-js/src/templates/js/
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
"security/manager/ssl/ipcclientcerts",
"security/manager/ssl/osclientcerts",
"testing/geckodriver",
"toolkit/components/uniffi-bindgen-gecko-js",
"toolkit/crashreporter/rust_minidump_writer_linux",
"toolkit/crashreporter/mozwer-rust",
"toolkit/library/gtest/rust",
Expand Down
59 changes: 59 additions & 0 deletions dom/chrome-webidl/UniFFI.webidl
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */

// Interface for making UniFFI scaffolding calls
//
// Gecko uses UniFFI to generate privileged JS bindings for Rust components.
// UniFFI defines a C-ABI FFI layer for calling into Rust, called the
// scaffolding. This interface is a bridge that allows the JS code to make
// scaffolding calls
//
// See https://mozilla.github.io/uniffi-rs/ for details.

// Opaque type used to represent a pointer from Rust
[ChromeOnly, Exposed=Window]
interface UniFFIPointer {};

// Types that can be passed or returned from scaffolding functions
//
// - double is used for all numeric types (including bool, which the JS code
// coerces to an int)
// - ArrayBuffer is used for RustBuffer
// - UniFFIPointer is used for Arc pointers
typedef (double or ArrayBuffer or UniFFIPointer) UniFFIScaffoldingType;

// The result of a call into UniFFI scaffolding call
enum UniFFIScaffoldingCallCode {
"success", // Successful return
"error", // Rust Err return
"internal-error", // Internal/unexpected error
};

dictionary UniFFIScaffoldingCallResult {
required UniFFIScaffoldingCallCode code;
// For success, this will be the return value for non-void returns
// For error, this will be an ArrayBuffer storing the serialized error value
UniFFIScaffoldingType data;
// For internal-error, this will be a utf-8 string describing the error
ByteString internalErrorMessage;
};

// Functions to facilitate UniFFI scaffolding calls
//
// These should only be called by the generated code from UniFFI.
[ChromeOnly, Exposed=Window]
namespace UniFFIScaffolding {
[Throws]
Promise<UniFFIScaffoldingCallResult> callAsync(unsigned long long id, UniFFIScaffoldingType... args);

[Throws]
UniFFIScaffoldingCallResult callSync(unsigned long long id, UniFFIScaffoldingType... args);

[Throws]
UniFFIPointer readPointer(unsigned long long id, ArrayBuffer buff, long position);

[Throws]
void writePointer(unsigned long long id, UniFFIPointer ptr, ArrayBuffer buff, long position);
};
1 change: 1 addition & 0 deletions dom/chrome-webidl/moz.build
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ WEBIDL_FILES = [
"SessionStoreUtils.webidl",
"StructuredCloneHolder.webidl",
"TelemetryStopwatch.webidl",
"UniFFI.webidl",
"UserInteraction.webidl",
"WebExtensionContentScript.webidl",
"WebExtensionPolicy.webidl",
Expand Down
1 change: 1 addition & 0 deletions toolkit/components/moz.build
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ DIRS += [
"timermanager",
"tooltiptext",
"typeaheadfind",
"uniffi-js",
"utils",
"url-classifier",
"urlformatter",
Expand Down
19 changes: 19 additions & 0 deletions toolkit/components/uniffi-bindgen-gecko-js/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "uniffi-bindgen-gecko-js"
version = "0.1.0"
edition = "2018"

[[bin]]
name = "uniffi-bindgen-gecko-js"
path = "src/main.rs"

[dependencies]
anyhow = "1"
askama = { version = "0.11", default-features = false, features = ["config"] }
clap = { version = "3.1", features = ["std", "derive"] }
extend = "1.1"
heck = "0.4"
uniffi_bindgen = "0.18"
serde = "1"
toml = "0.5"
camino = "1.0.8"
2 changes: 2 additions & 0 deletions toolkit/components/uniffi-bindgen-gecko-js/askama.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[general]
dirs = ["src/templates"]
139 changes: 139 additions & 0 deletions toolkit/components/uniffi-bindgen-gecko-js/src/ci_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/* 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/. */

//! Manage the universe of ComponentInterfaces
//!
//! uniffi-bindgen-gecko-js is unique because it generates bindings over a set of UDL files rather
//! than just one. This is because we want to generate the WebIDL statically rather than generate
//! it. To accomplish that, each WebIDL function inputs an opaque integer id that identifies which
//! version of it should run, for example `CallSync` inputs a function id. Operating on all UDL
//! files at once simplifies the task of ensuring those ids are to be unique and consistent between
//! the JS and c++ code.
//!
//! This module manages the list of ComponentInterface and the object ids.
use crate::render::cpp::ComponentInterfaceCppExt;
use anyhow::{bail, Context, Result};
use camino::Utf8PathBuf;
use std::collections::{BTreeSet, HashMap, HashSet};
use uniffi_bindgen::interface::{ComponentInterface, FFIFunction, Object};

pub struct ComponentInterfaceUniverse {
ci_list: Vec<ComponentInterface>,
fixture_ci_list: Vec<ComponentInterface>,
}

impl ComponentInterfaceUniverse {
pub fn new(udl_files: Vec<Utf8PathBuf>, fixture_udl_files: Vec<Utf8PathBuf>) -> Result<Self> {
let ci_list = udl_files
.into_iter()
.map(parse_udl_file)
.collect::<Result<Vec<_>>>()?;
let fixture_ci_list = fixture_udl_files
.into_iter()
.map(parse_udl_file)
.collect::<Result<Vec<_>>>()?;
Self::check_udl_namespaces_unique(&ci_list, &fixture_ci_list)?;
Ok(Self {
ci_list,
fixture_ci_list,
})
}

fn check_udl_namespaces_unique(
ci_list: &Vec<ComponentInterface>,
fixture_ci_list: &Vec<ComponentInterface>,
) -> Result<()> {
let mut set = HashSet::new();
for ci in ci_list.iter().chain(fixture_ci_list.iter()) {
if !set.insert(ci.namespace()) {
bail!("UDL files have duplicate namespace: {}", ci.namespace());
}
}
Ok(())
}

pub fn ci_list(&self) -> &Vec<ComponentInterface> {
&self.ci_list
}

pub fn fixture_ci_list(&self) -> &Vec<ComponentInterface> {
&self.fixture_ci_list
}

pub fn iter_all(&self) -> impl Iterator<Item = &ComponentInterface> {
self.ci_list.iter().chain(self.fixture_ci_list.iter())
}
}

fn parse_udl_file(udl_file: Utf8PathBuf) -> Result<ComponentInterface> {
let udl = std::fs::read_to_string(&udl_file).context("Error reading UDL file")?;
ComponentInterface::from_webidl(&udl).context("Failed to parse UDL")
}

pub struct FunctionIds<'a> {
// Map (CI namespace, func name) -> Ids
map: HashMap<(&'a str, &'a str), usize>,
}

impl<'a> FunctionIds<'a> {
pub fn new(cis: &'a ComponentInterfaceUniverse) -> Self {
Self {
map: cis
.iter_all()
.flat_map(|ci| {
ci.exposed_functions()
.into_iter()
.map(move |f| (ci.namespace(), f.name()))
})
.enumerate()
.map(|(i, (namespace, name))| ((namespace, name), i))
// Sort using BTreeSet to guarantee the IDs remain stable across runs
.collect::<BTreeSet<_>>()
.into_iter()
.collect(),
}
}

pub fn get(&self, ci: &ComponentInterface, func: &FFIFunction) -> usize {
return *self.map.get(&(ci.namespace(), func.name())).unwrap();
}

pub fn name(&self, ci: &ComponentInterface, func: &FFIFunction) -> String {
format!("{}:{}", ci.namespace(), func.name())
}
}

pub struct ObjectIds<'a> {
// Map (CI namespace, object name) -> Ids
map: HashMap<(&'a str, &'a str), usize>,
}

impl<'a> ObjectIds<'a> {
pub fn new(cis: &'a ComponentInterfaceUniverse) -> Self {
Self {
map: cis
.iter_all()
.flat_map(|ci| {
ci.object_definitions()
.iter()
.map(move |o| (ci.namespace(), o.name()))
})
.enumerate()
.map(|(i, (namespace, name))| ((namespace, name), i))
// Sort using BTreeSet to guarantee the IDs remain stable across runs
.collect::<BTreeSet<_>>()
.into_iter()
.collect(),
}
}

pub fn get(&self, ci: &ComponentInterface, obj: &Object) -> usize {
return *self.map.get(&(ci.namespace(), obj.name())).unwrap();
}

pub fn name(&self, ci: &ComponentInterface, obj: &Object) -> String {
format!("{}:{}", ci.namespace(), obj.name())
}
}
123 changes: 123 additions & 0 deletions toolkit/components/uniffi-bindgen-gecko-js/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/* 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 anyhow::{Context, Result};
use askama::Template;
use camino::Utf8PathBuf;
use clap::Parser;
use heck::ToTitleCase;
use std::fs::File;
use std::io::Write;

mod ci_list;
mod render;

pub use ci_list::{ComponentInterfaceUniverse, FunctionIds, ObjectIds};
pub use render::cpp::CPPScaffoldingTemplate;
pub use render::js::JSBindingsTemplate;
use uniffi_bindgen::ComponentInterface;

#[derive(Debug, Parser)]
#[clap(name = "uniffi-bindgen-gecko-js")]
#[clap(version = clap::crate_version!())]
#[clap(about = "JS bindings generator for Rust")]
#[clap(propagate_version = true)]
struct CliArgs {
// This is a really convoluted set of arguments, but we're only expecting to be called by
// `mach_commands.py`
#[clap(long, value_name = "FILE")]
js_dir: Utf8PathBuf,

#[clap(long, value_name = "FILE")]
fixture_js_dir: Utf8PathBuf,

#[clap(long, value_name = "FILE")]
cpp_path: Utf8PathBuf,

#[clap(long, value_name = "FILE")]
fixture_cpp_path: Utf8PathBuf,

#[clap(long, multiple_values = true, value_name = "FILES")]
udl_files: Vec<Utf8PathBuf>,

#[clap(long, multiple_values = true, value_name = "FILES")]
fixture_udl_files: Vec<Utf8PathBuf>,
}

fn render(out_path: Utf8PathBuf, template: impl Template) -> Result<()> {
println!("rendering {}", out_path);
let contents = template.render()?;
let mut f =
File::create(&out_path).context(format!("Failed to create {:?}", out_path.file_name()))?;
write!(f, "{}\n", contents).context(format!("Failed to write to {}", out_path))
}

fn render_cpp(
path: Utf8PathBuf,
prefix: &str,
ci_list: &Vec<ComponentInterface>,
function_ids: &FunctionIds,
object_ids: &ObjectIds,
) -> Result<()> {
render(
path,
CPPScaffoldingTemplate {
prefix,
ci_list,
function_ids: &function_ids,
object_ids: &object_ids,
},
)
}

fn render_js(
out_dir: Utf8PathBuf,
ci_list: &Vec<ComponentInterface>,
function_ids: &FunctionIds,
object_ids: &ObjectIds,
) -> Result<()> {
for ci in ci_list {
let path = out_dir.join(format!("{}.jsm", ci.namespace().to_title_case()));
render(
path,
JSBindingsTemplate {
ci,
function_ids: &function_ids,
object_ids: &object_ids,
},
)?;
}
Ok(())
}

pub fn run_main() -> Result<()> {
let args = CliArgs::parse();
let cis = ComponentInterfaceUniverse::new(args.udl_files, args.fixture_udl_files)?;
let function_ids = FunctionIds::new(&cis);
let object_ids = ObjectIds::new(&cis);

render_cpp(
args.cpp_path,
"UniFFI",
cis.ci_list(),
&function_ids,
&object_ids,
)?;
render_cpp(
args.fixture_cpp_path,
"UniFFIFixtures",
cis.fixture_ci_list(),
&function_ids,
&object_ids,
)?;
render_js(args.js_dir, cis.ci_list(), &function_ids, &object_ids)?;
render_js(
args.fixture_js_dir,
cis.fixture_ci_list(),
&function_ids,
&object_ids,
)?;

Ok(())
}
Loading

0 comments on commit c1816ba

Please sign in to comment.