[Ovmsdev] Duktape Persistent Function References
Michael Balzer
dexter at expeedo.de
Mon Sep 7 02:55:10 HKT 2020
Mark,
> Still struggling with this. It seems like your DuktapeObject will do
> this, but I can’t work out how it works.
I admit my documentation has some shortcomings. I'll try to fill that
gap first:
*_Concept #1: Reference counting_*
A DuktapeObject is meant to be a C++/OS extension of a Javascript
object, for example to hold some system binding associated with the JS
object.
The primary goal is to provide asynchronous operations on the system
side, initiating JS callbacks when finishing/failing. For example a HTTP
operation can be started by a script, passing some "done" callback. The
system then starts the network operation asynchronously, the JS task can
continue processing other scripts. Once the network operation is done,
the DuktapeObject sends a request to the Duktape task to execute the
"done" callback on itself.
So the DuktapeObject is normally shared by multiple contexts and
parallel operations. Asynchronous operation also means the JS object or
context may already be gone when the operation finishes. So the
DuktapeObject needs to stay locked in memory independent of the JS context.
That's implemented by counting the active references to the
DuktapeObject (methods Ref() / Unref()). The last Unref() automatically
deletes the DuktapeObject instance.
For example: JS requests a network operation. The initial JS binding
(see coupling) sets the reference count to 1. The DuktapeObject starts
the network request, increasing the ref count to 2. If the JS context
now dies (decreasing the reference count), the DuktapeObject will still
remain valid until the network operation returns.
As the Ref/Unref operations need a (recursive) mutex, that's also part
of the DuktapeObject and exposed by the API for other uses: Lock() /
Unlock().
*_Concept #2: Coupling_*
Javascript does not have a destructor concept. JS objects get deleted by
heap destruction or by the garbage collector when no reference to the JS
object is left. In both cases, the actual object deletion is called
"finalization", and a special finalizer method/callback can be installed
on a JS object to be called just before the object gets deallocated.
That is done by the DuktapeObject::Couple() method (implicitly called
when constructed directly with a JS object reference).
There is no way to force finalization on a JS object. So a DuktapeObject
cannot tell Duktape to delete it's coupled object, that means a
DuktapeObject should normally not be deleted from outside the Duktape
context, at least not if still coupled to the JS object. Coupling and
decoupling can only be done in the Duktape context.
The standard finalizer DuktapeObject::Finalizer() simply decouples,
automatically deleting itself if the coupling was the last reference.
This is a virtual method, so can be overridden as necessary.
The coupling operation additionally adds a hidden pointer to the
DuktapeObject instance in the JS object. That allows to check for and
retreive associated DuktapeObject instances from any JS object, which is
provided by the GetInstance() call.
*_Concept #3: Registration_*
For asynchronous operations, it's normally very convenient to have a
"fire & forget" API. Example from the documentation:
VFS.Save({
path: "/sd/mydata/telemetry.json",
data: Duktape.enc('jx', telemetry),
fail: function(error) {
print("Error saving telemetry: " + error);
}
});
I.e. you simply pass the operation arguments including the done/fail
callbacks to an API method and don't need to care about storing a
reference to some handle. In JS that normally means the object used
won't have any reference left after the call, so would be deleted by the
garbage collector on the next run.
To avoid garbage collection and lock the JS object in memory, we need to
store a reference to it in a "public" place. Duktape provides a special
public place for this, hidden from scripts, called the global stash.
DuktapeObject maintains a dedicated global object registry in that stash.
Adding and removing the coupled object reference to/from that registry
is done by the Register() and Deregister() methods.
So for asynchronous system operations, or system integrations that shall
be persistent, you normally do a Register() call together with the
coupling, unless some ressource isn't available. Deregistration is then
normally done when all pending JS callbacks have been executed, or when
the persistent system integration has been unbound.
Other API designs are possible here: if you'd rather like the script
needing to store a reference to your operation handle, you don't need to
do a registration. The object will then be deleted (finalized) by the
garbage collector automatically after the script deletes the reference.
*_Concept #4: Callback invocation_*
Triggers on the system side, for example a finished or failed network
operation, shall normally trigger a JS method execution.
JS callback methods are simply passed as part of the arguments object in
modern JS APIs. This allows to pass simple function definitions inline,
as well as to reference a separately defined general handler function.
JS allows functions to be excuted in the context of any object, and
callbacks normally are executed in the context of the API object. This
adds even more convenience, as the callbacks can easily access the other
API arguments still stored in the object, as well as additional data
added by the call.
JS callbacks cannot be executed directly from any system context, they
need to run in the Duktape context. So the DuktapeObject callback
invocation mechanism includes a general method to request a callback
execution by Duktape: RequestCallback()
Note: RequestCallback() is an asynchronous operation. A synchronous
variant can be added if necessary (and probably will be for command
execution from a console).
A pending callback automatically increments the reference count, so the
object is locked in memory until the callback has been executed (or
aborted) by the Duktape task.
The callback invocation API provides a void* for simple data (e.g. a
fixed string) to be passed to the callback method, but for more complex
data, you will normally fill some DuktapeObject member variables before
invoking the callback.
In Duktape context, the callback invocation translates the data returned
or provided by the system side into the Duktape callback arguments and
then runs the callback (if the object actually has the requested
callback set). The default implementation for this is
DuktapeObject::CallMethod(), which can be used directly for simple
callbacks without arguments. For more complex handling, override this
with your custom implementation.
The callbacks are by default executed on the coupled JS object, so data
can also be transported by setting properties on that object. The
callback can then simply access them via "this".
To simplify callback invocation from code parts that may run outside or
inside Duktape, it's convenient to allow calling CallMethod() without a
Duktape context, and let CallMethod() translate that into a
RequestCallback() call as necessary. Pattern:
duk_ret_t DuktapeHTTPRequest::CallMethod(duk_context *ctx, const char*
method, void* data /*=NULL*/)
{
if (!ctx)
{
RequestCallback(method, data);
return 0;
}
…
A CallMethod() implementation isn't limited to executing a single
callback. A common example is an API defining "done" & "fail" callbacks,
as well as a general final "always" callback.
DuktapeHTTPRequest::CallMethod() also serves as an example
implementation for this.
Wow… that's become more to write & read than I expected. Please provide
some feedback: is that explanation sufficient & clear? I'll refine it
for the developer docs then.
Regards,
Michael
Am 01.09.20 um 19:52 schrieb Michael Balzer:
> Mark,
>
> I'll have a look.
>
> Regards,
> Michael
>
>
> Am 01.09.20 um 07:30 schrieb Mark Webb-Johnson:
>> Michael,
>>
>> Still struggling with this. It seems like your DuktapeObject will do
>> this, but I can’t work out how it works.
>>
>> Here are some notes one what I have done so far:
>>
>> 1. Created a stub DuktapeConsoleCommand (derived from DuktapeObject)
>> in ovms_duktape.{h,cpp}. This should hold enough to be able to
>> call the javascript callback method for that object. It also
>> stores the module filename (so the registration can be removed
>> when the module is unloaded).
>>
>> 2. Provide a DuktapeCommandMap m_cmdmap in
>> ovms_duktape.{h,cpp} OvmsDuktape class that stores a mapping from
>> OvmsCommand to DuktapeConsoleCommand.
>>
>> 3. Created a OvmsDuktape::RegisterDuktapeConsoleCommand in
>> ovms_duktape.{h,cpp) that (a) creates the OvmsCommand() object,
>> (b) registers it, (c) creates the DuktapeConsoleCommand() object,
>> and (d) updates a map from OvmsCommand->DuktapeConsoleCommand.
>> There Is also a single callback DukOvmsCommandRegisterRun
>> designed to be run by all.
>>
>> 4. Created hooks NotifyDuktapeModuleLoad, NotifyDuktapeModuleUnload,
>> and NotifyDuktapeModuleUnloadAll in OvmsDuktape. The javascript
>> module is identified by filename (path to module or script on
>> vfs, usually, but may also be an internal module). The Unload
>> functions look through the m_cmdmap and unregister commands for
>> javascript modules being unloaded.
>>
>> 5. Provide an implementation for ovms_command DukOvmsCommandRegister
>> to support registering commands from Javascript modules. This
>> should extract the details, and then call
>> OvmsDuktape::RegisterDuktapeConsoleCommand to do the actual
>> registration. This has been implemented, except for the callback
>> method (and somehow passing that method from Javascript in the
>> OvmsCommand.Register javascript call).
>>
>> 6. Provide a stub implementation for DukOvmsCommandRegisterRun. This
>> uses m_cmdmap to lookup the DuktapeConsoleCommand object for
>> the command to be run. It should execute the callback method (but
>> that part is not yet implemented).
>>
>>
>>
>> I still need help with #5 and #6. What needs to be implemented
>> in DuktapeConsoleCommand, and how is the parameter
>> in OvmsCommand.Register used to store the callback (#5)? Then how to
>> callback the command method from DukOvmsCommandRegisterRun (#6)? If
>> you have time, it is probably much quicker for you to simply make
>> those changes.
>>
>> An alternative implementation would be to do something like
>> the pubsub framework, where the mapping command->callback is done
>> from within a javascript module. That I could do, but it seems
>> your DuktapeObject can do it better.
>>
>> Thanks, Mark.
>>
>>> On 15 Jul 2020, at 3:34 PM, Michael Balzer <dexter at expeedo.de
>>> <mailto:dexter at expeedo.de>> wrote:
>>>
>>> Mark,
>>>
>>> yes, I needed that persistence for the HTTP and VFS classes, but I
>>> also needed to be able to couple a dynamic C++ instance with a JS
>>> object and have a mechanism to prevent garbage collection while the
>>> C++ side is still in use. If the C++ side is no longer needed, the
>>> JS finalizer also needs to imply the C++ instance can be deleted.
>>>
>>> That is all implemented by DuktapeObject. DuktapeObject also
>>> provides JS method invocation on the coupled JS object and a mutex
>>> for concurrency protection.
>>>
>>> We probably need some more framework documentation than the header
>>> comments (applies to all of our framework classes…):
>>>
>>> /***************************************************************************************************
>>> * DuktapeObject: coupled C++ / JS object
>>> *
>>> * Intended for API methods to attach internal API state to a JS
>>> object and provide
>>> * a standard callback invocation interface for JS objects in
>>> local scopes.
>>> *
>>> * - Override CallMethod() to implement specific method calls
>>> * - Override Finalize() for specific destruction in JS context
>>> (garbage collection)
>>> * - call Register() to prevent normal garbage collection (but not
>>> heap destruction)
>>> * - call Ref() to protect against deletion (reference count)
>>> * - call Lock() to protect concurrent access (recursive mutex)
>>> *
>>> * - GetInstance() retrieves the DuktapeObject associated with a JS
>>> object if any
>>> * - Push() pushes the JS object onto the Duktape stack
>>> *
>>> * Note: the DuktapeObject may persist after the JS object has been
>>> finalized, e.g.
>>> * if some callbacks are pending after the Duktape heap has been
>>> destroyed.
>>> * Use IsCoupled() to check if the JS object is still available.
>>> *
>>> * Ref/Unref:
>>> * Normal life cycle is from construction to finalization.
>>> Pending callbacks extend
>>> * the life until the last callback has been processed. A
>>> subclass may extend the life
>>> * by calling Ref(), which increases the reference count. Unref()
>>> deletes the instance
>>> * if no references are left.
>>> */
>>>
>>> You normally just need to use Register/Deregister & Ref/Unref, and
>>> to implement the constructor and CallMethod. Coupling of the
>>> instances normally is done on construction, as a JS object is
>>> normally already needed for the parameters and can simply be
>>> attached to.
>>>
>>> Have a look at DuktapeHTTPRequest, DuktapeVFSLoad and
>>> DuktapeVFSSave, these are the current subclasses using this.
>>>
>>> For the command registration I would probably couple the OvmsCommand
>>> instance with a JS command object providing an execution method.
>>>
>>> Tell me if you need more info.
>>>
>>> Regards,
>>> Michael
>>>
>>>
>>> Am 15.07.20 um 08:12 schrieb Mark Webb-Johnson:
>>>>
>>>> @Michael this is probably for you.
>>>>
>>>> I am trying to implement javascript command registration. The idea
>>>> is that a javascript module can call something like:
>>>>
>>>> OvmsCommand.Register(basecommand, name, title, callbackfn,
>>>> usage, min, max)
>>>>
>>>>
>>>> Then we reflect that into MyCommandApp.RegisterCommand, and keep a
>>>> track of which command is for which javascript callbackfn. When the
>>>> command is executed, we pass it into duktape.
>>>>
>>>> I also have tracking for javascript module loading and unloading,
>>>> so I can DeregisterCommand() if duktape is reloaded (and also
>>>> protected against commands being registered in short-lived scripts
>>>> run from the command line).
>>>>
>>>> To implement this, I need to store the callbackfn as a persistent
>>>> reference to a duktape javascript function.
>>>>
>>>> The issue with callback function references in duktape is
>>>> summarised here:
>>>>
>>>> https://wiki.duktape.org/howtonativepersistentreferences
>>>>
>>>> /When a Duktape/C function is called, Duktape places the call
>>>> arguments on the value stack. While the arguments are on the
>>>> value stack, they're guaranteed to be reachable and the
>>>> Duktape/C function can safely work with the arguments.
>>>>
>>>> However, when the Duktape/C function returns, the value stack
>>>> is unwound and references in the function's value stack frame
>>>> are lost. If the last reference to a particular value was in
>>>> the function's value stack frame, the value will be garbage
>>>> collected when the function return is processed./
>>>>
>>>>
>>>> The standard approach is to store the reference back in the
>>>> duktape duk_push_global_stash so it won’t get garbage-collected.
>>>> But, that seems messy.
>>>>
>>>> I see that Michael has already implemented something that seems
>>>> similar in ovms_script.{h, cpp}, for the async http callbacks.
>>>> Presumably to avoid this issue. But, the approach seems very
>>>> different, and I am not sure if it is stopping _all_ garbage
>>>> collection for the duration of the async query, or just that
>>>> particular object being garbage collected. The work seems extensive
>>>> (quite a few objects involved).
>>>>
>>>> So @Michael, any suggestions for this? I don’t want to reinvent the
>>>> wheel...
>>>>
>>>> Regards, Mark.
>>>>
>>>> _______________________________________________
>>>> OvmsDev mailing list
>>>> OvmsDev at lists.openvehicles.com
>>>> http://lists.openvehicles.com/mailman/listinfo/ovmsdev
>>>
>>> --
>>> Michael Balzer * Helkenberger Weg 9 * D-58256 Ennepetal
>>> Fon 02333 / 833 5735 * Handy 0176 / 206 989 26
>>> _______________________________________________
>>> OvmsDev mailing list
>>> OvmsDev at lists.openvehicles.com <mailto:OvmsDev at lists.openvehicles.com>
>>> http://lists.openvehicles.com/mailman/listinfo/ovmsdev
>>
>>
>> _______________________________________________
>> OvmsDev mailing list
>> OvmsDev at lists.openvehicles.com
>> http://lists.openvehicles.com/mailman/listinfo/ovmsdev
>
> --
> Michael Balzer * Helkenberger Weg 9 * D-58256 Ennepetal
> Fon 02333 / 833 5735 * Handy 0176 / 206 989 26
>
> _______________________________________________
> OvmsDev mailing list
> OvmsDev at lists.openvehicles.com
> http://lists.openvehicles.com/mailman/listinfo/ovmsdev
--
Michael Balzer * Helkenberger Weg 9 * D-58256 Ennepetal
Fon 02333 / 833 5735 * Handy 0176 / 206 989 26
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.openvehicles.com/pipermail/ovmsdev/attachments/20200906/67612d35/attachment.htm>
More information about the OvmsDev
mailing list