Skip to content
This repository has been archived by the owner on Aug 31, 2021. It is now read-only.

[[ HTML5Callbacks ]] Allow specified LiveCode handlers to be called from JavaScript #5602

Merged
merged 8 commits into from Jun 14, 2017
27 changes: 27 additions & 0 deletions docs/notes/feature-html5-callbacks.md
@@ -0,0 +1,27 @@
# HTML5 Callbacks - enable calling handlers in LiveCode emscripten standalones from JavaScript

**Note** This is still an experimental feature - details may change as development continues.

## Stack setup

Each stack can be configured to expose its handlers & functions to JavaScript. This is done through a custom property of the stack - `cJavascriptHandlers`. (This will be replaced in the future by a `javascriptHandlers` property which will then be a reserved keyword). The `cJavascriptHandlers` property is a return-delimited list of handler names. The named handlers do not have to be defined at the time the stack is loaded, however calling an undefined handler from JavaScript will result in an error.


## Calling from JavaScript

The standalone engine will create a `liveCode` object on the DOM `document` object. To this object will be attached the `findStackWithName` method that can be called to return a JavaScript stack object. Each stack object will have methods corresponding to the exposed handlers of that stack. For instance, a stack with the `cJavascriptHandlers` property set to :
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we make the name 'livecode' - I think liveCode will be prone to errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

liveCode is consistent with the naming in the browser widget, although the usage there is slightly different

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's fine - we'll keep it consistent for now - we can add an alias at a later date.


```performAction
setProperty
getProperty
```

will have methods named accordingly, which when executed will call the corresponding handler with the provided arguments.

## JavaScript Example:

```var myStack = document.liveCode.findStackWithName(“HTMLTest”);
var oldDocTitle = myStack.getProperty(‘documentTitle’);
myStack.setProperty(‘documentTitle’, ‘Important Document’);
myStack.performAction(‘sendEmail’);
```
175 changes: 174 additions & 1 deletion engine/src/em-system.cpp
Expand Up @@ -37,7 +37,12 @@ along with LiveCode. If not see <http://www.gnu.org/licenses/>. */
#include <sys/time.h>
#include <emscripten.h>

// These functions are implemented in javascript
/* ----------------------------------------------------------------
* Functions implemented in em-system.js
* ---------------------------------------------------------------- */

extern "C" bool MCEmscriptenSystemInitializeJS(void);
extern "C" void MCEmscriptenSystemFinalizeJS(void);
extern "C" int32_t MCEmscriptenSystemEvaluateJavaScript(const unichar_t* p_script, size_t p_script_length, unichar_t** r_result, size_t* r_result_length);

