summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGustavo Sverzut Barbieri <barbieri@profusion.mobi>2017-08-25 15:54:34 -0300
committerGustavo Sverzut Barbieri <barbieri@profusion.mobi>2017-08-27 11:47:55 -0300
commitd10a35628cc2b64b888648137ef5d218a1eb3c5b (patch)
treefeb0fa20fab168bc03e49cfa0f1b137d06a42fb4
parent4d7661b1746dcb2ba5d146700de887ecbe2e462e (diff)
downloadefl-d10a35628cc2b64b888648137ef5d218a1eb3c5b.tar.gz
eina: add Eina_Coro - coroutine support.
Coroutines are cooperative tasks, in the sense that the caller will stop until the target function runs. The target function must either yield control back to the caller (main thread), or exit. There is no preemption of the two tasks, thus no special care needs to be taken regarding shared data. If the target coroutine yields control using eina_coro_yield(), then it will be paused until it's manually ran again by caller (main thread), which is executed with eina_coro_run(). Another common usage is to await for another task to be completed, this can be done by waiting for a future to be resolved. It will automatically yield and inform the caller of the future so it can schedule properly instead of keep calling the task. Waiting for many tasks can be achieved by using eina_future_all() or eina_future_race(). This is done with eina_coro_await(). Due portability it was implemented using Eina_Thread, Eina_Lock and Eina_Condition. Regular threads will ensure that the state is fully preserved (stack, registers) in a platform independent way. Each thread will wait on its own turn using the Eina_Lock and Eina_Condition, thus it's guaranteed that only one is being executed at the same time. The API is small and should allow different implementations shall we need them, like manually saving the stack and registers, then restoring those -- problem is doing that in a portable way, setjmp()/longjmp() won't save the stack, makecontext()/swapcontext() doesn't work right on MacOS... Hooks can be used to be informed when the main routine exits and then enters, likewise when the coroutine enters and exits. These will be used, for instance, to automatically get, adopt and return Efl_Domain_Data needed to make Efl_Object work in such environment. The flow is simple: - main exit (called from main thread) - coroutine enter (called from worker thread) - coroutine exit (called from worker thread) - main enter (called from main thead) Performance may not be optimal, however this is meant as easy-to-use and it shouldn't be an issue in real life. It will be mostly exposed in two layers: - Efl.Loop.coro: will wrap eina_coro and and schedule using its main loop instance, returns an Eina_Future so it's easy to chain. - Eina_Promise/Eina_Future "async/await"-like behavior: will allow to write "synchronous" code that can wait for promises to be resolved. When eina_future_await(), it will actually register a new Eina_Future in the chain and then eina_coro_yield(). Once the future is called back it will call eina_coro_run() and allow the coroutine to resume. This is done on top fo eina_coro_await().
-rw-r--r--src/Makefile_Eina.am9
-rw-r--r--src/lib/eina/Eina.h1
-rw-r--r--src/lib/eina/eina_coro.c642
-rw-r--r--src/lib/eina/eina_coro.h554
-rw-r--r--src/lib/eina/eina_main.c3
-rw-r--r--src/lib/eina/eina_promise.c48
-rw-r--r--src/lib/eina/eina_promise.h62
-rw-r--r--src/tests/eina/eina_suite.c1
-rw-r--r--src/tests/eina/eina_suite.h1
-rw-r--r--src/tests/eina/eina_test_coro.c562
10 files changed, 1878 insertions, 5 deletions
diff --git a/src/Makefile_Eina.am b/src/Makefile_Eina.am
index bd74cdcb64..64761feb84 100644
--- a/src/Makefile_Eina.am
+++ b/src/Makefile_Eina.am
@@ -107,7 +107,8 @@ lib/eina/eina_slice.h \
lib/eina/eina_inline_slice.x \
lib/eina/eina_inline_modinfo.x \
lib/eina/eina_freeq.h \
-lib/eina/eina_slstr.h
+lib/eina/eina_slstr.h \
+lib/eina/eina_coro.h
lib_eina_libeina_la_SOURCES = \
@@ -183,7 +184,8 @@ lib/eina/eina_quaternion.c \
lib/eina/eina_bezier.c \
lib/eina/eina_safepointer.c \
lib/eina/eina_freeq.c \
-lib/eina/eina_slstr.c
+lib/eina/eina_slstr.c \
+lib/eina/eina_coro.c
if HAVE_WIN32
@@ -356,7 +358,8 @@ tests/eina/eina_test_bezier.c \
tests/eina/eina_test_safepointer.c \
tests/eina/eina_test_slice.c \
tests/eina/eina_test_freeq.c \
-tests/eina/eina_test_slstr.c
+tests/eina/eina_test_slstr.c \
+tests/eina/eina_test_coro.c
tests_eina_eina_suite_CPPFLAGS = -I$(top_builddir)/src/lib/efl \
-DTESTS_WD=\"`pwd`\" \
diff --git a/src/lib/eina/Eina.h b/src/lib/eina/Eina.h
index 76b6d0adf6..5668a5c977 100644
--- a/src/lib/eina/Eina.h
+++ b/src/lib/eina/Eina.h
@@ -274,6 +274,7 @@ extern "C" {
#include <eina_slstr.h>
#include <eina_debug.h>
#include <eina_promise.h>
+#include <eina_coro.h>
#undef EAPI
#define EAPI
diff --git a/src/lib/eina/eina_coro.c b/src/lib/eina/eina_coro.c
new file mode 100644
index 0000000000..b19e34fe81
--- /dev/null
+++ b/src/lib/eina/eina_coro.c
@@ -0,0 +1,642 @@
+/* EINA - EFL data type library
+ * Copyright (C) 2017 ProFUSION embedded systems
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library;
+ * if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <stdlib.h>
+#include <limits.h>
+
+#include "eina_config.h"
+#include "eina_private.h"
+#include "eina_log.h"
+#include "eina_mempool.h"
+#include "eina_lock.h"
+#include "eina_thread.h"
+#include "eina_inarray.h"
+#include "eina_promise.h"
+
+/* undefs EINA_ARG_NONULL() so NULL checks are not compiled out! */
+#include "eina_safety_checks.h"
+
+#include "eina_coro.h"
+#include "eina_value.h"
+#include "eina_value_util.h"
+
+static Eina_Mempool *_eina_coro_mp = NULL;
+static Eina_Lock _eina_coro_lock;
+
+static int _eina_coro_log_dom = -1;
+static int _eina_coro_usage = 0;
+
+static int _eina_coro_hooks_walking = 0;
+static Eina_Inarray _eina_coro_hooks;
+
+#ifdef CRIT
+#undef CRIT
+#endif
+#define CRIT(...) EINA_LOG_DOM_CRIT(_eina_coro_log_dom, __VA_ARGS__)
+
+#ifdef ERR
+#undef ERR
+#endif
+#define ERR(...) EINA_LOG_DOM_ERR(_eina_coro_log_dom, __VA_ARGS__)
+
+#ifdef INF
+#undef INF
+#endif
+#define INF(...) EINA_LOG_DOM_INFO(_eina_coro_log_dom, __VA_ARGS__)
+
+#ifdef DBG
+#undef DBG
+#endif
+#define DBG(...) EINA_LOG_DOM_DBG(_eina_coro_log_dom, __VA_ARGS__)
+
+typedef enum _Eina_Coro_Turn {
+ EINA_CORO_TURN_MAIN = 0,
+ EINA_CORO_TURN_COROUTINE
+} Eina_Coro_Turn;
+
+struct _Eina_Coro {
+ Eina_Coro_Cb func;
+ const void *data;
+ Eina_Future *awaiting;
+ Eina_Lock lock;
+ Eina_Condition condition;
+ Eina_Thread main;
+ Eina_Thread coroutine;
+ Eina_Bool finished;
+ Eina_Bool canceled;
+ Eina_Coro_Turn turn;
+};
+
+#define CORO_TURN_STR(turn) \
+ (((turn) == EINA_CORO_TURN_MAIN) ? "MAIN" : "COROUTINE")
+
+#define CORO_FMT "coro=%p {func=%p data=%p turn=%s threads={%p%c %p%c} awaiting=%p}"
+#define CORO_EXP(coro) \
+ coro, coro->func, coro->data, \
+ CORO_TURN_STR(coro->turn), \
+ (void *)coro->coroutine, \
+ eina_thread_self() == coro->coroutine ? '*' : 0, \
+ (void *)coro->main, \
+ eina_thread_self() == coro->main ? '*' : 0, \
+ coro->awaiting
+
+#define EINA_CORO_CHECK(coro, turn, ...) \
+ do \
+ { \
+ if ((!_eina_coro_mp) || (!eina_mempool_from(_eina_coro_mp, (coro)))) \
+ { \
+ CRIT(#coro "=%p is invalid.", (coro)); \
+ return __VA_ARGS__; \
+ } \
+ else if ((turn == EINA_CORO_TURN_COROUTINE) && ((coro)->coroutine != eina_thread_self())) \
+ { \
+ CRIT("must be called from coroutine! " CORO_FMT, CORO_EXP((coro))); \
+ return __VA_ARGS__; \
+ } \
+ else if ((turn == EINA_CORO_TURN_MAIN) && ((coro)->main != eina_thread_self())) \
+ { \
+ CRIT("must be called from main thread! " CORO_FMT, CORO_EXP((coro))); \
+ return __VA_ARGS__; \
+ } \
+ } \
+ while (0)
+
+#define EINA_CORO_CHECK_GOTO(coro, turn, label) \
+ do \
+ { \
+ if ((!_eina_coro_mp) || (!eina_mempool_from(_eina_coro_mp, (coro)))) \
+ { \
+ CRIT(#coro "=%p is invalid.", (coro)); \
+ goto label; \
+ } \
+ else if ((turn == EINA_CORO_TURN_COROUTINE) && ((coro)->coroutine != eina_thread_self())) \
+ { \
+ CRIT("must be called from coroutine! " CORO_FMT, CORO_EXP((coro))); \
+ goto label; \
+ } \
+ else if ((turn == EINA_CORO_TURN_MAIN) && ((coro)->main != eina_thread_self())) \
+ { \
+ CRIT("must be called from main thread! " CORO_FMT, CORO_EXP((coro))); \
+ goto label; \
+ } \
+ } \
+ while (0)
+
+
+typedef struct _Eina_Coro_Hook {
+ Eina_Coro_Hook_Coro_Enter_Cb coro_enter;
+ Eina_Coro_Hook_Coro_Exit_Cb coro_exit;
+ Eina_Coro_Hook_Main_Enter_Cb main_enter;
+ Eina_Coro_Hook_Main_Exit_Cb main_exit;
+ const void *data;
+} Eina_Coro_Hook;
+
+EAPI Eina_Bool
+eina_coro_hook_add(Eina_Coro_Hook_Coro_Enter_Cb coro_enter, Eina_Coro_Hook_Coro_Exit_Cb coro_exit, Eina_Coro_Hook_Main_Enter_Cb main_enter, Eina_Coro_Hook_Main_Exit_Cb main_exit, const void *data)
+{
+ Eina_Coro_Hook hook = { coro_enter, coro_exit, main_enter, main_exit, data };
+ int idx;
+
+ eina_lock_take(&_eina_coro_lock);
+
+ EINA_SAFETY_ON_TRUE_GOTO(_eina_coro_hooks_walking > 0, error);
+
+ idx = eina_inarray_push(&_eina_coro_hooks, &hook);
+ EINA_SAFETY_ON_TRUE_GOTO(idx < 0, error);
+
+ eina_lock_release(&_eina_coro_lock);
+ return EINA_TRUE;
+
+ error:
+ eina_lock_release(&_eina_coro_lock);
+ return EINA_FALSE;
+}
+
+EAPI Eina_Bool
+eina_coro_hook_del(Eina_Coro_Hook_Coro_Enter_Cb coro_enter, Eina_Coro_Hook_Coro_Exit_Cb coro_exit, Eina_Coro_Hook_Main_Enter_Cb main_enter, Eina_Coro_Hook_Main_Exit_Cb main_exit, const void *data)
+{
+ Eina_Coro_Hook hook = { coro_enter, coro_exit, main_enter, main_exit, data };
+ int idx;
+
+ eina_lock_take(&_eina_coro_lock);
+
+ EINA_SAFETY_ON_TRUE_GOTO(_eina_coro_hooks_walking > 0, error);
+
+ idx = eina_inarray_remove(&_eina_coro_hooks, &hook);
+ EINA_SAFETY_ON_TRUE_GOTO(idx < 0, error);
+
+ eina_lock_release(&_eina_coro_lock);
+ return EINA_TRUE;
+
+ error:
+ eina_lock_release(&_eina_coro_lock);
+ return EINA_FALSE;
+}
+
+
+// opposite of the coro_exit, similar to coro_enter
+static void
+_eina_coro_hooks_main_exit(Eina_Coro *coro)
+{
+ const Eina_Coro_Hook *itr;
+
+ eina_lock_take(&_eina_coro_lock);
+ _eina_coro_hooks_walking++;
+ eina_lock_release(&_eina_coro_lock);
+
+ EINA_INARRAY_FOREACH(&_eina_coro_hooks, itr)
+ {
+ if (!itr->main_exit) continue;
+ if (itr->main_exit((void *)itr->data, coro)) continue;
+ coro->canceled = EINA_TRUE;
+ ERR("failed hook exit=%p data=%p for main routine " CORO_FMT,
+ itr->main_exit, itr->data, CORO_EXP(coro));
+ }
+}
+
+// opposite of the coro_enter, similar to coro_exit
+static void
+_eina_coro_hooks_main_enter(Eina_Coro *coro)
+{
+ const Eina_Coro_Hook *itr;
+
+ EINA_INARRAY_REVERSE_FOREACH(&_eina_coro_hooks, itr)
+ {
+ if (!itr->main_enter) continue;
+ itr->main_enter((void *)itr->data, coro);
+ }
+
+ eina_lock_take(&_eina_coro_lock);
+ _eina_coro_hooks_walking--;
+ eina_lock_release(&_eina_coro_lock);
+}
+
+static Eina_Bool
+_eina_coro_hooks_coro_enter(Eina_Coro *coro)
+{
+ const Eina_Coro_Hook *itr;
+ Eina_Bool r = EINA_TRUE;
+
+ eina_lock_take(&_eina_coro_lock);
+ _eina_coro_hooks_walking++;
+ eina_lock_release(&_eina_coro_lock);
+
+ EINA_INARRAY_FOREACH(&_eina_coro_hooks, itr)
+ {
+ if (!itr->coro_enter) continue;
+ if (itr->coro_enter((void *)itr->data, coro)) continue;
+ r = EINA_FALSE;
+ ERR("failed hook enter=%p data=%p for coroutine " CORO_FMT,
+ itr->coro_enter, itr->data, CORO_EXP(coro));
+ }
+
+ return r;
+}
+
+static void
+_eina_coro_hooks_coro_exit(Eina_Coro *coro)
+{
+ const Eina_Coro_Hook *itr;
+
+ EINA_INARRAY_REVERSE_FOREACH(&_eina_coro_hooks, itr)
+ {
+ if (!itr->coro_exit) continue;
+ itr->coro_exit((void *)itr->data, coro);
+ }
+
+ eina_lock_take(&_eina_coro_lock);
+ _eina_coro_hooks_walking--;
+ eina_lock_release(&_eina_coro_lock);
+}
+
+static void
+_eina_coro_signal(Eina_Coro *coro, Eina_Coro_Turn turn)
+{
+ DBG("signal turn=%s " CORO_FMT, CORO_TURN_STR(turn), CORO_EXP(coro));
+
+ eina_lock_take(&coro->lock);
+ coro->turn = turn;
+ eina_condition_signal(&coro->condition);
+ eina_lock_release(&coro->lock);
+}
+
+static void
+_eina_coro_wait(Eina_Coro *coro, Eina_Coro_Turn turn)
+{
+ DBG("waiting turn=%s " CORO_FMT, CORO_TURN_STR(turn), CORO_EXP(coro));
+
+ eina_lock_take(&coro->lock);
+ while (coro->turn != turn)
+ eina_condition_wait(&coro->condition);
+ eina_lock_release(&coro->lock);
+
+ eina_main_loop_define();
+
+ DBG("wait is over: turn=%s " CORO_FMT, CORO_TURN_STR(turn), CORO_EXP(coro));
+}
+
+static Eina_Bool
+_eina_coro_hooks_coro_enter_and_get_canceled(Eina_Coro *coro)
+{
+ if (!_eina_coro_hooks_coro_enter(coro)) return EINA_TRUE;
+ return coro->canceled;
+}
+
+static void *
+_eina_coro_thread(void *data, Eina_Thread t EINA_UNUSED)
+{
+ Eina_Coro *coro = data;
+ void *result = NULL;
+ Eina_Bool canceled = EINA_FALSE;
+
+ _eina_coro_wait(coro, EINA_CORO_TURN_COROUTINE);
+
+ canceled = _eina_coro_hooks_coro_enter_and_get_canceled(coro);
+
+ DBG("call (canceled=%hhu) " CORO_FMT, canceled, CORO_EXP(coro));
+ result = (void *)coro->func((void *)coro->data, canceled, coro);
+ DBG("finished with result=%p " CORO_FMT, result, CORO_EXP(coro));
+
+ _eina_coro_hooks_coro_exit(coro);
+
+ coro->finished = EINA_TRUE;
+ _eina_coro_signal(coro, EINA_CORO_TURN_MAIN);
+
+ return result;
+}
+
+static Eina_Coro *
+_eina_coro_alloc(void)
+{
+ Eina_Coro *coro = NULL;
+
+ eina_lock_take(&_eina_coro_lock);
+ if (EINA_UNLIKELY(!_eina_coro_mp))
+ {
+ const char *choice, *tmp;
+
+#ifdef EINA_DEFAULT_MEMPOOL
+ choice = "pass_through";
+#else
+ choice = "chained_mempool";
+#endif
+ tmp = getenv("EINA_MEMPOOL");
+ if (tmp && tmp[0])
+ choice = tmp;
+
+ _eina_coro_mp = eina_mempool_add
+ (choice, "coro", NULL, sizeof(Eina_Coro), 16);
+
+ if (!_eina_coro_mp)
+ {
+ ERR("Mempool for coro cannot be allocated in coro init.");
+ goto end;
+ }
+ }
+ coro = eina_mempool_calloc(_eina_coro_mp, sizeof(Eina_Coro));
+ if (coro) _eina_coro_usage++;
+
+ end:
+ eina_lock_release(&_eina_coro_lock);
+
+ return coro;
+}
+
+static void
+_eina_coro_free(Eina_Coro *coro)
+{
+ EINA_SAFETY_ON_NULL_RETURN(coro);
+
+ eina_lock_take(&_eina_coro_lock);
+
+ eina_mempool_free(_eina_coro_mp, coro);
+ _eina_coro_usage--;
+ if (_eina_coro_usage == 0)
+ {
+ eina_mempool_del(_eina_coro_mp);
+ _eina_coro_mp = NULL;
+ }
+
+ eina_lock_release(&_eina_coro_lock);
+}
+
+EAPI Eina_Coro *
+eina_coro_new(Eina_Coro_Cb func, const void *data, size_t stack_size)
+{
+ Eina_Coro *coro;
+ Eina_Bool r;
+
+ EINA_SAFETY_ON_NULL_RETURN_VAL(func, NULL);
+
+ coro = _eina_coro_alloc();
+ EINA_SAFETY_ON_NULL_RETURN_VAL(coro, NULL);
+
+ coro->func = func;
+ coro->data = data;
+ r = eina_lock_new(&coro->lock);
+ EINA_SAFETY_ON_FALSE_GOTO(r, failed_lock);
+ r = eina_condition_new(&coro->condition, &coro->lock);
+ EINA_SAFETY_ON_FALSE_GOTO(r, failed_condition);
+ coro->main = eina_thread_self();
+ coro->coroutine = 0;
+ coro->finished = EINA_FALSE;
+ coro->canceled = EINA_FALSE;
+ coro->turn = EINA_CORO_TURN_MAIN;
+
+ /* eina_thread_create() doesn't take attributes so we can set stack size */
+ if (stack_size)
+ DBG("currently stack size is ignored! Using thread default.");
+
+ if (!eina_thread_create(&coro->coroutine,
+ EINA_THREAD_NORMAL, -1,
+ _eina_coro_thread, coro))
+ {
+ ERR("could not create thread for " CORO_FMT, CORO_EXP(coro));
+ goto failed_thread;
+ }
+
+ INF(CORO_FMT, CORO_EXP(coro));
+ return coro;
+
+ failed_thread:
+ eina_condition_free(&coro->condition);
+ failed_condition:
+ eina_lock_free(&coro->lock);
+ failed_lock:
+ _eina_coro_free(coro);
+ return NULL;
+}
+
+EAPI Eina_Bool
+eina_coro_yield(Eina_Coro *coro)
+{
+ EINA_CORO_CHECK(coro, EINA_CORO_TURN_COROUTINE, EINA_FALSE);
+
+ _eina_coro_hooks_coro_exit(coro);
+
+ _eina_coro_signal(coro, EINA_CORO_TURN_MAIN);
+ _eina_coro_wait(coro, EINA_CORO_TURN_COROUTINE);
+
+ return !_eina_coro_hooks_coro_enter_and_get_canceled(coro);
+}
+
+EAPI Eina_Bool
+eina_coro_run(Eina_Coro **p_coro, void **p_result, Eina_Future **p_awaiting)
+{
+ Eina_Coro *coro;
+
+ if (p_result) *p_result = NULL;
+ if (p_awaiting) *p_awaiting = NULL;
+
+ EINA_SAFETY_ON_NULL_RETURN_VAL(p_coro, EINA_FALSE);
+ EINA_CORO_CHECK(*p_coro, EINA_CORO_TURN_MAIN, EINA_FALSE);
+
+ coro = *p_coro;
+
+ _eina_coro_hooks_main_exit(coro);
+
+ _eina_coro_signal(coro, EINA_CORO_TURN_COROUTINE);
+ _eina_coro_wait(coro, EINA_CORO_TURN_MAIN);
+
+ _eina_coro_hooks_main_enter(coro);
+
+ if (EINA_UNLIKELY(coro->finished)) {
+ void *result;
+ DBG("coroutine finished, join thread " CORO_FMT, CORO_EXP(coro));
+
+ result = eina_thread_join(coro->coroutine);
+ INF("coroutine finished with result=%p " CORO_FMT,
+ result, CORO_EXP(coro));
+ if (p_result) *p_result = result;
+ if (coro->awaiting) eina_future_cancel(coro->awaiting);
+ eina_condition_free(&coro->condition);
+ eina_lock_free(&coro->lock);
+ _eina_coro_free(coro);
+ *p_coro = NULL;
+ return EINA_FALSE;
+ }
+
+ if (p_awaiting) *p_awaiting = coro->awaiting;
+
+ DBG("coroutine yielded, must run again " CORO_FMT, CORO_EXP(coro));
+ return EINA_TRUE;
+}
+
+typedef struct _Eina_Coro_Await_Data {
+ Eina_Coro *coro;
+ Eina_Value *p_value;
+ Eina_Bool resolved;
+} Eina_Coro_Await_Data;
+
+static Eina_Value
+_eina_coro_await_cb(void *data, const Eina_Value value, const Eina_Future *dead_future)
+{
+ Eina_Coro_Await_Data *d = data;
+
+ DBG("future %p resolved with value type %p (%s) " CORO_FMT,
+ dead_future, value.type, value.type ? value.type->name : "EMPTY",
+ CORO_EXP(d->coro));
+
+ if (d->p_value)
+ {
+ // copy is needed as value contents is flushed when this function returns.
+ if (!value.type) *d->p_value = value;
+ else if (!eina_value_copy(&value, d->p_value))
+ {
+ ERR("Value cannot be copied - unusable with Eina_Future: %p (%s)", value.type, value.type->name);
+ eina_value_setup(d->p_value, EINA_VALUE_TYPE_ERROR);
+ eina_value_set(d->p_value, ENOTSUP);
+ }
+ }
+
+ d->resolved = EINA_TRUE;
+
+ return value;
+}
+
+EAPI Eina_Bool
+eina_coro_await(Eina_Coro *coro, Eina_Future *f, Eina_Value *p_value)
+{
+ Eina_Coro_Await_Data data = { coro, p_value, EINA_FALSE };
+
+ if (p_value) *p_value = EINA_VALUE_EMPTY;
+
+ EINA_CORO_CHECK_GOTO(coro, EINA_CORO_TURN_COROUTINE, no_coro);
+ EINA_SAFETY_ON_TRUE_GOTO(coro->awaiting != NULL, no_coro);
+
+ // storage will be NULL once future dies...
+ f = eina_future_then(f, _eina_coro_await_cb, &data, &coro->awaiting);
+ if (!f) return EINA_FALSE;
+
+ INF("await future %p " CORO_FMT, f, CORO_EXP(coro));
+ while (eina_coro_yield(coro) && !data.resolved)
+ DBG("future %p still pending " CORO_FMT, f, CORO_EXP(coro));
+
+ if (!data.resolved)
+ {
+ INF("future %p still pending and coroutine was canceled " CORO_FMT,
+ f, CORO_EXP(coro));
+ if (p_value)
+ {
+ eina_value_flush(p_value);
+ *p_value = eina_value_error_init(ECANCELED);
+ }
+ return EINA_FALSE;
+ }
+
+ INF("future %p resolved! continue coroutine " CORO_FMT, f, CORO_EXP(coro));
+ return EINA_TRUE;
+
+ no_coro:
+ if (p_value) *p_value = eina_value_error_init(EINVAL);
+ eina_future_cancel(f);
+ return EINA_FALSE;
+}
+
+EAPI void *
+eina_coro_cancel(Eina_Coro **p_coro)
+{
+ void *result = NULL;
+ Eina_Coro *coro;
+
+ EINA_SAFETY_ON_NULL_RETURN_VAL(p_coro, NULL);
+ EINA_CORO_CHECK(*p_coro, EINA_CORO_TURN_MAIN, NULL);
+
+ coro = *p_coro;
+
+ coro->canceled = EINA_TRUE;
+
+ if (coro->awaiting) eina_future_cancel(coro->awaiting);
+
+ DBG("marked as canceled, run so it can exit... " CORO_FMT, CORO_EXP(coro));
+ while (eina_coro_run(p_coro, &result, NULL))
+ DBG("did not exited, try running again..." CORO_FMT, CORO_EXP(coro));
+
+ // be careful, coro is dead!
+ INF("coroutine %p canceled and returned %p", coro, result);
+
+ return result;
+}
+
+/**
+ * @internal
+ * @brief Initialize the coroutine module.
+ *
+ * @return #EINA_TRUE on success, #EINA_FALSE on failure.
+ *
+ * This function sets up the coroutine module of Eina. It is called by
+ * eina_init().
+ *
+ * This function creates mempool to speed up and keep safety of coro
+ * handles, using EINA_MEMPOOL environment variable if it is set to
+ * choose the memory pool type to use.
+ *
+ * @see eina_init()
+ */
+Eina_Bool
+eina_coro_init(void)
+{
+ _eina_coro_log_dom = eina_log_domain_register("eina_coro",
+ EINA_LOG_COLOR_DEFAULT);
+ if (_eina_coro_log_dom < 0)
+ {
+ EINA_LOG_ERR("Could not register log domain: eina_coro");
+ return EINA_FALSE;
+ }
+
+ eina_lock_new(&_eina_coro_lock);
+ _eina_coro_usage = 0;
+ _eina_coro_hooks_walking = 0;
+ eina_inarray_step_set(&_eina_coro_hooks, sizeof(_eina_coro_hooks),
+ sizeof(Eina_Coro_Hook), 1);
+
+ return EINA_TRUE;
+}
+
+/**
+ * @internal
+ * @brief Shut down the coroutine module.
+ *
+ * @return #EINA_TRUE on success, #EINA_FALSE on failure.
+ *
+ * This function shuts down the coroutine module set up by
+ * eina_coro_init(). It is called by eina_shutdown().
+ *
+ * @see eina_shutdown()
+ */
+Eina_Bool
+eina_coro_shutdown(void)
+{
+ eina_lock_take(&_eina_coro_lock);
+ EINA_SAFETY_ON_TRUE_GOTO(_eina_coro_usage > 0, in_use);
+ eina_lock_release(&_eina_coro_lock);
+ eina_lock_free(&_eina_coro_lock);
+ eina_inarray_flush(&_eina_coro_hooks);
+
+ eina_log_domain_unregister(_eina_coro_log_dom);
+ _eina_coro_log_dom = -1;
+ return EINA_TRUE;
+
+ in_use:
+ eina_lock_release(&_eina_coro_lock);
+ return EINA_FALSE;
+}
diff --git a/src/lib/eina/eina_coro.h b/src/lib/eina/eina_coro.h
new file mode 100644
index 0000000000..aecb16e841
--- /dev/null
+++ b/src/lib/eina/eina_coro.h
@@ -0,0 +1,554 @@
+/* EINA - EFL data type library
+ * Copyright (C) 2017 ProFUSION embedded systems
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library;
+ * if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef EINA_CORO_H_
+#define EINA_CORO_H_
+
+#include "eina_config.h"
+#include "eina_types.h"
+#include "eina_error.h"
+
+typedef struct _Eina_Future Eina_Future;
+typedef struct _Eina_Value Eina_Value;
+
+/**
+ * @addtogroup Eina_Tools_Group Tools
+ *
+ * @{
+ */
+
+/**
+ * @defgroup Eina_Coro_Group Co-routines
+ *
+ * Co-routines are cooperative threads, that is, their execution will
+ * stop the caller's thread, execute the coroutine until it finishes
+ * or yield, then give back control to the caller thread.
+ *
+ * The purpose of this primitive is to allow two functions to run with
+ * their own stack and registers and be sure that the caller thread
+ * won't run meanwhile, then shared context (variables, pointers) do
+ * not need locks as this is done implicitly by the design: if one
+ * thread is running you can be sure the other is not. The coroutine
+ * must explicitly give back control to the caller thread, either by
+ * eina_coro_yield() or by return.
+ *
+ * Multiple coroutines may exist at a given time, however if they are
+ * managed by the same caller then it's guaranteed that they will
+ * cooperate among themselves.
+ *
+ * @note The current implementation @b may use real threads with a
+ * lock and condition variable to ensure the behavior, this is
+ * an implementation detail that must not be relied upon.
+ * Depending on the platform it may use ucontext.h (SysV-like)
+ * or custom task save/restore. Libraries can use
+ * eina_coro_hook_add() to be called when the coroutine code
+ * will enter and exit, being able to retrieve context and set
+ * some other locks such or Efl_Object's efl_domain_data_adopt()
+ * (done automatically from efl_object_init()).
+ *
+ * @see @ref Eina_Thread_Group for regular concurrent threads.
+ *
+ * @since 1.21
+ * @{
+ */
+
+typedef struct _Eina_Coro Eina_Coro;
+
+/**
+ * @typedef Eina_Coro_Hook_Coro_Enter_Cb
+ *
+ * @brief Type for the definition of a coroutine hook.
+ *
+ * The pointer will be called back with the given @c data and the
+ * coroutine that will be entered or exited.
+ *
+ * The coroutine "enters" when eina_coro_run() is called and "exits"
+ * when the provided function returns or calls eina_coro_yield().
+ *
+ * If the callback returns #EINA_FALSE, then eina_coro_yield() will
+ * return that value, meaning the coroutine should voluntarily exit.
+ *
+ * All hooks are called, and if any of them returns #EINA_FALSE,
+ * eina_coro_yield() will return the same.
+ *
+ * If #EINA_FALSE is returned prior the first execution of the
+ * coroutine, then the coroutine will get a canceled #EINA_TRUE as
+ * parameter. This allows coroutine to cleanup whatever is needed
+ * and return.
+ *
+ * @since 1.21
+ */
+typedef Eina_Bool (*Eina_Coro_Hook_Coro_Enter_Cb)(void *data, const Eina_Coro *coro);
+
+/**
+ * @typedef Eina_Coro_Hook_Coro_Exit_Cb
+ *
+ * @brief Type for the definition of a coroutine exit hook.
+ *
+ * The pointer will be called back with the given @c data and the
+ * coroutine that exited.
+ *
+ * The coroutine "enters" when eina_coro_run() is called and "exits"
+ * when the provided function returns or calls eina_coro_yield().
+ *
+ * Exit hooks are always called in the reverse order they were added,
+ * that is, the last added hook will run first (stack).
+ *
+ * @since 1.21
+ */
+typedef void (*Eina_Coro_Hook_Coro_Exit_Cb)(void *data, const Eina_Coro *coro);
+
+/**
+ * @typedef Eina_Coro_Hook_Main_Enter_Cb
+ *
+ * @brief Type for the definition of a main routine hook.
+ *
+ * The pointer will be called back with the given @c data and the
+ * coroutine that will be entered or exited.
+ *
+ * The coroutine "enters" when eina_coro_run() is called and "exits"
+ * when the provided function returns or calls eina_coro_yield().
+ *
+ * Unlike the coroutine exit hooks, the main routine Exit hooks are
+ * always called in the reverse order they were added, that is, the
+ * last added hook will run first (stack). This is because they match
+ * Eina_Coro_Hook_Coro_Exit_Cb, a coroutine exits so the main routine
+ * can enter.
+ *
+ * @since 1.21
+ */
+typedef void (*Eina_Coro_Hook_Main_Enter_Cb)(void *data, const Eina_Coro *coro);
+
+/**
+ * @typedef Eina_Coro_Hook_Main_Exit_Cb
+ *
+ * @brief Type for the definition of a main routine exit hook.
+ *
+ * The pointer will be called back with the given @c data and the
+ * coroutine that exited.
+ *
+ * The coroutine "enters" when eina_coro_run() is called and "exits"
+ * when the provided function returns or calls eina_coro_yield().
+ *
+ * Unlike the coroutine enter hooks, the main routine Enter hooks are
+ * called in forward order, that is, the first added hook will run
+ * first. This is because they match Eina_Coro_Hook_Coro_Enter_Cb, a
+ * main routine exits so the coroutine can enter.
+ *
+ * If the callback returns #EINA_FALSE, then eina_coro_yield() will
+ * return that value, meaning the coroutine should voluntarily exit.
+ *
+ * All hooks are called, and if any of them returns #EINA_FALSE,
+ * eina_coro_yield() will return the same.
+ *
+ * If #EINA_FALSE is returned prior the first execution of the
+ * coroutine, then the coroutine will get a canceled #EINA_TRUE as
+ * parameter. This allows coroutine to cleanup whatever is needed
+ * and return.
+ *
+ * @since 1.21
+ */
+typedef Eina_Bool (*Eina_Coro_Hook_Main_Exit_Cb)(void *data, const Eina_Coro *coro);
+
+
+/**
+ * Adds a hook to the coroutine subsystem.
+ *
+ * The coroutine "enters" when eina_coro_run() is called and "exits"
+ * when the provided function returns or calls eina_coro_yield().
+ *
+ * The main routine (the caller) is the opposite: when eina_coro_run()
+ * it will "exit" and it will "enter" before eina_coro_run() returns.
+ *
+ * Enter hooks are executed in order, while exit hooks are always
+ * called in the reverse order they were added, that is, the last
+ * added hook will run first (stack).
+ *
+ * If any enter hooks fail, then eina_coro_yield() will return
+ * #EINA_FALSE or the parameter @c canceled as #EINA_TRUE will be
+ * given to the coroutine function. This allows coroutines to cleanup
+ * and exit.
+ *
+ * The flow is the following:
+ *
+ * @li main_exit is called to notify main routine will be stopped.
+ * @li coro_enter is called to notify the coroutine will be started.
+ * @li coro_exit is called to notify the coroutine stopped.
+ * @li main_enter is called to notify main routine will be resumed.
+ *
+ * They may be useful to properly setup environment prior to callback
+ * user code, like Efl_Object must check if we're running in an actual
+ * thread and adopt Efl_Domain_Data so objects are accessible from the
+ * coroutine -- this is done automatically by efl_object_init().
+ *
+ * @note this must @b NOT be called from within the coroutine itself,
+ * usually do this from your "init".
+ *
+ * @param coro_enter the hook to be called when the coroutine
+ * enters. May be #NULL if no enter hook is needed.
+ * @param coro_exit the hook to be called when the coroutine
+ * exits. May be #NULL if no exit hook is needed.
+ * @param main_enter the hook to be called when the caller
+ * enters. May be #NULL if no enter hook is needed.
+ * @param main_exit the hook to be called when the caller
+ * exits. May be #NULL if no exit hook is needed.
+ * @param data the context to pass to hooks. May be #NULL if no
+ * context is needed.
+ * @return #EINA_TRUE on success, #EINA_FALSE on failure.
+ *
+ * @since 1.12
+ */
+EAPI Eina_Bool eina_coro_hook_add(Eina_Coro_Hook_Coro_Enter_Cb coro_enter, Eina_Coro_Hook_Coro_Exit_Cb coro_exit, Eina_Coro_Hook_Main_Enter_Cb main_enter, Eina_Coro_Hook_Main_Exit_Cb main_exit, const void *data);
+
+/**
+ * Removes a hook from the coroutine subsystem.
+ *
+ * @note this must @b NOT be called from within the coroutine itself,
+ * usually do this from your "init".
+ *
+ * @param coro_enter the pointer that was given to eina_coro_hook_add().
+ * @param coro_exit the pointer that was given to eina_coro_hook_add().
+ * @param main_enter the pointer that was given to eina_coro_hook_add().
+ * @param main_exit the pointer that was given to eina_coro_hook_add().
+ * @param data the pointer that was given to eina_coro_hook_add().
+ * @return #EINA_TRUE on success, #EINA_FALSE on failure (ie: not found).
+ *
+ * @since 1.12
+ */
+EAPI Eina_Bool eina_coro_hook_del(Eina_Coro_Hook_Coro_Enter_Cb coro_enter, Eina_Coro_Hook_Coro_Exit_Cb coro_exit, Eina_Coro_Hook_Main_Enter_Cb main_enter, Eina_Coro_Hook_Main_Exit_Cb main_exit, const void *data);
+
+/**
+ * @typedef Eina_Coro_Cb
+ * Type for the definition of a coroutine callback function.
+ *
+ * Once eina_coro_new() returns non-NULL, this function will be always
+ * called. However it may receive @c canceled as #EINA_TRUE, in this
+ * case it should cleanup and exit as soon as possible.
+ *
+ * @note The current implementation @b may use real threads with a
+ * lock and condition variable to ensure the behavior, this is
+ * an implementation detail that must not be relied upon.
+ * Depending on the platform it may use ucontext.h (SysV-like)
+ * or custom task save/restore. Libraries can use
+ * eina_coro_hook_add() to be called when the coroutine code
+ * will enter and exit, being able to retrieve context and set
+ * some other locks such as the Python's GIL or Efl_Object's
+ * efl_domain_data_adopt() (done automatically from
+ * efl_object_init()).
+ *
+ * @since 1.21
+ */
+typedef const void *(*Eina_Coro_Cb)(void *data, Eina_Bool canceled, Eina_Coro *coro);
+
+/**
+ * @def EINA_CORO_STACK_SIZE_DEFAULT
+ *
+ * Use the system's default stack size, usually @c PTHREAD_STACK_MIN
+ * (16Kb - 16384 bytes).
+ *
+ * @since 1.21
+ */
+#define EINA_CORO_STACK_SIZE_DEFAULT 0
+
+/**
+ * @brief Creates a new coroutine.
+ *
+ * Allocates a coroutine environment using the given @a stack_size to
+ * execute @a func with the given context @a data. The coroutine must
+ * be explicitly executed with eina_coro_run(), that will stop the
+ * caller and let the coroutine work.
+ *
+ * @note The coroutine is @b not executed when it's created, that is
+ * managed by eina_coro_run().
+ *
+ * @note Currently @a stack_size is ignored, the default thread stack
+ * size will be used.
+ *
+ * @param func function to run in the coroutine. Must @b not be @c NULL.
+ * @param data context data to provide to @a func as first argument.
+ * @param stack_size defines the stack size to use to run the function
+ * @a func. Usually must be multiple of @c PAGE_SIZE and most
+ * systems will define a minimum stack limit such as 16Kb -
+ * those nuances are handled automatically for you. Most users
+ * want #EINA_CORO_STACK_SIZE_DEFAULT.
+ * @return newly allocated coroutine handle on success, #NULL on failure.
+ *
+ * @see eina_coro_run()
+ *
+ * @since 1.21
+ */
+EAPI Eina_Coro *eina_coro_new(Eina_Coro_Cb func, const void *data, size_t stack_size) EINA_ARG_NONNULL(1) EINA_WARN_UNUSED_RESULT;
+
+/**
+ * @brief Yields control from coroutine to caller.
+ *
+ * This @b must be called from within the @b coroutine and will pause it,
+ * giving back control to the caller of eina_coro_run(). The coroutine
+ * will remain stalled until eina_coro_run() is executed again.
+ *
+ * Yield can return #EINA_FALSE if any hooks fail, see
+ * eina_coro_hook_add(), or if the coroutine is canceled with
+ * eina_coro_cancel().
+ *
+ * @note The current implementation @b may use real threads with a
+ * lock and condition variable to ensure the behavior, this is
+ * an implementation detail that must not be relied upon.
+ * Depending on the platform it may use ucontext.h (SysV-like)
+ * or custom task save/restore. Libraries can use
+ * eina_coro_hook_add() to be called when the coroutine code
+ * will enter and exit, being able to retrieve context and set
+ * some other locks such as the Python's GIL or Efl_Object's
+ * efl_domain_data_adopt() (done automatically from
+ * efl_object_init()).
+ *
+ * @param coro the coroutine that will yield control. Must not be #NULL.
+ *
+ * @return #EINA_TRUE if coroutine is allowed to continue,
+ * #EINA_FALSE if the coroutine should cleanup and exit.
+ * Refer to macros eina_coro_yield_or_return() or
+ * eina_coro_yield_or_goto() to aid such tasks.
+ *
+ * @see eina_coro_run()
+ * @see eina_coro_new()
+ *
+ * @since 1.21
+ */
+EAPI Eina_Bool eina_coro_yield(Eina_Coro *coro) EINA_ARG_NONNULL(1) EINA_WARN_UNUSED_RESULT;
+
+/**
+ * #def eina_coro_yield_or_return(coro, val)
+ *
+ * This will yield control back to the main thread and wait. If the
+ * yield returns #EINA_FALSE then it will return the given value.
+ *
+ * @param coro the coroutine handle. Must not be #NULL.
+ * @param val the value to return (void *) if yield returns #EINA_FALSE.
+ *
+ * @see eina_coro_yield()
+ * @see eina_coro_yield_or_goto()
+ *
+ * @since 1.21
+ */
+#define eina_coro_yield_or_return(coro, val) \
+ do { if (!eina_coro_yield(coro)) return (val); } while (0)
+
+/**
+ * #def eina_coro_yield_or_goto(coro, label)
+ *
+ * This will yield control back to the main thread and wait. If the
+ * yield returns #EINA_FALSE then it will goto the given label.
+ *
+ * @param coro the coroutine handle. Must not be #NULL.
+ * @param label the label to jump if yield returns #EINA_FALSE.
+ *
+ * @see eina_coro_yield()
+ * @see eina_coro_yield_or_return()
+ *
+ * @since 1.21
+ */
+#define eina_coro_yield_or_goto(coro, label) \
+ do { if (!eina_coro_yield(coro)) goto label; } while (0)
+
+/**
+ * @brief Yield control from coroutine to the caller and report it's
+ * awaiting a future to be resolved.
+ *
+ * This @b must be called from within the @b coroutine and will pause
+ * it, giving back control to the caller of eina_coro_run(). The
+ * coroutine will remain stalled until eina_coro_run() is executed
+ * again. The caller of eina_coro_run() will get @c p_awaiting set so
+ * it can wait for the future to be resolved and improve its
+ * scheduling. Note that this will busy wait using eina_coro_yield()
+ * until the future resolves, then it's safe to naively call
+ * eina_coro_run() if the future is still pending -- but it will keep
+ * CPU consumption to its maximum.
+ *
+ * Await can return #EINA_FALSE if any hooks fail, see
+ * eina_coro_hook_add(), or if the coroutine is canceled with
+ * eina_coro_cancel().
+ *
+ * @note The current implementation @b may use real threads with a
+ * lock and condition variable to ensure the behavior, this is
+ * an implementation detail that must not be relied upon.
+ * Depending on the platform it may use ucontext.h (SysV-like)
+ * or custom task save/restore. Libraries can use
+ * eina_coro_hook_add() to be called when the coroutine code
+ * will enter and exit, being able to retrieve context and set
+ * some other locks such as the Python's GIL or Efl_Object's
+ * efl_domain_data_adopt() (done automatically from
+ * efl_object_init()).
+ *
+ * @param coro the coroutine that will await control. Must not be #NULL.
+ * @param future the future that this will wait. Must not be #NULL.
+ * @param[out] p_value the pointer to value containing the future
+ * resolution. If given the value will be owned by caller and
+ * must be released with eina_value_flush(). May be #NULL.
+ *
+ * @return #EINA_TRUE if coroutine is allowed to continue,
+ * #EINA_FALSE if the coroutine should cleanup and exit.
+ * Refer to macros eina_coro_await_or_return() or
+ * eina_coro_await_or_goto() to aid such tasks.
+ *
+ * @see eina_coro_run()
+ * @see eina_coro_new()
+ * @see eina_coro_yield()
+ *
+ * @since 1.21
+ */
+EAPI Eina_Bool eina_coro_await(Eina_Coro *coro, Eina_Future *future, Eina_Value *p_value) EINA_ARG_NONNULL(1, 2) EINA_WARN_UNUSED_RESULT;
+
+/**
+ * #def eina_coro_await_or_return(coro, future, p_value, val)
+ *
+ * This will yield control back to the main thread and wait for future
+ * to be resolved. If the await returns #EINA_FALSE then it will
+ * return the given value.
+ *
+ * @param coro the coroutine handle. Must not be #NULL.
+ * @param future the future that this will wait. Must not be #NULL.
+ * @param[out] p_value the pointer to value containing the future
+ * resolution. If given the value will be owned by caller and
+ * must be released with eina_value_flush(). May be #NULL.
+ * @param val the value to return (void *) if await returns #EINA_FALSE.
+ *
+ * @see eina_coro_await()
+ * @see eina_coro_await_or_goto()
+ *
+ * @since 1.21
+ */
+#define eina_coro_await_or_return(coro, future, p_value, val) \
+ do { if (!eina_coro_await(coro, future, p_value)) return (val); } while (0)
+
+/**
+ * #def eina_coro_await_or_goto(coro, label)
+ *
+ * This will yield control back to the main thread and wait for future
+ * to be resolved. If the await returns #EINA_FALSE then it will goto
+ * the given label.
+ *
+ * @param coro the coroutine handle. Must not be #NULL.
+ * @param future the future that this will wait. Must not be #NULL.
+ * @param[out] p_value the pointer to value containing the future
+ * resolution. If given the value will be owned by caller and
+ * must be released with eina_value_flush(). May be #NULL.
+ * @param label the label to jump if await returns #EINA_FALSE.
+ *
+ * @see eina_coro_await()
+ * @see eina_coro_await_or_return()
+ *
+ * @since 1.21
+ */
+#define eina_coro_await_or_goto(coro, future, p_value, label) \
+ do { if (!eina_coro_await(coro, future, p_value)) goto label; } while (0)
+
+
+/**
+ * @brief Run the coroutine and report if it's still alive.
+ *
+ * This @b must be called from @b outside the coroutine, what we call
+ * "the caller side", also known as "main thread". The caller will be
+ * paused and control will be handled to the coroutine until it
+ * eina_coro_yield() or exits (returns).
+ *
+ * If the coroutine exits (returns), then this function will free the
+ * coroutine created, make @a p_coro #NULL, set @a p_result to
+ * whatever the Eina_Coro_Cb returned and return #EINA_FALSE.
+ *
+ * If the coroutine yields, then this function will return #EINA_TRUE,
+ * meaning it must be called again.
+ *
+ * @note The current implementation @b may use real threads with a
+ * lock and condition variable to ensure the behavior, this is
+ * an implementation detail that must not be relied upon.
+ * Depending on the platform it may use ucontext.h (SysV-like)
+ * or custom task save/restore. Libraries can use
+ * eina_coro_hook_add() to be called when the coroutine code
+ * will enter and exit, being able to retrieve context and set
+ * some other locks such as the Python's GIL or Efl_Object's
+ * efl_domain_data_adopt() (done automatically from
+ * efl_object_init()).
+ *
+ * @param[inout] p_coro pointer to the coroutine that will be
+ * executed. Must @b not be #NULL.
+ * @param[out] p_result if the coroutine exited, then will be set to the
+ * returned value. May be #NULL.
+ * @param[out] p_awaiting if the coroutine is awaiting a future to be
+ * resolved, then the handle. The scheduler should consider
+ * eina_future_then() and only call the coroutine once it's
+ * resolved. This happens when coroutine used eina_coro_await()
+ * instead of eina_coro_yield().
+ *
+ * @return #EINA_TRUE if the coroutine yielded and the user must call
+ * eina_coro_run() again. #EINA_FALSE if the coroutine exited,
+ * then @a p_coro will point to #NULL and if @a p_result is
+ * given it will be set to whatever the coroutine @c func returned.
+ *
+ * @since 1.21
+ */
+EAPI Eina_Bool eina_coro_run(Eina_Coro **p_coro, void **p_result, Eina_Future **p_awaiting) EINA_ARG_NONNULL(1);
+
+/**
+ * @brief Cancel the coroutine and wait for it to finish.
+ *
+ * This @b must be called from @b outside the coroutine, what we call
+ * "the caller side", also known as "main thread". The caller will be
+ * paused and control will be handled to the coroutine until it exits
+ * (returns).
+ *
+ * This function will free the coroutine created, make @a p_coro
+ * #NULL, set @a p_result to whatever the Eina_Coro_Cb returned and
+ * return #EINA_FALSE.
+ *
+ * @note This will busy wait on eina_coro_run() until the coroutine @b
+ * voluntarily finishes, it's not aborted in any way. The
+ * primitive eina_coro_yield() will return #EINA_FALSE when the
+ * coroutine is canceled, this gives the opportunity to cleanup
+ * and exit. Likewise, if canceled before it's ever ran, then
+ * it will pass @c canceled as #EINA_TRUE to the Eina_Coro_Cb
+ * given to eina_coro_new()
+ *
+ * @note The current implementation @b may use real threads with a
+ * lock and condition variable to ensure the behavior, this is
+ * an implementation detail that must not be relied upon.
+ * Depending on the platform it may use ucontext.h (SysV-like)
+ * or custom task save/restore. Libraries can use
+ * eina_coro_hook_add() to be called when the coroutine code
+ * will enter and exit, being able to retrieve context and set
+ * some other locks such as the Python's GIL or Efl_Object's
+ * efl_domain_data_adopt() (done automatically from
+ * efl_object_init()).
+ *
+ * @param[inout] p_coro pointer to the coroutine that will be
+ * executed. Must @b not be #NULL.
+ *
+ * @return The value returned by the coroutine function, same as would
+ * be returned in eina_coro_run()'s @c p_result parameter.
+ *
+ * @since 1.21
+ */
+EAPI void *eina_coro_cancel(Eina_Coro **p_coro) EINA_ARG_NONNULL(1);
+
+/**
+ * @}
+ */
+
+/**
+ * @}
+ */
+
+#endif
diff --git a/src/lib/eina/eina_main.c b/src/lib/eina/eina_main.c
index b3a716f135..a863b04772 100644
--- a/src/lib/eina/eina_main.c
+++ b/src/lib/eina/eina_main.c
@@ -74,6 +74,7 @@
#include "eina_evlog.h"
#include "eina_freeq.h"
#include "eina_slstr.h"
+#include "eina_coro.h"
/*============================================================================*
* Local *
@@ -160,6 +161,7 @@ EAPI Eina_Inlist *_eina_tracking = NULL;
S(safepointer);
S(slstr);
S(promise);
+ S(coro);
#undef S
struct eina_desc_setup
@@ -207,6 +209,7 @@ static const struct eina_desc_setup _eina_desc_setup[] = {
S(safepointer),
S(slstr),
S(promise),
+ S(coro),
#undef S
};
static const size_t _eina_desc_setup_len = sizeof(_eina_desc_setup) /
diff --git a/src/lib/eina/eina_promise.c b/src/lib/eina/eina_promise.c
index 588c94ff2b..e01e6904bb 100644
--- a/src/lib/eina/eina_promise.c
+++ b/src/lib/eina/eina_promise.c
@@ -3,13 +3,20 @@
#endif
#include "eina_private.h"
-#include "eina_promise.h"
#include "eina_mempool.h"
-#include "eina_promise_private.h"
+#include "eina_coro.h"
#include <errno.h>
#include <stdarg.h>
#include <assert.h>
+/* undefs EINA_ARG_NONULL() so NULL checks are not compiled out! */
+#include "eina_safety_checks.h"
+
+#include "eina_promise.h"
+#include "eina_promise_private.h"
+#include "eina_value_util.h"
+
+
#define EINA_FUTURE_DISPATCHED ((Eina_Future_Cb)(0x01))
#define EFL_MEMPOOL_CHECK_RETURN(_type, _mp, _p) \
@@ -1271,3 +1278,40 @@ eina_promise_race_array(Eina_Future *array[])
_future2_array_cancel(array);
return NULL;
}
+
+EAPI Eina_Value
+eina_future_await(Eina_Future *f, Eina_Coro *coro, const Eina_Value_Type *success_type)
+{
+ Eina_Value value = EINA_VALUE_EMPTY;
+
+ if (!eina_coro_await(coro, f, &value))
+ goto log; // value will be some EINA_VALUE_TYPE_ERROR
+
+ if ((value.type != EINA_VALUE_TYPE_ERROR) &&
+ ((success_type) && (value.type != success_type)))
+ {
+ ERR("Future %p, expected success_type %p (%s), got %p (%s)",
+ f, success_type, success_type->name,
+ value.type, value.type ? value.type->name : "EMPTY");
+
+ eina_value_flush(&value);
+ value = eina_value_error_init(EINVAL);
+ }
+
+ log:
+ if (EINA_UNLIKELY(eina_log_domain_level_check(_promise2_log_dom,
+ EINA_LOG_LEVEL_DBG)))
+ {
+ if (!value.type) DBG("Awaited future %p, got empty value", f);
+ else
+ {
+ char *str = eina_value_to_string(&value);
+ DBG("Awaited future %p - Value Type: %s Contents: %s (success_type: %s)",
+ f, value.type->name, str,
+ success_type ? success_type->name : "ALL");
+ free(str);
+ }
+ }
+
+ return value;
+}
diff --git a/src/lib/eina/eina_promise.h b/src/lib/eina/eina_promise.h
index febbeef323..0b8531b13e 100644
--- a/src/lib/eina/eina_promise.h
+++ b/src/lib/eina/eina_promise.h
@@ -9,6 +9,8 @@ extern "C" {
#include "eina_types.h"
#include "eina_value.h"
+typedef struct _Eina_Coro Eina_Coro;
+
/**
* @ingroup Eina_Promise
*
@@ -1416,6 +1418,66 @@ eina_future_race_array(Eina_Future *array[])
*/
#define eina_future_chain_easy(_prev, ...) eina_future_chain_easy_array(_prev, (Eina_Future_Cb_Easy_Desc[]) {__VA_ARGS__, {NULL, NULL, NULL, NULL, NULL}})
+/**
+ * Allows an Eina_Coro to wait for a future to be resolved in a
+ * synchronous way.
+ *
+ * This is another way to use future: instead of being called back
+ * when it resolves, one can use an Eina_Coro coroutine and that will
+ * "block" waiting for the value. Note that only the coroutine is
+ * blocked, the main routine (ie: main loop) will still run so it can
+ * resolve futures.
+ *
+ * Internally it will call eina_coro_await() and if the coroutine is
+ * canceled ECANCELED is returned.
+ *
+ * Using pure Eina_Coro callbacks, it looks like:
+ *
+ * @code
+ * const void *
+ * my_coroutine(void *data, Eina_Bool canceled, Eina_Coro *coro)
+ * {
+ * Eina_Future *f = get_file_size_async("/MyFile.txt");
+ * Eina_Value v = eina_future_await(f, coro, EINA_VALUE_TYPE_INT);
+ * int size;
+ * if (v.type == EINA_VALUE_TYPE_ERROR)
+ * {
+ * Eina_Error err;
+ * eina_value_get(&v, &err);
+ * fprintf(stderr, "Could not read the file size. Reason: %s\n", eina_error_msg_get(err));
+ * ecore_main_loop_quit();
+ * return NULL;
+ * }
+ * eina_value_get(&v, &size);
+ * printf("File size is %d bytes\n", size);
+ * return NULL;
+ * }
+ * @endcode
+ *
+ * However Efl_Loop offers efl_loop_coro() to create coroutines that
+ * will be scheduled in the main loop, it's more convenient and can be
+ * used as well. The only change is the function signature.
+ *
+ * @note this is a helper over eina_coro_await(), doing type checking
+ * and returning as a value so it's easier to use.
+ *
+ * @param f A future to wait
+ * @param coro A coroutine handle. Must be called from the coroutine itself.
+ * @param success_type if non-NULL, will ensure the success value is
+ * of the given type. Note that EINA_VALUE_TYPE_ERROR will be
+ * returned, such as ECANCELED, EINVAL, ENOMEM and so on.
+ *
+ * @return A value. The caller owns it and must call
+ * eina_value_flush().
+ *
+ * @see eina_future_new()
+ * @see eina_future_then()
+ * @see eina_future_chain()
+ * @see eina_coro_new()
+ * @see eina_coro_await()
+ * @see efl_loop_coro()
+ */
+EAPI Eina_Value eina_future_await(Eina_Future *f, Eina_Coro *coro, const Eina_Value_Type *success_type) EINA_ARG_NONNULL(1, 2) EINA_WARN_UNUSED_RESULT;
/**
* @}
diff --git a/src/tests/eina/eina_suite.c b/src/tests/eina/eina_suite.c
index 8e833356f3..26e1387f0e 100644
--- a/src/tests/eina/eina_suite.c
+++ b/src/tests/eina/eina_suite.c
@@ -87,6 +87,7 @@ static const Efl_Test_Case etc[] = {
{ "Free Queue", eina_test_freeq },
{ "Util", eina_test_util },
{ "Short Lived Strings", eina_test_slstr },
+ { "Coroutines", eina_test_coro },
{ NULL, NULL }
};
diff --git a/src/tests/eina/eina_suite.h b/src/tests/eina/eina_suite.h
index 7bf643e478..f8dd388d00 100644
--- a/src/tests/eina/eina_suite.h
+++ b/src/tests/eina/eina_suite.h
@@ -74,5 +74,6 @@ void eina_test_safepointer(TCase *tc);
void eina_test_slice(TCase *tc);
void eina_test_freeq(TCase *tc);
void eina_test_slstr(TCase *tc);
+void eina_test_coro(TCase *tc);
#endif /* EINA_SUITE_H_ */
diff --git a/src/tests/eina/eina_test_coro.c b/src/tests/eina/eina_test_coro.c
new file mode 100644
index 0000000000..60479340ae
--- /dev/null
+++ b/src/tests/eina/eina_test_coro.c
@@ -0,0 +1,562 @@
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <Eina.h>
+
+#include "eina_suite.h"
+
+struct ctx {
+ int a, b;
+};
+
+#define VAL_A 1234
+#define VAL_B 4567
+#define RETVAL (void*)8901
+#define CANCELVAL (void*)0xCA
+#define COUNT 100
+
+#ifdef EINA_SAFETY_CHECKS
+struct log_ctx {
+ const char *msg;
+ const char *fnc;
+ int level;
+ Eina_Bool did;
+ Eina_Bool just_fmt;
+};
+
+/* tests should not output on success, just uncomment this for debugging */
+//#define SHOW_LOG 1
+
+static void
+_eina_test_safety_print_cb(const Eina_Log_Domain *d, Eina_Log_Level level, const char *file, const char *fnc, int line, const char *fmt, void *data, va_list args EINA_UNUSED)
+{
+ struct log_ctx *ctx = data;
+ va_list cp_args;
+ const char *str;
+
+ va_copy(cp_args, args);
+ str = va_arg(cp_args, const char *);
+ va_end(cp_args);
+
+ ck_assert_int_eq(level, ctx->level);
+ if (ctx->just_fmt)
+ ck_assert_str_eq(fmt, ctx->msg);
+ else
+ {
+ ck_assert_str_eq(fmt, "%s");
+ ck_assert_str_eq(ctx->msg, str);
+ }
+ ck_assert_str_eq(ctx->fnc, fnc);
+ ctx->did = EINA_TRUE;
+
+#ifdef SHOW_LOG
+ eina_log_print_cb_stderr(d, level, file, fnc, line, fmt, NULL, args);
+#else
+ (void)d;
+ (void)file;
+ (void)line;
+#endif
+}
+#endif
+
+
+static const void *
+coro_func_noyield(void *data, Eina_Bool canceled, Eina_Coro *coro EINA_UNUSED)
+{
+ struct ctx *ctx = data;
+
+ ck_assert_ptr_nonnull(ctx);
+ ck_assert_int_eq(ctx->a, VAL_A);
+
+ if (canceled) return CANCELVAL;
+
+ ctx->b = VAL_B;
+
+ return RETVAL;
+}
+
+START_TEST(coro_noyield)
+{
+ Eina_Coro *coro;
+ struct ctx ctx = {
+ .a = VAL_A,
+ .b = 0,
+ };
+ void *result = NULL;
+ int i = 0;
+
+ eina_init();
+
+ coro = eina_coro_new(coro_func_noyield, &ctx, EINA_CORO_STACK_SIZE_DEFAULT);
+ ck_assert_ptr_nonnull(coro);
+
+ while (eina_coro_run(&coro, &result, NULL))
+ {
+ i++;
+ ck_assert_int_le(i, 1);
+ }
+ ck_assert_ptr_null(coro);
+
+ ck_assert_int_eq(ctx.b, VAL_B);
+ ck_assert_ptr_eq(result, RETVAL);
+
+ eina_shutdown();
+}
+END_TEST
+
+static const void *
+coro_func_yield(void *data, Eina_Bool canceled, Eina_Coro *coro)
+{
+ struct ctx *ctx = data;
+ char buf[256] = "hi there";
+ int i;
+
+ ck_assert_ptr_nonnull(ctx);
+ ck_assert_int_eq(ctx->a, VAL_A);
+
+ if (canceled) return CANCELVAL;
+
+ ctx->b = 1;
+ eina_coro_yield_or_return(coro, CANCELVAL);
+
+ ctx->b = 2;
+ eina_coro_yield_or_return(coro, CANCELVAL);
+
+ for (i = 0; i < COUNT; i++) {
+ ctx->b = i * 10;
+ /* have some stuff on stack and write to it, so we validate
+ * non-thread based solutions are really saving their stack.
+ */
+ snprintf(buf, sizeof(buf), "b=%d -----------------", ctx->b);
+ eina_coro_yield_or_return(coro, CANCELVAL);
+ }
+
+ ctx->b = VAL_B;
+
+ return RETVAL;
+}
+
+START_TEST(coro_yield)
+{
+ Eina_Coro *coro;
+ struct ctx ctx = {
+ .a = VAL_A,
+ .b = 0,
+ };
+ Eina_Bool r;
+ void *result = NULL;
+ int i = 0;
+
+ eina_init();
+
+ coro = eina_coro_new(coro_func_yield, &ctx, EINA_CORO_STACK_SIZE_DEFAULT);
+ ck_assert_ptr_nonnull(coro);
+
+ r = eina_coro_run(&coro, NULL, NULL);
+ ck_assert_int_eq(r, EINA_TRUE);
+ ck_assert_int_eq(ctx.b, 1);
+
+ r = eina_coro_run(&coro, NULL, NULL);
+ ck_assert_int_eq(r, EINA_TRUE);
+ ck_assert_int_eq(ctx.b, 2);
+
+ while (eina_coro_run(&coro, &result, NULL))
+ {
+ ck_assert_int_eq(ctx.b, i * 10);
+ i++;
+ ck_assert_int_le(i, COUNT);
+ /* change caller's stack to guarantee coroutine stack is
+ * being properly persisted.
+ */
+ memset(alloca(10), 0xff, 10);
+ }
+ ck_assert_ptr_null(coro);
+ ck_assert_int_eq(i, COUNT);
+
+ ck_assert_int_eq(ctx.b, VAL_B);
+ ck_assert_ptr_eq(result, RETVAL);
+
+ eina_shutdown();
+}
+END_TEST
+
+START_TEST(coro_cancel)
+{
+ Eina_Coro *coro;
+ struct ctx ctx = {
+ .a = VAL_A,
+ .b = 0,
+ };
+ Eina_Bool r;
+
+ eina_init();
+
+ // cancel before it runs
+ coro = eina_coro_new(coro_func_yield, &ctx, EINA_CORO_STACK_SIZE_DEFAULT);
+ ck_assert_ptr_nonnull(coro);
+
+ ck_assert_ptr_eq(eina_coro_cancel(&coro), CANCELVAL);
+ ck_assert_ptr_null(coro);
+
+ // cancel after single run
+ coro = eina_coro_new(coro_func_yield, &ctx, EINA_CORO_STACK_SIZE_DEFAULT);
+ ck_assert_ptr_nonnull(coro);
+
+ r = eina_coro_run(&coro, NULL, NULL);
+ ck_assert_int_eq(r, EINA_TRUE);
+ ck_assert_int_eq(ctx.b, 1);
+
+ ck_assert_ptr_eq(eina_coro_cancel(&coro), CANCELVAL);
+ ck_assert_ptr_null(coro);
+ ck_assert_int_eq(ctx.b, 1); // it's yielding after setting b=1
+
+ eina_shutdown();
+}
+END_TEST
+
+static Eina_Bool
+_coro_hook_enter_success(void *data, const Eina_Coro *coro EINA_UNUSED)
+{
+ int *i = data;
+ (*i) += 1;
+ return EINA_TRUE;
+}
+
+static void
+_coro_hook_exit(void *data, const Eina_Coro *coro EINA_UNUSED)
+{
+ int *i = data;
+ (*i) += 100;
+}
+
+static void
+_main_hook_enter(void *data, const Eina_Coro *coro EINA_UNUSED)
+{
+ int *i = data;
+ (*i) += 10;
+}
+
+static Eina_Bool
+_main_hook_exit_success(void *data, const Eina_Coro *coro EINA_UNUSED)
+{
+ int *i = data;
+ (*i) += 1000;
+ return EINA_TRUE;
+}
+
+START_TEST(coro_hook)
+{
+ Eina_Coro *coro;
+ struct ctx ctx = {
+ .a = VAL_A,
+ .b = 0,
+ };
+ void *result = NULL;
+ int i = 0, hooks_result = 0;
+
+ eina_init();
+
+ fail_unless(eina_coro_hook_add(_coro_hook_enter_success,
+ _coro_hook_exit,
+ _main_hook_enter,
+ _main_hook_exit_success,
+ &hooks_result));
+
+ coro = eina_coro_new(coro_func_noyield, &ctx, EINA_CORO_STACK_SIZE_DEFAULT);
+ ck_assert_ptr_nonnull(coro);
+
+ while (eina_coro_run(&coro, &result, NULL))
+ {
+ i++;
+ ck_assert_int_le(i, 1);
+ }
+ ck_assert_ptr_null(coro);
+
+ ck_assert_int_eq(ctx.b, VAL_B);
+ ck_assert_ptr_eq(result, RETVAL);
+
+ fail_unless(eina_coro_hook_del(_coro_hook_enter_success,
+ _coro_hook_exit,
+ _main_hook_enter,
+ _main_hook_exit_success,
+ &hooks_result));
+
+ ck_assert_int_eq(hooks_result, 1111);
+
+ eina_shutdown();
+}
+END_TEST
+
+#ifdef EINA_SAFETY_CHECKS
+START_TEST(coro_new_null)
+{
+ Eina_Coro *coro;
+
+ eina_init();
+
+#ifdef SHOW_LOG
+ fprintf(stderr, "you should have a safety check failure below:\n");
+#endif
+ struct log_ctx lctx;
+
+#define TEST_MAGIC_SAFETY(fn, _msg) \
+ lctx.msg = _msg; \
+ lctx.fnc = fn; \
+ lctx.just_fmt = EINA_FALSE; \
+ lctx.level = EINA_LOG_LEVEL_ERR; \
+ lctx.did = EINA_FALSE
+
+ eina_log_print_cb_set(_eina_test_safety_print_cb, &lctx);
+
+ TEST_MAGIC_SAFETY("eina_coro_new", "safety check failed: func == NULL");
+
+ coro = eina_coro_new(NULL, NULL, EINA_CORO_STACK_SIZE_DEFAULT);
+ ck_assert_ptr_null(coro);
+
+ fail_unless(lctx.did);
+
+ eina_log_print_cb_set(eina_log_print_cb_stderr, NULL);
+#undef TEST_MAGIC_SAFETY
+
+ eina_shutdown();
+}
+END_TEST
+
+START_TEST(coro_yield_incorrect)
+{
+ Eina_Coro *coro;
+ struct ctx ctx = {
+ .a = VAL_A,
+ .b = 0,
+ };
+ void *result = NULL;
+ int i = 0;
+
+ eina_init();
+
+#ifdef SHOW_LOG
+ fprintf(stderr, "you should have a safety check failure below:\n");
+#endif
+ struct log_ctx lctx;
+
+#define TEST_MAGIC_SAFETY(fn, _msg) \
+ lctx.msg = _msg; \
+ lctx.fnc = fn; \
+ lctx.just_fmt = EINA_TRUE; \
+ lctx.level = EINA_LOG_LEVEL_CRITICAL; \
+ lctx.did = EINA_FALSE
+
+ eina_log_print_cb_set(_eina_test_safety_print_cb, &lctx);
+
+ coro = eina_coro_new(coro_func_noyield, &ctx, EINA_CORO_STACK_SIZE_DEFAULT);
+ ck_assert_ptr_nonnull(coro);
+
+ TEST_MAGIC_SAFETY("eina_coro_yield", "must be called from coroutine! coro=%p {func=%p data=%p turn=%s threads={%p%c %p%c} awaiting=%p}");
+ fail_if(eina_coro_yield(coro));
+ fail_unless(lctx.did);
+
+ eina_log_print_cb_set(eina_log_print_cb_stderr, NULL);
+#undef TEST_MAGIC_SAFETY
+
+ while (eina_coro_run(&coro, &result, NULL))
+ {
+ i++;
+ ck_assert_int_le(i, 1);
+ }
+ ck_assert_ptr_null(coro);
+
+ ck_assert_int_eq(ctx.b, VAL_B);
+ ck_assert_ptr_eq(result, RETVAL);
+
+ eina_shutdown();
+}
+END_TEST
+
+static const void *
+coro_func_run_incorrect(void *data, Eina_Bool canceled, Eina_Coro *coro)
+{
+#ifdef SHOW_LOG
+ fprintf(stderr, "you should have a safety check failure below:\n");
+#endif
+ struct log_ctx lctx;
+
+#define TEST_MAGIC_SAFETY(fn, _msg) \
+ lctx.msg = _msg; \
+ lctx.fnc = fn; \
+ lctx.just_fmt = EINA_TRUE; \
+ lctx.level = EINA_LOG_LEVEL_CRITICAL; \
+ lctx.did = EINA_FALSE
+
+ eina_log_print_cb_set(_eina_test_safety_print_cb, &lctx);
+
+ TEST_MAGIC_SAFETY("eina_coro_run", "must be called from main thread! coro=%p {func=%p data=%p turn=%s threads={%p%c %p%c} awaiting=%p}");
+ fail_if(eina_coro_run(&coro, NULL, NULL));
+ fail_unless(lctx.did);
+
+ eina_log_print_cb_set(eina_log_print_cb_stderr, NULL);
+#undef TEST_MAGIC_SAFETY
+
+ return coro_func_noyield(data, canceled, coro);
+}
+
+START_TEST(coro_run_incorrect)
+{
+ Eina_Coro *coro;
+ struct ctx ctx = {
+ .a = VAL_A,
+ .b = 0,
+ };
+ void *result = NULL;
+ int i = 0;
+
+ eina_init();
+
+ coro = eina_coro_new(coro_func_run_incorrect, &ctx, EINA_CORO_STACK_SIZE_DEFAULT);
+ ck_assert_ptr_nonnull(coro);
+
+ while (eina_coro_run(&coro, &result, NULL))
+ {
+ i++;
+ ck_assert_int_le(i, 1);
+ }
+ ck_assert_ptr_null(coro);
+
+ ck_assert_int_eq(ctx.b, VAL_B);
+ ck_assert_ptr_eq(result, RETVAL);
+
+ eina_shutdown();
+}
+END_TEST
+
+static Eina_Bool
+_coro_hook_enter_failed(void *data, const Eina_Coro *coro EINA_UNUSED)
+{
+ int *i = data;
+ (*i) += 2;
+ return EINA_FALSE;
+}
+
+ static Eina_Bool
+_main_hook_exit_failed(void *data, const Eina_Coro *coro EINA_UNUSED)
+{
+ int *i = data;
+ (*i) += 2000;
+ return EINA_FALSE;
+}
+
+START_TEST(coro_hook_failed)
+{
+ Eina_Coro *coro;
+ struct ctx ctx = {
+ .a = VAL_A,
+ .b = 0,
+ };
+ void *result = NULL;
+ int i = 0, hooks_result = 0;
+
+ eina_init();
+
+#ifdef SHOW_LOG
+ fprintf(stderr, "you should have a safety check failure below:\n");
+#endif
+ struct log_ctx lctx;
+
+#define TEST_MAGIC_SAFETY(fn, _msg) \
+ lctx.msg = _msg; \
+ lctx.fnc = fn; \
+ lctx.just_fmt = EINA_TRUE; \
+ lctx.level = EINA_LOG_LEVEL_ERR; \
+ lctx.did = EINA_FALSE
+
+ eina_log_print_cb_set(_eina_test_safety_print_cb, &lctx);
+
+ fail_unless(eina_coro_hook_add(_coro_hook_enter_failed,
+ _coro_hook_exit,
+ _main_hook_enter,
+ _main_hook_exit_success,
+ &hooks_result));
+
+ coro = eina_coro_new(coro_func_noyield, &ctx, EINA_CORO_STACK_SIZE_DEFAULT);
+ ck_assert_ptr_nonnull(coro);
+
+ TEST_MAGIC_SAFETY("_eina_coro_hooks_coro_enter", "failed hook enter=%p data=%p for coroutine coro=%p {func=%p data=%p turn=%s threads={%p%c %p%c} awaiting=%p}");
+
+ while (eina_coro_run(&coro, &result, NULL))
+ {
+ i++;
+ ck_assert_int_le(i, 1);
+ }
+ ck_assert_ptr_null(coro);
+
+ ck_assert_int_eq(ctx.b, 0);
+ ck_assert_ptr_eq(result, CANCELVAL);
+
+ fail_unless(lctx.did);
+
+ fail_unless(eina_coro_hook_del(_coro_hook_enter_failed,
+ _coro_hook_exit,
+ _main_hook_enter,
+ _main_hook_exit_success,
+ &hooks_result));
+
+ ck_assert_int_eq(hooks_result, 1112);
+
+ // now fail main exit
+ hooks_result = 0;
+
+ fail_unless(eina_coro_hook_add(_coro_hook_enter_success,
+ _coro_hook_exit,
+ _main_hook_enter,
+ _main_hook_exit_failed,
+ &hooks_result));
+
+ coro = eina_coro_new(coro_func_noyield, &ctx, EINA_CORO_STACK_SIZE_DEFAULT);
+ ck_assert_ptr_nonnull(coro);
+
+ TEST_MAGIC_SAFETY("_eina_coro_hooks_main_exit", "failed hook exit=%p data=%p for main routine coro=%p {func=%p data=%p turn=%s threads={%p%c %p%c} awaiting=%p}");
+
+ while (eina_coro_run(&coro, &result, NULL))
+ {
+ i++;
+ ck_assert_int_le(i, 1);
+ }
+ ck_assert_ptr_null(coro);
+
+ ck_assert_int_eq(ctx.b, 0);
+ ck_assert_ptr_eq(result, CANCELVAL);
+
+ fail_unless(lctx.did);
+
+ fail_unless(eina_coro_hook_del(_coro_hook_enter_success,
+ _coro_hook_exit,
+ _main_hook_enter,
+ _main_hook_exit_failed,
+ &hooks_result));
+
+ ck_assert_int_eq(hooks_result, 2111);
+
+ eina_log_print_cb_set(eina_log_print_cb_stderr, NULL);
+#undef TEST_MAGIC_SAFETY
+
+ eina_shutdown();
+}
+END_TEST
+#endif
+
+void
+eina_test_coro(TCase *tc)
+{
+ tcase_add_test(tc, coro_noyield);
+ tcase_add_test(tc, coro_yield);
+ // coro_await is tested in ecore_suite, so it hooks into the main loop
+ tcase_add_test(tc, coro_cancel);
+ tcase_add_test(tc, coro_hook);
+
+#ifdef EINA_SAFETY_CHECKS
+ tcase_add_test(tc, coro_new_null);
+ tcase_add_test(tc, coro_yield_incorrect);
+ tcase_add_test(tc, coro_run_incorrect);
+ tcase_add_test(tc, coro_hook_failed);
+#endif
+}