From d8f0b5feed4d8172f2aa595d5d94c654206e1520 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Wed, 4 Oct 2023 10:50:48 -0400 Subject: [PATCH] Foreign-implemented trait interfaces This adds support for the foreign bindings side to implement Rust traits and for both sides to pass `Arc` objects back and forth. I thought this would be a good time to redo the callback interface code. The trait interface code uses a foreign-supplied vtable rather than a single callback and method indexes like the old code used. 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. -------------------- TODO ----------------------------- This currently requires "wrapping" the object every time it's passed across the FFI. If the foreign code receives a trait object, then passes it back to Rust. Rust now has a handle to the foreign impl and that foreign impl just calls back into Rust. I think #1730 could help solve this. I think there should be better tests for reference counts, but I'm waiting until we address the previous issue to implement them. Python requires a hack to return RustBuffer from callbacks (RubiconCtypesPatch.py) because of an old ctypes bug (https://bugs.python.org/issue5710). The options I can see are: - Go back to the callback interface style, where we input a RustBuffer and write the output to it. - Pass the return type as an out pointer. This requires some special handling for void returns though. - Implement #1779, which changes RustBuffer to a simple `*mut u8` and then Python could return it. This is my preference, I don't like the idea of making the ABI worse just because of Python. TODO: If I make the Python tests fail, I see `RuntimeWarning: memory leak in callback function` in the console. I believe this is because of the rubicon hack, but I'm not sure. We should double check this before merging. Kotlin is not implemented yet. I think when we do implement it, we're going to want to face #1394 and make a decision there. I don't think it's reasonable to have consumers have to call a `close()` method on these interfaces. I also don't know how we would deal with this in Rust. ISTM that the same logic that says Kotlin objects need to be manually closed dictates that if you use a Kotlin object in Rust, there should be a `close()` method or something analogous to a to `AutoCloseable`. The Ruby coverall tests are now failing because they don't implement trait interfaces. We need to figure out a way for them to pass the test suite. Remove all the debugging printouts --- fixtures/coverall/src/lib.rs | 12 + .../coverall/tests/bindings/test_coverall.py | 89 ++++++- .../tests/bindings/test_coverall.swift | 4 +- .../src/bindings/kotlin/gen_kotlin/mod.rs | 1 + .../src/bindings/python/gen_python/mod.rs | 47 ++++ .../src/bindings/python/templates/Helpers.py | 36 ++- .../python/templates/ObjectTemplate.py | 37 ++- .../python/templates/PointerManager.py | 13 +- .../python/templates/RubiconCtypesPatch.py | 244 ++++++++++++++++++ .../python/templates/RustBufferTemplate.py | 4 + .../python/templates/TraitObjectImpl.py | 67 +++++ .../src/bindings/python/templates/wrapper.py | 2 + .../src/bindings/ruby/gen_ruby/mod.rs | 1 + .../src/bindings/swift/gen_swift/mod.rs | 137 ++++++++-- .../src/bindings/swift/gen_swift/object.rs | 13 +- .../swift/templates/BridgingHeaderTemplate.h | 23 +- .../bindings/swift/templates/Helpers.swift | 40 ++- .../swift/templates/ObjectTemplate.swift | 47 +++- .../swift/templates/TraitObjectImpl.swift | 61 +++++ uniffi_bindgen/src/interface/callbacks.rs | 2 +- uniffi_bindgen/src/interface/ffi.rs | 5 + uniffi_bindgen/src/interface/mod.rs | 10 +- uniffi_bindgen/src/interface/object.rs | 45 +++- uniffi_core/src/ffi/foreigncallbacks.rs | 30 ++- uniffi_macros/src/export.rs | 4 +- uniffi_macros/src/export/scaffolding.rs | 27 +- uniffi_macros/src/export/trait_interface.rs | 172 +++++++++++- uniffi_macros/src/fnsig.rs | 9 +- uniffi_meta/src/ffi_names.rs | 8 +- uniffi_meta/src/lib.rs | 13 + 30 files changed, 1108 insertions(+), 95 deletions(-) create mode 100644 uniffi_bindgen/src/bindings/python/templates/RubiconCtypesPatch.py create mode 100644 uniffi_bindgen/src/bindings/python/templates/TraitObjectImpl.py create mode 100644 uniffi_bindgen/src/bindings/swift/templates/TraitObjectImpl.swift diff --git a/fixtures/coverall/src/lib.rs b/fixtures/coverall/src/lib.rs index 82b7236cc9..54868cc9df 100644 --- a/fixtures/coverall/src/lib.rs +++ b/fixtures/coverall/src/lib.rs @@ -20,6 +20,12 @@ pub enum CoverallError { TooManyHoles, } +impl From for CoverallError { + fn from(_e: uniffi::UnexpectedUniFFICallbackError) -> Self { + panic!("Saw UnexpectedUniFFICallbackError when a CoverallError was expected") + } +} + #[derive(Debug, thiserror::Error)] pub enum CoverallFlatError { #[error("Too many variants: {num}")] @@ -90,6 +96,12 @@ pub enum ComplexError { UnknownError, } +impl From for ComplexError { + fn from(_e: uniffi::UnexpectedUniFFICallbackError) -> Self { + Self::UnknownError + } +} + #[derive(Debug, thiserror::Error, uniffi::Error)] pub enum ComplexMacroError { #[error("OsError: {code} ({extended_code})")] diff --git a/fixtures/coverall/tests/bindings/test_coverall.py b/fixtures/coverall/tests/bindings/test_coverall.py index d4efebe2f8..ade659bfc8 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.py +++ b/fixtures/coverall/tests/bindings/test_coverall.py @@ -278,11 +278,69 @@ def test_bytes(self): coveralls = Coveralls("test_bytes") self.assertEqual(coveralls.reverse(b"123"), b"321") +class PyGetters: + def get_bool(self, v, arg2): + print(f"get_bool {v} {arg2}", v ^ arg2) + return v ^ arg2 + + def get_string(self, v, arg2): + if v == "too-many-holes": + raise CoverallError.TooManyHoles + elif v == "unexpected-error": + raise RuntimeError("unexpected error") + elif arg2: + return v.upper() + else: + return v + + def get_option(self, v, arg2): + if v == "os-error": + raise ComplexError.OsError(100, 200) + elif v == "unknown-error": + raise ComplexError.UnknownError + elif arg2: + if v: + return v.upper() + else: + return None + else: + return v + + def get_list(self, v, arg2): + if arg2: + return v + else: + return [] + + def get_nothing(self, _v): + return None + +class PyNode: + def __init__(self): + self.parent = None + + def name(self): + return "node-py" + + def set_parent(self, parent): + self.parent = parent + + def get_parent(self): + return self.parent + + def strong_count(self): + return 0 # TODO + class TraitsTest(unittest.TestCase): # Test traits implemented in Rust - def test_rust_getters(self): - test_getters(make_rust_getters()) - self.check_getters_from_python(make_rust_getters()) + # def test_rust_getters(self): + # test_getters(None) + # self.check_getters_from_python(make_rust_getters()) + + # Test traits implemented in Rust + def test_python_getters(self): + test_getters(PyGetters()) + #self.check_getters_from_python(PyGetters()) def check_getters_from_python(self, getters): self.assertEqual(getters.get_bool(True, True), False); @@ -317,6 +375,7 @@ def check_getters_from_python(self, getters): getters.get_string("unexpected-error", True) def test_path(self): + # Get traits creates 2 objects that implement the trait traits = get_traits() self.assertEqual(traits[0].name(), "node-1") # Note: strong counts are 1 more than you might expect, because the strong_count() method @@ -326,11 +385,33 @@ def test_path(self): self.assertEqual(traits[1].name(), "node-2") self.assertEqual(traits[1].strong_count(), 2) + # Let's try connecting them together traits[0].set_parent(traits[1]) + # Note: this doesn't increase the Rust strong count, since we wrap the Rust impl with a + # python impl before passing it to `set_parent()` + self.assertEqual(traits[1].strong_count(), 2) self.assertEqual(ancestor_names(traits[0]), ["node-2"]) self.assertEqual(ancestor_names(traits[1]), []) - self.assertEqual(traits[1].strong_count(), 3) self.assertEqual(traits[0].get_parent().name(), "node-2") + + # Throw in a Python implementation of the trait + # The ancestry chain now goes traits[0] -> traits[1] -> py_node + py_node = PyNode() + traits[1].set_parent(py_node) + self.assertEqual(ancestor_names(traits[0]), ["node-2", "node-py"]) + self.assertEqual(ancestor_names(traits[1]), ["node-py"]) + self.assertEqual(ancestor_names(py_node), []) + + # Rotating things. + # The ancestry chain now goes py_node -> traits[0] -> traits[1] + traits[1].set_parent(None) + py_node.set_parent(traits[0]) + self.assertEqual(ancestor_names(py_node), ["node-1", "node-2"]) + self.assertEqual(ancestor_names(traits[0]), ["node-2"]) + self.assertEqual(ancestor_names(traits[1]), []) + + # Make sure we don't crash when undoing it all + py_node.set_parent(None) traits[0].set_parent(None) if __name__=='__main__': diff --git a/fixtures/coverall/tests/bindings/test_coverall.swift b/fixtures/coverall/tests/bindings/test_coverall.swift index 9cd5a152ea..2fa538591a 100644 --- a/fixtures/coverall/tests/bindings/test_coverall.swift +++ b/fixtures/coverall/tests/bindings/test_coverall.swift @@ -319,10 +319,12 @@ do { assert(traits[1].name() == "node-2") assert(traits[1].strongCount() == 2) + // Note: this doesn't increase the Rust strong count, since we wrap the Rust impl with a + // python impl before passing it to `set_parent()` traits[0].setParent(parent: traits[1]) assert(ancestorNames(node: traits[0]) == ["node-2"]) assert(ancestorNames(node: traits[1]) == []) - assert(traits[1].strongCount() == 3) + assert(traits[1].strongCount() == 2) assert(traits[0].getParent()!.name() == "node-2") traits[0].setParent(parent: nil) } diff --git a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs index 0008916b7d..320caeca39 100644 --- a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs +++ b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs @@ -313,6 +313,7 @@ impl KotlinCodeOracle { "UniFffiRustFutureContinuationCallbackType".to_string() } FfiType::RustFutureContinuationData => "USize".to_string(), + FfiType::VTable(_name) => unimplemented!(), } } } diff --git a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs index 0fd99cab02..ffabbe0fac 100644 --- a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs +++ b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs @@ -314,6 +314,33 @@ impl PythonCodeOracle { FfiType::RustFutureHandle => "ctypes.c_void_p".to_string(), FfiType::RustFutureContinuationCallback => "_UNIFFI_FUTURE_CONTINUATION_T".to_string(), FfiType::RustFutureContinuationData => "ctypes.c_size_t".to_string(), + // VTables are passed as a pointer to the struct. Use a c_void_p since we want this to + // work before the struct is defined. + FfiType::VTable(_) => format!("ctypes.c_void_p"), + } + } + + fn return_type_default(callable: impl Callable) -> String { + match callable.return_type() { + None => "None".to_owned(), + Some(t) => match t.clone().into() { + FfiType::Int8 + | FfiType::UInt8 + | FfiType::Int16 + | FfiType::UInt16 + | FfiType::Int32 + | FfiType::UInt32 + | FfiType::Int64 + | FfiType::UInt64 => "0".to_owned(), + | FfiType::Float32 + | FfiType::Float64 => "0.0".to_owned(), + FfiType::RustArcPtr(_) => "None".to_owned(), + FfiType::RustBuffer(maybe_suffix) => match maybe_suffix { + Some(suffix) => format!("_UniffiRustBuffer{suffix}.default()"), + None => "_UniffiRustBuffer.default()".to_owned(), + }, + _ => panic!("Don't know how to return {t:?}"), + }, } } } @@ -412,6 +439,26 @@ pub mod filters { Ok(type_.into()) } + pub fn return_type_default(callable: impl Callable) -> Result { + Ok(PythonCodeOracle::return_type_default(callable)) + } + + pub fn ctypes_ffi_prototype(ffi_func: &FfiFunction) -> Result { + Ok(format!( + "ctypes.CFUNCTYPE({}, {}, ctypes.POINTER(_UniffiRustCallStatus))", + match &ffi_func.return_type() { + Some(v) => PythonCodeOracle::ffi_type_label(v), + None => "None".to_owned(), + }, + ffi_func + .arguments() + .into_iter() + .map(|a| PythonCodeOracle::ffi_type_label(&a.type_())) + .collect::>() + .join(", ") + )) + } + /// Get the Python syntax for representing a given low-level `FfiType`. pub fn ffi_type_name(type_: &FfiType) -> Result { Ok(PythonCodeOracle::ffi_type_label(type_)) diff --git a/uniffi_bindgen/src/bindings/python/templates/Helpers.py b/uniffi_bindgen/src/bindings/python/templates/Helpers.py index dca962f176..a6c7078cfb 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Helpers.py +++ b/uniffi_bindgen/src/bindings/python/templates/Helpers.py @@ -16,18 +16,22 @@ class _UniffiRustCallStatus(ctypes.Structure): # These match the values from the uniffi::rustcalls module CALL_SUCCESS = 0 CALL_ERROR = 1 - CALL_PANIC = 2 + CALL_UNEXPECTED_ERROR = 2 def __str__(self): if self.code == _UniffiRustCallStatus.CALL_SUCCESS: return "_UniffiRustCallStatus(CALL_SUCCESS)" elif self.code == _UniffiRustCallStatus.CALL_ERROR: return "_UniffiRustCallStatus(CALL_ERROR)" - elif self.code == _UniffiRustCallStatus.CALL_PANIC: - return "_UniffiRustCallStatus(CALL_PANIC)" + elif self.code == _UniffiRustCallStatus.CALL_UNEXPECTED_ERROR: + return "_UniffiRustCallStatus(CALL_UNEXPECTED_ERROR)" else: return "_UniffiRustCallStatus()" + @classmethod + def default(cls): + cls(_UniffiRustCallStatus.CALL_SUCCESS, _UniffiRustBuffer.default()) + def _rust_call(fn, *args): # Call a rust function return _rust_call_with_error(None, fn, *args) @@ -53,7 +57,7 @@ def _uniffi_check_call_status(error_ffi_converter, call_status): raise InternalError("_rust_call_with_error: CALL_ERROR, but error_ffi_converter is None") else: raise error_ffi_converter.lift(call_status.error_buf) - elif call_status.code == _UniffiRustCallStatus.CALL_PANIC: + elif call_status.code == _UniffiRustCallStatus.CALL_UNEXPECTED_ERROR: # When the rust code sees a panic, it tries to construct a _UniffiRustBuffer # with the message. But if that code panics, then it just sends back # an empty buffer. @@ -66,6 +70,30 @@ def _uniffi_check_call_status(error_ffi_converter, call_status): raise InternalError("Invalid _UniffiRustCallStatus code: {}".format( call_status.code)) +def _uniffi_trait_interface_call(call_status, make_call, lower_fn, default_return): + try: + return lower_fn(make_call()) + except Exception as e: + import traceback + print(f"Unexpected error in _uniffi_trait_interface_call: {e}") + call_status.code = _UniffiRustCallStatus.CALL_UNEXPECTED_ERROR + call_status.error_buf = {{ Type::String.borrow()|lower_fn }}(str(e)) + return default_return + +def _uniffi_trait_interface_call_with_error(call_status, make_call, lower_fn, default_return, error_type, lower_error): + try: + try: + return lower_fn(make_call()) + except error_type as e: + call_status.code = _UniffiRustCallStatus.CALL_ERROR + call_status.error_buf = lower_error(e) + return default_return + except Exception as e: + print(f"Unexpected error in _uniffi_trait_interface_call_with_error: {e}") + call_status.code = _UniffiRustCallStatus.CALL_UNEXPECTED_ERROR + call_status.error_buf = {{ Type::String.borrow()|lower_fn }}(str(e)) + return default_return + # A function pointer for a callback as defined by UniFFI. # Rust definition `fn(handle: u64, method: u32, args: _UniffiRustBuffer, buf_ptr: *mut _UniffiRustBuffer) -> int` _UNIFFI_FOREIGN_CALLBACK_T = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_ulonglong, ctypes.c_ulong, ctypes.POINTER(ctypes.c_char), ctypes.c_int, ctypes.POINTER(_UniffiRustBuffer)) diff --git a/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py index 7e98f7c46f..87e3a6dc5d 100644 --- a/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py @@ -63,7 +63,8 @@ def __ne__(self, other: object) -> {{ ne.return_type().unwrap()|type_name }}: {% endmatch %} {% endfor %} - +{%- match imp %} +{%- when ObjectImpl::Struct %} class {{ ffi_converter_name }}: @classmethod def read(cls, buf): @@ -72,16 +73,46 @@ def read(cls, buf): raise InternalError("Raw pointer value was null") return cls.lift(ptr) + @staticmethod + def lift(value): + return {{ type_name }}._make_instance_(value) + @classmethod def write(cls, value, buf): if not isinstance(value, {{ type_name }}): raise TypeError("Expected {{ type_name }} instance, {} found".format(type(value).__name__)) buf.write_u64(cls.lower(value)) + @staticmethod + def lower(value): + return value._pointer +{%- when ObjectImpl::Trait %} +{%- let trait_impl="UniffiTraitImpl{name}"|format %} +{%- include "TraitObjectImpl.py" %} +class {{ ffi_converter_name }}: + # Lift and read Rust-backed trait objects @staticmethod def lift(value): - return {{ type_name }}._make_instance_(value) + obj = {{ type_name }}._make_instance_(value) + print(f"lift: {obj} {value:x}", flush=False) + return obj + + @classmethod + def read(cls, buf): + ptr = buf.read_u64() + if ptr == 0: + raise InternalError("Raw pointer value was null") + return cls.lift(ptr) + # Lower and write Python-backed trait objects @staticmethod def lower(value): - return value._pointer + ptr = {{ trait_impl }}._UniffiPointerManager.new_pointer(value) + print(f"lower: {value} {ptr:x}", flush=False) + return ptr + + @classmethod + def write(cls, value, buf): + buf.write_u64(cls.lower(value)) + +{%- endmatch %} diff --git a/uniffi_bindgen/src/bindings/python/templates/PointerManager.py b/uniffi_bindgen/src/bindings/python/templates/PointerManager.py index 23aa28eab4..f387640a11 100644 --- a/uniffi_bindgen/src/bindings/python/templates/PointerManager.py +++ b/uniffi_bindgen/src/bindings/python/templates/PointerManager.py @@ -44,7 +44,7 @@ class _UniffiPointerManagerGeneral: def __init__(self): self._map = {} self._lock = threading.Lock() - self._current_handle = 0 + self._current_handle = 1 def new_pointer(self, obj): with self._lock: @@ -62,7 +62,10 @@ def lookup(self, handle): return self._map[handle] # Pick an pointer manager implementation based on the platform -if platform.python_implementation() == 'CPython': - _UniffiPointerManager = _UniffiPointerManagerCPython # type: ignore -else: - _UniffiPointerManager = _UniffiPointerManagerGeneral # type: ignore +# Unfortunately, the _UniffiPointerManagerCPython is not compatible with the rubicon hack, so we +# always use the slower _UniffiPointerManagerGeneral +# if platform.python_implementation() == 'CPython': +# _UniffiPointerManager = _UniffiPointerManagerCPython # type: ignore +# else: +# _UniffiPointerManager = _UniffiPointerManagerGeneral # type: ignore +_UniffiPointerManager = _UniffiPointerManagerGeneral # type: ignore diff --git a/uniffi_bindgen/src/bindings/python/templates/RubiconCtypesPatch.py b/uniffi_bindgen/src/bindings/python/templates/RubiconCtypesPatch.py new file mode 100644 index 0000000000..a2e87b1be9 --- /dev/null +++ b/uniffi_bindgen/src/bindings/python/templates/RubiconCtypesPatch.py @@ -0,0 +1,244 @@ +# Hack taken from rubicon-objc (https://github.com/beeware/rubicon-objc/) to work around +# https://bugs.python.org/issue5710, which prevents returning structs from ctypes. We apply this to +# _UniffiRustBuffer so that we can return those. + +import types + +# This module relies on the layout of a few internal Python and ctypes +# structures. Because of this, it's possible (but not all that likely) that +# things will break on newer/older Python versions. +if sys.version_info < (3, 6) or sys.version_info >= (3, 13): + v = sys.version_info + warnings.warn( + "rubicon.objc.ctypes_patch has only been tested with Python 3.6 through 3.12. " + f"You are using Python {v.major}.{v.minor}.{v.micro}. Most likely things will " + "work properly, but you may experience crashes if Python's internals have " + "changed significantly." + ) + + +# The PyTypeObject struct from "Include/object.h". +# This is a forward declaration, fields are set later once PyVarObject has been declared. +class PyTypeObject(ctypes.Structure): + pass + + +# The PyObject struct from "Include/object.h". +class PyObject(ctypes.Structure): + _fields_ = [ + ("ob_refcnt", ctypes.c_ssize_t), + ("ob_type", ctypes.POINTER(PyTypeObject)), + ] + + +# The PyVarObject struct from "Include/object.h". +class PyVarObject(ctypes.Structure): + _fields_ = [ + ("ob_base", PyObject), + ("ob_size", ctypes.c_ssize_t), + ] + + +# This structure is not stable across Python versions, but the few fields that +# we use probably won't change. +PyTypeObject._fields_ = [ + ("ob_base", PyVarObject), + ("tp_name", ctypes.c_char_p), + ("tp_basicsize", ctypes.c_ssize_t), + ("tp_itemsize", ctypes.c_ssize_t), + # There are many more fields, but we're only interested in the size fields, + # so we can leave out everything else. +] + + +# The PyTypeObject structure for the dict class. +# This is used to determine the size of the PyDictObject structure. +PyDict_Type = PyTypeObject.from_address(id(dict)) + + +# The PyDictObject structure from "Include/dictobject.h". This structure is not +# stable across Python versions, and did indeed change in recent Python +# releases. Because we only care about the size of the structure and not its +# actual contents, we can declare it as an opaque byte array, with the length +# taken from PyDict_Type. +class PyDictObject(ctypes.Structure): + _fields_ = [ + ("PyDictObject_opaque", (ctypes.c_ubyte * PyDict_Type.tp_basicsize)), + ] + + +# The mappingproxyobject struct from "Objects/descrobject.c". This structure is +# not officially stable across Python versions, but its layout hasn't changed +# since 2001. +class mappingproxyobject(ctypes.Structure): + _fields_ = [ + ("ob_base", PyObject), + ("mapping", ctypes.py_object), + ] + + +# The ffi_type structure from libffi's "include/ffi.h". This is a forward +# declaration, because the structure contains pointers to itself. +class ffi_type(ctypes.Structure): + pass + + +ffi_type._fields_ = [ + ("size", ctypes.c_size_t), + ("alignment", ctypes.c_ushort), + ("type", ctypes.c_ushort), + ("elements", ctypes.POINTER(ctypes.POINTER(ffi_type))), +] + + +# The GETFUNC and SETFUNC typedefs from "Modules/_ctypes/ctypes.h". +GETFUNC = ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p, ctypes.c_ssize_t) +# The return type of SETFUNC is declared here as a c_void_p instead of py_object to work +# around a ctypes bug (https://github.com/python/cpython/issues/81061). See the comment +# in make_callback_returnable's setfunc for details. +SETFUNC = ctypes.PYFUNCTYPE( + ctypes.c_void_p, ctypes.c_void_p, ctypes.py_object, ctypes.c_ssize_t +) + + +# The StgDictObject structure from "Modules/_ctypes/ctypes.h". This structure is +# not officially stable across Python versions, but it basically hasn't changed +# since ctypes was originally added to Python in 2009. +class StgDictObject(ctypes.Structure): + _fields_ = [ + ("dict", PyDictObject), + ("size", ctypes.c_ssize_t), + ("align", ctypes.c_ssize_t), + ("length", ctypes.c_ssize_t), + ("ffi_type_pointer", ffi_type), + ("proto", ctypes.py_object), + ("setfunc", SETFUNC), + ("getfunc", GETFUNC), + # There are a few more fields, but we leave them out again because we don't need them. + ] + + +ctypes.pythonapi.Py_IncRef.restype = None +ctypes.pythonapi.Py_IncRef.argtypes = [ctypes.POINTER(PyObject)] + + +def unwrap_mappingproxy(proxy): + """Return the mapping contained in a mapping proxy object.""" + + if not isinstance(proxy, types.MappingProxyType): + raise TypeError( + f"Expected a mapping proxy object, not {type(proxy).__module__}.{type(proxy).__qualname__}" + ) + + return mappingproxyobject.from_address(id(proxy)).mapping + + +def get_stgdict_of_type(tp): + """Return the given ctypes type's StgDict object. If the object's dict is + not a StgDict, an error is raised. + + This function is roughly equivalent to the PyType_stgdict function in the + ctypes source code. We cannot use that function directly, because it is not + part of CPython's public C API, and thus not accessible on some systems (see + #113). + """ + + if not isinstance(tp, type): + raise TypeError( + f"Expected a type object, not {type(tp).__module__}.{type(tp).__qualname__}" + ) + + stgdict = tp.__dict__ + if isinstance(stgdict, types.MappingProxyType): + # If the type's __dict__ is wrapped in a mapping proxy, we need to + # unwrap it. (This appears to always be the case, so the isinstance + # check above could perhaps be left out, but it doesn't hurt to check.) + stgdict = unwrap_mappingproxy(stgdict) + + # The StgDict type is not publicly exposed anywhere, so we can't use + # isinstance. Checking the name is the best we can do here. + if type(stgdict).__name__ != "StgDict": + raise TypeError( + f"The given type's dict must be a StgDict, not {type(stgdict).__module__}.{type(stgdict).__qualname__}" + ) + + return stgdict + + +def make_callback_returnable(ctype): + """Modify the given ctypes type so it can be returned from a callback + function. + + This function may be used as a decorator on a struct/union declaration. + + The method is idempotent; it only modifies the type the first time it + is invoked on a type. + """ + # The presence of the _rubicon_objc_ctypes_patch_getfunc attribute is a + # sentinel for whether the type has been modified previously. + if hasattr(ctype, "_rubicon_objc_ctypes_patch_getfunc"): + return ctype + + # Extract the StgDict from the ctype. + stgdict = get_stgdict_of_type(ctype) + stgdict_c = StgDictObject.from_address(id(stgdict)) + + # Ensure that there is no existing getfunc or setfunc on the stgdict. + if ctypes.cast(stgdict_c.getfunc, ctypes.c_void_p).value is not None: + raise ValueError( + f"The ctype {ctype.__module__}.{ctype.__name__} already has a getfunc" + ) + elif ctypes.cast(stgdict_c.setfunc, ctypes.c_void_p).value is not None: + raise ValueError( + f"The ctype {ctype.__module__}.{ctype.__name__} already has a setfunc" + ) + + # Define the getfunc and setfunc. + @GETFUNC + def getfunc(ptr, size): + actual_size = ctypes.sizeof(ctype) + if size != 0 and size != actual_size: + raise ValueError( + f"getfunc for ctype {ctype}: Requested size {size} " + f"does not match actual size {actual_size}" + ) + + return ctype.from_buffer_copy(ctypes.string_at(ptr, actual_size)) + + @SETFUNC + def setfunc(ptr, value, size): + actual_size = ctypes.sizeof(ctype) + if size != 0 and size != actual_size: + raise ValueError( + f"setfunc for ctype {ctype}: Requested size {size} " + f"does not match actual size {actual_size}" + ) + + ctypes.memmove(ptr, ctypes.addressof(value), actual_size) + # Because of a ctypes bug (https://github.com/python/cpython/issues/81061), + # returning None from a callback with restype py_object causes a reference + # counting error that can crash Python. To work around this bug, the restype of + # SETFUNC is declared as c_void_p instead. This way ctypes performs no automatic + # reference counting for the returned object, which avoids the bug. However, + # this way we have to manually convert the Python object to a pointer and adjust + # its reference count. + none_ptr = ctypes.cast(id(None), ctypes.POINTER(PyObject)) + # The return value of a SETFUNC is expected to have an extra reference + # (which will be owned by the caller of the SETFUNC). + ctypes.pythonapi.Py_IncRef(none_ptr) + # The returned pointer must be returned as a plain int, not as a c_void_p, + # otherwise ctypes won't recognize it and will raise a TypeError. + return ctypes.cast(none_ptr, ctypes.c_void_p).value + + # Store the getfunc and setfunc as attributes on the ctype, so they don't + # get garbage-collected. + ctype._rubicon_objc_ctypes_patch_getfunc = getfunc + ctype._rubicon_objc_ctypes_patch_setfunc = setfunc + # Put the getfunc and setfunc into the stgdict fields. + stgdict_c.getfunc = getfunc + stgdict_c.setfunc = setfunc + + # Return the passed in ctype, so this function can be used as a decorator. + return ctype + +make_callback_returnable(_UniffiRustBuffer) diff --git a/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py b/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py index c317a632fc..c7bb82f55f 100644 --- a/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py @@ -6,6 +6,10 @@ class _UniffiRustBuffer(ctypes.Structure): ("data", ctypes.POINTER(ctypes.c_char)), ] + @classmethod + def default(cls): + cls(0, 0, None) + @staticmethod def alloc(size): return _rust_call(_UniffiLib.{{ ci.ffi_rustbuffer_alloc().name() }}, size) diff --git a/uniffi_bindgen/src/bindings/python/templates/TraitObjectImpl.py b/uniffi_bindgen/src/bindings/python/templates/TraitObjectImpl.py new file mode 100644 index 0000000000..d2390eb7f2 --- /dev/null +++ b/uniffi_bindgen/src/bindings/python/templates/TraitObjectImpl.py @@ -0,0 +1,67 @@ +# Put all the bits inside a class to keep the top-level namespace clean +class {{ trait_impl }}: + _UniffiPointerManager = _UniffiPointerManager() + {%- for meth in obj.methods() %} + + @{{ meth.ffi_func()|ctypes_ffi_prototype }} + def {{ meth.name() }}( + uniffi_handle, + {%- for arg in meth.arguments() %} + {{ arg.name() }}, + {%- endfor %} + uniffi_call_status_ptr, + ): + uniffi_obj = {{ trait_impl }}._UniffiPointerManager.lookup(uniffi_handle) + print(f"Handling {{ meth.name() }} using {uniffi_obj}", {% for arg in meth.arguments() %}{{ arg.name() }}, {% endfor %}) + def make_call(): + args = ({% for arg in meth.arguments() %}{{ arg|lift_fn }}({{ arg.name() }}), {% endfor %}) + method = uniffi_obj.{{ meth.name() }} + print(f"calling {method} with {args}") + return method(*args) + {%- match meth.return_type() %} + {%- when Some(return_type) %} + lower_fn = {{ return_type|lower_fn }} + {%- when None %} + lower_fn = lambda v: None + {%- endmatch %} + + {%- match meth.throws_type() %} + {%- when None %} + return_value = _uniffi_trait_interface_call( + uniffi_call_status_ptr.contents, + make_call, + lower_fn, + {{ meth|return_type_default }}, + ) + {%- when Some(error) %} + return_value = _uniffi_trait_interface_call_with_error( + uniffi_call_status_ptr.contents, + make_call, + lower_fn, + {{ meth|return_type_default }}, + {{ error|type_name }}, + {{ error|lower_fn }}, + ) + {%- endmatch %} + print("return: ", return_value) + return return_value + {%- endfor %} + + @ctypes.CFUNCTYPE(None, ctypes.c_void_p) + def uniffi_free(uniffi_handle): + {{ trait_impl }}._UniffiPointerManager.release_pointer(uniffi_handle) + + class UniffiVtable(ctypes.Structure): + _fields_ = [ + {%- for meth in obj.methods() %} + ("{{ meth.name() }}", {{ meth.ffi_func()|ctypes_ffi_prototype }}), + {%- endfor %} + ("uniffi_free", ctypes.CFUNCTYPE(None, ctypes.c_void_p)), + ] + uniffi_vtable = UniffiVtable( + {%- for meth in obj.methods() %} + {{ meth.name() }}, + {%- endfor %} + uniffi_free + ) + _UniffiLib.{{ obj.ffi_init_trait_callback().name() }}(ctypes.byref(uniffi_vtable)) diff --git a/uniffi_bindgen/src/bindings/python/templates/wrapper.py b/uniffi_bindgen/src/bindings/python/templates/wrapper.py index 24c3290ff7..66303597e0 100644 --- a/uniffi_bindgen/src/bindings/python/templates/wrapper.py +++ b/uniffi_bindgen/src/bindings/python/templates/wrapper.py @@ -20,6 +20,7 @@ import struct import contextlib import datetime +import threading import typing {%- if ci.has_async_fns() %} import asyncio @@ -33,6 +34,7 @@ _DEFAULT = object() {% include "RustBufferTemplate.py" %} +{% include "RubiconCtypesPatch.py" %} {% include "Helpers.py" %} {% include "PointerManager.py" %} {% include "RustBufferHelper.py" %} diff --git a/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs b/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs index 1f7260d00b..e2bd661456 100644 --- a/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs +++ b/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs @@ -167,6 +167,7 @@ mod filters { | FfiType::RustFutureContinuationData => { unimplemented!("Async functions are not implemented") } + FfiType::VTable(_name) => unimplemented!(), }) } diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs index 0855bcb282..827362556c 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs @@ -406,7 +406,7 @@ impl SwiftCodeOracle { Type::Duration => Box::new(miscellany::DurationCodeType), Type::Enum { name, .. } => Box::new(enum_::EnumCodeType::new(name)), - Type::Object { name, .. } => Box::new(object::ObjectCodeType::new(name)), + Type::Object { name, imp, .. } => Box::new(object::ObjectCodeType::new(name, imp)), Type::Record { name, .. } => Box::new(record::RecordCodeType::new(name)), Type::CallbackInterface { name, .. } => { Box::new(callback_interface::CallbackInterfaceCodeType::new(name)) @@ -451,6 +451,10 @@ impl SwiftCodeOracle { nm.to_string().to_lower_camel_case() } + fn vtable_name(&self, obj_name: &str) -> String { + format!("UniffiTraitVTable{obj_name}") + } + fn ffi_type_label_raw(&self, ffi_type: &FfiType) -> String { match ffi_type { FfiType::Int8 => "Int8".into(), @@ -473,6 +477,7 @@ impl SwiftCodeOracle { FfiType::RustFutureHandle | FfiType::RustFutureContinuationData => { "UnsafeMutableRawPointer".into() } + FfiType::VTable(name) => format!("UnsafePointer<{}>", self.vtable_name(name)), } } @@ -482,13 +487,85 @@ impl SwiftCodeOracle { | FfiType::ForeignExecutorCallback | FfiType::RustFutureHandle | FfiType::RustFutureContinuationCallback - | FfiType::RustFutureContinuationData => { + | FfiType::RustFutureContinuationData + | FfiType::VTable(_) => { format!("{} _Nonnull", self.ffi_type_label_raw(ffi_type)) } _ => self.ffi_type_label_raw(ffi_type), } } + /// Like `ffi_type_label`, but used in `BridgingHeaderTemplate.h` which uses a slightly different + /// names. + pub fn header_ffi_type_label(&self, ffi_type: &FfiType) -> String { + match ffi_type { + FfiType::Int8 => "int8_t".into(), + FfiType::UInt8 => "uint8_t".into(), + FfiType::Int16 => "int16_t".into(), + FfiType::UInt16 => "uint16_t".into(), + FfiType::Int32 => "int32_t".into(), + FfiType::UInt32 => "uint32_t".into(), + FfiType::Int64 => "int64_t".into(), + FfiType::UInt64 => "uint64_t".into(), + FfiType::Float32 => "float".into(), + FfiType::Float64 => "double".into(), + FfiType::RustArcPtr(_) => "void*_Nonnull".into(), + FfiType::RustBuffer(_) => "RustBuffer".into(), + FfiType::ForeignBytes => "ForeignBytes".into(), + FfiType::ForeignCallback => "ForeignCallback _Nonnull".into(), + FfiType::ForeignExecutorCallback => "UniFfiForeignExecutorCallback _Nonnull".into(), + FfiType::ForeignExecutorHandle => "size_t".into(), + FfiType::RustFutureContinuationCallback => { + "UniFfiRustFutureContinuation _Nonnull".into() + } + FfiType::RustFutureHandle | FfiType::RustFutureContinuationData => { + "void* _Nonnull".into() + } + FfiType::VTable(name) => format!("const {}* _Nonnull", self.vtable_name(name)), + } + } + + /// FFI function prototype for the `BridgingHeaderTemplate.h` + pub fn header_prototype(&self, func: &FfiFunction, name: &str) -> String { + let return_type = match func.return_type() { + Some(v) => self.header_ffi_type_label(v), + None => "void".to_owned(), + }; + let mut arg_list: Vec = func + .arguments() + .into_iter() + .map(|a| format!("{} {}", self.header_ffi_type_label(&a.type_()), a.name())) + .collect(); + if func.has_rust_call_status_arg() { + arg_list.push("RustCallStatus *_Nonnull out_status".to_owned()); + } + let args = if arg_list.is_empty() { + "void".to_owned() + } else { + arg_list.join(", ") + }; + format!("{return_type} {name}({args})") + } + + pub fn return_type_default(&self, return_type: Option<&Type>) -> String { + match return_type { + None => "()".to_owned(), + Some(t) => match t.into() { + FfiType::Int8 + | FfiType::UInt8 + | FfiType::Int16 + | FfiType::UInt16 + | FfiType::Int32 + | FfiType::UInt32 + | FfiType::Int64 + | FfiType::UInt64 => "0".to_owned(), + FfiType::Float32 | FfiType::Float64 => "0.0".to_owned(), + FfiType::RustBuffer(_) => "RustBuffer()".to_owned(), + _ => panic!("Don't know how to return {return_type:?}"), + }, + } + } + fn ffi_canonical_name(&self, ffi_type: &FfiType) -> String { self.ffi_type_label_raw(ffi_type) } @@ -514,6 +591,31 @@ pub mod filters { Ok(oracle().find(&as_type.as_type()).ffi_converter_name()) } + // For an object, get the name of: + // - The protocol to define + // - The generated class that implements that protocol by calling into Rust + pub fn object_type_names( + type_name: &str, + obj_impl: &ObjectImpl, + ) -> Result<(String, String), askama::Error> { + Ok(match obj_impl { + // For struct-based objects, the type is the generated class and the protocol name is + // derived from that + ObjectImpl::Struct => (format!("{type_name}Protocol"), type_name.to_owned()), + // For trait interfaces, the type is the protocol the generated class name is derived from + // that. + ObjectImpl::Trait => (type_name.to_owned(), format!("UniffiImplRust{type_name}")), + }) + } + + pub fn return_type_default(return_type: Option<&Type>) -> Result { + Ok(oracle().return_type_default(return_type)) + } + + pub fn vtable_name(obj_name: &str) -> Result { + Ok(oracle().vtable_name(obj_name)) + } + pub fn lower_fn(as_type: &impl AsType) -> Result { Ok(oracle().find(&as_type.as_type()).lower()) } @@ -546,33 +648,12 @@ pub mod filters { Ok(oracle().ffi_canonical_name(ffi_type)) } - /// Like `ffi_type_name`, but used in `BridgingHeaderTemplate.h` which uses a slightly different - /// names. pub fn header_ffi_type_name(ffi_type: &FfiType) -> Result { - Ok(match ffi_type { - FfiType::Int8 => "int8_t".into(), - FfiType::UInt8 => "uint8_t".into(), - FfiType::Int16 => "int16_t".into(), - FfiType::UInt16 => "uint16_t".into(), - FfiType::Int32 => "int32_t".into(), - FfiType::UInt32 => "uint32_t".into(), - FfiType::Int64 => "int64_t".into(), - FfiType::UInt64 => "uint64_t".into(), - FfiType::Float32 => "float".into(), - FfiType::Float64 => "double".into(), - FfiType::RustArcPtr(_) => "void*_Nonnull".into(), - FfiType::RustBuffer(_) => "RustBuffer".into(), - FfiType::ForeignBytes => "ForeignBytes".into(), - FfiType::ForeignCallback => "ForeignCallback _Nonnull".into(), - FfiType::ForeignExecutorCallback => "UniFfiForeignExecutorCallback _Nonnull".into(), - FfiType::ForeignExecutorHandle => "size_t".into(), - FfiType::RustFutureContinuationCallback => { - "UniFfiRustFutureContinuation _Nonnull".into() - } - FfiType::RustFutureHandle | FfiType::RustFutureContinuationData => { - "void* _Nonnull".into() - } - }) + Ok(oracle().ffi_type_label(ffi_type)) + } + + pub fn header_prototype(func: &FfiFunction, name: &str) -> Result { + Ok(oracle().header_prototype(func, name)) } /// Get the idiomatic Swift rendering of a class name (for enums, records, errors, etc). diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift/object.rs b/uniffi_bindgen/src/bindings/swift/gen_swift/object.rs index 241d694fce..3cbd211775 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift/object.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift/object.rs @@ -3,15 +3,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use crate::backend::CodeType; +use crate::interface::ObjectImpl; #[derive(Debug)] pub struct ObjectCodeType { id: String, + imp: ObjectImpl, } impl ObjectCodeType { - pub fn new(id: String) -> Self { - Self { id } + pub fn new(id: String, imp: ObjectImpl) -> Self { + Self { id, imp } } } @@ -23,4 +25,11 @@ impl CodeType for ObjectCodeType { fn canonical_name(&self) -> String { format!("Type{}", self.id) } + + fn initialization_fn(&self) -> Option { + match self.imp { + ObjectImpl::Trait => Some(format!("UniffiImplSwift{}.initialize", self.id)), + _ => None, + } + } } diff --git a/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h b/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h index 87698e359f..9b6965a928 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h +++ b/uniffi_bindgen/src/bindings/swift/templates/BridgingHeaderTemplate.h @@ -62,18 +62,21 @@ typedef struct RustCallStatus { // Continuation callback for UniFFI Futures typedef void (*UniFfiRustFutureContinuation)(void * _Nonnull, int8_t); +// Vtables for interface types +{%- for obj in ci.object_definitions() %} +{%- if obj.is_trait_interface() %} +typedef struct {{ obj.name()|vtable_name }} { + {%- for meth in obj.methods() %} + {%- let fp_name = "(*{})"|format(meth.name()|fn_name) %} + {{ meth.ffi_func()|header_prototype(fp_name) }}; + {%- endfor %} + void (*uniffiFree)(void * _Nonnull); +} {{ obj.name()|vtable_name }}; +{%- endif %} +{%- endfor %} // Scaffolding functions {%- for func in ci.iter_ffi_function_definitions() %} -{% match func.return_type() -%}{%- when Some with (type_) %}{{ type_|header_ffi_type_name }}{% when None %}void{% endmatch %} {{ func.name() }}( - {%- if func.arguments().len() > 0 %} - {%- for arg in func.arguments() %} - {{- arg.type_().borrow()|header_ffi_type_name }} {{ arg.name() -}}{% if !loop.last || func.has_rust_call_status_arg() %}, {% endif %} - {%- endfor %} - {%- if func.has_rust_call_status_arg() %}RustCallStatus *_Nonnull out_status{% endif %} - {%- else %} - {%- if func.has_rust_call_status_arg() %}RustCallStatus *_Nonnull out_status{%- else %}void{% endif %} - {% endif %} -); +{{ func|header_prototype(func.name()) }}; {%- endfor %} {% import "macros.swift" as swift %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift b/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift index a34b128e23..15f3dd7ff1 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift @@ -28,7 +28,7 @@ fileprivate enum UniffiInternalError: LocalizedError { fileprivate let CALL_SUCCESS: Int8 = 0 fileprivate let CALL_ERROR: Int8 = 1 -fileprivate let CALL_PANIC: Int8 = 2 +fileprivate let CALL_UNEXPECTED_ERROR: Int8 = 2 fileprivate let CALL_CANCELLED: Int8 = 3 fileprivate extension RustCallStatus { @@ -81,7 +81,7 @@ private func uniffiCheckCallStatus( throw UniffiInternalError.unexpectedRustCallError } - case CALL_PANIC: + case CALL_UNEXPECTED_ERROR: // When the rust code sees a panic, it tries to construct a RustBuffer // with the message. But if that code panics, then it just sends back // an empty buffer. @@ -99,3 +99,39 @@ private func uniffiCheckCallStatus( throw UniffiInternalError.unexpectedRustCallStatusCode } } + +private func uniffiTraitInterfaceCall( + callStatus: UnsafeMutablePointer, + makeCall: () throws -> T, + lowerFn: (T) -> R, + defaultReturn: R +) -> R { + do { + return try lowerFn(makeCall()) + } catch let error { + callStatus.pointee.code = CALL_UNEXPECTED_ERROR + callStatus.pointee.errorBuf = {{ Type::String.borrow()|lower_fn }}(String(describing: error)) + return defaultReturn + } +} + +private func uniffiTraitInterfaceCallWithError( + callStatus: UnsafeMutablePointer, + makeCall: () throws -> T, + lowerFn: (T) -> R, + defaultReturn: R, + lowerError: (E) -> RustBuffer +) -> R { + do { + return try lowerFn(makeCall()) + } catch let error as E { + callStatus.pointee.code = CALL_ERROR + callStatus.pointee.errorBuf = lowerError(error) + return defaultReturn + } catch { + callStatus.pointee.code = CALL_UNEXPECTED_ERROR + callStatus.pointee.errorBuf = {{ Type::String.borrow()|lower_fn }}(String(describing: error)) + return defaultReturn + } +} + diff --git a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift index 57a77ca6df..22402eb055 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift @@ -1,5 +1,7 @@ {%- let obj = ci|get_object_definition(name) %} -public protocol {{ obj.name() }}Protocol { +{%- let (protocol_name, rust_impl_class_name) = type_name|object_type_names(imp) %} + +public protocol {{ protocol_name }} : AnyObject { {% for meth in obj.methods() -%} func {{ meth.name()|fn_name }}({% call swift::arg_list_protocol(meth) %}) {% call swift::async(meth) %} {% call swift::throws(meth) -%} {%- match meth.return_type() -%} @@ -9,7 +11,7 @@ public protocol {{ obj.name() }}Protocol { {% endfor %} } -public class {{ type_name }}: {{ obj.name() }}Protocol { +public class {{ rust_impl_class_name }}: {{ protocol_name }} { fileprivate let pointer: UnsafeMutableRawPointer // TODO: We'd like this to be `private` but for Swifty reasons, @@ -95,10 +97,20 @@ public class {{ type_name }}: {{ obj.name() }}Protocol { {% endfor %} } +{%- match imp %} +{%- when ObjectImpl::Struct %} public struct {{ ffi_converter_name }}: FfiConverter { typealias FfiType = UnsafeMutableRawPointer typealias SwiftType = {{ type_name }} + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> {{ type_name }} { + return {{ type_name }}(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: {{ type_name }}) -> UnsafeMutableRawPointer { + return value.pointer + } + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ type_name }} { let v: UInt64 = try readInt(&buf) // The Rust code won't compile if a pointer won't fit in a UInt64. @@ -116,14 +128,41 @@ public struct {{ ffi_converter_name }}: FfiConverter { writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) } +} +{%- when ObjectImpl::Trait %} +{% include "TraitObjectImpl.swift" %} + +public struct {{ ffi_converter_name }}: FfiConverter { + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = {{ type_name }} + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> {{ type_name }} { - return {{ type_name}}(unsafeFromRawPointer: pointer) + return {{ rust_impl_class_name }}(unsafeFromRawPointer: pointer) } public static func lower(_ value: {{ type_name }}) -> UnsafeMutableRawPointer { - return value.pointer + Unmanaged.passRetained(value as AnyObject).toOpaque() } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> {{ type_name }} { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if (ptr == nil) { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: {{ type_name }}, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } + } +{%- endmatch %} {# We always write these public functions just in case the enum is used as diff --git a/uniffi_bindgen/src/bindings/swift/templates/TraitObjectImpl.swift b/uniffi_bindgen/src/bindings/swift/templates/TraitObjectImpl.swift new file mode 100644 index 0000000000..db9a85be0b --- /dev/null +++ b/uniffi_bindgen/src/bindings/swift/templates/TraitObjectImpl.swift @@ -0,0 +1,61 @@ +private struct UniffiImplSwift{{ name }} { + static var vtable: {{ name|vtable_name }} = {{ name|vtable_name }}( + {%- for meth in obj.methods() %} + {{ meth.name()|fn_name }}: { (uniffiHandle: UnsafeMutableRawPointer, + {%- for arg in meth.arguments() %} + {{- arg.name() }}: {{ FfiType::from(arg.as_type()).borrow()|ffi_type_name }}, + {%- endfor -%} + uniffiCallStatus: UnsafeMutablePointer + ) + {%- match meth.return_type() %} + {%- when Some(t) %} -> {{ FfiType::from(t.clone()).borrow()|ffi_type_name }} + {%- when None %} -> () + {%- endmatch %} in + // Take an unretained pointer since we are calling by reference + let swiftObj = Unmanaged.fromOpaque(uniffiHandle).takeUnretainedValue() as! {{ type_name }} + + {%- match meth.throws_type() %} + {%- when None %} + return uniffiTraitInterfaceCall( + callStatus: uniffiCallStatus, + makeCall: { swiftObj.{{ meth.name()|fn_name }}( + {%- for arg in meth.arguments() %} + {{ arg.name()|arg_name }}: try {{ arg|lift_fn }}({{ arg.name() }}){% if !loop.last %},{% endif %} + {%- endfor %} + ) }, + {% match meth.return_type() %} + {%- when Some(t) %}lowerFn: {{ t|lower_fn }}, + {%- when None %}lowerFn: { (_: ()) in () }, + {%- endmatch %} + defaultReturn: {{ meth.return_type()|return_type_default }} + ) + {%- when Some(error_type) %} + return uniffiTraitInterfaceCallWithError( + callStatus: uniffiCallStatus, + makeCall: { try swiftObj.{{ meth.name()|fn_name }}( + {%- for arg in meth.arguments() %} + {{ arg.name()|arg_name }}: try {{ arg|lift_fn }}({{ arg.name() }}){% if !loop.last %},{% endif %} + {%- endfor %} + ) }, + {% match meth.return_type() %} + {%- when Some(t) %}lowerFn: {{ t|lower_fn }}, + {%- when None %}lowerFn: { (_: ()) in () }, + {%- endmatch %} + defaultReturn: {{ meth.return_type()|return_type_default }}, + lowerError: {{ error_type|lower_fn }} + ) + + return {{ meth.return_type()|return_type_default }} + {%- endmatch %} + }, + {%- endfor %} + uniffiFree: { (uniffiHandle: UnsafeMutableRawPointer) -> () in + // Take a retained pointer to balance the pointer released in lower() + let _ = Unmanaged.fromOpaque(uniffiHandle).takeRetainedValue() + } + ) + + static func initialize() { + {{ obj.ffi_init_trait_callback().name() }}(&vtable) + } +} diff --git a/uniffi_bindgen/src/interface/callbacks.rs b/uniffi_bindgen/src/interface/callbacks.rs index e3bca4f966..369cfbb8d4 100644 --- a/uniffi_bindgen/src/interface/callbacks.rs +++ b/uniffi_bindgen/src/interface/callbacks.rs @@ -78,7 +78,7 @@ impl CallbackInterface { pub(super) fn derive_ffi_funcs(&mut self) { self.ffi_init_callback.name = - uniffi_meta::init_callback_fn_symbol_name(&self.module_path, &self.name); + uniffi_meta::init_trait_callback_fn_symbol_name(&self.module_path, &self.name); self.ffi_init_callback.arguments = vec![FfiArgument { name: "callback_stub".to_string(), type_: FfiType::ForeignCallback, diff --git a/uniffi_bindgen/src/interface/ffi.rs b/uniffi_bindgen/src/interface/ffi.rs index d18aaf8262..be3d71e8a7 100644 --- a/uniffi_bindgen/src/interface/ffi.rs +++ b/uniffi_bindgen/src/interface/ffi.rs @@ -59,6 +59,11 @@ pub enum FfiType { /// Continuation function for a Rust future RustFutureContinuationCallback, RustFutureContinuationData, + /// VTable for a trait interface + /// + /// This is a C struct with fields for each method and for the free method. See + /// `uniffi_macros/src/export/trait_interface.rs` for the exact layout of this struct. + VTable(String), // TODO: you can imagine a richer structural typesystem here, e.g. `Ref` or something. // We don't need that yet and it's possible we never will, so it isn't here for now. } diff --git a/uniffi_bindgen/src/interface/mod.rs b/uniffi_bindgen/src/interface/mod.rs index 8e4df2149b..d4f5ebdfe3 100644 --- a/uniffi_bindgen/src/interface/mod.rs +++ b/uniffi_bindgen/src/interface/mod.rs @@ -241,13 +241,19 @@ impl ComponentInterface { let fielded = !e.is_flat(); // For flat errors, we should only generate read() methods if we need them to support // callback interface errors - let used_in_callback_interface = self + let used_in_foreign_interface = self .callback_interface_definitions() .iter() .flat_map(|cb| cb.methods()) + .chain( + self.object_definitions() + .iter() + .filter(|o| o.is_trait_interface()) + .flat_map(|o| o.methods()), + ) .any(|m| m.throws_type() == Some(&e.as_type())); - self.is_name_used_as_error(&e.name) && (fielded || used_in_callback_interface) + self.is_name_used_as_error(&e.name) && (fielded || used_in_foreign_interface) } /// Get details about all `Type::External` types. diff --git a/uniffi_bindgen/src/interface/object.rs b/uniffi_bindgen/src/interface/object.rs index 942032b3c6..b992316c81 100644 --- a/uniffi_bindgen/src/interface/object.rs +++ b/uniffi_bindgen/src/interface/object.rs @@ -92,7 +92,7 @@ pub struct Object { // a regular method (albeit with a generated name) // XXX - this should really be a HashSet, but not enough transient types support hash to make it worthwhile now. pub(super) uniffi_traits: Vec, - // We don't include the FfiFunc in the hash calculation, because: + // We don't include the FfiFuncs in the hash calculation, because: // - it is entirely determined by the other fields, // so excluding it is safe. // - its `name` property includes a checksum derived from the very @@ -100,6 +100,8 @@ pub struct Object { // avoids a weird circular dependency in the calculation. #[checksum_ignore] pub(super) ffi_func_free: FfiFunction, + #[checksum_ignore] + pub(super) ffi_init_trait_callback: Option, } impl Object { @@ -118,6 +120,10 @@ impl Object { &self.imp } + pub fn is_trait_interface(&self) -> bool { + matches!(self.imp, ObjectImpl::Trait) + } + pub fn constructors(&self) -> Vec<&Constructor> { self.constructors.iter().collect() } @@ -155,8 +161,26 @@ impl Object { &self.ffi_func_free } + /// For trait interfaces, the initialization function + /// + /// The foreign side passes a trait VTable pointer to this function to handle + /// foreign-implemented trait objects. + pub fn ffi_init_trait_callback(&self) -> &FfiFunction { + if self.is_trait_interface() { + self.ffi_init_trait_callback.as_ref().unwrap_or_else(|| { + panic!( + "No ffi_init_trait_callback set for trait interface {}", + self.name + ) + }) + } else { + panic!("{} is not a trait interface", self.name); + } + } + pub fn iter_ffi_function_definitions(&self) -> impl Iterator { iter::once(&self.ffi_func_free) + .chain(self.ffi_init_trait_callback.iter()) .chain(self.constructors.iter().map(|f| &f.ffi_func)) .chain(self.methods.iter().map(|f| &f.ffi_func)) .chain( @@ -175,11 +199,19 @@ impl Object { pub fn derive_ffi_funcs(&mut self) -> Result<()> { assert!(!self.ffi_func_free.name().is_empty()); self.ffi_func_free.arguments = vec![FfiArgument { - name: "ptr".to_string(), - type_: FfiType::RustArcPtr(self.name.to_string()), + name: "ptr".to_owned(), + type_: FfiType::RustArcPtr(self.name.to_owned()), }]; self.ffi_func_free.return_type = None; self.ffi_func_free.is_object_free_function = true; + if let Some(ffi_init_trait_callback) = &mut self.ffi_init_trait_callback { + ffi_init_trait_callback.arguments = vec![FfiArgument { + name: "vtable".to_owned(), + type_: FfiType::VTable(self.name.to_owned()), + }]; + ffi_init_trait_callback.return_type = None; + ffi_init_trait_callback.has_rust_call_status_arg = false; + } for cons in self.constructors.iter_mut() { cons.derive_ffi_func(); @@ -219,6 +251,12 @@ impl AsType for Object { impl From for Object { fn from(meta: uniffi_meta::ObjectMetadata) -> Self { let ffi_free_name = meta.free_ffi_symbol_name(); + let ffi_init_trait_callback = + meta.init_trait_callback_ffi_symbol_name() + .map(|name| FfiFunction { + name, + ..Default::default() + }); Object { module_path: meta.module_path, name: meta.name, @@ -230,6 +268,7 @@ impl From for Object { name: ffi_free_name, ..Default::default() }, + ffi_init_trait_callback, } } } diff --git a/uniffi_core/src/ffi/foreigncallbacks.rs b/uniffi_core/src/ffi/foreigncallbacks.rs index ac2463cd8e..d0da417186 100644 --- a/uniffi_core/src/ffi/foreigncallbacks.rs +++ b/uniffi_core/src/ffi/foreigncallbacks.rs @@ -8,7 +8,10 @@ //! code loads the exported library. For each callback type, we also define a "cell" type for //! storing the callback. -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::{ + ptr::{null_mut, NonNull}, + sync::atomic::{AtomicPtr, AtomicUsize, Ordering}, +}; use crate::{ForeignExecutorHandle, RustBuffer, RustTaskCallback}; @@ -101,3 +104,28 @@ macro_rules! impl_foreign_callback_cell { impl_foreign_callback_cell!(ForeignCallback, ForeignCallbackCell); impl_foreign_callback_cell!(ForeignExecutorCallback, ForeignExecutorCallbackCell); + +// Cell type that stores any NonNull +#[doc(hidden)] +pub struct UniffiForeignPointerCell(AtomicPtr); + +impl UniffiForeignPointerCell { + pub const fn new() -> Self { + Self(AtomicPtr::new(null_mut())) + } + + pub fn set(&self, callback: NonNull) { + self.0.store(callback.as_ptr(), Ordering::Relaxed); + } + + pub fn get(&self) -> &mut T { + unsafe { + NonNull::new(self.0.load(Ordering::Relaxed)) + .expect("Foreign pointer not set. This is likely a uniffi bug.") + .as_mut() + } + } +} + +unsafe impl Send for UniffiForeignPointerCell {} +unsafe impl Sync for UniffiForeignPointerCell {} diff --git a/uniffi_macros/src/export.rs b/uniffi_macros/src/export.rs index 2c2af091ef..3d50307345 100644 --- a/uniffi_macros/src/export.rs +++ b/uniffi_macros/src/export.rs @@ -10,8 +10,8 @@ mod attributes; mod callback_interface; mod item; mod scaffolding; -mod utrait; mod trait_interface; +mod utrait; use self::{ item::{ExportItem, ImplItem}, @@ -98,7 +98,7 @@ pub(crate) fn expand_export( .unwrap_or_else(|e| vec![e.into_compile_error()]); let init_ident = Ident::new( - &uniffi_meta::init_callback_fn_symbol_name(&mod_path, &trait_name), + &uniffi_meta::init_trait_callback_fn_symbol_name(&mod_path, &trait_name), Span::call_site(), ); diff --git a/uniffi_macros/src/export/scaffolding.rs b/uniffi_macros/src/export/scaffolding.rs index f120ccc880..db974c155a 100644 --- a/uniffi_macros/src/export/scaffolding.rs +++ b/uniffi_macros/src/export/scaffolding.rs @@ -125,24 +125,39 @@ impl ScaffoldingBits { udl_mode: bool, ) -> Self { let ident = &sig.ident; - let ffi_converter = if is_trait { + let lift_impl = if is_trait { quote! { - <::std::sync::Arc as ::uniffi::FfiConverter> + <::std::sync::Arc as ::uniffi::Lift> } } else { quote! { - <::std::sync::Arc<#self_ident> as ::uniffi::FfiConverter> + <::std::sync::Arc<#self_ident> as ::uniffi::Lift> } }; - let params: Vec<_> = iter::once(quote! { uniffi_self_lowered: #ffi_converter::FfiType }) + let params: Vec<_> = iter::once(quote! { uniffi_self_lowered: #lift_impl::FfiType }) .chain(sig.scaffolding_params()) .collect(); + let try_lift_self = if is_trait { + // For trait interfaces we need to special case this. Trait interfaces normally lift + // foreign trait impl pointers. However, for a method call, we want to lift a Rust + // pointer. + quote! { + { + let foreign_arc = ::std::boxed::Box::leak(unsafe { Box::from_raw(uniffi_self_lowered as *mut ::std::sync::Arc) }); + Ok(::std::sync::Arc::clone(foreign_arc)) + } + } + } else { + quote! { #lift_impl::try_lift(uniffi_self_lowered) } + }; + let lift_closure = sig.lift_closure(Some(quote! { - match #ffi_converter::try_lift(uniffi_self_lowered) { + match #try_lift_self { Ok(v) => v, Err(e) => return Err(("self", e)) } })); + let call_params = sig.rust_call_params(true); let rust_fn_call = quote! { uniffi_args.0.#ident(#call_params) }; // UDL mode adds an extra conversion (#1749) @@ -216,7 +231,7 @@ pub(super) fn gen_ffi_function( let ffi_ident = sig.scaffolding_fn_ident()?; let name = &sig.name; - let return_impl = &sig.return_impl(); + let return_impl = &sig.lower_return_impl(); Ok(if !sig.is_async { quote! { diff --git a/uniffi_macros/src/export/trait_interface.rs b/uniffi_macros/src/export/trait_interface.rs index def8a81ef8..c071a289e0 100644 --- a/uniffi_macros/src/export/trait_interface.rs +++ b/uniffi_macros/src/export/trait_interface.rs @@ -3,14 +3,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use proc_macro2::{Ident, Span, TokenStream}; -use quote::{quote, quote_spanned}; +use quote::{format_ident, quote, quote_spanned}; use crate::{ export::{attributes::ExportAttributeArguments, gen_method_scaffolding, item::ImplItem}, + fnsig::{FnKind, FnSignature, ReceiverArg}, object::interface_meta_static_var, util::{ident_to_string, tagged_impl_header}, }; -use uniffi_meta::free_fn_symbol_name; pub(super) fn gen_trait_scaffolding( mod_path: &str, @@ -24,7 +24,11 @@ pub(super) fn gen_trait_scaffolding( } let name = ident_to_string(&self_ident); - let free_fn_ident = Ident::new(&free_fn_symbol_name(mod_path, &name), Span::call_site()); + let foreign_impl_struct = format_ident!("UniFfiTraitForeignImpl{name}"); + let free_fn_ident = Ident::new( + &uniffi_meta::free_fn_symbol_name(mod_path, &name), + Span::call_site(), + ); let free_tokens = quote! { #[doc(hidden)] @@ -41,6 +45,7 @@ pub(super) fn gen_trait_scaffolding( } }; + let foreign_impl = foreign_impl(mod_path, &self_ident, &name, &foreign_impl_struct, &items)?; let impl_tokens: TokenStream = items .into_iter() .map(|item| match item { @@ -61,17 +66,161 @@ pub(super) fn gen_trait_scaffolding( interface_meta_static_var(&self_ident, true, mod_path) .unwrap_or_else(syn::Error::into_compile_error) }); - let ffi_converter_tokens = ffi_converter(mod_path, &self_ident, false); + let ffi_converter_tokens = ffi_converter(mod_path, &self_ident, &foreign_impl_struct, false); Ok(quote_spanned! { self_ident.span() => #meta_static_var #free_tokens + #foreign_impl #ffi_converter_tokens #impl_tokens }) } -pub(crate) fn ffi_converter(mod_path: &str, trait_ident: &Ident, udl_mode: bool) -> TokenStream { +/// Generate a Foreign implementation for the trait +/// +/// This generates: +/// * A `repr(C)` VTable struct where each field is the FFI function for the trait method. +/// * A FFI function for foreign code to set their VTable for the interface +/// * An implementation of the trait using that VTable +pub(super) fn foreign_impl( + mod_path: &str, + trait_ident: &Ident, + name: &str, + foreign_impl_struct: &Ident, + items: &[ImplItem], +) -> syn::Result { + let vtable_type = format_ident!("UniFfiTraitVtable{name}"); + let vtable_cell = format_ident!("UNIFFI_TRAIT_CELL_{}", name.to_uppercase()); + let init_callback = Ident::new( + &uniffi_meta::init_trait_callback_fn_symbol_name(mod_path, name), + Span::call_site(), + ); + + let methods = items + .into_iter() + .map(|item| match item { + ImplItem::Constructor(sig) => Err(syn::Error::new( + sig.span, + "Constructors not allowed in trait interfaces", + )), + ImplItem::Method(sig) => Ok(sig), + }) + .collect::>>()?; + + let vtable_fields = methods.iter() + .map(|sig| { + let ident = &sig.ident; + let params = sig.scaffolding_params(); + let lower_return = sig.lower_return_impl(); + quote! { + #ident: extern "C" fn(handle: *const ::std::os::raw::c_void, #(#params,)* &mut ::uniffi::RustCallStatus) -> #lower_return::ReturnType, + } + }); + + let trait_impl_methods = methods + .iter() + .map(|sig| gen_method_impl(sig, &vtable_cell)) + .collect::>>()?; + + Ok(quote! { + struct #vtable_type { + #(#vtable_fields)* + uniffi_free: extern "C" fn(handle: *const ::std::os::raw::c_void), + } + + static #vtable_cell: ::uniffi::UniffiForeignPointerCell::<#vtable_type> = ::uniffi::UniffiForeignPointerCell::<#vtable_type>::new(); + + #[no_mangle] + extern "C" fn #init_callback(vtable: ::std::ptr::NonNull<#vtable_type>) { + #vtable_cell.set(vtable); + } + + #[derive(Debug)] + struct #foreign_impl_struct { + handle: *const ::std::os::raw::c_void, + } + + // Implement Send + Sync to declare that we handle the pointer correctly and you can safely + // share this type between threads. + unsafe impl Send for #foreign_impl_struct { } + unsafe impl Sync for #foreign_impl_struct { } + + impl #trait_ident for #foreign_impl_struct { + #(#trait_impl_methods)* + } + + impl ::std::ops::Drop for #foreign_impl_struct { + fn drop(&mut self) { + let vtable = #vtable_cell.get(); + (vtable.uniffi_free)(self.handle); + } + } + }) +} + +/// Generate a single method for for [foreign_impl]. This implements a trait method by invoking a +/// foreign-supplied callback. +fn gen_method_impl(sig: &FnSignature, vtable_cell: &Ident) -> syn::Result { + let FnSignature { + ident, + return_ty, + kind, + receiver, + name, + span, + .. + } = sig; + + if !matches!(kind, FnKind::TraitMethod { .. }) { + return Err(syn::Error::new( + *span, + format!( + "Internal UniFFI error: Unexpected function kind for callback interface {name}: {kind:?}", + ), + )); + } + + let self_param = match receiver { + Some(ReceiverArg::Ref) => quote! { &self }, + Some(ReceiverArg::Arc) => quote! { self: Arc }, + None => { + return Err(syn::Error::new( + *span, + "callback interface methods must take &self as their first argument", + )); + } + }; + + let params = sig.params(); + let lower_exprs = sig.args.iter().map(|a| { + let lower_impl = a.lower_impl(); + let ident = &a.ident; + quote! { #lower_impl::lower(#ident) } + }); + + let lift_return = sig.lift_return_impl(); + + Ok(quote! { + fn #ident(#self_param, #(#params),*) -> #return_ty { + let vtable = #vtable_cell.get(); + let mut uniffi_call_status = ::uniffi::RustCallStatus::new(); + println!("Calling {}", #name); + let return_value = (vtable.#ident)(self.handle, #(#lower_exprs,)* &mut uniffi_call_status); + println!("return from {}: {return_value:?} ({:?})", #name, uniffi_call_status.code); + let lifted = #lift_return::lift_foreign_return(return_value, uniffi_call_status); + println!("lifted: {:?}", lifted); + lifted + } + }) +} + +pub(crate) fn ffi_converter( + mod_path: &str, + trait_ident: &Ident, + foreign_impl_struct: &Ident, + udl_mode: bool, +) -> TokenStream { let impl_spec = tagged_impl_header("FfiConverterArc", "e! { dyn #trait_ident }, udl_mode); let lift_ref_impl_spec = tagged_impl_header("LiftRef", "e! { dyn #trait_ident }, udl_mode); let name = ident_to_string(trait_ident); @@ -87,15 +236,10 @@ pub(crate) fn ffi_converter(mod_path: &str, trait_ident: &Ident, udl_mode: bool) type FfiType = *const ::std::os::raw::c_void; fn lower(obj: ::std::sync::Arc) -> Self::FfiType { + println!("lower: {obj:p}"); ::std::boxed::Box::into_raw(::std::boxed::Box::new(obj)) as *const ::std::os::raw::c_void } - fn try_lift(v: Self::FfiType) -> ::uniffi::Result<::std::sync::Arc> { - let foreign_arc = ::std::boxed::Box::leak(unsafe { Box::from_raw(v as *mut ::std::sync::Arc) }); - // Take a clone for our own use. - Ok(::std::sync::Arc::clone(foreign_arc)) - } - fn write(obj: ::std::sync::Arc, buf: &mut Vec) { ::uniffi::deps::static_assertions::const_assert!(::std::mem::size_of::<*const ::std::ffi::c_void>() <= 8); ::uniffi::deps::bytes::BufMut::put_u64( @@ -104,6 +248,11 @@ pub(crate) fn ffi_converter(mod_path: &str, trait_ident: &Ident, udl_mode: bool) ); } + fn try_lift(v: Self::FfiType) -> ::uniffi::Result<::std::sync::Arc> { + println!("try_lift: {v:p}"); + Ok(Arc::new(#foreign_impl_struct { handle: v })) + } + fn try_read(buf: &mut &[u8]) -> ::uniffi::Result<::std::sync::Arc> { ::uniffi::deps::static_assertions::const_assert!(::std::mem::size_of::<*const ::std::ffi::c_void>() <= 8); ::uniffi::check_remaining(buf, 8)?; @@ -122,4 +271,3 @@ pub(crate) fn ffi_converter(mod_path: &str, trait_ident: &Ident, udl_mode: bool) } } } - diff --git a/uniffi_macros/src/fnsig.rs b/uniffi_macros/src/fnsig.rs index 69b6529d1e..47640562f2 100644 --- a/uniffi_macros/src/fnsig.rs +++ b/uniffi_macros/src/fnsig.rs @@ -100,13 +100,20 @@ impl FnSignature { }) } - pub fn return_impl(&self) -> TokenStream { + pub fn lower_return_impl(&self) -> TokenStream { let return_ty = &self.return_ty; quote! { <#return_ty as ::uniffi::LowerReturn> } } + pub fn lift_return_impl(&self) -> TokenStream { + let return_ty = &self.return_ty; + quote! { + <#return_ty as ::uniffi::LiftReturn> + } + } + /// Generate a closure that tries to lift all arguments into a tuple. /// /// The closure moves all scaffolding arguments into itself and returns: diff --git a/uniffi_meta/src/ffi_names.rs b/uniffi_meta/src/ffi_names.rs index 44a5bc3e63..7b5b149110 100644 --- a/uniffi_meta/src/ffi_names.rs +++ b/uniffi_meta/src/ffi_names.rs @@ -39,10 +39,10 @@ pub fn free_fn_symbol_name(namespace: &str, object_name: &str) -> String { format!("uniffi_{namespace}_fn_free_{object_name}") } -/// FFI symbol name for the `init_callback` function for a callback interface -pub fn init_callback_fn_symbol_name(namespace: &str, callback_interface_name: &str) -> String { - let callback_interface_name = callback_interface_name.to_ascii_lowercase(); - format!("uniffi_{namespace}_fn_init_callback_{callback_interface_name}") +/// FFI symbol name for the `init_callback` function for a trait/callback interface +pub fn init_trait_callback_fn_symbol_name(namespace: &str, interface_name: &str) -> String { + let interface_name = interface_name.to_ascii_lowercase(); + format!("uniffi_{namespace}_fn_init_trait_callback_{interface_name}") } /// FFI checksum symbol name for a top-level function diff --git a/uniffi_meta/src/lib.rs b/uniffi_meta/src/lib.rs index e486d84d89..7c6c295638 100644 --- a/uniffi_meta/src/lib.rs +++ b/uniffi_meta/src/lib.rs @@ -325,6 +325,19 @@ impl ObjectMetadata { pub fn free_ffi_symbol_name(&self) -> String { free_fn_symbol_name(&self.module_path, &self.name) } + + /// FFI symbol name for the `init_trait_callback` function for this object, if there is one. + /// + /// This function is initialize the foreign side of a trait interface + pub fn init_trait_callback_ffi_symbol_name(&self) -> Option { + match &self.imp { + types::ObjectImpl::Trait => Some(init_trait_callback_fn_symbol_name( + &self.module_path, + &self.name, + )), + _ => None, + } + } } /// The list of traits we support generating helper methods for.