/* ================================================================
Expand Down Expand Up @@ -110,12 +115,14 @@ MCEmscriptenSystem::Initialize()
IO_stdout = OpenFd(1, kMCOpenFileModeWrite);
IO_stderr = OpenFd(2, kMCOpenFileModeWrite);

MCEmscriptenSystemInitializeJS();
return true;
}

void
MCEmscriptenSystem::Finalize()
{
MCEmscriptenSystemFinalizeJS();
}

/* ----------------------------------------------------------------
Expand Down Expand Up @@ -1156,3 +1163,169 @@ MCEmscriptenSystem::ShowMessageDialog(MCStringRef p_title,
t_message_u16.Lock(p_message);
MCEmscriptenDialogShowAlert(t_message_u16.Ptr(), t_message_u16.Size());
}

///////////

#include "dispatch.h"

MCStack* _emscripten_get_stack(void *p_stack_ptr)
{
MCStack *t_stacks = MCdispatcher->getstacks();

MCStack *t_stack = t_stacks;
do
{
if (t_stack == p_stack_ptr)
return t_stack;

t_stack = t_stack->next();
}
while (t_stack != t_stacks);

return nil;
}

extern "C" MC_DLLEXPORT_DEF
MCProperListRef MCEmscriptenSystemGetJavascriptHandlersOfStack(void *p_stack)
{
MCStackHandle t_stack(_emscripten_get_stack(p_stack));

// Ensure stack pointer is valid
if (!t_stack.IsValid())
return MCValueRetain(kMCEmptyProperList);

bool t_success = true;

// get javascripthandlers property
MCExecValue t_value;
MCExecContext ctxt(nil, nil, nil);
bool t_lock_messages = MClockmessages;
MClockmessages = true;
t_success = t_stack->getcustomprop(ctxt, t_stack->getdefaultpropsetname(), MCNAME("cjavascripthandlers"), nil, t_value);
MClockmessages = t_lock_messages;

MCAutoStringRef t_prop_string;
if (t_success)
{
MCExecTypeConvertAndReleaseAlways(ctxt, t_value.type, &t_value, kMCExecValueTypeStringRef, &(&t_prop_string));
t_success = *t_prop_string != nil;
}

MCAutoProperListRef t_list;
if (t_success)
t_success = MCStringSplitByDelimiter(*t_prop_string, kMCLineEndString, kMCStringOptionCompareExact, &t_list);

if (!t_success)
return MCValueRetain(kMCEmptyProperList);

return t_list.Take();
}

extern void MCEngineFreeScriptParameters(MCParameter* p_params);
extern bool MCEngineConvertToScriptParameters(MCExecContext& ctxt, MCProperListRef p_arguments, MCParameter*& r_script_params);

extern Exec_stat _MCEngineExecDoDispatch(MCExecContext &ctxt, int p_handler_type, MCNameRef p_message, MCObjectPtr *p_target, MCParameter *p_parameters);

extern "C" MC_DLLEXPORT_DEF
MCStringRef MCEmscriptenSystemCallStackHandler(void *p_stack, MCStringRef p_handler, MCProperListRef p_params)
{
bool t_success = true;

// Ensure stack pointer is valid
MCStackHandle t_stack;

if (t_success)
{
t_stack = _emscripten_get_stack(p_stack);
t_success = t_stack.IsValid();
}

// Ensure handler is allowed to be called from JavaScript
if (t_success)
{
MCAutoProperListRef t_handler_list;
t_handler_list.Give(MCEmscriptenSystemGetJavascriptHandlersOfStack(p_stack));
bool t_found = false;
for (uint32_t i = 0; !t_found && i < MCProperListGetLength(*t_handler_list); i++)
{
MCStringRef t_handler = static_cast<MCStringRef>(MCProperListFetchElementAtIndex(*t_handler_list, i));
t_found = MCStringIsEqualTo(t_handler, p_handler, kMCStringOptionCompareCaseless);
}

t_success = t_found;
}

MCNewAutoNameRef t_handler_name;
if (t_success)
t_success = MCNameCreate(p_handler, &t_handler_name);

MCExecContext ctxt;
MCAutoCustomPointer<MCParameter, MCEngineFreeScriptParameters> t_params;
if (t_success)
t_success = MCEngineConvertToScriptParameters(ctxt, p_params, &t_params);

if (t_success)
{
MCObjectPtr t_stack_obj(t_stack, 0);
MCresult->clear();

Exec_stat t_stat;
// try to dispatch handler as message
t_stat = _MCEngineExecDoDispatch(ctxt, HT_MESSAGE, *t_handler_name, &t_stack_obj, *t_params);
t_success = t_stat != ES_ERROR;

if (t_success && t_stat != ES_NORMAL)
{
// not handled as message, try as function
t_stat = _MCEngineExecDoDispatch(ctxt, HT_FUNCTION, *t_handler_name, &t_stack_obj, *t_params);
t_success = t_stat != ES_ERROR;
}
}

// fetch result (as string)
MCAutoValueRef t_result;
if (t_success)
t_success = MCresult->eval(ctxt, &t_result);

MCAutoStringRef t_result_string;
if (t_success)
t_success = ctxt.ConvertToString(*t_result, &t_result_string);

if (!t_success)
return nil;

return t_result_string.Take();
}

// Return the named stack, or NULL if not found
extern "C" MC_DLLEXPORT_DEF
MCStack *MCEmscriptenResolveStack(MCStringRef p_name)
{
MCNewAutoNameRef t_name;
if (!MCNameCreate(p_name, &t_name))
return nil;

return MCdispatcher->findstackname(*t_name);
}

//////////

extern "C" MC_DLLEXPORT_DEF
MCStringRef MCEmscriptenUtilCreateStringWithCharsAndRelease(unichar_t *p_utf16_string, uint32_t p_length)
{
MCStringRef t_string = nil;
if (!MCStringCreateWithCharsAndRelease(p_utf16_string, p_length, t_string))
return nil;

return t_string;
}

extern "C" MC_DLLEXPORT_DEF
MCProperListRef MCEmscriptenUtilCreateMutableProperList()
{
MCProperListRef t_list = nil;
if (!MCProperListCreateMutable(t_list))
return nil;

return t_list;
}
124 changes: 122 additions & 2 deletions engine/src/em-system.js
Expand Up @@ -18,8 +18,22 @@

mergeInto(LibraryManager.library, {

$LiveCodeSystem__deps: ['$LiveCodeUtil'],
$LiveCodeSystem__deps: ['$LiveCodeUtil', '$LiveCodeAsync'],
$LiveCodeSystem: {

initialize: function() {
if (!document['liveCode'])
document['liveCode'] = {}
document['liveCode']['findStackWithName'] = function(name) {
return LiveCodeSystem.getStackWithName(name);
};
},

finalize: function() {
delete document['liveCodeGetStackWithName'];
},

//////////

evaluateJavaScript: function(script_buffer, script_length, return_buffer, return_length) {
var script = LiveCodeUtil.stringFromUTF16(script_buffer, script_length);
Expand Down Expand Up @@ -55,13 +69,119 @@ mergeInto(LibraryManager.library, {
{{{ makeSetValue('return_length', '0', 'buffer_length', 'i32') }}};

return success;
}
},

//////////

_callStackHandler: function(stack, handler, paramList)
{
return Module.ccall('MCEmscriptenSystemCallStackHandler', 'number', ['number', 'number', 'number'], [stack, handler, paramList]);
},

_getStackHandlerList: function(stack)
{
return Module.ccall('MCEmscriptenSystemGetJavascriptHandlersOfStack', 'number', ['number'], [stack]);
},

// Convert from javascript list to MCProperListRef
_convertParams: function(paramList)
{
var listRef = LiveCodeUtil.properListCreateMutable();
var count = paramList.length;

for (var i = 0; i < count; i++)
{
// For now, convert parameters to strings
// TODO - support other types
stringRef = LiveCodeUtil.stringToMCStringRef(String(paramList[i]));
LiveCodeUtil.properListPushElementOntoBack(listRef, stringRef);
LiveCodeUtil.valueRelease(stringRef)
}

return listRef
},

_makeHandlerProxy: function(stack, handler)
{
return function()
{
var tParams = Array.prototype.slice.call(arguments);
var tResultString = null;
LiveCodeAsync.resume(function() {
var tHandlerName = LiveCodeUtil.stringToMCStringRef(handler);
var tConvertedParams = LiveCodeSystem._convertParams(tParams);
var tResult = LiveCodeSystem._callStackHandler(stack, tHandlerName, tConvertedParams);
LiveCodeUtil.valueRelease(tHandlerName);
LiveCodeUtil.valueRelease(tConvertedParams);

if (tResult == 0)
tResultString = null;
else
{
tResultString = LiveCodeUtil.stringFromMCStringRef(tResult);
LiveCodeUtil.valueRelease(tResult);
}
});

return tResultString;
}
},

_stackHandle: function(stack)
{
var stackHandle = { _stack: stack };
var handlerList = LiveCodeSystem._getStackHandlerList(stack);
var count = LiveCodeUtil.properListGetLength(handlerList);
for (var i = 0; i < count; i++)
{
var stringref = LiveCodeUtil.properListFetchElementAtIndex(handlerList, i);
var handler_name = LiveCodeUtil.stringFromMCStringRef(stringref);
stackHandle[handler_name] = LiveCodeSystem._makeHandlerProxy(stack, handler_name);
}

LiveCodeUtil.valueRelease(handlerList);

return stackHandle;
},

_resolveStack: function(stack_name)
{
var stringref = LiveCodeUtil.stringToMCStringRef(stack_name);
var stack = Module.ccall('MCEmscriptenResolveStack', 'number', ['number'], [stringref]);
LiveCodeUtil.valueRelease(stringref);

return stack
},

getStackWithName: function(name)
{
var stackHandle = null;
LiveCodeAsync.resume(function() {
var stack = LiveCodeSystem._resolveStack(name);
if (stack != 0)
stackHandle = LiveCodeSystem._stackHandle(stack);

});
return stackHandle;
},
},

MCEmscriptenSystemEvaluateJavaScript__deps: ['$LiveCodeSystem'],
MCEmscriptenSystemEvaluateJavaScript: function(script_buffer, script_length, return_buffer, return_length) {
return LiveCodeSystem.evaluateJavaScript(script_buffer, script_length, return_buffer, return_length);
},

MCEmscriptenSystemInitializeJS__deps: ['$LiveCodeSystem'],
MCEmscriptenSystemInitializeJS: function() {
LiveCodeSystem.initialize();
return true;
},

MCEmscriptenSystemFinalizeJS__deps: ['$LiveCodeSystem'],
MCEmscriptenSystemFinalizeJS: function() {
LiveCodeSystem.finalize();
},

});

/*
Expand Down