summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLauro Moura <lauromoura@expertisesolutions.com.br>2018-04-20 18:17:26 -0300
committerLauro Moura <lauromoura@expertisesolutions.com.br>2018-05-17 16:56:25 -0300
commitfff0c86d99fa63bf7167830da27118d9f2366b4b (patch)
treeb14c316660b48ed16252f42982c5e7bb06f84ae1
parent95c8a7d28c2d2443389a3bcbffeae36966a7dc47 (diff)
downloadefl-fff0c86d99fa63bf7167830da27118d9f2366b4b.tar.gz
efl_mono: Initial support for Futures/Promises
Summary: Promise/Future cleanup: In the promises, we use a wrapper Eina_Promise_Cancel_Cb to invalidate the wrapper if it ever gets cancelled from outside. When invalidating from C#, we can do it directly. For the futures, likewise, in order to be able to invalidate the wrapper when the chain it belongs to gets resolved we then() an internal future with a callback to invalidate the wrapper we return to C#. The return of this intermediate then() is the future we actually return to the user. Also added ECANCELED to the list of default eina.Errors Depends on D6173 Reviewers: felipealmeida Reviewed By: felipealmeida Subscribers: cedric, zmike Tags: #efl Differential Revision: https://phab.enlightenment.org/D6174
-rw-r--r--src/Makefile_Efl_Mono.am2
-rw-r--r--src/bin/eolian_mono/eolian/mono/blacklist.hh4
-rw-r--r--src/bindings/mono/eina_mono/eina_error.cs1
-rw-r--r--src/bindings/mono/eina_mono/eina_promises.cs298
-rw-r--r--src/tests/efl_mono/Promises.cs235
5 files changed, 539 insertions, 1 deletions
diff --git a/src/Makefile_Efl_Mono.am b/src/Makefile_Efl_Mono.am
index 4afc577d4a..64b4a7cbf2 100644
--- a/src/Makefile_Efl_Mono.am
+++ b/src/Makefile_Efl_Mono.am
@@ -22,6 +22,7 @@ efl_eina_mono_files = \
bindings/mono/eina_mono/eina_stringshare.cs \
bindings/mono/eina_mono/eina_error.cs \
bindings/mono/eina_mono/eina_value.cs \
+ bindings/mono/eina_mono/eina_promises.cs \
bindings/mono/eina_mono/eina_strbuf.cs
efl_eldbus_mono_files = \
@@ -439,6 +440,7 @@ tests_efl_mono_efl_mono_SOURCES = \
tests/efl_mono/FunctionPointers.cs \
tests/efl_mono/FunctionPointerMarshalling.cs \
tests/efl_mono/Parts.cs \
+ tests/efl_mono/Promises.cs \
tests/efl_mono/Strbuf.cs \
tests/efl_mono/Strings.cs \
tests/efl_mono/Structs.cs \
diff --git a/src/bin/eolian_mono/eolian/mono/blacklist.hh b/src/bin/eolian_mono/eolian/mono/blacklist.hh
index 6fe4d584c4..1db4c6108a 100644
--- a/src/bin/eolian_mono/eolian/mono/blacklist.hh
+++ b/src/bin/eolian_mono/eolian/mono/blacklist.hh
@@ -60,7 +60,9 @@ inline bool is_struct_blacklisted(std::string const& full_name)
|| full_name == "Eina.Binbuf"
|| full_name == "Eina.Strbuf"
|| full_name == "Eina.Slice"
- || full_name == "Eina.Rw_Slice";
+ || full_name == "Eina.Rw_Slice"
+ || full_name == "Eina.Promise"
+ || full_name == "Eina.Future";
}
inline bool is_struct_blacklisted(attributes::struct_def const& struct_)
diff --git a/src/bindings/mono/eina_mono/eina_error.cs b/src/bindings/mono/eina_mono/eina_error.cs
index 58495aa904..4271df8cea 100644
--- a/src/bindings/mono/eina_mono/eina_error.cs
+++ b/src/bindings/mono/eina_mono/eina_error.cs
@@ -19,6 +19,7 @@ public struct Error : IComparable<Error>
public static Error NO_ERROR = new Error(0);
public static Error EPERM = new Error(1);
public static Error ENOENT = new Error(2);
+ public static Error ECANCELED = new Error(125);
public Error(int value) { code = value; }
static public implicit operator Error(int val)
diff --git a/src/bindings/mono/eina_mono/eina_promises.cs b/src/bindings/mono/eina_mono/eina_promises.cs
new file mode 100644
index 0000000000..46fe800869
--- /dev/null
+++ b/src/bindings/mono/eina_mono/eina_promises.cs
@@ -0,0 +1,298 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Collections.Generic;
+using System.Linq;
+
+
+using static eina.EinaNative.PromiseNativeMethods;
+
+namespace eina {
+
+namespace EinaNative {
+
+static internal class PromiseNativeMethods
+{
+ internal delegate void Promise_Cancel_Cb(IntPtr data, IntPtr dead);
+
+ [DllImport(efl.Libs.Ecore)]
+ internal static extern IntPtr efl_loop_promise_new(IntPtr obj, Promise_Cancel_Cb cancel_cb, IntPtr data);
+
+ [DllImport(efl.Libs.Eina)]
+ internal static extern IntPtr eina_promise_new(IntPtr scheduler, Promise_Cancel_Cb cancel_cb, IntPtr data);
+
+ [DllImport(efl.Libs.Eina)]
+ internal static extern void eina_promise_resolve(IntPtr scheduler, eina.Value_Native value);
+
+ [DllImport(efl.Libs.Eina)]
+ internal static extern void eina_promise_reject(IntPtr scheduler, eina.Error reason);
+
+ [DllImport(efl.Libs.Eina)]
+ internal static extern IntPtr eina_future_new(IntPtr promise);
+
+ [DllImport(efl.Libs.Eina)]
+ internal static extern void eina_future_cancel(IntPtr future);
+
+ [DllImport(efl.Libs.Ecore)]
+ internal static extern IntPtr efl_loop_future_scheduler_get(IntPtr obj);
+
+ [DllImport(efl.Libs.Eina)]
+ internal static extern IntPtr eina_future_then_from_desc(IntPtr prev, FutureDesc desc);
+
+ [DllImport(efl.Libs.Eina)]
+ internal static extern IntPtr eina_future_chain_array(IntPtr prev, FutureDesc[] desc);
+
+ internal delegate eina.Value_Native FutureCb(IntPtr data, eina.Value_Native value, IntPtr dead_future);
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct FutureDesc
+ {
+ internal FutureCb cb;
+ internal IntPtr data;
+ internal IntPtr storage; // Internal use by eina
+
+ public FutureDesc(FutureCb cb, IntPtr data, IntPtr storage)
+ {
+ this.cb = cb;
+ this.data = data;
+ this.storage = storage;
+ }
+ }
+}
+
+} // namespace EinaNative
+
+/// <summary>
+/// Promises act as placeholders for a value that may be available in the future.
+///
+/// With a Promise you can attach futures to it, which will be used to notify of the value being available.
+/// </summary>
+public class Promise
+{
+ internal IntPtr Handle;
+ private GCHandle CleanupHandle;
+
+ /// <summary>Delegate for functions that will be called upon a promise cancellation.</summary>
+ public delegate void CancelCb();
+
+ /// <summary>
+ /// Creates a new Promise with the given callback.
+ ///
+ /// Currently, creating a promise directly uses the Main Loop scheduler the source of notifications (i.e. the
+ /// future callbacks will be called mainly from a loop iteration).
+ /// </summary>
+ public Promise(CancelCb cancelCb=null)
+ {
+ efl.ILoop loop = efl.App.GetLoopMain();
+
+ // Should we be able to pass different schedulers?
+ IntPtr scheduler = efl_loop_future_scheduler_get(loop.raw_handle);
+
+ IntPtr cb_data = IntPtr.Zero;
+
+ // A safety clean callback to mark this wrapper as invalid
+ CancelCb safetyCb = () => {
+ Handle = IntPtr.Zero;
+ if (cancelCb != null)
+ cancelCb();
+ };
+
+ CleanupHandle = GCHandle.Alloc(safetyCb);
+ cb_data = GCHandle.ToIntPtr(CleanupHandle);
+
+ this.Handle = eina_promise_new(scheduler, NativeCancelCb, cb_data);
+ }
+
+ private static void NativeCancelCb(IntPtr data, IntPtr dead)
+ {
+ if (data == IntPtr.Zero)
+ return;
+
+ GCHandle handle = GCHandle.FromIntPtr(data);
+ CancelCb cb = handle.Target as CancelCb;
+ if (cb != null)
+ cb();
+ else
+ eina.Log.Info("Null promise CancelCb found");
+ handle.Free();
+ }
+
+ private void SanityChecks()
+ {
+ if (this.Handle == IntPtr.Zero)
+ throw new ObjectDisposedException(GetType().Name);
+ }
+
+ /// <summary>
+ /// Fulfills a promise with the given value.
+ ///
+ /// This will make all futures attached to it to be called with the given value as payload.
+ /// </summary>
+ public void Resolve(eina.Value value)
+ {
+ SanityChecks();
+ eina_promise_resolve(this.Handle, value);
+ this.Handle = IntPtr.Zero;
+ // Resolving a cb does *not* call its cancellation callback, so we have to release the
+ // lambda created in the constructor for cleanup.
+ CleanupHandle.Free();
+ }
+
+ /// <summary>
+ /// Rejects a promise.
+ ///
+ /// The future chain attached to this promise will be called with an eina.Value of type
+ /// eina.ValueType.Error and payload eina.Error.ECANCELED.
+ /// </summary>
+ public void Reject(eina.Error reason)
+ {
+ SanityChecks();
+ eina_promise_reject(this.Handle, reason);
+ this.Handle = IntPtr.Zero;
+ }
+}
+
+/// <summary>
+/// Futures are the structures holding the callbacks to be notified of a promise fullfillment
+/// or cancellation.
+/// </summary>
+public class Future
+{
+ /// <summary>
+ /// Callback attached to a future and to be called when resolving/rejecting a promise.
+ ///
+ /// The eina.Value as argument can come with an eina.Error.ECANCELED as payload if the
+ /// promise/future was rejected/cancelled.
+ ///
+ /// The return value usually is same as the argument, forwarded, but can be changed in
+ /// case were the chain act as a transforming pipeline.
+ /// </summary>
+ public delegate eina.Value ResolvedCb(eina.Value value);
+
+ private IntPtr Handle;
+
+ /// <summary>
+ /// Creates a Future from a native pointer.
+ /// </summary>
+ public Future(IntPtr handle)
+ {
+ Handle = ThenRaw(handle, (eina.Value value) => {
+ Handle = IntPtr.Zero;
+ return value;
+ });
+ }
+
+ /// <summary>
+ /// Creates a Future attached to the given Promise.
+ ///
+ /// Optionally a resolved callback may be provided. If so, it will be chained
+ /// before the returned future.
+ /// </summary>
+ public Future(Promise promise, ResolvedCb cb=null)
+ {
+ IntPtr intermediate = eina_future_new(promise.Handle);
+ Handle = ThenRaw(intermediate, (eina.Value value) => {
+ if (cb != null)
+ value = cb(value);
+ Handle = IntPtr.Zero;
+ return value;
+ });
+ }
+
+ private void SanityChecks()
+ {
+ if (this.Handle == IntPtr.Zero)
+ throw new ObjectDisposedException(GetType().Name);
+ }
+
+ /// <summary>
+ /// Cancels this future and the chain it belongs to, along with the promise linked against it.
+ ///
+ /// The callbacks will still be called with eina.Error.ECANCELED as payload. The promise cancellation
+ /// callback will also be called if present.
+ /// </summary>
+ public void Cancel()
+ {
+ SanityChecks();
+ eina_future_cancel(this.Handle);
+ }
+
+ /// <summary>
+ /// Creates a new future to be called after this one.
+ ///
+ /// Once the promise this future is attached to resolves, the callbacks on the chain
+ /// are called in the order they were chained.
+ ///
+ /// CAUTION: Calling Then() on a future that had it called before will replace the previous chain
+ /// from this point on.
+ /// </summary>
+ public Future Then(ResolvedCb cb)
+ {
+ SanityChecks();
+ return new Future(ThenRaw(Handle, cb));
+ }
+
+ // Helper function to attach a cb to a future without creating a new wrapper directly.
+ // It'll be used in the construtor, to attach the cleaning cb to an intermediate future.
+ private static IntPtr ThenRaw(IntPtr previous, ResolvedCb cb)
+ {
+ FutureDesc desc = new FutureDesc();
+ desc.cb = NativeResolvedCb;
+ GCHandle handle = GCHandle.Alloc(cb);
+ desc.data = GCHandle.ToIntPtr(handle);
+ return eina_future_then_from_desc(previous, desc);
+ }
+ private static eina.Value_Native NativeResolvedCb(IntPtr data, eina.Value_Native value, IntPtr dead_future)
+ {
+ GCHandle handle = GCHandle.FromIntPtr(data);
+ ResolvedCb cb = handle.Target as ResolvedCb;
+ if (cb != null)
+ value = cb(value);
+ else
+ eina.Log.Warning("Failed to get future callback.");
+ handle.Free();
+ return value;
+ }
+
+ /// <summary>
+ /// Helper method for chaining a group of callbacks in a single go.
+ ///
+ /// It is just syntatic sugar for sequential Then() calls, without creating intermediate
+ /// futures explicitly.
+ /// </summary>
+ public Future Chain(IEnumerable<ResolvedCb> cbs)
+ {
+ SanityChecks();
+ System.Collections.Generic.IList<ResolvedCb> cbsList = cbs.ToList();
+ FutureDesc[] descs = new FutureDesc[cbsList.Count() + 1]; // +1 due to the null-cb terminating descriptor.
+ int i = 0;
+ try
+ {
+ for (; i < cbsList.Count(); i++)
+ {
+ ResolvedCb cb = cbsList[i];
+ descs[i].cb = NativeResolvedCb;
+ GCHandle handle = GCHandle.Alloc(cb);
+ descs[i].data = GCHandle.ToIntPtr(handle);
+ }
+
+ descs[i].cb = null;
+ descs[i].data = IntPtr.Zero;
+ }
+ catch (Exception e)
+ {
+ for (int j = 0; j <= i; j++)
+ {
+ if (descs[i].data == IntPtr.Zero)
+ continue;
+
+ GCHandle handle = GCHandle.FromIntPtr(descs[i].data);
+ handle.Free();
+ }
+ eina.Log.Error($"Failed to create native future description for callbacks. Error: {e.ToString()}");
+ return null;
+ }
+ return new Future(eina_future_chain_array(Handle, descs));
+ }
+}
+
+} // namespace eina
diff --git a/src/tests/efl_mono/Promises.cs b/src/tests/efl_mono/Promises.cs
new file mode 100644
index 0000000000..5759f56800
--- /dev/null
+++ b/src/tests/efl_mono/Promises.cs
@@ -0,0 +1,235 @@
+using System;
+using System.Collections.Generic;
+
+namespace TestSuite
+{
+
+class TestPromises
+{
+ public static void test_simple_cancel()
+ {
+ bool cleanCalled = false;
+ eina.Promise promise = new eina.Promise(() => { cleanCalled = true; });
+ eina.Future future = new eina.Future(promise);
+ future.Cancel();
+ Test.Assert(cleanCalled, "Promise clean callback should have been called.");
+ Test.AssertRaises<ObjectDisposedException>(() => { promise.Resolve(null); });
+ Test.AssertRaises<ObjectDisposedException>(future.Cancel);
+ }
+
+ public static void test_simple_resolve()
+ {
+ bool callbackCalled = false;
+ eina.Value received_value = null;
+
+ efl.ILoop loop = efl.App.GetLoopMain();
+ eina.Promise promise = new eina.Promise();
+ eina.Future future = new eina.Future(promise);
+
+ future = future.Then((eina.Value value) => {
+ callbackCalled = true;
+ received_value = value;
+ return value;
+ } );
+
+ eina.Value reference_value = new eina.Value(eina.ValueType.Int32);
+ reference_value.Set(1984);
+ promise.Resolve(reference_value);
+
+ loop.Iterate();
+
+ Test.Assert(callbackCalled, "Future callback should have been called.");
+ Test.AssertEquals(received_value, reference_value);
+ }
+
+ public static void test_simple_reject()
+ {
+ bool callbackCalled = false;
+ eina.Error received_error = eina.Error.NO_ERROR;
+
+ efl.ILoop loop = efl.App.GetLoopMain();
+ eina.Promise promise = new eina.Promise();
+ eina.Future future = new eina.Future(promise);
+
+ future = future.Then((eina.Value value) => {
+ callbackCalled = true;
+ value.Get(out received_error);
+ return value;
+ });
+
+ promise.Reject(eina.Error.EPERM);
+
+ loop.Iterate();
+
+ Test.Assert(callbackCalled, "Future callback should have been called.");
+ Test.AssertEquals(received_error, eina.Error.EPERM);
+
+ Test.AssertRaises<ObjectDisposedException>(() => { promise.Resolve(null); });
+ Test.AssertRaises<ObjectDisposedException>(future.Cancel);
+ }
+
+ public static void test_simple_future_cancel()
+ {
+ bool callbackCalled = false;
+ bool promiseCallbackCalled = false;
+ eina.Error received_error = eina.Error.NO_ERROR;
+
+ eina.Promise promise = new eina.Promise(() => { promiseCallbackCalled = true; });
+ eina.Future future = new eina.Future(promise);
+
+ future = future.Then((eina.Value value) => {
+ callbackCalled = true;
+ value.Get(out received_error);
+ return value;
+ });
+
+ future.Cancel();
+
+ Test.Assert(promiseCallbackCalled, "Promise cancel callback should have been called.");
+ Test.Assert(callbackCalled, "Future callback should have been called.");
+ Test.AssertEquals(received_error, eina.Error.ECANCELED);
+ }
+
+
+ private delegate eina.Future.ResolvedCb FutureCbGenerator(int x);
+ public static void test_then_chaining()
+ {
+ bool[] callbacksCalled = {false, false, false, false};
+ eina.Value[] received_value = {null, null, null, null};
+
+ FutureCbGenerator genResolvedCb = (int i) => {
+ return (eina.Value value) => {
+ callbacksCalled[i] = true;
+ int x;
+ value.Get(out x);
+ value.Set(x + i);
+ received_value[i] = value;
+ return value;
+ };
+ };
+
+ efl.ILoop loop = efl.App.GetLoopMain();
+ eina.Promise promise = new eina.Promise();
+ eina.Future future = new eina.Future(promise);
+ for (int i = 0; i < 4; i++)
+ future = future.Then(genResolvedCb(i));
+
+ eina.Value reference_value = new eina.Value(eina.ValueType.Int32);
+ reference_value.Set(0);
+ promise.Resolve(reference_value);
+
+ loop.Iterate();
+
+ int current_value = 0;
+ for (int i = 0; i < 4; i++)
+ {
+ current_value += i;
+ Test.Assert(callbacksCalled[i], $"Future callback {i} should have been called.");
+ int received;
+ received_value[i].Get(out received);
+ Test.AssertEquals(received, current_value);
+ }
+
+ Test.AssertRaises<ObjectDisposedException>(() => { promise.Resolve(null); });
+ Test.AssertRaises<ObjectDisposedException>(future.Cancel);
+ }
+
+ public static void test_then_chain_array()
+ {
+ bool[] callbacksCalled = {false, false, false, false};
+ eina.Value[] received_value = {null, null, null, null};
+
+ FutureCbGenerator genResolvedCb = (int i) => {
+ return (eina.Value value) => {
+ callbacksCalled[i] = true;
+ int x;
+ value.Get(out x);
+ value.Set(x + i);
+ received_value[i] = value;
+ return value;
+ };
+ };
+
+ var cbs = new List<eina.Future.ResolvedCb>();
+ for (int i = 0; i < 4; i++)
+ cbs.Add(genResolvedCb(i));
+
+ efl.ILoop loop = efl.App.GetLoopMain();
+ eina.Promise promise = new eina.Promise();
+ eina.Future future = new eina.Future(promise);
+ future = future.Chain(cbs);
+
+ eina.Value reference_value = new eina.Value(eina.ValueType.Int32);
+ reference_value.Set(0);
+ promise.Resolve(reference_value);
+
+ loop.Iterate();
+
+ int current_value = 0;
+ for (int i = 0; i < 4; i++)
+ {
+ current_value += i;
+ Test.Assert(callbacksCalled[i], $"Future chained callback {i} should have been called.");
+ int received;
+ received_value[i].Get(out received);
+ Test.AssertEquals(received, current_value);
+ }
+
+ Test.AssertRaises<ObjectDisposedException>(() => { promise.Resolve(null); });
+ Test.AssertRaises<ObjectDisposedException>(future.Cancel);
+ }
+
+ public static void test_cancel_after_resolve()
+ {
+ bool callbackCalled = false;
+ eina.Error received_error = eina.Error.NO_ERROR;
+
+ efl.ILoop loop = efl.App.GetLoopMain();
+ eina.Promise promise = new eina.Promise();
+ eina.Future future = new eina.Future(promise);
+
+ future = future.Then((eina.Value value) => {
+ callbackCalled = true;
+ value.Get(out received_error);
+ return value;
+ });
+
+ promise.Reject(eina.Error.EPERM);
+ future.Cancel();
+
+ loop.Iterate();
+
+ Test.Assert(callbackCalled, "Future callback should have been called.");
+ Test.AssertEquals(received_error, eina.Error.ECANCELED);
+
+ Test.AssertRaises<ObjectDisposedException>(() => { promise.Resolve(null); });
+ Test.AssertRaises<ObjectDisposedException>(future.Cancel);
+ }
+
+ public static void test_constructor_with_callback()
+ {
+ bool callbackCalled = false;
+ eina.Value received_value = null;
+
+ efl.ILoop loop = efl.App.GetLoopMain();
+ eina.Promise promise = new eina.Promise();
+#pragma warning disable 0219
+ eina.Future future = new eina.Future(promise,(eina.Value value) => {
+ callbackCalled = true;
+ received_value = value;
+ return value;
+ } );
+#pragma warning restore 0219
+
+ eina.Value reference_value = new eina.Value(eina.ValueType.Int32);
+ reference_value.Set(1984);
+ promise.Resolve(reference_value);
+
+ loop.Iterate();
+
+ Test.Assert(callbackCalled, "Future callback should have been called.");
+ Test.AssertEquals(received_value, reference_value);
+ }
+}
+
+}