Skip to content

Commit bd96b78

Browse files
committed
Implement metacall_await for Python loader
Add support for awaiting async functions from Python port: - Implement py_loader_port_await in py_loader_port.c with resolve/reject callbacks that bridge MetaCall's async mechanism to Python Futures - Add accessor functions for asyncio_loop and thread_background_module in py_loader_impl.c/.h - Add metacall_await function and MetaCallFunction class to api.py supporting both sync and async function calls - Update find_handle to use MetaCallFunction with automatic async detection via metacall_inspect metadata - Export new symbols in __init__.py - Add comprehensive Python unit tests in test_await.py (~35 tests) - Add C++ integration tests in metacall_python_port_await_test - Update CMakeLists.txt to include new test targets - Fix missing 'import threading' in metacall_python_await_test.cpp
1 parent 85e0712 commit bd96b78

File tree

11 files changed

+1361
-37
lines changed

11 files changed

+1361
-37
lines changed

source/loaders/py_loader/include/py_loader/py_loader_impl.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ PY_LOADER_NO_EXPORT PyObject *py_loader_impl_capsule_new_null(void);
6262

6363
PY_LOADER_NO_EXPORT int py_loader_impl_initialize_asyncio_module(loader_impl_py py_impl, const int host);
6464

65+
PY_LOADER_NO_EXPORT PyObject *py_loader_impl_get_asyncio_loop(loader_impl_py py_impl);
66+
67+
PY_LOADER_NO_EXPORT PyObject *py_loader_impl_get_thread_background_module(loader_impl_py py_impl);
68+
6569
#ifdef __cplusplus
6670
}
6771
#endif

source/loaders/py_loader/source/py_loader_impl.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1989,6 +1989,16 @@ int py_loader_impl_initialize_asyncio_module(loader_impl_py py_impl, const int h
19891989
return 1;
19901990
}
19911991

1992+
PyObject *py_loader_impl_get_asyncio_loop(loader_impl_py py_impl)
1993+
{
1994+
return py_impl->asyncio_loop;
1995+
}
1996+
1997+
PyObject *py_loader_impl_get_thread_background_module(loader_impl_py py_impl)
1998+
{
1999+
return py_impl->thread_background_module;
2000+
}
2001+
19922002
int py_loader_impl_initialize_traceback(loader_impl impl, loader_impl_py py_impl)
19932003
{
19942004
(void)impl;

source/loaders/py_loader/source/py_loader_port.c

Lines changed: 204 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -562,8 +562,119 @@ static PyObject *py_loader_port_invoke(PyObject *self, PyObject *var_args)
562562
return result;
563563
}
564564

565-
// TODO
566-
#if 0
565+
/* Context passed to resolve/reject callbacks for await */
566+
typedef struct py_loader_port_await_context_type
567+
{
568+
loader_impl_py py_impl;
569+
PyObject *future;
570+
} py_loader_port_await_context;
571+
572+
static void *py_loader_port_await_resolve(void *result, void *data)
573+
{
574+
py_loader_port_await_context *ctx = (py_loader_port_await_context *)data;
575+
loader_impl impl = loader_get_impl(py_loader_tag);
576+
577+
py_loader_thread_acquire();
578+
579+
/* Convert metacall value to Python object */
580+
PyObject *py_result = NULL;
581+
582+
if (result != NULL)
583+
{
584+
py_result = py_loader_impl_value_to_capi(impl, value_type_id(result), result);
585+
}
586+
587+
if (py_result == NULL)
588+
{
589+
Py_IncRef(Py_None);
590+
py_result = Py_None;
591+
}
592+
593+
/* Get thread background module and asyncio loop */
594+
PyObject *thread_bg_module = py_loader_impl_get_thread_background_module(ctx->py_impl);
595+
PyObject *asyncio_loop = py_loader_impl_get_asyncio_loop(ctx->py_impl);
596+
597+
/* Call future_resolve(tl, future, value) */
598+
PyObject *future_resolve_func = PyObject_GetAttrString(thread_bg_module, "future_resolve");
599+
600+
if (future_resolve_func != NULL && PyCallable_Check(future_resolve_func))
601+
{
602+
PyObject *args = PyTuple_Pack(3, asyncio_loop, ctx->future, py_result);
603+
PyObject *call_result = PyObject_Call(future_resolve_func, args, NULL);
604+
605+
Py_XDecRef(call_result);
606+
Py_DecRef(args);
607+
Py_DecRef(future_resolve_func);
608+
}
609+
610+
Py_DecRef(py_result);
611+
Py_DecRef(ctx->future);
612+
613+
py_loader_thread_release();
614+
615+
free(ctx);
616+
617+
return NULL;
618+
}
619+
620+
static void *py_loader_port_await_reject(void *result, void *data)
621+
{
622+
py_loader_port_await_context *ctx = (py_loader_port_await_context *)data;
623+
loader_impl impl = loader_get_impl(py_loader_tag);
624+
625+
py_loader_thread_acquire();
626+
627+
/* Convert to Python exception object */
628+
PyObject *py_exception = NULL;
629+
630+
if (result != NULL)
631+
{
632+
py_exception = py_loader_impl_value_to_capi(impl, value_type_id(result), result);
633+
}
634+
635+
if (py_exception == NULL)
636+
{
637+
py_exception = PyExc_RuntimeErrorPtr();
638+
Py_IncRef(py_exception);
639+
}
640+
641+
/* Create an Exception instance if we got a string or other value */
642+
if (!PyExceptionInstance_Check(py_exception) && !PyExceptionClass_Check(py_exception))
643+
{
644+
PyObject *exc_args = PyTuple_Pack(1, py_exception);
645+
PyObject *new_exc = PyObject_Call(PyExc_RuntimeErrorPtr(), exc_args, NULL);
646+
Py_DecRef(exc_args);
647+
Py_DecRef(py_exception);
648+
py_exception = new_exc;
649+
}
650+
651+
/* Get thread background module and asyncio loop */
652+
PyObject *thread_bg_module = py_loader_impl_get_thread_background_module(ctx->py_impl);
653+
PyObject *asyncio_loop = py_loader_impl_get_asyncio_loop(ctx->py_impl);
654+
655+
/* Call future_reject(tl, future, exception) */
656+
PyObject *future_reject_func = PyObject_GetAttrString(thread_bg_module, "future_reject");
657+
658+
if (future_reject_func != NULL && PyCallable_Check(future_reject_func))
659+
{
660+
PyObject *args = PyTuple_Pack(3, asyncio_loop, ctx->future, py_exception);
661+
PyObject *call_result = PyObject_Call(future_reject_func, args, NULL);
662+
663+
Py_XDecRef(call_result);
664+
Py_DecRef(args);
665+
Py_DecRef(future_reject_func);
666+
}
667+
668+
Py_DecRef(py_exception);
669+
Py_DecRef(ctx->future);
670+
671+
py_loader_thread_release();
672+
673+
free(ctx);
674+
675+
return NULL;
676+
}
677+
567678
static PyObject *py_loader_port_await(PyObject *self, PyObject *var_args)
568679
{
569680
PyObject *name, *result = NULL;
@@ -573,38 +684,50 @@ static PyObject *py_loader_port_await(PyObject *self, PyObject *var_args)
573684
size_t args_size = 0, args_count;
574685
Py_ssize_t var_args_size;
575686
loader_impl impl;
687+
loader_impl_py py_impl;
576688

577689
(void)self;
578690

579691
/* Obtain Python loader implementation */
580692
impl = loader_get_impl(py_loader_tag);
693+
py_impl = loader_impl_get(impl);
694+
695+
/* Check if asyncio is initialized */
696+
PyObject *asyncio_loop = py_loader_impl_get_asyncio_loop(py_impl);
697+
698+
if (asyncio_loop == NULL)
699+
{
700+
PyErr_SetString(PyExc_RuntimeErrorPtr(), "Asyncio loop not initialized. Cannot use metacall_await.");
701+
return Py_ReturnNone();
702+
}
581703

582704
var_args_size = PyTuple_Size(var_args);
583705

584706
if (var_args_size == 0)
585707
{
586-
PyErr_SetString(PyExc_TypeErrorPtr(), "Invalid number of arguments, use it like: metacall('function_name', 'asd', 123, [7, 4]);");
708+
PyErr_SetString(PyExc_TypeErrorPtr(), "Invalid number of arguments, use it like: metacall_await('function_name', arg1, arg2, ...);");
587709
return Py_ReturnNone();
588710
}
589711

712+
/* Get function name */
590713
name = PyTuple_GetItem(var_args, 0);
591714

592-
#if PY_MAJOR_VERSION == 2
715+
#if PY_MAJOR_VERSION == 2
593716
{
594717
if (!(PyString_Check(name) && PyString_AsStringAndSize(name, &name_str, &name_length) != -1))
595718
{
596719
name_str = NULL;
597720
}
598721
}
599-
#elif PY_MAJOR_VERSION == 3
722+
#elif PY_MAJOR_VERSION == 3
600723
{
601724
name_str = PyUnicode_Check(name) ? (char *)PyUnicode_AsUTF8AndSize(name, &name_length) : NULL;
602725
}
603-
#endif
726+
#endif
604727

605728
if (name_str == NULL)
606729
{
607-
PyErr_SetString(PyExc_TypeErrorPtr(), "Invalid function name string conversion, first parameter must be a string");
730+
PyErr_SetString(PyExc_TypeErrorPtr(), "First parameter must be a string (function name)");
608731
return Py_ReturnNone();
609732
}
610733

@@ -618,7 +741,7 @@ static PyObject *py_loader_port_await(PyObject *self, PyObject *var_args)
618741

619742
if (value_args == NULL)
620743
{
621-
PyErr_SetString(PyExc_ValueErrorPtr(), "Invalid argument allocation");
744+
PyErr_SetString(PyExc_MemoryErrorPtr(), "Failed to allocate arguments");
622745
return Py_ReturnNone();
623746
}
624747

@@ -631,57 +754,103 @@ static PyObject *py_loader_port_await(PyObject *self, PyObject *var_args)
631754
}
632755
}
633756

