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:
- 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).
- Provide a DuktapeCommandMap
m_cmdmap in ovms_duktape.{h,cpp} OvmsDuktape
class that stores a mapping from OvmsCommand
to DuktapeConsoleCommand.
- 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.
- 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.
- 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).
- 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.
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:
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@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@lists.openvehicles.com
http://lists.openvehicles.com/mailman/listinfo/ovmsdev
_______________________________________________
OvmsDev mailing list
OvmsDev@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@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