diff --git a/Cargo.lock b/Cargo.lock index bc238a0..54d6a76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -702,9 +702,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.15.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "oneshot" @@ -1244,6 +1244,7 @@ dependencies = [ "uniffi-fixture-callbacks", "uniffi-fixture-coverall", "uniffi-fixture-docstring", + "uniffi-fixture-futures", "uniffi-fixture-time", "uniffi-fixture-trait-methods", ] @@ -1371,6 +1372,16 @@ dependencies = [ "uniffi_testing", ] +[[package]] +name = "uniffi-fixture-futures" +version = "0.21.0" +dependencies = [ + "once_cell", + "thiserror", + "tokio", + "uniffi", +] + [[package]] name = "uniffi-fixture-time" version = "0.22.0" diff --git a/bindgen/src/gen_cs/mod.rs b/bindgen/src/gen_cs/mod.rs index 72b0530..24fc155 100644 --- a/bindgen/src/gen_cs/mod.rs +++ b/bindgen/src/gen_cs/mod.rs @@ -338,7 +338,7 @@ impl CsCodeOracle { FfiType::UInt8 => "byte".to_string(), FfiType::Float32 => "float".to_string(), FfiType::Float64 => "double".to_string(), - FfiType::RustArcPtr(name) => format!("{}SafeHandle", self.class_name(name).as_str()), + FfiType::RustArcPtr(_) => "IntPtr".to_string(), FfiType::RustBuffer(_) => "RustBuffer".to_string(), FfiType::ForeignBytes => "ForeignBytes".to_string(), FfiType::ForeignCallback => "ForeignCallback".to_string(), diff --git a/bindgen/templates/Async.cs b/bindgen/templates/Async.cs new file mode 100644 index 0000000..5020c56 --- /dev/null +++ b/bindgen/templates/Async.cs @@ -0,0 +1,95 @@ +{{ self.add_import("System.Threading.Tasks")}} + +[UnmanagedFunctionPointer(CallingConvention.Cdecl)] +delegate void UniFfiFutureCallback(IntPtr continuationHandle, byte pollResult); + +internal static class _UniFFIAsync { + internal const byte UNIFFI_RUST_FUTURE_POLL_READY = 0; + // internal const byte UNIFFI_RUST_FUTURE_POLL_MAYBE_READY = 1; + + static _UniFFIAsync() { + UniffiRustFutureContinuationCallback.Register(); + } + + // FFI type for Rust future continuations + internal class UniffiRustFutureContinuationCallback + { + public static UniFfiFutureCallback callback = Callback; + + public static void Callback(IntPtr continuationHandle, byte pollResult) + { + GCHandle handle = GCHandle.FromIntPtr(continuationHandle); + if (handle.Target is TaskCompletionSource tcs) + { + tcs.SetResult(pollResult); + } + else + { + throw new InternalException("Unable to cast unmanaged IntPtr to TaskCompletionSource"); + } + } + + public static void Register() + { + IntPtr fn = Marshal.GetFunctionPointerForDelegate(callback); + _UniFFILib.{{ ci.ffi_rust_future_continuation_callback_set().name() }}(fn); + } + } + + public delegate F CompleteFuncDelegate(IntPtr ptr, ref RustCallStatus status); + + public delegate void CompleteActionDelegate(IntPtr ptr, ref RustCallStatus status); + + private static async Task PollFuture(IntPtr rustFuture, Action pollFunc) + { + byte pollResult; + do + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var handle = GCHandle.Alloc(tcs); + pollFunc(rustFuture, GCHandle.ToIntPtr(handle)); + pollResult = await tcs.Task; + handle.Free(); + } + while(pollResult != UNIFFI_RUST_FUTURE_POLL_READY); + } + + public static async Task UniffiRustCallAsync( + IntPtr rustFuture, + Action pollFunc, + CompleteFuncDelegate completeFunc, + Action freeFunc, + Func liftFunc, + CallStatusErrorHandler errorHandler + ) where E : UniffiException + { + try { + await PollFuture(rustFuture, pollFunc); + var result = _UniffiHelpers.RustCallWithError(errorHandler, (ref RustCallStatus status) => completeFunc(rustFuture, ref status)); + return liftFunc(result); + } + finally + { + freeFunc(rustFuture); + } + } + + public static async Task UniffiRustCallAsync( + IntPtr rustFuture, + Action pollFunc, + CompleteActionDelegate completeFunc, + Action freeFunc, + CallStatusErrorHandler errorHandler + ) where E : UniffiException + { + try { + await PollFuture(rustFuture, pollFunc); + _UniffiHelpers.RustCallWithError(errorHandler, (ref RustCallStatus status) => completeFunc(rustFuture, ref status)); + + } + finally + { + freeFunc(rustFuture); + } + } +} \ No newline at end of file diff --git a/bindgen/templates/Helpers.cs b/bindgen/templates/Helpers.cs index 1caf028..cddc596 100644 --- a/bindgen/templates/Helpers.cs +++ b/bindgen/templates/Helpers.cs @@ -129,3 +129,40 @@ public static void RustCall(RustCallAction callback) { }); } } + +static class FFIObjectUtil { + public static void DisposeAll(params Object?[] list) { + foreach (var obj in list) { + Dispose(obj); + } + } + + // Dispose is implemented by recursive type inspection at runtime. This is because + // generating correct Dispose calls for recursive complex types, e.g. List> + // is quite cumbersome. + private static void Dispose(dynamic? obj) { + if (obj == null) { + return; + } + + if (obj is IDisposable disposable) { + disposable.Dispose(); + return; + } + + var type = obj.GetType(); + if (type != null) { + if (type.IsGenericType) { + if (type.GetGenericTypeDefinition().IsAssignableFrom(typeof(List<>))) { + foreach (var value in obj) { + Dispose(value); + } + } else if (type.GetGenericTypeDefinition().IsAssignableFrom(typeof(Dictionary<,>))) { + foreach (var value in obj.Values) { + Dispose(value); + } + } + } + } + } +} diff --git a/bindgen/templates/ObjectRuntime.cs b/bindgen/templates/ObjectRuntime.cs index cb081bf..c8fe6aa 100644 --- a/bindgen/templates/ObjectRuntime.cs +++ b/bindgen/templates/ObjectRuntime.cs @@ -2,87 +2,83 @@ * 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/. */#} -// `SafeHandle` implements the semantics outlined below, i.e. its thread safe, and the dispose -// method will only be called once, once all outstanding native calls have completed. -// https://github.com/mozilla/uniffi-rs/blob/0dc031132d9493ca812c3af6e7dd60ad2ea95bf0/uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt#L31 -// https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.criticalhandle +{{ config.access_modifier() }} abstract class FFIObject: IDisposable { + protected IntPtr pointer; -{{ config.access_modifier() }} abstract class FFIObject: IDisposable where THandle : FFISafeHandle { - private THandle handle; + private int _wasDestroyed = 0; + private long _callCounter = 1; - public FFIObject(THandle handle) { - this.handle = handle; + protected FFIObject(IntPtr pointer) { + this.pointer = pointer; } - public THandle GetHandle() { - return handle; - } + protected abstract void FreeRustArcPtr(); - public void Dispose() { - handle.Dispose(); + public void Destroy() + { + // Only allow a single call to this method. + if (Interlocked.CompareExchange(ref _wasDestroyed, 1, 0) == 0) + { + // This decrement always matches the initial count of 1 given at creation time. + if (Interlocked.Decrement(ref _callCounter) == 0) + { + FreeRustArcPtr(); + } + } } -} -{{ config.access_modifier() }} abstract class FFISafeHandle: SafeHandle { - public FFISafeHandle(): base(new IntPtr(0), true) { + public void Dispose() + { + Destroy(); + GC.SuppressFinalize(this); // Suppress finalization to avoid unnecessary GC overhead. } - public FFISafeHandle(IntPtr pointer): this() { - this.SetHandle(pointer); + ~FFIObject() + { + Destroy(); } - public override bool IsInvalid { - get { - return handle.ToInt64() == 0; - } - } + private void IncrementCallCounter() + { + // Check and increment the call counter, to keep the object alive. + // This needs a compare-and-set retry loop in case of concurrent updates. + long count; + do + { + count = Interlocked.Read(ref _callCounter); + if (count == 0L) throw new System.ObjectDisposedException(String.Format("'{0}' object has already been destroyed", this.GetType().Name)); + if (count == long.MaxValue) throw new System.OverflowException(String.Format("'{0}' call counter would overflow", this.GetType().Name)); - // TODO(CS) this completely breaks any guarantees offered by SafeHandle.. Extracting - // raw value from SafeHandle puts responsiblity on the consumer of this function to - // ensure that SafeHandle outlives the stream, and anyone who might have read the raw - // value from the stream and are holding onto it. Otherwise, the result might be a use - // after free, or free while method calls are still in flight. - // - // This is also relevant for Kotlin. - // - public IntPtr DangerousGetRawFfiValue() { - return handle; + } while (Interlocked.CompareExchange(ref _callCounter, count + 1, count) != count); } -} -static class FFIObjectUtil { - public static void DisposeAll(params Object?[] list) { - foreach (var obj in list) { - Dispose(obj); + private void DecrementCallCounter() + { + // This decrement always matches the increment we performed above. + if (Interlocked.Decrement(ref _callCounter) == 0) { + FreeRustArcPtr(); } } - // Dispose is implemented by recursive type inspection at runtime. This is because - // generating correct Dispose calls for recursive complex types, e.g. List> - // is quite cumbersome. - private static void Dispose(dynamic? obj) { - if (obj == null) { - return; + internal void CallWithPointer(Action action) + { + IncrementCallCounter(); + try { + action(this.pointer); } - - if (obj is IDisposable disposable) { - disposable.Dispose(); - return; + finally { + DecrementCallCounter(); } + } - var type = obj.GetType(); - if (type != null) { - if (type.IsGenericType) { - if (type.GetGenericTypeDefinition().IsAssignableFrom(typeof(List<>))) { - foreach (var value in obj) { - Dispose(value); - } - } else if (type.GetGenericTypeDefinition().IsAssignableFrom(typeof(Dictionary<,>))) { - foreach (var value in obj.Values) { - Dispose(value); - } - } - } + internal T CallWithPointer(Func func) + { + IncrementCallCounter(); + try { + return func(this.pointer); + } + finally { + DecrementCallCounter(); } } } diff --git a/bindgen/templates/ObjectTemplate.cs b/bindgen/templates/ObjectTemplate.cs index a75c2d7..1026daa 100644 --- a/bindgen/templates/ObjectTemplate.cs +++ b/bindgen/templates/ObjectTemplate.cs @@ -1,9 +1,9 @@ {#/* 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/. */#} +{{- self.add_import("System.Threading")}} {%- let obj = ci.get_object_definition(name).unwrap() %} -{%- let safe_handle_type = format!("{}SafeHandle", type_name) %} {%- if self.include_once_check("ObjectRuntime.cs") %}{% include "ObjectRuntime.cs" %}{% endif %} {%- call cs::docstring(obj, 0) %} @@ -15,29 +15,16 @@ {%- else -%} {%- endmatch -%} {%- endfor %} { - {% for meth in obj.methods() -%} + {%- for meth in obj.methods() %} {%- call cs::docstring(meth, 4) %} {%- call cs::method_throws_annotation(meth.throws_type()) %} - {% match meth.return_type() -%} {%- when Some with (return_type) -%} {{ return_type|type_name }} {%- when None %}void{%- endmatch %} {{ meth.name()|fn_name }}({% call cs::arg_list_decl(meth) %}); - {% endfor %} -} - -{{ config.access_modifier() }} class {{ safe_handle_type }}: FFISafeHandle { - public {{ safe_handle_type }}(): base() { - } - public {{ safe_handle_type }}(IntPtr pointer): base(pointer) { - } - override protected bool ReleaseHandle() { - _UniffiHelpers.RustCall((ref RustCallStatus status) => { - _UniFFILib.{{ obj.ffi_object_free().name() }}(this.handle, ref status); - }); - return true; - } + {% call cs::return_type(meth) %} {{ meth.name()|fn_name }}({% call cs::arg_list_decl(meth) %}); + {%- endfor %} } {%- call cs::docstring(obj, 0) %} -{{ config.access_modifier() }} class {{ type_name }}: FFIObject<{{ safe_handle_type }}>, I{{ type_name }} { - public {{ type_name }}({{ safe_handle_type }} pointer): base(pointer) {} +{{ config.access_modifier() }} class {{ type_name }}: FFIObject, I{{ type_name }} { + public {{ type_name }}(IntPtr pointer) : base(pointer) {} {%- match obj.primary_constructor() %} {%- when Some with (cons) %} @@ -47,34 +34,75 @@ override protected bool ReleaseHandle() { {%- when None %} {%- endmatch %} + protected override void FreeRustArcPtr() { + _UniffiHelpers.RustCall((ref RustCallStatus status) => { + _UniFFILib.{{ obj.ffi_object_free().name() }}(this.pointer, ref status); + }); + } + {% for meth in obj.methods() -%} {%- call cs::docstring(meth, 4) %} {%- call cs::method_throws_annotation(meth.throws_type()) %} - {%- match meth.return_type() -%} + {%- if meth.is_async() %} + public async {% call cs::return_type(meth) %} {{ meth.name()|fn_name }}({%- call cs::arg_list_decl(meth) -%}) { + {%- if meth.return_type().is_some() %} + return {% endif %}await _UniFFIAsync.UniffiRustCallAsync( + // Get rust future + CallWithPointer(thisPtr => { + return _UniFFILib.{{ meth.ffi_func().name() }}(thisPtr{%- if meth.arguments().len() > 0 %}, {% endif -%}{% call cs::lower_arg_list(meth) %}); + }), + // Poll + (IntPtr future, IntPtr continuation) => _UniFFILib.{{ meth.ffi_rust_future_poll(ci) }}(future, continuation), + // Complete + (IntPtr future, ref RustCallStatus status) => { + {%- if meth.return_type().is_some() %} + return {% endif %}_UniFFILib.{{ meth.ffi_rust_future_complete(ci) }}(future, ref status); + }, + // Free + (IntPtr future) => _UniFFILib.{{ meth.ffi_rust_future_free(ci) }}(future), + {%- match meth.return_type() %} + {%- when Some(return_type) %} + // Lift + (result) => {{ return_type|lift_fn }}(result), + {% else %} + {% endmatch -%} + // Error + {%- match meth.throws_type() %} + {%- when Some(e) %} + {{ e|as_error|ffi_converter_name }}.INSTANCE + {%- when None %} + NullCallStatusErrorHandler.INSTANCE + {% endmatch %} + ); + } + + {%- else %} + {%- match meth.return_type() -%} {%- when Some with (return_type) %} public {{ return_type|type_name }} {{ meth.name()|fn_name }}({% call cs::arg_list_decl(meth) %}) { - return {{ return_type|lift_fn }}({%- call cs::to_ffi_call_with_prefix("this.GetHandle()", meth) %}); + return CallWithPointer(thisPtr => {{ return_type|lift_fn }}({%- call cs::to_ffi_call_with_prefix("thisPtr", meth) %})); } {%- when None %} public void {{ meth.name()|fn_name }}({% call cs::arg_list_decl(meth) %}) { - {%- call cs::to_ffi_call_with_prefix("this.GetHandle()", meth) %}; + CallWithPointer(thisPtr => {%- call cs::to_ffi_call_with_prefix("thisPtr", meth) %}); } {% endmatch %} + {% endif %} {% endfor %} {%- for tm in obj.uniffi_traits() -%} {%- match tm %} {%- when UniffiTrait::Display { fmt } %} public override string ToString() { - return {{ Type::String.borrow()|lift_fn }}({%- call cs::to_ffi_call_with_prefix("this.GetHandle()", fmt) %}); + return CallWithPointer(thisPtr => {{ Type::String.borrow()|lift_fn }}({%- call cs::to_ffi_call_with_prefix("thisPtr", fmt) %})); } {%- when UniffiTrait::Eq { eq, ne } %} public bool Equals({{type_name}}? other) { if (other is null) return false; - return {{ Type::Boolean.borrow()|lift_fn }}({%- call cs::to_ffi_call_with_prefix("this.GetHandle()", eq) %}); + return CallWithPointer(thisPtr => {{ Type::Boolean.borrow()|lift_fn }}({%- call cs::to_ffi_call_with_prefix("thisPtr", eq) %})); } public override bool Equals(object? obj) { @@ -83,7 +111,7 @@ public override bool Equals(object? obj) } {%- when UniffiTrait::Hash { hash } %} public override int GetHashCode() { - return (int){{ Type::UInt64.borrow()|lift_fn }}({%- call cs::to_ffi_call_with_prefix("this.GetHandle()", hash) %}); + return (int)CallWithPointer(thisPtr => {{ Type::UInt64.borrow()|lift_fn }}({%- call cs::to_ffi_call_with_prefix("thisPtr", hash) %})); } {%- else %} {%- endmatch %} @@ -100,19 +128,19 @@ public override int GetHashCode() { {% endif %} } -class {{ obj|ffi_converter_name }}: FfiConverter<{{ type_name }}, {{ safe_handle_type }}> { +class {{ obj|ffi_converter_name }}: FfiConverter<{{ type_name }}, IntPtr> { public static {{ obj|ffi_converter_name }} INSTANCE = new {{ obj|ffi_converter_name }}(); - public override {{ safe_handle_type }} Lower({{ type_name }} value) { - return value.GetHandle(); + public override IntPtr Lower({{ type_name }} value) { + return value.CallWithPointer(thisPtr => thisPtr); } - public override {{ type_name }} Lift({{ safe_handle_type }} value) { + public override {{ type_name }} Lift(IntPtr value) { return new {{ type_name }}(value); } public override {{ type_name }} Read(BigEndianStream stream) { - return Lift(new {{ safe_handle_type }}(new IntPtr(stream.ReadLong()))); + return Lift(new IntPtr(stream.ReadLong())); } public override int AllocationSize({{ type_name }} value) { @@ -120,6 +148,6 @@ public override int AllocationSize({{ type_name }} value) { } public override void Write({{ type_name }} value, BigEndianStream stream) { - stream.WriteLong(Lower(value).DangerousGetRawFfiValue().ToInt64()); + stream.WriteLong(Lower(value).ToInt64()); } } diff --git a/bindgen/templates/TopLevelFunctionTemplate.cs b/bindgen/templates/TopLevelFunctionTemplate.cs index 1885692..5e9af8c 100644 --- a/bindgen/templates/TopLevelFunctionTemplate.cs +++ b/bindgen/templates/TopLevelFunctionTemplate.cs @@ -4,6 +4,38 @@ {%- call cs::docstring(func, 4) %} {%- call cs::method_throws_annotation(func.throws_type()) %} +{%- if func.is_async() %} + public static async {% call cs::return_type(func) %} {{ func.name()|fn_name }}({%- call cs::arg_list_decl(func) -%}) + { + {%- if func.return_type().is_some() %} + return {% endif %} await _UniFFIAsync.UniffiRustCallAsync( + // Get rust future + _UniFFILib.{{ func.ffi_func().name() }}({% call cs::lower_arg_list(func) %}), + // Poll + (IntPtr future, IntPtr continuation) => _UniFFILib.{{ func.ffi_rust_future_poll(ci) }}(future, continuation), + // Complete + (IntPtr future, ref RustCallStatus status) => { + {%- if func.return_type().is_some() %} + return {% endif %}_UniFFILib.{{ func.ffi_rust_future_complete(ci) }}(future, ref status); + }, + // Free + (IntPtr future) => _UniFFILib.{{ func.ffi_rust_future_free(ci) }}(future), + {%- match func.return_type() %} + {%- when Some(return_type) %} + // Lift + (result) => {{ return_type|lift_fn }}(result), + {% else %} + {% endmatch -%} + // Error + {%- match func.throws_type() %} + {%- when Some(e) %} + {{ e|as_error|ffi_converter_name }}.INSTANCE + {%- when None %} + NullCallStatusErrorHandler.INSTANCE + {% endmatch %} + ); + } +{%- else %} {%- match func.return_type() -%} {%- when Some with (return_type) %} public static {{ return_type|type_name }} {{ func.name()|fn_name }}({%- call cs::arg_list_decl(func) -%}) { @@ -14,3 +46,4 @@ {% call cs::to_ffi_call(func) %}; } {% endmatch %} +{% endif %} diff --git a/bindgen/templates/Types.cs b/bindgen/templates/Types.cs index b90ae90..49e278e 100644 --- a/bindgen/templates/Types.cs +++ b/bindgen/templates/Types.cs @@ -103,3 +103,7 @@ {%- endmatch %} {%- endfor %} + +{%- if ci.has_async_fns() %} +{% include "Async.cs" %} +{%- endif %} \ No newline at end of file diff --git a/bindgen/templates/macros.cs b/bindgen/templates/macros.cs index 42b6f47..8081427 100644 --- a/bindgen/templates/macros.cs +++ b/bindgen/templates/macros.cs @@ -5,7 +5,7 @@ {# // Template to call into rust. Used in several places. // Variable names in `arg_list_decl` should match up with arg lists -// passed to rust via `_arg_list_ffi_call` +// passed to rust via `lower_arg_list` #} {%- macro to_ffi_call(func) -%} @@ -15,7 +15,7 @@ {%- else %} _UniffiHelpers.RustCall( {%- endmatch %} (ref RustCallStatus _status) => - _UniFFILib.{{ func.ffi_func().name() }}({% call _arg_list_ffi_call(func) -%}{% if func.arguments().len() > 0 %},{% endif %} ref _status) + _UniFFILib.{{ func.ffi_func().name() }}({% call lower_arg_list(func) -%}{% if func.arguments().len() > 0 %},{% endif %} ref _status) ) {%- endmacro -%} @@ -27,11 +27,11 @@ _UniffiHelpers.RustCall( {%- endmatch %} (ref RustCallStatus _status) => _UniFFILib.{{ func.ffi_func().name() }}( - {{- prefix }}, {% call _arg_list_ffi_call(func) -%}{% if func.arguments().len() > 0 %},{% endif %} ref _status) + {{- prefix }}, {% call lower_arg_list(func) -%}{% if func.arguments().len() > 0 %},{% endif %} ref _status) ) {%- endmacro -%} -{%- macro _arg_list_ffi_call(func) %} +{%- macro lower_arg_list(func) %} {%- for arg in func.arguments() %} {{- arg|lower_fn }}({{ arg.name()|var_name }}) {%- if !loop.last %}, {% endif %} @@ -104,3 +104,21 @@ {%- else %} {%- endmatch %} {%- endmacro %} + +{%- macro return_type(func) -%} +{%- if func.is_async() -%} +{%- match func.return_type() -%} +{%- when Some(return_type) -%} +Task<{{ return_type|type_name }}> +{%- when None -%} +Task +{%- endmatch -%} +{%- else -%} +{%- match func.return_type() -%} +{%- when Some(return_type) -%} +{{ return_type|type_name }} +{%- when None -%} +void +{%- endmatch -%} +{%- endif -%} +{%- endmacro -%} \ No newline at end of file diff --git a/bindgen/templates/wrapper.cs b/bindgen/templates/wrapper.cs index d07457f..bf7c78f 100644 --- a/bindgen/templates/wrapper.cs +++ b/bindgen/templates/wrapper.cs @@ -57,6 +57,7 @@ namespace {{ config.namespace() }}; {%- when None %} {{ config.access_modifier() }} static class {{ ci.namespace().to_upper_camel_case() }}Methods { {%- endmatch %} + {%- for func in ci.function_definitions() %} {%- include "TopLevelFunctionTemplate.cs" %} {%- endfor %} diff --git a/dotnet-tests/UniffiCS.BindingTests/OptionalParameterTests.cs b/dotnet-tests/UniffiCS.BindingTests/OptionalParameterTests.cs index 25f0484..b72e7bf 100644 --- a/dotnet-tests/UniffiCS.BindingTests/OptionalParameterTests.cs +++ b/dotnet-tests/UniffiCS.BindingTests/OptionalParameterTests.cs @@ -5,7 +5,7 @@ using uniffi.uniffi_cs_optional_parameters; using static uniffi.uniffi_cs_optional_parameters.UniffiCsOptionalParametersMethods; -namespace UniffiCS.binding_tests; +namespace UniffiCS.BindingTests; public class OptionalParameterTests { diff --git a/dotnet-tests/UniffiCS.BindingTests/TestCoverall.cs b/dotnet-tests/UniffiCS.BindingTests/TestCoverall.cs index 9d0e96e..e5d8211 100644 --- a/dotnet-tests/UniffiCS.BindingTests/TestCoverall.cs +++ b/dotnet-tests/UniffiCS.BindingTests/TestCoverall.cs @@ -11,12 +11,12 @@ namespace UniffiCS.BindingTests; public class TestCoverall { [Fact] - public void FFIObjectSafeHandleDropsNativeReferenceOutsideOfUsingBlock() + public void ObjectDecrementsReference() { Assert.Equal(0UL, CoverallMethods.GetNumAlive()); var closure = () => { - var coveralls = new Coveralls("safe_handle_drops_native_reference"); + var coveralls = new Coveralls("FFIObject_drops_native_reference"); Assert.Equal(1UL, CoverallMethods.GetNumAlive()); }; closure(); diff --git a/dotnet-tests/UniffiCS.BindingTests/TestFutures.cs b/dotnet-tests/UniffiCS.BindingTests/TestFutures.cs new file mode 100644 index 0000000..64bdad9 --- /dev/null +++ b/dotnet-tests/UniffiCS.BindingTests/TestFutures.cs @@ -0,0 +1,162 @@ +/* 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/. */ + +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using System; +using uniffi.futures; + +namespace UniffiCS.BindingTests; + +public class TestFutures { + static async Task MeasureTimeMillis(Func callback) { + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + await callback(); + stopwatch.Stop(); + return stopwatch.ElapsedMilliseconds; + } + + static async Task ReturnsImmediately(Func callback) { + var time = await MeasureTimeMillis(callback); + AssertApproximateTime(0, 20, time); + } + + static async Task ReturnsIn(long expected, Func callback) { + await ReturnsIn(expected, 50, callback); + } + + static async Task ReturnsIn(long expected, long tolerance, Func callback) { + var time = await MeasureTimeMillis(callback); + AssertApproximateTime(expected, tolerance, time); + } + + static void AssertApproximateTime(long expected, long tolerance, long actual) { + long difference = Math.Abs(expected - actual); + Assert.True(difference <= tolerance, $"Expected: {expected}, Tolerance: {tolerance}, Actual: {actual}"); + } + + [Fact] + public async void TestAlwaysReady() { + await ReturnsImmediately(async () => { + Assert.True(await FuturesMethods.AlwaysReady()); + }); + } + + [Fact] + public async void TestVoid() { + await ReturnsImmediately(async () => { + await FuturesMethods.Void(); + }); + } + + [Fact] + public async void TestSleep() { + await ReturnsIn(200, async () => { + await FuturesMethods.Sleep(200); + }); + } + + [Fact] + public async void TestSequentialFutures() { + await ReturnsIn(300, async () => { + for (int i = 0; i < 10; i++) { + var result = await FuturesMethods.SayAfter(30, i.ToString()); + Assert.Equal($"Hello, {i}!", result); + } + }); + } + + [Fact] + public async void TestConcurrentFutures() { + await ReturnsIn(100, async () => { + List tasks = new List(); + for (int i = 0; i < 100; i++) { + var index = i; + Func task = async () => { + var result = await FuturesMethods.SayAfter(100, index.ToString()); + Assert.Equal($"Hello, {index}!", result); + }; + tasks.Add(task()); + } + await Task.WhenAll(tasks); + }); + } + + [Fact] + public async void TestAsyncMethods() { + using (var megaphone = FuturesMethods.NewMegaphone()) { + await ReturnsIn(200, async () => { + var result = await megaphone.SayAfter(200, "Alice"); + Assert.Equal("HELLO, ALICE!", result); + }); + } + } + + [Fact] + public async void TestAsyncReturningOptionalObject() { + var megaphone = await FuturesMethods.AsyncMaybeNewMegaphone(true); + Assert.NotNull(megaphone); + if (megaphone != null) { + megaphone.Dispose(); + } + + megaphone = await FuturesMethods.AsyncMaybeNewMegaphone(false); + Assert.Null(megaphone); + } + + [Fact] + public async void TestAsyncWithTokioRuntime() { + await ReturnsIn(200, async () => { + var result = await FuturesMethods.SayAfterWithTokio(200, "Alice"); + Assert.Equal("Hello, Alice (with Tokio)!", result); + }); + } + + [Fact] + public async void TestAsyncFallibleFunctions() { + await ReturnsImmediately(async () => { + await FuturesMethods.FallibleMe(false); + await Assert.ThrowsAsync(() => FuturesMethods.FallibleMe(true)); + using (var megaphone = FuturesMethods.NewMegaphone()) { + Assert.Equal(42, await megaphone.FallibleMe(false)); + await Assert.ThrowsAsync(() => megaphone.FallibleMe(true)); + } + }); + } + + [Fact] + public async void TestAsyncFallibleStruct() { + await ReturnsImmediately(async () => { + await FuturesMethods.FallibleStruct(false); + await Assert.ThrowsAsync(() => FuturesMethods.FallibleStruct(true)); + }); + } + + [Fact] + public async void TestRecord() { + for (int i = 0; i < 1000; i++) { + await ReturnsImmediately(async () => { + var record = await FuturesMethods.NewMyRecord("foo", 42U); + Assert.Equal("foo", record.a); + Assert.Equal(42U, record.b); + }); + } + } + + [Fact] + public async void TestBrokenSleep() { + await ReturnsIn(500, 100, async () => { + // calls the waker twice immediately + await FuturesMethods.BrokenSleep(100, 0); + // wait for possible failure + await Task.Delay(100); + // calls the waker a second time after 1s + await FuturesMethods.BrokenSleep(100, 100); + // wait for possible failure + await Task.Delay(200); + }); + } +} \ No newline at end of file diff --git a/fixtures/Cargo.toml b/fixtures/Cargo.toml index 036807a..5ad4a2b 100644 --- a/fixtures/Cargo.toml +++ b/fixtures/Cargo.toml @@ -28,5 +28,6 @@ uniffi-example-todolist = { path = "../3rd-party/uniffi-rs/examples/todolist" } uniffi-fixture-callbacks = { path = "../3rd-party/uniffi-rs/fixtures/callbacks" } uniffi-fixture-coverall = { path = "../3rd-party/uniffi-rs/fixtures/coverall" } uniffi-fixture-docstring = { path = "../3rd-party/uniffi-rs/fixtures/docstring" } +uniffi-fixture-futures = { path = "../3rd-party/uniffi-rs/fixtures/futures" } uniffi-fixture-time = { path = "../3rd-party/uniffi-rs/fixtures/uniffi-fixture-time" } uniffi-fixture-trait-methods = { path = "../3rd-party/uniffi-rs/fixtures/trait-methods" } diff --git a/fixtures/src/lib.rs b/fixtures/src/lib.rs index a80e725..d34d6e0 100644 --- a/fixtures/src/lib.rs +++ b/fixtures/src/lib.rs @@ -15,6 +15,7 @@ mod uniffi_fixtures { uniffi_coverall::uniffi_reexport_scaffolding!(); uniffi_fixture_callbacks::uniffi_reexport_scaffolding!(); uniffi_fixture_docstring::uniffi_reexport_scaffolding!(); + uniffi_futures::uniffi_reexport_scaffolding!(); uniffi_trait_methods::uniffi_reexport_scaffolding!(); global_methods_class_name::uniffi_reexport_scaffolding!();