634-
/* Execute the await */
757+
/* Create Python Future */
758+
PyObject *thread_bg_module = py_loader_impl_get_thread_background_module(py_impl);
759+
PyObject *future_create_func = PyObject_GetAttrString(thread_bg_module, "future_create");
760+
761+
if (future_create_func == NULL || !PyCallable_Check(future_create_func))
635762
{
636-
void *ret;
763+
PyErr_SetString(PyExc_RuntimeErrorPtr(), "Failed to get future_create function");
764+
Py_XDecRef(future_create_func);
765+
goto cleanup_args;
766+
}
637767

638-
py_loader_thread_release();
768+
PyObject *future_args = PyTuple_Pack(1, asyncio_loop);
769+
PyObject *future = PyObject_Call(future_create_func, future_args, NULL);
770+
Py_DecRef(future_args);
771+
Py_DecRef(future_create_func);
639772

640-
/* TODO: */
641-
/*
642-
if (value_args != NULL)
643-
{
644-
ret = metacallv_s(name_str, value_args, args_size);
645-
}
646-
else
773+
if (future == NULL)
774+
{
775+
if (PyErr_Occurred() == NULL)
647776
{
648-
ret = metacallv_s(name_str, metacall_null_args, 0);
777+
PyErr_SetString(PyExc_RuntimeErrorPtr(), "Failed to create Future");
649778
}
650-
*/
779+
goto cleanup_args;
780+
}
651781

652-
py_loader_thread_acquire();
782+
/* Create callback context */
783+
py_loader_port_await_context *ctx = (py_loader_port_await_context *)malloc(sizeof(py_loader_port_await_context));
653784

654-
if (ret == NULL)
655-
{
656-
result = Py_ReturnNone();
657-
goto clear;
658-
}
785+
if (ctx == NULL)
786+
{
787+
Py_DecRef(future);
788+
PyErr_SetString(PyExc_MemoryErrorPtr(), "Failed to allocate context");
789+
goto cleanup_args;
790+
}
659791

660-
result = py_loader_impl_value_to_capi(impl, value_type_id(ret), ret);
792+
ctx->py_impl = py_impl;
793+
ctx->future = future;
794+
Py_IncRef(future); /* Keep reference for callback */
661795

662-
value_type_destroy(ret);
796+
/* Execute the await call */
797+
py_loader_thread_release();
663798

664-
if (result == NULL)
799+
void *ret = metacall_await_s(
800+
name_str,
801+
value_args != NULL ? value_args : metacall_null_args,
802+
args_size,
803+
py_loader_port_await_resolve,
804+
py_loader_port_await_reject,
805+
ctx);
806+
807+
py_loader_thread_acquire();
808+
809+
/* Check for immediate errors (e.g., function not found) */
810+
if (ret != NULL && value_type_id(ret) == TYPE_THROWABLE)
811+
{
812+
PyObject *error = py_loader_impl_value_to_capi(impl, TYPE_THROWABLE, ret);
813+
if (error != NULL)
665814
{
666-
result = Py_ReturnNone();
667-
goto clear;
815+
PyErr_SetObject(PyExc_RuntimeErrorPtr(), error);
816+
Py_DecRef(error);
668817
}
818+
else
819+
{
820+
PyErr_SetString(PyExc_RuntimeErrorPtr(), "Async call failed");
821+
}
822+
Py_DecRef(future);
823+
Py_DecRef(ctx->future);
824+
free(ctx);
825+
value_type_destroy(ret);
826+
result = Py_ReturnNone();
827+
goto cleanup_args;
669828
}
670829

671-
clear:
830+
if (ret != NULL)
831+
{
832+
value_type_destroy(ret);
833+
}
834+
835+
result = future;
836+
837+
cleanup_args:
672838
if (value_args != NULL)
673839
{
840+
py_loader_thread_release();
841+
674842
for (args_count = 0; args_count < args_size; ++args_count)
675843
{
676844
value_type_destroy(value_args[args_count]);
677845
}
678846

847+
py_loader_thread_acquire();
848+
679849
free(value_args);
680850
}
681851

682852
return result;
683853
}
684-
#endif
685854

686855
static PyObject *py_loader_port_inspect(PyObject *self, PyObject *args)
687856
{
@@ -925,6 +1094,8 @@ static PyMethodDef metacall_methods[] = {
9251094
"Get information about all loaded objects." },
9261095
{ "metacall", py_loader_port_invoke, METH_VARARGS,
9271096
"Call a function anonymously." },
1097+
{ "metacall_await", py_loader_port_await, METH_VARARGS,
1098+
"Call an async function and return a Future." },
9281099
{ "metacall_value_create_ptr", py_loader_port_value_create_ptr, METH_VARARGS,
9291100
"Create a new value of type Pointer." },
9301101
{ "metacall_value_reference", py_loader_port_value_reference, METH_VARARGS,

source/ports/py_port/metacall/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,15 @@
1717
# See the License for the specific language governing permissions and
1818
# limitations under the License.
1919

20-
from metacall.api import metacall, metacall_load_from_file, metacall_load_from_memory, metacall_load_from_package, metacall_inspect, metacall_value_create_ptr, metacall_value_reference, metacall_value_dereference
20+
from metacall.api import (
21+
metacall,
22+
metacall_load_from_file,
23+
metacall_load_from_memory,
24+
metacall_load_from_package,
25+
metacall_inspect,
26+
metacall_value_create_ptr,
27+
metacall_value_reference,
28+
metacall_value_dereference,
29+
metacall_await,
30+
MetaCallFunction
31+
)

0 commit comments

Comments
 (0)