Skip to content

Commit

Permalink
Add support for Custom Types (#12)
Browse files Browse the repository at this point in the history
* Working, with tests. Depends on local changes to uniffi.toml

* Now with some uniffi-extras.toml

* Add to the readme

* Update test to set things

* Get rid of Disposable

* PR review suggestions

---------

Co-authored-by: Murph Murphy <[email protected]>
  • Loading branch information
coltfred and skeet70 authored Jun 12, 2024
1 parent a89a49b commit dc8113f
Show file tree
Hide file tree
Showing 11 changed files with 447 additions and 64 deletions.
204 changes: 149 additions & 55 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ path = "src/main.rs"
anyhow = "1"
askama = { version = "0.12", default-features = false, features = ["config"] }
camino = "1.1.6"
clap = { version = "4", default-features = false, features = ["derive", "std", "cargo"] }
clap = { version = "4", default-features = false, features = [
"derive",
"std",
"cargo",
] }
heck = "0.5"
paste = "1"
regex = "1.10.4"
Expand All @@ -28,10 +32,10 @@ uniffi_meta = { git = "https://github.com/mozilla/uniffi-rs.git", branch = "main
[dev-dependencies]
cargo_metadata = "0.18.1"
glob = "0.3"
itertools = "0.13.0"
uniffi-example-arithmetic = { git = "https://github.com/mozilla/uniffi-rs.git", branch = "main" }
uniffi-example-custom-types = { git = "https://github.com/mozilla/uniffi-rs.git", branch = "main" }
uniffi-example-geometry = { git = "https://github.com/mozilla/uniffi-rs.git", branch = "main" }
uniffi-example-rondpoint = { git = "https://github.com/mozilla/uniffi-rs.git", branch = "main" }
uniffi-fixture-time = { git = "https://github.com/mozilla/uniffi-rs.git", branch = "main" }
uniffi_testing = { git = "https://github.com/mozilla/uniffi-rs.git", branch = "main" }


2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@
## Testing

We pull down the pinned examples directly from Uniffi and run Java tests using the generated bindings. Just run `cargo t` to run all of them.

Note that if you need additional toml entries for your test, you can put a `uniffi-extras.toml` as a sibling of the test and it will be read in addition to the base `uniffi.toml` for the example. See [CustomTypes](./tests/scripts/TestCustomTypes/) for an example.
27 changes: 27 additions & 0 deletions src/gen_java/custom.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* 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 super::CodeType;
use crate::ComponentInterface;

#[derive(Debug)]
pub struct CustomCodeType {
name: String,
}

impl CustomCodeType {
pub fn new(name: String) -> Self {
CustomCodeType { name }
}
}

impl CodeType for CustomCodeType {
fn type_label(&self, ci: &ComponentInterface) -> String {
super::JavaCodeOracle.class_name(ci, &self.name)
}

fn canonical_name(&self) -> String {
format!("Type{}", self.name)
}
}
3 changes: 2 additions & 1 deletion src/gen_java/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use uniffi_bindgen::{
};

mod compounds;
mod custom;
mod enum_;
mod miscellany;
mod object;
Expand Down Expand Up @@ -448,7 +449,7 @@ impl AsCodeType for Type {
(**value_type).clone(),
)),
Type::External { name, .. } => unimplemented!(), //Box::new(external::ExternalCodeType::new(name)),
Type::Custom { name, .. } => unimplemented!(), //Box::new(custom::CustomCodeType::new(name)),
Type::Custom { name, .. } => Box::new(custom::CustomCodeType::new(name.clone())),
}
}
}
Expand Down
131 changes: 131 additions & 0 deletions src/templates/CustomTypeTemplate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
{%- let package_name = config.package_name() %}
{%- let ffi_type_name=builtin|ffi_type|ffi_type_name_by_value %}
{%- match config.custom_types.get(name.as_str()) %}
{%- when None %}
{#- Define a newtype record that delegates to the builtin #}

package {{ package_name }};

public record {{ type_name }}(
{{ builtin|type_name(ci) }} value
) {
}

package {{ package_name }};
import java.nio.ByteBuffer;

public enum {{ ffi_converter_name }} implements FfiConverter<{{ type_name }}, {{ ffi_type_name}}> {
INSTANCE;
@Override
public {{ type_name }} lift({{ ffi_type_name }} value) {
var builtinValue = {{ builtin|lift_fn }}(value);
return new {{ type_name }}(builtinValue);
}
@Override
public {{ ffi_type_name }} lower({{ type_name }} value) {
var builtinValue = value.value();
return {{ builtin|lower_fn }}(builtinValue);
}
@Override
public {{ type_name }} read(ByteBuffer buf) {
var builtinValue = {{ builtin|read_fn }}(buf);
return new {{ type_name }}(builtinValue);
}
@Override
public long allocationSize({{ type_name }} value) {
var builtinValue = value.value();
return {{ builtin|allocation_size_fn }}(builtinValue);
}
@Override
public void write({{ type_name }} value, ByteBuffer buf) {
var builtinValue = value.value();
{{ builtin|write_fn }}(builtinValue, buf);
}
}

{%- when Some with (config) %}

{#
When the config specifies a different type name, use that other type inside our newtype.
Lift/lower using their configured code.
#}
{%- match config.type_name %}
{%- when Some(concrete_type_name) %}

package {{ package_name }};

{%- match config.imports %}
{%- when Some(imports) %}
{%- for import_name in imports %}
import {{ import_name }};
{%- endfor %}
{%- else %}
{%- endmatch %}

public record {{ type_name }}(
{{ concrete_type_name }} value
) {}

{%- else %}
{%- endmatch %}

package {{ package_name }};
import java.nio.ByteBuffer;

{%- match config.imports %}
{%- when Some(imports) %}
{%- for import_name in imports %}
import {{ import_name }};
{%- endfor %}
{%- else %}
{%- endmatch %}
// FFI converter with custom code.
public enum {{ ffi_converter_name }} implements FfiConverter<{{ type_name }}, {{ ffi_type_name }}> {
INSTANCE;
@Override
public {{ type_name }} lift({{ ffi_type_name }} value) {
var builtinValue = {{ builtin|lift_fn }}(value);
try{
return new {{ type_name}}({{ config.into_custom.render("builtinValue") }});
} catch(Exception e){
throw new RuntimeException(e);
}
}
@Override
public {{ ffi_type_name }} lower({{ type_name }} value) {
try{
var builtinValue = {{ config.from_custom.render("value.value()") }};
return {{ builtin|lower_fn }}(builtinValue);
} catch(Exception e){
throw new RuntimeException(e);
}
}
@Override
public {{ type_name }} read(ByteBuffer buf) {
try{
var builtinValue = {{ builtin|read_fn }}(buf);
return new {{ type_name }}({{ config.into_custom.render("builtinValue") }});
} catch(Exception e){
throw new RuntimeException(e);
}
}
@Override
public long allocationSize({{ type_name }} value) {
try {
var builtinValue = {{ config.from_custom.render("value.value()") }};
return {{ builtin|allocation_size_fn }}(builtinValue);
} catch(Exception e){
throw new RuntimeException(e);
}
}
@Override
public void write({{ type_name }} value, ByteBuffer buf) {
try {
var builtinValue = {{ config.from_custom.render("value.value()") }};
{{ builtin|write_fn }}(builtinValue, buf);
} catch(Exception e){
throw new RuntimeException(e);
}
}
}
{%- endmatch %}
6 changes: 3 additions & 3 deletions src/templates/Types.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ private NoPointer() {}
{%- when Type::Boolean %}
{%- include "BooleanHelper.java" %}

{%- when Type::Custom { module_path, name, builtin } %}
{% include "CustomTypeTemplate.java" %}

{%- when Type::String %}
{%- include "StringHelper.java" %}

Expand Down Expand Up @@ -122,9 +125,6 @@ private NoPointer() {}
{%- when Type::CallbackInterface { module_path, name } %}
{% include "CallbackInterfaceTemplate.kt" %}

{%- when Type::Custom { module_path, name, builtin } %}
{% include "CustomTypeTemplate.kt" %}

{%- when Type::External { module_path, name, namespace, kind, tagged } %}
{% include "ExternalTypeTemplate.kt" %}
#}
Expand Down
96 changes: 94 additions & 2 deletions tests/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use anyhow::{bail, Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use cargo_metadata::{Message, MetadataCommand, Package, Target};
use std::env::consts::{ARCH, DLL_EXTENSION};
use std::io::{Read, Write};
use std::process::{Command, Stdio};
use std::time::{SystemTime, UNIX_EPOCH};
use std::{env, fs};
use uniffi_bindgen::library_mode::generate_bindings;
use uniffi_bindgen_java::JavaBindingGenerator;
Expand All @@ -19,12 +21,48 @@ fn run_test(fixture_name: &str, test_file: &str) -> Result<()> {
let out_dir = test_helper.create_out_dir(env!("CARGO_TARGET_TMPDIR"), &test_path)?;
let cdylib_path = find_cdylib_path(fixture_name)?;

// This whole block in designed to create a new TOML file if there is one in the fixture or a uniffi-extras.toml as a sibling of the test. The extras
// will be concatenated to the end of the base with extra if available.
let maybe_new_uniffi_toml_filename = {
let maybe_base_uniffi_toml_string =
find_uniffi_toml(fixture_name)?.and_then(read_file_contents);
let maybe_extra_uniffi_toml_string =
read_file_contents(dbg!(test_path.with_file_name("uniffi-extras.toml")));

// final_string will be "" if there aren't any toml files to read.
let final_string: String = itertools::Itertools::intersperse(
vec![
maybe_base_uniffi_toml_string,
maybe_extra_uniffi_toml_string,
]
.into_iter()
.filter_map(|s| s),
"\n".to_string(),
)
.collect();

// If there wasn't anything read from the files, just return none so the default config file can be used.
if final_string == "" {
None
} else {
//Create a unique(ish) filename for the fixture. We'll just accept that nanosecond uniqueness is good enough per fixture_name.
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_nanos();
let new_filename =
out_dir.with_file_name(format!("{}-{}.toml", fixture_name, current_time));
write_file_contents(&new_filename, &final_string)?;
Some(new_filename)
}
};

// generate the fixture bindings
generate_bindings(
&cdylib_path,
None,
&JavaBindingGenerator,
None,
maybe_new_uniffi_toml_filename.as_deref(),
&out_dir,
true,
)?;
Expand Down Expand Up @@ -81,6 +119,37 @@ fn run_test(fixture_name: &str, test_file: &str) -> Result<()> {
Ok(())
}

/// Get the uniffi_toml of the fixture if it exists.
/// It looks for it in the root directory of the project `name`.
fn find_uniffi_toml(name: &str) -> Result<Option<Utf8PathBuf>> {
let metadata = MetadataCommand::new()
.exec()
.expect("error running cargo metadata");
let matching: Vec<&Package> = metadata
.packages
.iter()
.filter(|p| p.name == name)
.collect();
let package = match matching.len() {
1 => matching[0].clone(),
n => bail!("cargo metadata return {n} packages named {name}"),
};
let cdylib_targets: Vec<&Target> = package
.targets
.iter()
.filter(|t| t.crate_types.iter().any(|t| t == "cdylib"))
.collect();
let target = match cdylib_targets.len() {
1 => cdylib_targets[0],
n => bail!("Found {n} cdylib targets for {}", package.name),
};
let maybe_uniffi_toml = target
.src_path
.parent()
.map(|uniffi_toml_dir| uniffi_toml_dir.with_file_name("uniffi.toml"));
Ok(maybe_uniffi_toml)
}

// REPRODUCTION of UniFFITestHelper::find_cdylib_path because it runs `cargo build` of this project
// to try to get the fixture cdylib, but that doesn't build dev dependencies
fn find_cdylib_path(name: &str) -> Result<Utf8PathBuf> {
Expand Down Expand Up @@ -200,6 +269,29 @@ fn calc_classpath(extra_paths: Vec<&Utf8PathBuf>) -> String {
.join(":")
}

/// Read the contents of the file. Any errors will be turned into None.
fn read_file_contents(path: Utf8PathBuf) -> Option<String> {
if let Ok(metadata) = fs::metadata(&path) {
if metadata.is_file() {
let mut content = String::new();
std::fs::File::open(path)
.ok()?
.read_to_string(&mut content)
.ok()?;
Some(content)
} else {
None
}
} else {
None
}
}

fn write_file_contents(path: &Utf8PathBuf, contents: &str) -> Result<()> {
std::fs::File::create(path)?.write_all(contents.as_bytes())?;
Ok(())
}

macro_rules! fixture_tests {
{
$(($test_name:ident, $fixture_name:expr, $test_script:expr),)*
Expand All @@ -221,7 +313,7 @@ fixture_tests! {
// (test_sprites, "uniffi-example-sprites", "scripts/test_sprites.java"),
// (test_coverall, "uniffi-fixture-coverall", "scripts/test_coverall.java"),
(test_chronological, "uniffi-fixture-time", "scripts/TestChronological.java"),
// (test_custom_types, "uniffi-example-custom-types", "scripts/test_custom_types.java"),
(test_custom_types, "uniffi-example-custom-types", "scripts/TestCustomTypes/TestCustomTypes.java"),
// (test_callbacks, "uniffi-fixture-callbacks", "scripts/test_callbacks.java"),
// (test_external_types, "uniffi-fixture-ext-types", "scripts/test_imported_types.java"),
}
Binary file removed tests/scripts/TestArithmetic.class
Binary file not shown.
21 changes: 21 additions & 0 deletions tests/scripts/TestCustomTypes/TestCustomTypes.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import java.net.MalformedURLException;
import java.net.URL;

import customtypes.*;

public class TestCustomTypes {

public static void main(String[] args) throws MalformedURLException {
// Get the custom types and check their data
CustomTypesDemo demo = CustomTypes.getCustomTypesDemo(null);
// URL is customized on the bindings side
assert demo.url().equals(new Url(new URL("http://example.com/")));
// Handle isn't, but because java doesn't have type aliases it's still wrapped.
assert demo.handle().equals(new Handle(123L));

// // Change some data and ensure that the round-trip works
demo.setUrl(new Url(new URL("http://new.example.com/")));
demo.setHandle(new Handle(456L));
assert demo.equals(CustomTypes.getCustomTypesDemo(demo));
}
}
11 changes: 11 additions & 0 deletions tests/scripts/TestCustomTypes/uniffi-extras.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[bindings.java]
package_name = "customtypes"

[bindings.java.custom_types.Url]
# Name of the type in the Kotlin code
type_name = "URL"
# Classes that need to be imported
imports = ["java.net.URI", "java.net.URL"]
# Functions to convert between strings and URLs
into_custom = "new URI({}).toURL()"
from_custom = "{}.toString()"

0 comments on commit dc8113f

Please sign in to comment.