Skip to content

Commit 5308aa0

Browse files
committed
Use VTables for callback interfaces
Instead of registering a single method to implement a callback interface, foreign code now registers a VTable. VTable sounds fancy, but it's just a repr(C) struct where each field is a callback function. This gives us some more flexibility with the method signatures. Before, all arguments were passed using a RustBuffer, but not all FFI types can be written to a RustBuffer. In particular, I want to be able to pass callback function pointers. This also makes the callback interface FFI closer to the Rust one. I wanted to make it match exactly, but it didn't work out. Unfortunately, we can't directly return the return value on Python because of an old ctypes bug (https://bugs.python.org/issue5710). Instead, input an out param for the return type. The other main possibility would be to change `RustBuffer` to be a simple `*mut u8` (mozilla#1779), which would then be returnable by Python. However, it seems bad to restrict ourselves from ever returning a struct in the future. Eventually, we want to stop using `RustBuffer` for all complex data types and that probably means using a struct instead in some cases. Renamed `CALL_PANIC` to `CALL_UNEXPECTED_ERROR` in the foreign bindings templates. This matches the name in the Rust code and makes more sense for foreign trait implementations. Switched the keywords fixture tests to use traits rather than callback interfaces. The old tests were testing unsupported things like returning callback interfaces which just happened to pass. Removed the reexport-scaffolding-macro fixture. I don't think this one is giving us a lot of value anymore and I don't want to keep updating it when the FFI changes.
1 parent b21fb1c commit 5308aa0

39 files changed

+837
-631
lines changed

fixtures/keywords/kotlin/src/keywords.udl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ interface break {
77
void object(u8? class);
88
};
99

10-
callback interface continue {
10+
[Trait]
11+
interface continue {
1112
return return(return v);
1213
continue? continue();
1314
record<u8, break> break(break? v);

fixtures/keywords/kotlin/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ impl r#break {
1515
}
1616

1717
#[allow(non_camel_case_types)]
18-
trait r#continue {
18+
trait r#continue: Send + Sync {
1919
fn r#return(&self, v: r#return) -> r#return;
20-
fn r#continue(&self) -> Option<Box<dyn r#continue>>;
20+
fn r#continue(&self) -> Option<Arc<dyn r#continue>>;
2121
fn r#break(&self, _v: Option<Arc<r#break>>) -> HashMap<u8, Arc<r#break>>;
2222
fn r#while(&self, _v: Vec<r#while>) -> r#while;
2323
fn class(&self, _v: HashMap<u8, Vec<class>>) -> Option<HashMap<u8, Vec<class>>>;

fixtures/keywords/rust/src/keywords.udl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ interface break {
1313
void async(u8? yield);
1414
};
1515

16-
callback interface continue {
16+
[Trait]
17+
interface continue {
1718
return return(return v);
1819
continue? continue();
1920
record<u8, break> break(break? v);

fixtures/keywords/rust/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ impl r#break {
1616
pub fn r#break(&self, v: HashMap<u8, Arc<r#break>>) -> Option<HashMap<u8, Arc<r#break>>> {
1717
Some(v)
1818
}
19-
fn r#continue(&self, _v: Vec<Box<dyn r#continue>>) {}
19+
fn r#continue(&self, _v: Vec<Arc<dyn r#continue>>) {}
2020
pub fn r#yield(&self, _async: u8) {}
2121
pub fn r#async(&self, _yield: Option<u8>) {}
2222
}
2323

2424
#[allow(non_camel_case_types)]
25-
pub trait r#continue {
25+
pub trait r#continue: Send + Sync {
2626
fn r#return(&self, v: r#return) -> r#return;
27-
fn r#continue(&self) -> Option<Box<dyn r#continue>>;
27+
fn r#continue(&self) -> Option<Arc<dyn r#continue>>;
2828
fn r#break(&self, _v: Option<Arc<r#break>>) -> HashMap<u8, Arc<r#break>>;
2929
fn r#while(&self, _v: Vec<r#while>) -> r#while;
3030
fn r#yield(&self, _v: HashMap<u8, Vec<r#yield>>) -> Option<HashMap<u8, Vec<r#yield>>>;

fixtures/proc-macro/tests/bindings/test_proc_macro.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,7 @@ class SwiftTestCallbackInterface : TestCallbackInterface {
7979
}
8080

8181
func callbackHandler(h: Object) -> UInt32 {
82-
var v = h.takeError(e: BasicError.InvalidInput)
83-
return v
82+
return h.takeError(e: BasicError.InvalidInput)
8483
}
8584
}
8685

uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -300,26 +300,55 @@ impl KotlinCodeOracle {
300300

301301
/// Get the idiomatic Kotlin rendering of a variable name.
302302
fn var_name(&self, nm: &str) -> String {
303-
format!("`{}`", nm.to_string().to_lower_camel_case())
303+
format!("`{}`", self.var_name_raw(nm))
304+
}
305+
306+
/// `var_name` without the backticks. Useful for using in `@Structure.FieldOrder`.
307+
pub fn var_name_raw(&self, nm: &str) -> String {
308+
nm.to_string().to_lower_camel_case()
304309
}
305310

306311
/// Get the idiomatic Kotlin rendering of an individual enum variant.
307312
fn enum_variant_name(&self, nm: &str) -> String {
308313
nm.to_string().to_shouty_snake_case()
309314
}
310315

311-
/// Get the idiomatic Python rendering of an FFI callback function
316+
/// Get the idiomatic Kotlin rendering of an FFI callback function name
312317
fn ffi_callback_name(&self, nm: &str) -> String {
313318
format!("Uniffi{}", nm.to_upper_camel_case())
314319
}
315320

321+
/// Get the idiomatic Kotlin rendering of an FFI struct name
322+
fn ffi_struct_name(&self, nm: &str) -> String {
323+
format!("Uniffi{}", nm.to_upper_camel_case())
324+
}
325+
316326
fn ffi_type_label_by_value(&self, ffi_type: &FfiType) -> String {
317327
match ffi_type {
318328
FfiType::RustBuffer(_) => format!("{}.ByValue", self.ffi_type_label(ffi_type)),
319329
_ => self.ffi_type_label(ffi_type),
320330
}
321331
}
322332

333+
fn ffi_type_label_by_reference(&self, ffi_type: &FfiType) -> String {
334+
match ffi_type {
335+
FfiType::Int8
336+
| FfiType::UInt8
337+
| FfiType::Int16
338+
| FfiType::UInt16
339+
| FfiType::Int32
340+
| FfiType::UInt32
341+
| FfiType::Int64
342+
| FfiType::UInt64
343+
| FfiType::Float32
344+
| FfiType::Float64 => format!("{}ByReference", self.ffi_type_label(ffi_type)),
345+
FfiType::RustArcPtr(_) => "PointerByReference".to_owned(),
346+
// JNA structs default to ByReference
347+
FfiType::RustBuffer(_) | FfiType::Struct(_) => self.ffi_type_label(ffi_type),
348+
_ => panic!("{ffi_type:?} by reference is not implemented"),
349+
}
350+
}
351+
323352
fn ffi_type_label(&self, ffi_type: &FfiType) -> String {
324353
match ffi_type {
325354
// Note that unsigned integers in Kotlin are currently experimental, but java.nio.ByteBuffer does not
@@ -337,8 +366,9 @@ impl KotlinCodeOracle {
337366
}
338367
FfiType::ForeignBytes => "ForeignBytes.ByValue".to_string(),
339368
FfiType::Callback(name) => self.ffi_callback_name(name),
340-
FfiType::ForeignCallback => "ForeignCallback".to_string(),
341-
FfiType::RustFutureHandle => "Pointer".to_string(),
369+
FfiType::Struct(name) => self.ffi_struct_name(name),
370+
FfiType::Reference(inner) => self.ffi_type_label_by_reference(inner),
371+
FfiType::VoidPointer | FfiType::RustFutureHandle => "Pointer".to_string(),
342372
FfiType::RustFutureContinuationData => "USize".to_string(),
343373
}
344374
}
@@ -493,6 +523,11 @@ mod filters {
493523
Ok(KotlinCodeOracle.var_name(nm))
494524
}
495525

526+
/// Get the idiomatic Kotlin rendering of a variable name.
527+
pub fn var_name_raw(nm: &str) -> Result<String, askama::Error> {
528+
Ok(KotlinCodeOracle.var_name_raw(nm))
529+
}
530+
496531
/// Get a String representing the name used for an individual enum variant.
497532
pub fn variant_name(v: &Variant) -> Result<String, askama::Error> {
498533
Ok(KotlinCodeOracle.enum_variant_name(v.name()))
@@ -508,6 +543,11 @@ mod filters {
508543
Ok(KotlinCodeOracle.ffi_callback_name(nm))
509544
}
510545

546+
/// Get the idiomatic Kotlin rendering of an FFI struct name
547+
pub fn ffi_struct_name(nm: &str) -> Result<String, askama::Error> {
548+
Ok(KotlinCodeOracle.ffi_struct_name(nm))
549+
}
550+
511551
pub fn object_names(
512552
obj: &Object,
513553
ci: &ComponentInterface,
Lines changed: 53 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,70 @@
11
{% if self.include_once_check("CallbackInterfaceRuntime.kt") %}{% include "CallbackInterfaceRuntime.kt" %}{% endif %}
22

3-
// Implement the foreign callback handler for {{ interface_name }}
4-
internal class {{ callback_handler_class }} : ForeignCallback {
5-
@Suppress("TooGenericExceptionCaught")
6-
override fun invoke(handle: Handle, method: Int, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int {
7-
val cb = {{ ffi_converter_name }}.handleMap.get(handle)
8-
return when (method) {
9-
IDX_CALLBACK_FREE -> {
10-
{{ ffi_converter_name }}.handleMap.remove(handle)
3+
{%- let trait_impl=format!("uniffiCallbackInterface{}", name) %}
114

12-
// Successful return
13-
// See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs`
14-
UNIFFI_CALLBACK_SUCCESS
15-
}
16-
{% for meth in methods.iter() -%}
17-
{% let method_name = format!("invoke_{}", meth.name())|fn_name -%}
18-
{{ loop.index }} -> {
19-
// Call the method, write to outBuf and return a status code
20-
// See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` for info
21-
try {
22-
this.{{ method_name }}(cb, argsData, argsLen, outBuf)
23-
} catch (e: Throwable) {
24-
// Unexpected error
25-
try {
26-
// Try to serialize the error into a string
27-
outBuf.setValue({{ Type::String.borrow()|ffi_converter_name }}.lower(e.toString()))
28-
} catch (e: Throwable) {
29-
// If that fails, then it's time to give up and just return
30-
}
31-
UNIFFI_CALLBACK_UNEXPECTED_ERROR
32-
}
33-
}
34-
{% endfor %}
35-
else -> {
36-
// An unexpected error happened.
37-
// See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs`
38-
try {
39-
// Try to serialize the error into a string
40-
outBuf.setValue({{ Type::String.borrow()|ffi_converter_name }}.lower("Invalid Callback index"))
41-
} catch (e: Throwable) {
42-
// If that fails, then it's time to give up and just return
43-
}
44-
UNIFFI_CALLBACK_UNEXPECTED_ERROR
5+
// Put the implementation in an object so we don't pollute the top-level namespace
6+
internal object {{ trait_impl }} {
7+
{%- for (ffi_callback, meth) in vtable_methods.iter() %}
8+
internal object {{ meth.name()|var_name }}: {{ ffi_callback.name()|ffi_callback_name }} {
9+
override fun callback(
10+
{%- for arg in ffi_callback.arguments() -%}
11+
{{ arg.name().borrow()|var_name }}: {{ arg.type_().borrow()|ffi_type_name_by_value }},
12+
{%- endfor -%}
13+
{%- if ffi_callback.has_rust_call_status_arg() -%}
14+
uniffiCallStatus: RustCallStatus,
15+
{%- endif -%}
16+
)
17+
{%- match ffi_callback.return_type() %}
18+
{%- when Some(return_type) %}: {{ return_type|ffi_type_name_by_value }},
19+
{%- when None %}
20+
{%- endmatch %} {
21+
val uniffiObj = {{ ffi_converter_name }}.handleMap.get(uniffiHandle)
22+
val makeCall = { ->
23+
uniffiObj.{{ meth.name()|fn_name() }}(
24+
{%- for arg in meth.arguments() %}
25+
{{ arg|lift_fn }}({{ arg.name()|var_name }}),
26+
{%- endfor %}
27+
)
4528
}
46-
}
47-
}
4829

49-
{% for meth in methods.iter() -%}
50-
{% let method_name = format!("invoke_{}", meth.name())|fn_name %}
51-
@Suppress("UNUSED_PARAMETER")
52-
private fun {{ method_name }}(kotlinCallbackInterface: {{ interface_name }}, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int {
53-
{%- if meth.arguments().len() > 0 %}
54-
val argsBuf = argsData.getByteBuffer(0, argsLen.toLong()).also {
55-
it.order(ByteOrder.BIG_ENDIAN)
56-
}
57-
{%- endif %}
30+
{%- match meth.return_type() %}
31+
{%- when Some(return_type) %}
32+
val writeReturn = { value: {{ return_type|type_name(ci) }} -> uniffiOutReturn.setValue({{ return_type|lower_fn }}(value)) }
33+
{%- when None %}
34+
val writeReturn = { _: Unit -> Unit }
35+
{%- endmatch %}
5836

59-
{%- match meth.return_type() %}
60-
{%- when Some with (return_type) %}
61-
fun makeCall() : Int {
62-
val returnValue = kotlinCallbackInterface.{{ meth.name()|fn_name }}(
63-
{%- for arg in meth.arguments() %}
64-
{{ arg|read_fn }}(argsBuf)
65-
{% if !loop.last %}, {% endif %}
66-
{%- endfor %}
67-
)
68-
outBuf.setValue({{ return_type|ffi_converter_name }}.lowerIntoRustBuffer(returnValue))
69-
return UNIFFI_CALLBACK_SUCCESS
70-
}
71-
{%- when None %}
72-
fun makeCall() : Int {
73-
kotlinCallbackInterface.{{ meth.name()|fn_name }}(
74-
{%- for arg in meth.arguments() %}
75-
{{ arg|read_fn }}(argsBuf)
76-
{%- if !loop.last %}, {% endif %}
77-
{%- endfor %}
37+
{%- match meth.throws_type() %}
38+
{%- when None %}
39+
uniffiTraitInterfaceCall(uniffiCallStatus, makeCall, writeReturn)
40+
{%- when Some(error_type) %}
41+
uniffiTraitInterfaceCallWithError(
42+
uniffiCallStatus,
43+
makeCall,
44+
writeReturn,
45+
{ e: {{error_type|type_name(ci) }} -> {{ error_type|lower_fn }}(e) }
7846
)
79-
return UNIFFI_CALLBACK_SUCCESS
47+
{%- endmatch %}
8048
}
81-
{%- endmatch %}
49+
}
50+
{%- endfor %}
8251

83-
{%- match meth.throws_type() %}
84-
{%- when None %}
85-
fun makeCallAndHandleError() : Int = makeCall()
86-
{%- when Some(error_type) %}
87-
fun makeCallAndHandleError() : Int = try {
88-
makeCall()
89-
} catch (e: {{ error_type|type_name(ci) }}) {
90-
// Expected error, serialize it into outBuf
91-
outBuf.setValue({{ error_type|ffi_converter_name }}.lowerIntoRustBuffer(e))
92-
UNIFFI_CALLBACK_ERROR
52+
internal object uniffiFree: {{ "CallbackInterfaceFree"|ffi_callback_name }} {
53+
override fun callback(handle: Long) {
54+
{{ ffi_converter_name }}.handleMap.remove(handle)
9355
}
94-
{%- endmatch %}
95-
96-
return makeCallAndHandleError()
9756
}
98-
{% endfor %}
57+
58+
internal var vtable = {{ vtable|ffi_type_name_by_value }}(
59+
{%- for (ffi_callback, meth) in vtable_methods.iter() %}
60+
{{ meth.name()|var_name() }},
61+
{%- endfor %}
62+
uniffiFree
63+
)
9964

10065
// Registers the foreign callback with the Rust side.
10166
// This method is generated for each callback interface.
10267
internal fun register(lib: _UniFFILib) {
103-
lib.{{ ffi_init_callback.name() }}(this)
68+
lib.{{ ffi_init_callback.name() }}(vtable)
10469
}
10570
}
106-
107-
internal val {{ callback_handler_obj }} = {{ callback_handler_class }}()

uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceRuntime.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,6 @@ internal class ConcurrentHandleMap<T>(
3232
}
3333
}
3434

35-
interface ForeignCallback : com.sun.jna.Callback {
36-
public fun invoke(handle: Handle, method: Int, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int
37-
}
38-
3935
// Magic number for the Rust proxy to call using the same mechanism as every other method,
4036
// to free the callback once it's dropped by Rust.
4137
internal const val IDX_CALLBACK_FREE = 0

uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{%- let cbi = ci|get_callback_interface_definition(name) %}
2-
{%- let callback_handler_class = format!("UniffiCallbackInterface{}", name) %}
3-
{%- let callback_handler_obj = format!("uniffiCallbackInterface{}", name) %}
42
{%- let ffi_init_callback = cbi.ffi_init_callback() %}
53
{%- let interface_name = cbi|type_name(ci) %}
6-
{%- let methods = cbi.methods() %}
74
{%- let interface_docstring = cbi.docstring() %}
5+
{%- let methods = cbi.methods() %}
6+
{%- let vtable = cbi.vtable() %}
7+
{%- let vtable_methods = cbi.vtable_methods() %}
88

99
{% include "Interface.kt" %}
1010
{% include "CallbackInterfaceImpl.kt" %}

0 commit comments

Comments
 (0)