Add support for optional slice types (#507)

* Shard the `convert.rs` module into sub-modules

Hopefully this'll make the organization a little nicer over time!

* Start adding support for optional types

This commit starts adding support for optional types to wasm-bindgen as
arguments/return values to functions. The strategy here is to add two new
traits, `OptionIntoWasmAbi` and `OptionFromWasmAbi`. These two traits are used
as a blanket impl to implement `IntoWasmAbi` and `FromWasmAbi` for `Option<T>`.

Some consequences of this design:

* It should be possible to ensure `Option<SomeForeignType>` implements to/from
  wasm traits. This is because the option-based traits can be implemented for
  foreign types.
* A specialized implementation is possible for all types, so there's no need for
  `Option<T>` to introduce unnecessary overhead.
* Two new traits is a bit unforutnate but I can't currently think of an
  alternative design that works for the above two constraints, although it
  doesn't mean one doesn't exist!
* The error messages for "can't use this type here" is actually halfway decent
  because it says these new traits need to be implemented, which provides a good
  place to document and talk about what's going on here!
* Nested references like `Option<&T>` can't implement `FromWasmAbi`. This means
  that you can't define a function in Rust which takes `Option<&str>`. It may be
  possible to do this one day but it'll likely require more trait trickery than
  I'm capable of right now.

* Add support for optional slices

This commit adds support for optional slice types, things like strings and
arrays. The null representation of these has a pointer value of 0, which should
never happen in normal Rust. Otherwise the various plumbing is done throughout
the tooling to enable these types in all locations.

* Fix `takeObject` on global sentinels

These don't have a reference count as they're always expected to work, so avoid
actually dropping a reference on them.

* Remove some no longer needed bindings

* Add support for optional anyref types

This commit adds support for optional imported class types. Each type imported
with `#[wasm_bindgen]` automatically implements the relevant traits and now
supports `Option<Foo>` in various argument/return positions.

* Fix building without the `std` feature

* Actually fix the build...

* Add support for optional types to WebIDL

Closes #502
This commit is contained in:
Alex Crichton
2018-07-19 14:44:23 -05:00
committed by GitHub
parent 6eef5f7b52
commit cbeb301371
20 changed files with 1214 additions and 725 deletions

View File

@ -33,6 +33,7 @@ tys! {
ENUM
RUST_STRUCT
CHAR
OPTIONAL
}
#[derive(Debug)]
@ -59,6 +60,7 @@ pub enum Descriptor {
Enum,
RustStruct(String),
Char,
Option(Box<Descriptor>),
}
#[derive(Debug)]
@ -115,6 +117,7 @@ impl Descriptor {
REFMUT => Descriptor::RefMut(Box::new(Descriptor::_decode(data))),
SLICE => Descriptor::Slice(Box::new(Descriptor::_decode(data))),
VECTOR => Descriptor::Vector(Box::new(Descriptor::_decode(data))),
OPTIONAL => Descriptor::Option(Box::new(Descriptor::_decode(data))),
STRING => Descriptor::String,
ANYREF => Descriptor::Anyref,
ENUM => Descriptor::Enum,

View File

@ -122,20 +122,31 @@ impl<'a, 'b> Js2Rust<'a, 'b> {
let i = self.arg_idx;
let name = self.abi_arg();
let (arg, optional) = match arg {
Descriptor::Option(t) => (&**t, true),
_ => (arg, false),
};
if let Some(kind) = arg.vector_kind() {
self.js_arguments
.push((name.clone(), kind.js_ty().to_string()));
let func = self.cx.pass_to_wasm_function(kind)?;
let val = if optional {
self.cx.expose_is_like_none();
format!("isLikeNone({}) ? [0, 0] : {}({})", name, func, name)
} else {
format!("{}({})", func, name)
};
self.prelude(&format!(
"\
const [ptr{i}, len{i}] = {func}({arg});\n\
",
"const [ptr{i}, len{i}] = {val};",
i = i,
func = func,
arg = name
val = val,
));
if arg.is_by_ref() {
if optional {
bail!("optional slices aren't currently supported");
}
if arg.is_mut_ref() {
let get = self.cx.memview_function(kind);
self.finally(&format!(
@ -165,6 +176,25 @@ impl<'a, 'b> Js2Rust<'a, 'b> {
return Ok(self);
}
if arg.is_anyref() {
self.js_arguments.push((name.clone(), "any".to_string()));
self.cx.expose_add_heap_object();
if optional {
self.cx.expose_is_like_none();
self.rust_arguments.push(format!(
"isLikeNone({0}) ? 0 : addHeapObject({0})",
name,
));
} else {
self.rust_arguments.push(format!("addHeapObject({})", name));
}
return Ok(self);
}
if optional {
bail!("unsupported optional argument to rust function {:?}", arg);
}
if let Some(s) = arg.rust_struct() {
self.js_arguments.push((name.clone(), s.to_string()));
@ -262,11 +292,6 @@ impl<'a, 'b> Js2Rust<'a, 'b> {
self.js_arguments.push((name.clone(), "string".to_string()));
self.rust_arguments.push(format!("{}.codePointAt(0)", name))
}
Descriptor::Anyref => {
self.js_arguments.push((name.clone(), "any".to_string()));
self.cx.expose_add_heap_object();
self.rust_arguments.push(format!("addHeapObject({})", name));
}
_ => bail!("unsupported argument to rust function {:?}", arg),
}
Ok(self)
@ -282,16 +307,10 @@ impl<'a, 'b> Js2Rust<'a, 'b> {
}
};
if ty.is_ref_anyref() {
self.ret_ty = "any".to_string();
self.cx.expose_get_object();
self.ret_expr = format!("return getObject(RET);");
return Ok(self);
}
if ty.is_by_ref() {
bail!("cannot return references from Rust to JS yet")
}
let (ty, optional) = match ty {
Descriptor::Option(t) => (&**t, true),
_ => (ty, false),
};
if let Some(ty) = ty.vector_kind() {
self.ret_ty = ty.js_ty().to_string();
@ -307,16 +326,42 @@ impl<'a, 'b> Js2Rust<'a, 'b> {
const mem = getUint32Memory();\n\
const ptr = mem[retptr / 4];\n\
const len = mem[retptr / 4 + 1];\n\
{guard}
const realRet = {}(ptr, len).slice();\n\
wasm.__wbindgen_free(ptr, len * {});\n\
return realRet;\n\
",
f,
ty.size()
ty.size(),
guard = if optional { "if (ptr === 0) return;" } else { "" },
);
return Ok(self);
}
// No need to worry about `optional` here, the abi representation means
// that `takeObject` will naturally pluck out `undefined`.
if ty.is_anyref() {
self.ret_ty = "any".to_string();
self.cx.expose_take_object();
self.ret_expr = format!("return takeObject(RET);");
return Ok(self);
}
if optional {
bail!("unsupported optional argument to rust function {:?}", ty);
}
if ty.is_ref_anyref() {
self.ret_ty = "any".to_string();
self.cx.expose_get_object();
self.ret_expr = format!("return getObject(RET);");
return Ok(self);
}
if ty.is_by_ref() {
bail!("cannot return references from Rust to JS yet")
}
if let Some(name) = ty.rust_struct() {
self.ret_ty = name.to_string();
self.ret_expr = format!("return {name}.__construct(RET);", name = name);
@ -360,11 +405,6 @@ impl<'a, 'b> Js2Rust<'a, 'b> {
self.ret_ty = "string".to_string();
self.ret_expr = format!("return String.fromCodePoint(RET);")
}
Descriptor::Anyref => {
self.ret_ty = "any".to_string();
self.cx.expose_take_object();
self.ret_expr = format!("return takeObject(RET);");
}
_ => bail!("unsupported return from Rust to JS {:?}", ty),
}
Ok(self)

View File

@ -53,6 +53,8 @@ pub struct SubContext<'a, 'b: 'a> {
pub cx: &'a mut Context<'b>,
}
const INITIAL_SLAB_VALUES: &[&str] = &["undefined", "null", "true", "false"];
impl<'a> Context<'a> {
fn export(&mut self, name: &str, contents: &str, comments: Option<String>) {
let contents = contents.trim();
@ -183,28 +185,6 @@ impl<'a> Context<'a> {
))
})?;
self.bind("__wbindgen_undefined_new", &|me| {
me.expose_add_heap_object();
Ok(String::from(
"
function() {
return addHeapObject(undefined);
}
",
))
})?;
self.bind("__wbindgen_null_new", &|me| {
me.expose_add_heap_object();
Ok(String::from(
"
function() {
return addHeapObject(null);
}
",
))
})?;
self.bind("__wbindgen_is_null", &|me| {
me.expose_get_object();
Ok(String::from(
@ -227,17 +207,6 @@ impl<'a> Context<'a> {
))
})?;
self.bind("__wbindgen_boolean_new", &|me| {
me.expose_add_heap_object();
Ok(String::from(
"
function(v) {
return addHeapObject(v === 1);
}
",
))
})?;
self.bind("__wbindgen_boolean_get", &|me| {
me.expose_get_object();
Ok(String::from(
@ -782,14 +751,16 @@ impl<'a> Context<'a> {
"
function dropRef(idx) {{
{}
let obj = slab[idx >> 1];
idx = idx >> 1;
if (idx < {}) return;
let obj = slab[idx];
{}
// If we hit 0 then free up our space in the slab
slab[idx >> 1] = slab_next;
slab_next = idx >> 1;
slab[idx] = slab_next;
slab_next = idx;
}}
",
validate_owned, dec_ref
validate_owned, INITIAL_SLAB_VALUES.len(), dec_ref
));
}
@ -820,12 +791,9 @@ impl<'a> Context<'a> {
if !self.exposed_globals.insert("slab") {
return;
}
let initial_values = [
"{ obj: null }",
"{ obj: undefined }",
"{ obj: true }",
"{ obj: false }",
];
let initial_values = INITIAL_SLAB_VALUES.iter()
.map(|s| format!("{{ obj: {} }}", s))
.collect::<Vec<_>>();
self.global(&format!("const slab = [{}];", initial_values.join(", ")));
if self.config.debug {
self.export(
@ -1575,6 +1543,17 @@ impl<'a> Context<'a> {
name
}
fn expose_is_like_none(&mut self) {
if !self.exposed_globals.insert("is_like_none") {
return
}
self.global("
function isLikeNone(x) {
return x === undefined || x === null;
}
");
}
fn gc(&mut self) -> Result<(), Error> {
let module = mem::replace(self.module, Module::default());
let wasm_bytes = parity_wasm::serialize(module)?;

View File

@ -82,25 +82,36 @@ impl<'a, 'b> Rust2Js<'a, 'b> {
fn argument(&mut self, arg: &Descriptor) -> Result<(), Error> {
let abi = self.shim_argument();
let (arg, optional) = match arg {
Descriptor::Option(t) => (&**t, true),
_ => (arg, false),
};
if let Some(ty) = arg.vector_kind() {
let abi2 = self.shim_argument();
let f = self.cx.expose_get_vector_from_wasm(ty);
self.prelude(&format!(
"let v{0} = {func}({0}, {1});",
"let v{0} = {prefix}{func}({0}, {1});",
abi,
abi2,
func = f
func = f,
prefix = if optional { format!("{} == 0 ? undefined : ", abi) } else { String::new() },
));
if !arg.is_by_ref() {
self.prelude(&format!(
"\
v{0} = v{0}.slice();\n\
wasm.__wbindgen_free({0}, {1} * {size});\
{start}
v{0} = v{0}.slice();
wasm.__wbindgen_free({0}, {1} * {size});
{end}\
",
abi,
abi2,
size = ty.size()
size = ty.size(),
start = if optional { format!("if ({} !== 0) {{", abi) } else { String::new() },
end = if optional { "}" } else { "" },
));
self.cx.require_internal_export("__wbindgen_free")?;
}
@ -108,6 +119,18 @@ impl<'a, 'b> Rust2Js<'a, 'b> {
return Ok(());
}
// No need to special case `optional` here because `takeObject` will
// naturally work.
if arg.is_anyref() {
self.cx.expose_take_object();
self.js_arguments.push(format!("takeObject({})", abi));
return Ok(())
}
if optional {
bail!("unsupported optional argument {:?}", arg);
}
if let Some(signed) = arg.get_64bit() {
let f = if signed {
self.cx.expose_int64_cvt_shim()
@ -233,10 +256,6 @@ impl<'a, 'b> Rust2Js<'a, 'b> {
ref d if d.is_number() => abi,
Descriptor::Boolean => format!("{} !== 0", abi),
Descriptor::Char => format!("String.fromCodePoint({})", abi),
Descriptor::Anyref => {
self.cx.expose_take_object();
format!("takeObject({})", abi)
}
ref d if d.is_ref_anyref() => {
self.cx.expose_get_object();
format!("getObject({})", abi)
@ -258,6 +277,10 @@ impl<'a, 'b> Rust2Js<'a, 'b> {
return Ok(());
}
};
let (ty, optional) = match ty {
Descriptor::Option(t) => (&**t, true),
_ => (ty, false),
};
if ty.is_by_ref() {
bail!("cannot return a reference from JS to Rust")
}
@ -265,17 +288,43 @@ impl<'a, 'b> Rust2Js<'a, 'b> {
let f = self.cx.pass_to_wasm_function(ty)?;
self.cx.expose_uint32_memory();
self.shim_arguments.insert(0, "ret".to_string());
let mut prelude = String::new();
let expr = if optional {
prelude.push_str("const val = JS;");
self.cx.expose_is_like_none();
format!("isLikeNone(val) ? [0, 0] : {}(val)", f)
} else {
format!("{}(JS)", f)
};
self.ret_expr = format!(
"\
const [retptr, retlen] = {}(JS);\n\
{}
const [retptr, retlen] = {};
const mem = getUint32Memory();
mem[ret / 4] = retptr;
mem[ret / 4 + 1] = retlen;
",
f
prelude,
expr
);
return Ok(());
}
if ty.is_anyref() {
self.cx.expose_add_heap_object();
if optional {
self.cx.expose_is_like_none();
self.ret_expr = "
const val = JS;
return isLikeNone(val) ? 0 : addHeapObject(val);
".to_string();
} else {
self.ret_expr = "return addHeapObject(JS);".to_string()
}
return Ok(())
}
if optional {
bail!("unsupported optional return type {:?}", ty);
}
if ty.is_number() {
self.ret_expr = "return JS;".to_string();
return Ok(());
@ -324,10 +373,6 @@ impl<'a, 'b> Rust2Js<'a, 'b> {
self.ret_expr = match *ty {
Descriptor::Boolean => "return JS ? 1 : 0;".to_string(),
Descriptor::Char => "return JS.codePointAt(0);".to_string(),
Descriptor::Anyref => {
self.cx.expose_add_heap_object();
"return addHeapObject(JS);".to_string()
}
_ => bail!("unimplemented return from JS to Rust: {:?}", ty),
};
Ok(())