summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGustavo Sverzut Barbieri <barbieri@profusion.mobi>2017-08-27 00:42:22 -0300
committerGustavo Sverzut Barbieri <barbieri@profusion.mobi>2017-08-27 11:47:55 -0300
commit306ec6937bb1cdfdbfbae817ce9dcbb6d49c8aeb (patch)
tree141883e823fa4085e16800d4a9040b52e02279fc
parent109bf1b387f2162d9d5d2eaf7ee4c9b6752891a0 (diff)
Efl.Loop.coro: easy to use coroutines attached to a main loop.devs/barbieri/coroutines
While Eina_Coro provides a solid base, to use the main loop to schedule coroutines it needs some manual work we want to avoid. Efl.Loop.coro method will take a function and schedule it using the given priority, the returned value is then resolved in a promise for the returned future. Basically all users must do is write a function that looks like a synchronous code and calls eina_coro_yield() (or helper macros), that will go back to the main loop and then it will reschedule the coroutine to run according to its priority. This should reduce the number of callbacks in user's code.
-rw-r--r--src/lib/ecore/ecore_main.c176
-rw-r--r--src/lib/ecore/efl_loop.eo64
-rw-r--r--src/tests/ecore/ecore_test_promise2.c99
3 files changed, 339 insertions, 0 deletions
diff --git a/src/lib/ecore/ecore_main.c b/src/lib/ecore/ecore_main.c
index 6819a05d7e..2e96f1b6ce 100644
--- a/src/lib/ecore/ecore_main.c
+++ b/src/lib/ecore/ecore_main.c
@@ -3216,6 +3216,182 @@ _efl_loop_Eina_FutureXXX_timeout(Eo *obj, Efl_Loop_Data *pd EINA_UNUSED, double
3216 return NULL; 3216 return NULL;
3217} 3217}
3218 3218
3219typedef struct _Efl_Loop_Coro {
3220 Eina_Promise *promise;
3221 Eina_Coro *coro;
3222 Efl_Loop *loop;
3223 Eina_Future *scheduled;
3224 Efl_Loop_Coro_Cb func;
3225 const void *func_data;
3226 Eina_Free_Cb func_free_cb;
3227 Efl_Loop_Coro_Prio prio;
3228 Eina_Value value;
3229} Efl_Loop_Coro;
3230
3231static void
3232_efl_loop_coro_free(Efl_Loop_Coro *lc)
3233{
3234 if (lc->func_free_cb) lc->func_free_cb((void *)lc->func_data);
3235 if (lc->scheduled) eina_future_cancel(lc->scheduled);
3236 eina_value_flush(&lc->value);
3237 efl_unref(lc->loop);
3238 free(lc);
3239}
3240
3241static void _efl_loop_coro_reschedule(Efl_Loop_Coro *lc);
3242
3243static Eina_Value
3244_efl_loop_coro_schedule_resolved(void *data, const Eina_Value value, const Eina_Future *dead_future EINA_UNUSED)
3245{
3246 Efl_Loop_Coro *lc = data;
3247 Eina_Future *awaiting = NULL;
3248
3249 if (value.type == EINA_VALUE_TYPE_ERROR)
3250 {
3251 Eina_Error err;
3252 eina_value_get(&value, &err);
3253 ERR("coro %p scheduled got error %s, try again.",
3254 lc, eina_error_msg_get(err));
3255 }
3256 else if (!eina_coro_run(&lc->coro, NULL, &awaiting))
3257 {
3258 INF("coroutine %p finished with value type=%p (%s)",
3259 lc, lc->value.type,
3260 lc->value.type ? lc->value.type->name : "EMPTY");
3261
3262 eina_promise_resolve(lc->promise, lc->value);
3263 lc->value = EINA_VALUE_EMPTY; // owned by promise
3264 _efl_loop_coro_free(lc);
3265 return value;
3266 }
3267 else if (awaiting)
3268 {
3269 DBG("coroutine %p is awaiting for future %p, do not reschedule", lc, awaiting);
3270 eina_future_chain(awaiting,
3271 {
3272 .cb = _efl_loop_coro_schedule_resolved,
3273 .data = lc,
3274 .storage = &lc->scheduled,
3275 },
3276 efl_future_cb(lc->loop));
3277 }
3278 else _efl_loop_coro_reschedule(lc);
3279
3280 return value;
3281}
3282
3283static void
3284_efl_loop_coro_reschedule(Efl_Loop_Coro *lc)
3285{
3286 Eina_Future *f;
3287
3288 // high uses 0-timeout instead of job, since job
3289 // is implemented using events and the Ecore implementation
3290 // will never run timers or anything else, just the new jobs :-/
3291 //
3292 // TODO: bug report ecore_main loop bug.
3293 if (lc->prio == EFL_LOOP_CORO_PRIO_HIGH)
3294 f = efl_loop_Eina_FutureXXX_timeout(lc->loop, 0);
3295 else
3296 f = efl_loop_Eina_FutureXXX_idle(lc->loop);
3297
3298 DBG("coroutine %p rescheduled as future=%p", lc, f);
3299
3300 // NOTE: efl_future_cb() doesn't allow for extra 'data', so it matches
3301 // methods more easily. However we need 'lc' and we can't store in
3302 // loop since we'd not know the key for efl_key_data_get().
3303 // Easy solution: use 2 futures, one to bind and another to resolve.
3304 eina_future_chain(f,
3305 {
3306 .cb = _efl_loop_coro_schedule_resolved,
3307 .data = lc,
3308 .storage = &lc->scheduled,
3309 },
3310 efl_future_cb(lc->loop));
3311}
3312
3313static void
3314_efl_loop_coro_cancel(void *data, const Eina_Promise *dead_promise EINA_UNUSED)
3315{
3316 Efl_Loop_Coro *lc = data;
3317
3318 INF("canceled coroutine %p (coro=%p)", lc, lc->coro);
3319
3320 eina_coro_cancel(&lc->coro);
3321
3322 _efl_loop_coro_free(lc);
3323}
3324
3325static const void *
3326_efl_loop_coro_cb(void *data, Eina_Bool canceled, Eina_Coro *coro)
3327{
3328 Efl_Loop_Coro *lc = data;
3329
3330 if (canceled) lc->value = eina_value_error_init(ECANCELED);
3331 else lc->value = lc->func((void *)lc->func_data, coro, lc->loop);
3332
3333 return lc;
3334}
3335
3336static Eina_Future *
3337_efl_loop_coro(Eo *obj, Efl_Loop_Data *pd EINA_UNUSED, Efl_Loop_Coro_Prio prio, void *func_data, Efl_Loop_Coro_Cb func, Eina_Free_Cb func_free_cb)
3338{
3339 Efl_Loop_Coro *lc;
3340 Eina_Promise *p;
3341 Eina_Future *f;
3342
3343 EINA_SAFETY_ON_NULL_RETURN_VAL(func, NULL);
3344
3345 lc = calloc(1, sizeof(Efl_Loop_Coro));
3346 EINA_SAFETY_ON_NULL_GOTO(lc, calloc_failed);
3347
3348 lc->loop = efl_ref(obj);
3349 lc->func = func;
3350 lc->func_data = func_data;
3351 lc->func_free_cb = func_free_cb;
3352 lc->prio = prio;
3353
3354 lc->coro = eina_coro_new(_efl_loop_coro_cb, lc, EINA_CORO_STACK_SIZE_DEFAULT);
3355 EINA_SAFETY_ON_NULL_GOTO(lc, coro_failed);
3356
3357 p = eina_promise_new(efl_loop_future_scheduler_get(obj),
3358 _efl_loop_coro_cancel, lc);
3359 // lc is dead if p is NULL
3360 EINA_SAFETY_ON_NULL_GOTO(p, promise_failed);
3361 lc->promise = p;
3362
3363 // must be done prior to reschedule, as it may resolve on errors
3364 // and promises without futures are simply ignored, will remain
3365 // alive.
3366 f = eina_future_new(p);
3367
3368 _efl_loop_coro_reschedule(lc);
3369
3370 INF("new coroutine %p (coro=%p)", lc, lc->coro);
3371
3372 // NOTE: Eolian should do efl_future_then() to bind future to object.
3373 return efl_future_Eina_FutureXXX_then(obj, f);
3374
3375 promise_failed:
3376 // _efl_loop_coro_cancel() was called, func was run... just return.
3377
3378 // NOTE: Eolian should do efl_future_then() to bind future to object.
3379 return efl_future_Eina_FutureXXX_then(obj,
3380 eina_future_resolved(efl_loop_future_scheduler_get(obj),
3381 eina_value_error_init(ENOMEM)));
3382
3383 coro_failed:
3384 _efl_loop_coro_free(lc);
3385
3386 calloc_failed:
3387 if (func_free_cb) func_free_cb((void *)func_data);
3388
3389 // NOTE: Eolian should do efl_future_then() to bind future to object.
3390 return efl_future_Eina_FutureXXX_then(obj,
3391 eina_future_resolved(efl_loop_future_scheduler_get(obj),
3392 eina_value_error_init(ENOMEM)));
3393}
3394
3219/* This event will be triggered when the main loop is destroyed and destroy its timers along */ 3395/* This event will be triggered when the main loop is destroyed and destroy its timers along */
3220static void _efl_loop_internal_cancel(Efl_Internal_Promise *p); 3396static void _efl_loop_internal_cancel(Efl_Internal_Promise *p);
3221 3397
diff --git a/src/lib/ecore/efl_loop.eo b/src/lib/ecore/efl_loop.eo
index a5610c20e2..778a94d186 100644
--- a/src/lib/ecore/efl_loop.eo
+++ b/src/lib/ecore/efl_loop.eo
@@ -7,6 +7,41 @@ struct Efl.Loop.Arguments {
7 initialization: bool; [[Set to $true when the program should initialize its internal state. This happen once per process instance.]] 7 initialization: bool; [[Set to $true when the program should initialize its internal state. This happen once per process instance.]]
8} 8}
9 9
10enum Efl.Loop.Coro.Prio {
11 [[Priority class for the coroutine.]]
12
13 high = 0, [[high priority coroutine, scheduled using zero-timers (will expire as soon as possible).]]
14 idle, [[low priority coroutine, scheduled when nothing else should run]]
15}
16
17function Efl.Loop.Coro.Cb {
18 params {
19 coro: ptr(Eina.Coro); [[The coroutine handle, used to $eina_coro_yield() and voluntarily give back control to the main loop until it's rescheduled.]]
20 loop: Efl.Loop; [[The loop that schedules this coroutine.]]
21 }
22 return: generic_value; [[Value that will resolve the promise,
23 being delivered to the future chain
24 attached to the coroutine. Note that the
25 value will be owned by the Efl_Loop_Coro
26 and Eina_Future subsystems and will be
27 flushed (eina_value_flush()) once
28 unused. Its contents must survive the
29 function return, that is, it shouldn't
30 keep pointers to the stack.
31 ]]
32}; [[Coroutine function, it will be called back from the
33 coroutine environment and when executed it's guaranteed that the
34 main loop will be paused, so shared resources are safe to access
35 (no locks are required).
36
37 \@note Eina_Coro may use threads, then take care to handle
38 thread-local-storage (TLS) details properly, eventually you
39 may consider eina_coro_hook_add() to be informed when the
40 main or coroutine will exit and enter. For instance this is
41 used by Efl_Object (handled transparently for the user).
42 ]]
43
44
10class Efl.Loop (Efl.Object) 45class Efl.Loop (Efl.Object)
11{ 46{
12 [[The Efl Main Loop 47 [[The Efl Main Loop
@@ -107,6 +142,35 @@ class Efl.Loop (Efl.Object)
107 } 142 }
108 return: own(ptr(Eina.Future)) /* future<void> */; [[The future handle.]] 143 return: own(ptr(Eina.Future)) /* future<void> */; [[The future handle.]]
109 } 144 }
145 coro {
146 [[A future promise that will be resolved using a coroutine.
147
148 A coroutine is a function that will be executed
149 cooperatively with the main loop. The main loop will
150 schedule the coroutine, explicitly giving control to it --
151 by then the main loop is paused. The coroutine must then
152 finish and return, or yield control back to the main loop
153 using $eina_coro_yield(). This allows for shared context
154 to be safely interchanged with the main loop -- it is
155 guaranteed that if the coroutine is running, the main loop
156 is pause; if the main loop is running the coroutine is
157 paused.
158
159 Coroutines are implemented with @Eina.Coro, see their API
160 and how it's exposed in your language -- it may be the
161 case that you don't need to worry and it will be managed
162 transparently by your language/binding.
163
164 Once finished the coroutine returns a value, that will be
165 used to resolve the promise, propagating thru the future
166 chain.
167 ]]
168 params {
169 @in priority: Efl.Loop.Coro.Prio; [[The priority used to schedule the coroutine.]]
170 @in func: Efl.Loop.Coro.Cb @nonull; [[The function to run as a coroutine.]]
171 }
172 return: own(ptr(Eina.Future)) /* future<> */; [[The future handle, it provides the value returned by $func once it exits.]]
173 }
110 job { 174 job {
111 [[Will execute that promise in the near future.]] 175 [[Will execute that promise in the near future.]]
112 params { 176 params {
diff --git a/src/tests/ecore/ecore_test_promise2.c b/src/tests/ecore/ecore_test_promise2.c
index 315e67d4ce..d91efdde68 100644
--- a/src/tests/ecore/ecore_test_promise2.c
+++ b/src/tests/ecore/ecore_test_promise2.c
@@ -893,6 +893,103 @@ START_TEST(efl_test_promise_eo_link)
893} 893}
894END_TEST 894END_TEST
895 895
896#define CORO_COUNT 10
897#define CORO_SLEEP 0.1
898
899static Eina_Value
900_coro(void *data, Eina_Coro *coro, Efl_Loop *loop EINA_UNUSED)
901{
902 int *pi = data;
903
904 for (; *pi < CORO_COUNT; (*pi)++)
905 {
906 usleep(CORO_SLEEP * 1000000);
907 eina_coro_yield_or_return(coro, EINA_VALUE_EMPTY);
908 }
909
910 // returned value is an EINA_VALUE_TYPE_PROMISE
911 return eina_future_as_value(_str_future_get());
912}
913
914static Eina_Bool
915_timer_test(void *data)
916{
917 int *pi = data;
918 (*pi)++;
919
920 return EINA_TRUE;
921}
922
923START_TEST(efl_test_coro)
924{
925 Eina_Future *f;
926 int coro_count = 0;
927 int timer_count = 0;
928
929 fail_if(!ecore_init());
930 f = eina_future_then(efl_loop_coro(ecore_main_loop_get(),
931 EFL_LOOP_CORO_PRIO_IDLE,
932 &coro_count, _coro, NULL),
933 .cb = _simple_ok);
934 fail_if(!f);
935
936 // timer is 2x faster so it will always expire
937 ecore_timer_add(CORO_SLEEP / 2, _timer_test, &timer_count);
938
939 ecore_main_loop_begin();
940 ecore_shutdown();
941
942 ck_assert_int_eq(coro_count, CORO_COUNT);
943 ck_assert_int_ge(timer_count, CORO_COUNT);
944}
945END_TEST
946
947static Eina_Value
948_await(void *data, Eina_Coro *coro, Efl_Loop *loop)
949{
950 int *pi = data;
951
952 for (; *pi < CORO_COUNT; (*pi)++)
953 {
954 Eina_Future *f = eina_future_chain(efl_loop_Eina_FutureXXX_timeout(loop, CORO_SLEEP),
955 // convert to string so we don't get dummy EMPTY...
956 // happened to me during development :-)
957 eina_future_cb_convert_to(EINA_VALUE_TYPE_STRING));
958 // await will eina_coro_yield() internally.
959 Eina_Value v = eina_future_await(f, coro, NULL);
960 if (v.type == EINA_VALUE_TYPE_ERROR) return v;
961 ck_assert_ptr_eq(v.type, EINA_VALUE_TYPE_STRING); // job delivers EINA_VALUE_EMPTY
962 }
963
964 // returned value is an EINA_VALUE_TYPE_PROMISE
965 return eina_future_as_value(_str_future_get());
966}
967
968START_TEST(efl_test_promise_future_await)
969{
970 Eina_Future *f;
971 int coro_count = 0;
972 int timer_count = 0;
973
974 fail_if(!ecore_init());
975 f = eina_future_then(efl_loop_coro(ecore_main_loop_get(),
976 EFL_LOOP_CORO_PRIO_IDLE,
977 &coro_count, _await, NULL),
978 .cb = _simple_ok);
979 fail_if(!f);
980
981 // timer is 2x faster so it will always expire
982 ecore_timer_add(CORO_SLEEP / 2, _timer_test, &timer_count);
983
984 ecore_main_loop_begin();
985 ecore_shutdown();
986
987 ck_assert_int_eq(coro_count, CORO_COUNT);
988 ck_assert_int_ge(timer_count, CORO_COUNT);
989}
990END_TEST
991
992
896void ecore_test_ecore_promise2(TCase *tc) 993void ecore_test_ecore_promise2(TCase *tc)
897{ 994{
898 tcase_add_test(tc, efl_test_timeout); 995 tcase_add_test(tc, efl_test_timeout);
@@ -913,4 +1010,6 @@ void ecore_test_ecore_promise2(TCase *tc)
913 //FIXME: We should move this to EO tests, however they depend on Ecore... 1010 //FIXME: We should move this to EO tests, however they depend on Ecore...
914 tcase_add_test(tc, efl_test_promise_eo); 1011 tcase_add_test(tc, efl_test_promise_eo);
915 tcase_add_test(tc, efl_test_promise_eo_link); 1012 tcase_add_test(tc, efl_test_promise_eo_link);
1013 tcase_add_test(tc, efl_test_coro);
1014 tcase_add_test(tc, efl_test_promise_future_await);
916} 1015}