-
-
Save Happy-Ferret/c0d97bcb6ba7b2891da6c7e7c0ee90eb to your computer and use it in GitHub Desktop.
Garbage collector safe example embedding ChakraCore in Spidermonkey shell using js-ctypes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Tested against release/1.4 branch of https://github.com/Microsoft/ChakraCore | |
commit: 6f4c890505a58bbf198035d4b93bf8a726033af3 | |
The below program will work on x86_64 Linux. It demonstrates how to safely call | |
in to libChakraCore from Spidermonkey using js-ctypes. Because the ChakraCore | |
GC scans the stack for JsValueRefs we must allocate JsValueRefs on the C stack. | |
When I say C stack I mean in the memory region between the current stack | |
pointer and the initial value of the stack pointer when the program started. | |
Values created through js-ctypes will be allocated on the heap. If we hand | |
these values to CharkaCore API functions this will cause JsValueRefs to be | |
created in the heap rather than on the stack. As far as the GC is concerned | |
these are garbage, and so can be collected. When this occurs the program | |
embedding ChakraCore may crash (I say may, because this is a nondeterministic | |
bug). We cannot work around this by calling JsAddRef to bump the reference | |
count (since the reference count starts at zero, if the GC runs between when | |
the JsValueRef is created and when JsAddRef is called we will crash). In order | |
to get around this issue we must allocated the JsValueRefs on the C stack. In | |
order to do this we use a helper function written in C (called | |
make_stack_variables). We call make_stack_variables from Spidermonkey using | |
js-ctypes, make_stack_variables then allocates some pointers on the stack and | |
calls back in to Spidermonkey (by using a function pointer to a js-ctypes | |
callback function (called js_callback)). The callback function then takes | |
pointers to the stack allocated variables and hands these in to any ChakraCore | |
API functions that we wish to call. | |
Once you have set everything up (instructions are further down in this file) you can run the following | |
js -e "unsafe=false;run_gc=true" example.js | |
There is lots of debug output. If all goes well, somewhere towards the end you should see the following: | |
ChakraCore Returned: | |
Hello world! | |
Note the above invocation does not segfault. The below invocation will segfault: | |
js -e "unsafe=true;run_gc=true" example.js | |
The "unsafe" variable controls whether the JsValueRefs from the ChakraCore API are stored on the C stack. | |
unsafe=false is safe, it stores JsValueRefs in the C stack | |
unsafe=true is unsafe, it stores the JsValueRefs in the heap (since the storage space is allocated using jsctypes). | |
the run_gc variable controls whether JsCollectGarbage will be explicitly called at several points during the | |
program. This turns a nondeterministic bug into a reproducable bug. | |
run the script in the same dir as libChakraCore.so and libstackhelper.so | |
In order to set everything up: | |
get Spidermonkey from here: | |
https://ftp.mozilla.org/pub/firefox/candidates/45.6.0esr-candidates/build1/jsshell-linux-x86_64.zip | |
SHA512SUM and signature here: | |
https://ftp.mozilla.org/pub/firefox/candidates/45.6.0esr-candidates/build1/linux-x86_64/en-US/firefox-45.6.0esr.checksums | |
https://ftp.mozilla.org/pub/firefox/candidates/45.6.0esr-candidates/build1/linux-x86_64/en-US/firefox-45.6.0esr.checksums.asc | |
If you can't get a hold of the above binary it my have been moved to the archive: | |
https://ftp.mozilla.org/pub/firefox/candidates/archived/ | |
failing that just pick another one from somewhere in: | |
https://ftp.mozilla.org/pub/firefox/candidates | |
set up Spidermonkey by unzipping it and adding it to your PATH and LD_LIBRARY_PATH: | |
mkdir spidermonkey | |
cd spidermonkey | |
unzip ../jsshell-linux-x86_64.zip | |
export PATH=${PWD}:$PATH | |
export LD_LIBRARY_PATH=${PWD} | |
For the purposes of this example we need a helper shared library in order to allocate variables on the C stack. | |
Source to stackhelper.c is below. To build the library do: | |
gcc -g -c -Wall -fpic stackhelper.c | |
gcc -shared -o libstackhelper.so stackhelper.o | |
File stackhelper.c : | |
#include <stdint.h> | |
#include <stdio.h> | |
void make_stack_variables(void (*f)(uint64_t,uint64_t,uint64_t,uint64_t,uint64_t,uint64_t,uint64_t,uint64_t)){ | |
void * p1=0; | |
void * p2=0; | |
void * p3=0; | |
void * p4=0; | |
void * p5=0; | |
void * p6=0; | |
void * p7=0; | |
void * p8=0; | |
printf("p1: Start our pointer is: %p\n",&p1); | |
printf("p1: Start our value is: %p\n\n",p1); | |
printf("p2: Start our pointer is: %p\n",&p2); | |
printf("p2: Start our value is: %p\n\n",p2); | |
printf("p3: Start our pointer is: %p\n",&p3); | |
printf("p3: Start our value is: %p\n\n",p3); | |
printf("p4: Start our pointer is: %p\n",&p4); | |
printf("p4: Start our value is: %p\n\n",p4); | |
printf("p5: Start our pointer is: %p\n",&p5); | |
printf("p5: Start our value is: %p\n\n",p5); | |
printf("p6: Start our pointer is: %p\n",&p6); | |
printf("p6: Start our value is: %p\n\n",p6); | |
printf("p7: Start our pointer is: %p\n",&p7); | |
printf("p7: Start our value is: %p\n\n",p7); | |
printf("p8: Start our pointer is: %p\n",&p8); | |
printf("p8: Start our value is: %p\n\n",p8); | |
(*f)((uint64_t)&p1,(uint64_t)&p2,(uint64_t)&p3,(uint64_t)&p4,(uint64_t)&p5,(uint64_t)&p6,(uint64_t)&p7,(uint64_t)&p8); | |
printf("p1: our pointer is: %p\n",&p1); | |
printf("p1: our value is: %p\n\n",p1); | |
printf("p2: our pointer is: %p\n",&p2); | |
printf("p2: our value is: %p\n\n",p2); | |
printf("p3: our pointer is: %p\n",&p3); | |
printf("p3: our value is: %p\n\n",p3); | |
printf("p4: our pointer is: %p\n",&p4); | |
printf("p4: our value is: %p\n\n",p4); | |
printf("p5: our pointer is: %p\n",&p5); | |
printf("p5: our value is: %p\n\n",p5); | |
printf("p6: our pointer is: %p\n",&p6); | |
printf("p6: our value is: %p\n\n",p6); | |
printf("p7: our pointer is: %p\n",&p7); | |
printf("p7: our value is: %p\n\n",p7); | |
printf("p8: our pointer is: %p\n",&p8); | |
printf("p8: our value is: %p\n\n",p8); | |
return; | |
} | |
End of file stackhelper.c | |
*/ | |
// once you have built the above library put it in the same dir as libChakraCore.so | |
// run this script in the same dir as libChakraCore.so | |
// Need to create a global window object to store some globals: | |
window=this; | |
take_address=function(x){return ctypes.cast(x.address(),ctypes.uint64_t)}; | |
p=print; | |
p(a=ctypes.open("./libChakraCore.so")); | |
p(DllMain = a.declare("DllMain", ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t)); | |
p(JsCollectGarbage = a.declare("JsCollectGarbage",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t)); | |
// p(JsCreateRuntime = a.declare("JsCreateRuntime", ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t)); | |
p(JsCreateRuntime = a.declare("JsCreateRuntime", ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t.ptr)); | |
// p(JsCreateContext = a.declare("JsCreateContext",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t)); | |
p(JsCreateContext = a.declare("JsCreateContext",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t.ptr)); | |
p(JsSetCurrentContext = a.declare("JsSetCurrentContext",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t)); | |
// p(JsCreateStringUtf8 = a.declare("JsCreateStringUtf8",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t)); | |
p(JsCreateStringUtf8 = a.declare("JsCreateStringUtf8",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t.ptr)); | |
//p(JsCreateExternalArrayBuffer = a.declare("JsCreateExternalArrayBuffer",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t)); | |
p(JsCreateExternalArrayBuffer = a.declare("JsCreateExternalArrayBuffer",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t.ptr)); | |
//p(JsRun = a.declare("JsRun",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t)); | |
p(JsRun = a.declare("JsRun",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t.ptr)); | |
// p(JsConvertValueToString = a.declare("JsConvertValueToString",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t)); | |
p(JsConvertValueToString = a.declare("JsConvertValueToString",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t.ptr)); | |
// p(JsCopyStringUtf8 = a.declare("JsCopyStringUtf8",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t)); | |
p(JsCopyStringUtf8 = a.declare("JsCopyStringUtf8",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t.ptr)); | |
p(pp=ctypes.open("./libstackhelper.so")); | |
p(make_stack_variables=pp.declare("make_stack_variables",ctypes.default_abi, ctypes.void_t,ctypes.voidptr_t)); | |
p(stack_helper_wrapper_function_type=ctypes.FunctionType(ctypes.default_abi, ctypes.void_t, [ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t])); | |
gc=function(){}; | |
try{ | |
run_gc; | |
} catch(e){ | |
run_gc=false; | |
} | |
try{ | |
unsafe; | |
} catch(e){ | |
unsafe=false; | |
} | |
if(run_gc){ | |
chakra_gc=JsCollectGarbage; | |
}else{ | |
chakra_gc=function(){}; | |
} | |
window.global_array=[]; | |
js_callback=function(p1,p2,p3,p4,p5,p6,p7,p8){ | |
var ptrs; | |
if(unsafe===false){ | |
ptrs=[ | |
ctypes.cast(ctypes.uint64_t(p1),ctypes.uint64_t.ptr), | |
ctypes.cast(ctypes.uint64_t(p2),ctypes.uint64_t.ptr), | |
ctypes.cast(ctypes.uint64_t(p3),ctypes.uint64_t.ptr), | |
ctypes.cast(ctypes.uint64_t(p4),ctypes.uint64_t.ptr), | |
ctypes.cast(ctypes.uint64_t(p5),ctypes.uint64_t.ptr), | |
ctypes.cast(ctypes.uint64_t(p6),ctypes.uint64_t.ptr), | |
ctypes.cast(ctypes.uint64_t(p7),ctypes.uint64_t.ptr), | |
ctypes.cast(ctypes.uint64_t(p8),ctypes.uint64_t.ptr)]; | |
} else{ | |
// Note the use of a global. I think a local may do (since we need Spidermonkey to keep | |
// these alive) but I'm not 100% certain that a clever JIT compiler won't garbage collect | |
// local variables after their final use, but before the end of the function (ie in this | |
// case "global_array" could become garbage immediately after the ptrs line and therefore | |
// ptrs could end up pointing at garbage). | |
window.global_array=[ctypes.uint64_t(ctypes.UInt64(0)), | |
ctypes.uint64_t(ctypes.UInt64(0)), | |
ctypes.uint64_t(ctypes.UInt64(0)), | |
ctypes.uint64_t(ctypes.UInt64(0)), | |
ctypes.uint64_t(ctypes.UInt64(0)), | |
ctypes.uint64_t(ctypes.UInt64(0)), | |
ctypes.uint64_t(ctypes.UInt64(0)), | |
ctypes.uint64_t(ctypes.UInt64(0))]; | |
ptrs=window.global_array.map(function(x){return ctypes.uint64_t.ptr(x.address());}); | |
p("Unsafe pointers"); | |
} | |
go(ptrs); | |
}; | |
p(start=stack_helper_wrapper_function_type.ptr(js_callback)); | |
p(make_stack_variables(start)); | |
function go(ptrs){ | |
ptrs.forEach(function(x,i){x.contents=0;console.log(x,x.contents)}); | |
p(DllMain(0, 1, 0)); | |
p(DllMain(0, 2, 0)); | |
// JsRuntimeHandle runtime; | |
runtime=ptrs[0]; | |
// JsContextRef context; | |
var context=ptrs[1]; | |
// JsValueRef result; | |
var result=ptrs[2]; | |
// unsigned currentSourceContext = 0; | |
var currentSourceContext = 0; | |
// Your script; try replace hello-world with something else | |
// const char* script = "(()=>{return \'Hello World!\';})()"; | |
var script="(()=>{return \'Hello world!\';})()"; | |
// Create a runtime. | |
// FAIL_CHECK(JsCreateRuntime(JsRuntimeAttributeNone, nullptr, &runtime)); | |
p(runtime.contents); | |
p(JsCreateRuntime(0, 0, runtime)); | |
p(runtime.contents); | |
// Create an execution context. | |
// FAIL_CHECK(JsCreateContext(runtime, &context)); | |
p(context.contents); | |
p(JsCreateContext(runtime.contents, context)); | |
p(context.contents); | |
// Now set the current execution context. | |
// FAIL_CHECK(JsSetCurrentContext(context)); | |
p(JsSetCurrentContext(context.contents)); | |
// JsValueRef fname; | |
var fname=ptrs[3]; | |
var fileName = "sample.js" | |
// FAIL_CHECK(JsCreateStringUtf8((const uint8_t*)"sample", strlen("sample"), &fname)); | |
p(fileName); | |
p(fname.contents); | |
// see comment in js_callback, this is global for a similar reason. | |
window.fileName_char_array=ctypes.char.array()(fileName); | |
p(JsCreateStringUtf8(take_address(window.fileName_char_array), fileName.length, fname)); | |
chakra_gc(runtime.contents); | |
p(fname.contents); | |
// JsValueRef scriptSource; | |
var scriptSource=ptrs[4]; | |
// FAIL_CHECK(JsCreateExternalArrayBuffer((void*)script, (unsigned int)strlen(script), | |
// nullptr, nullptr, &scriptSource)); | |
// ditto - see comment in js_callback, this is global for a similar reason. | |
window.script_buffer=ctypes.char.array()(script); | |
p(script); | |
p(scriptSource.contents); | |
p(JsCreateExternalArrayBuffer(take_address(window.script_buffer), script.length, 0, 0, scriptSource)); | |
chakra_gc(runtime.contents); | |
p(scriptSource.contents); | |
// Run the script. | |
// FAIL_CHECK(JsRun(scriptSource, currentSourceContext++, fname, JsParseScriptAttributeNone, &result)); | |
p("result"); | |
p(result.contents); | |
p(JsRun(scriptSource.contents, 0, fname.contents, 0, result)); | |
chakra_gc(runtime.contents); | |
p(result.contents); | |
// Convert your script result to String in JavaScript; redundant if your script returns a String | |
// JsValueRef resultJSString; | |
var resultJSString=ptrs[5]; | |
// FAIL_CHECK(JsConvertValueToString(result, &resultJSString)); | |
p("resultJSString.contents"); | |
p(resultJSString.contents); | |
p(JsConvertValueToString(result.contents, resultJSString)); | |
chakra_gc(runtime.contents); | |
p(resultJSString.contents); | |
// Project script result back to C++. | |
// uint8_t *resultSTR = nullptr; | |
// size_t stringLength; | |
var stringLength=ptrs[6]; | |
// FAIL_CHECK(JsCopyStringUtf8(resultJSString, nullptr, 0, &stringLength)); | |
p("stringLength.contents"); | |
p(stringLength.contents); | |
p(JsCopyStringUtf8(resultJSString.contents, 0, 0, stringLength)); | |
chakra_gc(runtime.contents); | |
p(stringLength.contents); | |
stringLength_js_number=+(stringLength.contents.toString()); | |
// resultSTR = (uint8_t*) malloc(stringLength + 1); | |
var resultSTR = ctypes.char.array(stringLength_js_number+1)(); | |
p("resultSTR"); | |
p(resultSTR); | |
// FAIL_CHECK(JsCopyStringUtf8(resultJSString, resultSTR, stringLength + 1, nullptr)); | |
p(JsCopyStringUtf8(resultJSString.contents, take_address(resultSTR), stringLength_js_number + 1, ctypes.uint64_t.ptr(0))); | |
chakra_gc(runtime.contents); | |
p(resultSTR); | |
resultSTR_js=resultSTR.readString(); | |
p("ChakraCore Returned: "); | |
p(resultSTR_js); | |
// resultSTR[stringLength] = 0; | |
// printf("Result -> %s \n", resultSTR); | |
// free(resultSTR); | |
// Dispose runtime | |
// FAIL_CHECK(JsSetCurrentContext(JS_INVALID_REFERENCE)); | |
// FAIL_CHECK(JsDisposeRuntime(runtime)); | |
//return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment