Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lib: Introduce async library #238

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open

lib: Introduce async library #238

wants to merge 7 commits into from

Conversation

IdWV
Copy link

@IdWV IdWV commented Oct 14, 2024

This library adds setTimout(), setInterval(), setImmediate(), clearTimeout(), Promise(), PromiseAll(), PromiseAny(), PromiseRace(), PromiseAllSettled(), throw(), uptime() and PumpEvents().
And it adds a c API to offload work to another thread. This is (partly) demonstrated in examples/async-worker.c. This c API adds no link-time dependencies, everything is handled through a single pointer in uc_vm_t. everything is handled through a registry entry.

The script functions (are supposed to) behave the same as their ECMAscript counterparts, except Promise() and throw(). I couldn't find a way to provide two functions (resolve,reject), so that became (resolver), where resolver has the methods resolve() and reject().
throw() is a function, not a keyword.

The uc_vm_t struct is expanded with one pointer, and I injected 2 inline functions using that pointer in libucode. One in uc_vm_execute() after the central uc_vm_execute_chunk() to pump events until no more timers and promises are pending,
and one in uc_vm_free(). To my surprise the size of libucode didn't change (on x86-64).

It's also possible to explicitly pump inside the script, using PumpEvents(), which in that case can also be used as delay() function.

Cleaned up
Documentation added.
@jow-
Copy link
Owner

jow- commented Oct 16, 2024

A couple of remarks:

  • In uc_vm_t there's a _reserved member holding the space of a former vector struct which is not used anymore, it has the layout struct { size_t; void *; }. You could change it like this to reuse the existing space and not change the uc_vm_t layout in order to retain ABI compatibility:
diff --git a/include/ucode/types.h b/include/ucode/types.h
index c0ccd38..755f8f8 100644
--- a/include/ucode/types.h
+++ b/include/ucode/types.h
@@ -308,7 +308,8 @@ struct uc_vm {
        uc_source_t *sources;
        uc_weakref_t values;
        uc_resource_types_t restypes;
-       char _reserved[sizeof(uc_modexports_t)];
+       size_t _reserved;
+       struct uc_async_manager *async_manager;
        union {
                uint32_t u32;
                int32_t s32;

Besides that I wonder, what's preventing you from storing the async manager in the VM registry? You can wrap arbitrary C data pointers using:

/* double registration? */
if (uc_vm_registry_exists(vm, "async.manager"))
    return; 

struct uc_async_manager *manager = xalloc(...);
uc_value_t *uv_manager = ucv_resource_new(NULL, manager);

uc_vm_registry_set(vm, "async.manager", uv_manager);

And later in your callbacks:

struct uc_async_manager *manager = ucv_resource_data(uc_vm_registry_get(vm, "async.manager"), NULL);
  • In order to achieve something resembling closures (to be able to pass a resolveFn() and rejectFn() as argument to the promise function) you could store additional bound context after the function name and access it through the vm callframe later:
uc_value_t *
create_c_closure(const char *name, uc_cfn_ptr_t cfunc, void *context)
{
    size_t namelen = strlen(name);
    uc_cfunction_t *cfn = xalloc(sizeof(*cfn) + ALIGN(namelen + 1) + sizeof(context));
    
    cfn->header.type = UC_CFUNCTION;
    cfn->header.refcount = 1;
    cfn->cfn = cfunc;
    memcpy(cfn->name, name, namelen);
    memcpy(cfn->name + ALIGN(namelen + 1), &context, sizeof(context));
    
    return &cfg->header;
}

static uc_value_t *
my_resolve_callback(uc_vm_t *vm, size_t nargs)
{
    // get handle to this function as ucode value
    uc_cfunction_t *callee = uc_vector_last(&vm->callframes)->cfunction;
    void *context = *(void *)(callee->name + ALIGN(strlen(callee->name) + 1));
    
    ...
}

// In code invoking the promise function:
uc_vm_stack_push(vm, ucv_get(promise_func));
uc_vm_stack_push(vm, create_c_closure("resolveFn", my_resolve_callback, promise_context));
uc_vm_stack_push(vm, create_c_closure("rejectFn", my_reject_callback, promise_context));

if (uc_vm_call(vm, false, 2) != EXCEPTION_NONE) {
    // do something with exception
}

// etc

@IdWV
Copy link
Author

IdWV commented Oct 16, 2024

In uc_vm_t there's a _reserved member holding the space of a former vector struct which is not used anymore, it has the > layout struct { size_t; void *; }. You could change it like this to reuse the existing space and not change the uc_vm_t layout > in order to retain ABI compatibility:

OK, I can change that.

Besides that I wonder, what's preventing you from storing the async manager in the VM registry?

3 reasons. When I wrote the main part of this code, I wasn't aware of this registry functions. Then, speed. The 'conversion' from vm to async_manager this way costs almost nil, while a registry lookup is as far as I can see a hashtable lookup, which is considerable more expensive.
The 3rd reason is lifetime and cleaning up. When the event pump exits normally, the async_manager destroys itself (and then there is not much to cleanup). When an exception occurs, this cannot be done (I don't know if that exception can be caught) and am I dependent on the function I injected in uc_vm_free(). Using the destroy function of a resource type could be an option, but I don't know if all the cleaning can be done using an half-destroyed vm (which pointer I should store myself, as the cleanup function doesn't get a vm pointer). Part of the cleaning is calling external functions which are queued to be executed in the event pump.

In order to achieve something resembling closures (to be able to pass a resolveFn() and rejectFn() as argument
to the promise function) you could store additional bound context after the function name and access it through the vm > callframe later:

That is a neat trick! I suppose it's not possible to inject a destroy function either? I can keep a list of pending resolvers, which are destroyed when one of the 2 functions is called, but how to know if the promise is abandoned?

IdWV and others added 2 commits October 18, 2024 17:37
…hanged the internals of async.c to rather pass a async_manager_t pointer than a uc_vm_t one.
@IdWV
Copy link
Author

IdWV commented Oct 19, 2024

Removed the async-manager pointer from uc_vm_t, now it's stored in the registry. To mitigate the speed penalty internally in async.so everywhere where possible an async_manager_t pointer is passed, instead of a uc_vm_t pointer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants