// // ScriptingCore.cpp // testmonkey // // Created by Rolando Abarca on 3/14/12. // Copyright (c) 2012 Zynga Inc. All rights reserved. // #include #include #include #include #include #include #include #include "ScriptingCore.h" #include "jsdbgapi.h" #include "cocos2d.h" #include "local-storage/LocalStorage.h" #include "cocos2d_specifics.hpp" #include "js_bindings_config.h" // for debug socket #if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32) #include #include #else #include #include #include #endif #include #ifdef ANDROID #include #include #include #endif #ifdef ANDROID #define LOG_TAG "ScriptingCore.cpp" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__) #else #define LOGD(...) js_log(__VA_ARGS__) #endif #include "js_bindings_config.h" #if DEBUG #define TRACE_DEBUGGER_SERVER(...) CCLOG(__VA_ARGS__) #else #define TRACE_DEBUGGER_SERVER(...) #endif // #if DEBUG #define BYTE_CODE_FILE_EXT ".jsc" static std::string inData; static std::string outData; static std::vector g_queue; static std::mutex g_qMutex; static std::mutex g_rwMutex; static int clientSocket = -1; static uint32_t s_nestedLoopLevel = 0; // server entry point for the bg thread static void serverEntryPoint(void); js_proxy_t *_native_js_global_ht = NULL; js_proxy_t *_js_native_global_ht = NULL; std::unordered_map _js_global_type_map; static char *_js_log_buf = NULL; static std::vector registrationList; // name ~> JSScript map static std::unordered_map filename_script; // port ~> socket map static std::unordered_map ports_sockets; // name ~> globals static std::unordered_map globals; static void ReportException(JSContext *cx) { if (JS_IsExceptionPending(cx)) { if (!JS_ReportPendingException(cx)) { JS_ClearPendingException(cx); } } } static void executeJSFunctionFromReservedSpot(JSContext *cx, JSObject *obj, jsval &dataVal, jsval &retval) { jsval func = JS_GetReservedSlot(obj, 0); if (func == JSVAL_VOID) { return; } jsval thisObj = JS_GetReservedSlot(obj, 1); JSAutoCompartment ac(cx, obj); if (thisObj == JSVAL_VOID) { JS_CallFunctionValue(cx, obj, func, 1, &dataVal, &retval); } else { assert(!JSVAL_IS_PRIMITIVE(thisObj)); JS_CallFunctionValue(cx, JSVAL_TO_OBJECT(thisObj), func, 1, &dataVal, &retval); } } static void getTouchesFuncName(EventTouch::EventCode eventCode, std::string &funcName) { switch(eventCode) { case EventTouch::EventCode::BEGAN: funcName = "onTouchesBegan"; break; case EventTouch::EventCode::ENDED: funcName = "onTouchesEnded"; break; case EventTouch::EventCode::MOVED: funcName = "onTouchesMoved"; break; case EventTouch::EventCode::CANCELLED: funcName = "onTouchesCancelled"; break; } } static void getTouchFuncName(EventTouch::EventCode eventCode, std::string &funcName) { switch(eventCode) { case EventTouch::EventCode::BEGAN: funcName = "onTouchBegan"; break; case EventTouch::EventCode::ENDED: funcName = "onTouchEnded"; break; case EventTouch::EventCode::MOVED: funcName = "onTouchMoved"; break; case EventTouch::EventCode::CANCELLED: funcName = "onTouchCancelled"; break; } } static void rootObject(JSContext *cx, JSObject *obj) { JS_AddNamedObjectRoot(cx, &obj, "unnamed"); } static void unRootObject(JSContext *cx, JSObject *obj) { JS_RemoveObjectRoot(cx, &obj); } static void getJSTouchObject(JSContext *cx, Touch *x, jsval &jsret) { js_proxy_t *proxy = js_get_or_create_proxy(cx, x); jsret = OBJECT_TO_JSVAL(proxy->obj); } static void removeJSTouchObject(JSContext *cx, Touch *x, jsval &jsret) { js_proxy_t* nproxy; js_proxy_t* jsproxy; void *ptr = (void*)x; nproxy = jsb_get_native_proxy(ptr); if (nproxy) { jsproxy = jsb_get_js_proxy(nproxy->obj); JS_RemoveObjectRoot(cx, &jsproxy->obj); jsb_remove_proxy(nproxy, jsproxy); } } void ScriptingCore::executeJSFunctionWithThisObj(jsval thisObj, jsval callback, uint32_t argc/* = 0*/, jsval* vp/* = NULL*/, jsval* retVal/* = NULL*/) { if (callback != JSVAL_VOID || thisObj != JSVAL_VOID) { JSB_AUTOCOMPARTMENT_WITH_GLOBAL_OBJCET // Very important: The last parameter 'retVal' passed to 'JS_CallFunctionValue' should not be a NULL pointer. // If it's a NULL pointer, crash will be triggered in 'JS_CallFunctionValue'. To find out the reason of this crash is very difficult. // So we have to check the availability of 'retVal'. if (retVal) { JS_CallFunctionValue(_cx, JSVAL_TO_OBJECT(thisObj), callback, argc, vp, retVal); } else { jsval jsRet; JS_CallFunctionValue(_cx, JSVAL_TO_OBJECT(thisObj), callback, argc, vp, &jsRet); } } } void js_log(const char *format, ...) { if (_js_log_buf == NULL) { _js_log_buf = (char *)calloc(sizeof(char), kMaxLogLen+1); _js_log_buf[kMaxLogLen] = '\0'; } va_list vl; va_start(vl, format); int len = vsnprintf(_js_log_buf, kMaxLogLen, format, vl); va_end(vl); if (len > 0) { CCLOG("JS: %s\n", _js_log_buf); } } #define JSB_COMPATIBLE_WITH_COCOS2D_HTML5_BASIC_TYPES 1 JSBool JSBCore_platform(JSContext *cx, uint32_t argc, jsval *vp) { if (argc!=0) { JS_ReportError(cx, "Invalid number of arguments in __getPlatform"); return JS_FALSE; } JSString * platform; // config.deviceType: Device Type // 'mobile' for any kind of mobile devices, 'desktop' for PCs, 'browser' for Web Browsers // #if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32) || (CC_TARGET_PLATFORM == CC_PLATFORM_LINUX) || (CC_TARGET_PLATFORM == CC_PLATFORM_MAC) // platform = JS_InternString(_cx, "desktop"); // #else platform = JS_InternString(cx, "mobile"); // #endif jsval ret = STRING_TO_JSVAL(platform); JS_SET_RVAL(cx, vp, ret); return JS_TRUE; }; JSBool JSBCore_version(JSContext *cx, uint32_t argc, jsval *vp) { if (argc!=0) { JS_ReportError(cx, "Invalid number of arguments in __getVersion"); return JS_FALSE; } char version[256]; snprintf(version, sizeof(version)-1, "%s", cocos2dVersion()); JSString * js_version = JS_InternString(cx, version); jsval ret = STRING_TO_JSVAL(js_version); JS_SET_RVAL(cx, vp, ret); return JS_TRUE; }; JSBool JSBCore_os(JSContext *cx, uint32_t argc, jsval *vp) { if (argc!=0) { JS_ReportError(cx, "Invalid number of arguments in __getOS"); return JS_FALSE; } JSString * os; // osx, ios, android, windows, linux, etc.. #if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS) os = JS_InternString(cx, "ios"); #elif (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) os = JS_InternString(cx, "android"); #elif (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32) os = JS_InternString(cx, "windows"); #elif (CC_TARGET_PLATFORM == CC_PLATFORM_MARMALADE) os = JS_InternString(cx, "marmalade"); #elif (CC_TARGET_PLATFORM == CC_PLATFORM_LINUX) os = JS_InternString(cx, "linux"); #elif (CC_TARGET_PLATFORM == CC_PLATFORM_BADA) os = JS_InternString(cx, "bada"); #elif (CC_TARGET_PLATFORM == CC_PLATFORM_BLACKBERRY) os = JS_InternString(cx, "blackberry"); #elif (CC_TARGET_PLATFORM == CC_PLATFORM_MAC) os = JS_InternString(cx, "osx"); #else os = JS_InternString(cx, "unknown"); #endif jsval ret = STRING_TO_JSVAL(os); JS_SET_RVAL(cx, vp, ret); return JS_TRUE; }; JSBool JSB_core_restartVM(JSContext *cx, uint32_t argc, jsval *vp) { JSB_PRECONDITION2(argc==0, cx, JS_FALSE, "Invalid number of arguments in executeScript"); ScriptingCore::getInstance()->reset(); JS_SET_RVAL(cx, vp, JSVAL_VOID); return JS_TRUE; }; void registerDefaultClasses(JSContext* cx, JSObject* global) { // first, try to get the ns JS::RootedValue nsval(cx); JSObject *ns; JS_GetProperty(cx, global, "cc", &nsval); if (nsval == JSVAL_VOID) { ns = JS_NewObject(cx, NULL, NULL, NULL); nsval = OBJECT_TO_JSVAL(ns); JS_SetProperty(cx, global, "cc", nsval); } else { JS_ValueToObject(cx, nsval, &ns); } // // Javascript controller (__jsc__) // JSObject *jsc = JS_NewObject(cx, NULL, NULL, NULL); JS::RootedValue jscVal(cx); jscVal = OBJECT_TO_JSVAL(jsc); JS_SetProperty(cx, global, "__jsc__", jscVal); JS_DefineFunction(cx, jsc, "garbageCollect", ScriptingCore::forceGC, 0, JSPROP_READONLY | JSPROP_PERMANENT | JSPROP_ENUMERATE ); JS_DefineFunction(cx, jsc, "dumpRoot", ScriptingCore::dumpRoot, 0, JSPROP_READONLY | JSPROP_PERMANENT | JSPROP_ENUMERATE ); JS_DefineFunction(cx, jsc, "addGCRootObject", ScriptingCore::addRootJS, 1, JSPROP_READONLY | JSPROP_PERMANENT | JSPROP_ENUMERATE ); JS_DefineFunction(cx, jsc, "removeGCRootObject", ScriptingCore::removeRootJS, 1, JSPROP_READONLY | JSPROP_PERMANENT | JSPROP_ENUMERATE ); JS_DefineFunction(cx, jsc, "executeScript", ScriptingCore::executeScript, 1, JSPROP_READONLY | JSPROP_PERMANENT | JSPROP_ENUMERATE ); // register some global functions JS_DefineFunction(cx, global, "require", ScriptingCore::executeScript, 1, JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(cx, global, "log", ScriptingCore::log, 0, JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(cx, global, "executeScript", ScriptingCore::executeScript, 1, JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(cx, global, "forceGC", ScriptingCore::forceGC, 0, JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(cx, global, "__getPlatform", JSBCore_platform, 0, JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(cx, global, "__getOS", JSBCore_os, 0, JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(cx, global, "__getVersion", JSBCore_version, 0, JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(cx, global, "__restartVM", JSB_core_restartVM, 0, JSPROP_READONLY | JSPROP_PERMANENT | JSPROP_ENUMERATE ); } static void sc_finalize(JSFreeOp *freeOp, JSObject *obj) { CCLOGINFO("jsbindings: finalizing JS object %p (global class)", obj); } static JSClass global_class = { "global", JSCLASS_GLOBAL_FLAGS, JS_PropertyStub, JS_DeletePropertyStub, JS_PropertyStub, JS_StrictPropertyStub, JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, sc_finalize, JSCLASS_NO_OPTIONAL_MEMBERS }; ScriptingCore::ScriptingCore() : _rt(nullptr) , _cx(nullptr) , _global(nullptr) , _debugGlobal(nullptr) { // set utf8 strings internally (we don't need utf16) // XXX: Removed in SpiderMonkey 19.0 //JS_SetCStringsAreUTF8(); this->addRegisterCallback(registerDefaultClasses); this->_runLoop = new SimpleRunLoop(); } void ScriptingCore::string_report(jsval val) { if (JSVAL_IS_NULL(val)) { LOGD("val : (JSVAL_IS_NULL(val)"); // return 1; } else if ((JSVAL_IS_BOOLEAN(val)) && (JS_FALSE == (JSVAL_TO_BOOLEAN(val)))) { LOGD("val : (return value is JS_FALSE"); // return 1; } else if (JSVAL_IS_STRING(val)) { JSString *str = JS_ValueToString(this->getGlobalContext(), val); if (NULL == str) { LOGD("val : return string is NULL"); } else { JSStringWrapper wrapper(str); LOGD("val : return string =\n%s\n", wrapper.get()); } } else if (JSVAL_IS_NUMBER(val)) { double number; if (JS_FALSE == JS_ValueToNumber(this->getGlobalContext(), val, &number)) { LOGD("val : return number could not be converted"); } else { LOGD("val : return number =\n%f", number); } } } JSBool ScriptingCore::evalString(const char *string, jsval *outVal, const char *filename, JSContext* cx, JSObject* global) { if (cx == NULL) cx = _cx; if (global == NULL) global = _global; JSAutoCompartment ac(cx, global); JSScript* script = JS_CompileScript(cx, global, string, strlen(string), filename, 1); if (script) { JSBool evaluatedOK = JS_ExecuteScript(cx, global, script, outVal); if (JS_FALSE == evaluatedOK) { fprintf(stderr, "(evaluatedOK == JS_FALSE)\n"); } return evaluatedOK; } return JS_FALSE; } void ScriptingCore::start() { // for now just this this->createGlobalContext(); } void ScriptingCore::addRegisterCallback(sc_register_sth callback) { registrationList.push_back(callback); } void ScriptingCore::removeAllRoots(JSContext *cx) { js_proxy_t *current, *tmp; HASH_ITER(hh, _js_native_global_ht, current, tmp) { JS_RemoveObjectRoot(cx, ¤t->obj); HASH_DEL(_js_native_global_ht, current); free(current); } HASH_ITER(hh, _native_js_global_ht, current, tmp) { HASH_DEL(_native_js_global_ht, current); free(current); } HASH_CLEAR(hh, _js_native_global_ht); HASH_CLEAR(hh, _native_js_global_ht); } static JSPrincipals shellTrustedPrincipals = { 1 }; static JSBool CheckObjectAccess(JSContext *cx, js::HandleObject obj, js::HandleId id, JSAccessMode mode, js::MutableHandleValue vp) { return JS_TRUE; } static JSSecurityCallbacks securityCallbacks = { CheckObjectAccess, NULL }; void ScriptingCore::createGlobalContext() { if (this->_cx && this->_rt) { ScriptingCore::removeAllRoots(this->_cx); JS_DestroyContext(this->_cx); JS_DestroyRuntime(this->_rt); this->_cx = NULL; this->_rt = NULL; } // Start the engine. Added in SpiderMonkey v25 if (!JS_Init()) return; // Removed from Spidermonkey 19. //JS_SetCStringsAreUTF8(); this->_rt = JS_NewRuntime(8L * 1024L * 1024L, JS_USE_HELPER_THREADS); JS_SetGCParameter(_rt, JSGC_MAX_BYTES, 0xffffffff); JS_SetTrustedPrincipals(_rt, &shellTrustedPrincipals); JS_SetSecurityCallbacks(_rt, &securityCallbacks); JS_SetNativeStackQuota(_rt, JSB_MAX_STACK_QUOTA); this->_cx = JS_NewContext(_rt, 8192); JS_SetOptions(this->_cx, JSOPTION_TYPE_INFERENCE); // JS_SetVersion(this->_cx, JSVERSION_LATEST); // Only disable METHODJIT on iOS. #if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS) // JS_SetOptions(this->_cx, JS_GetOptions(this->_cx) & ~JSOPTION_METHODJIT); // JS_SetOptions(this->_cx, JS_GetOptions(this->_cx) & ~JSOPTION_METHODJIT_ALWAYS); #endif JS_SetErrorReporter(this->_cx, ScriptingCore::reportError); #if defined(JS_GC_ZEAL) && defined(DEBUG) //JS_SetGCZeal(this->_cx, 2, JS_DEFAULT_ZEAL_FREQ); #endif this->_global = NewGlobalObject(_cx); JSAutoCompartment ac(_cx, _global); js::SetDefaultObjectForContext(_cx, _global); for (std::vector::iterator it = registrationList.begin(); it != registrationList.end(); it++) { sc_register_sth callback = *it; callback(this->_cx, this->_global); } } static std::string RemoveFileExt(const std::string& filePath) { size_t pos = filePath.rfind('.'); if (0 < pos) { return filePath.substr(0, pos); } else { return filePath; } } JSBool ScriptingCore::runScript(const char *path, JSObject* global, JSContext* cx) { if (!path) { return false; } cocos2d::FileUtils *futil = cocos2d::FileUtils::getInstance(); if (global == NULL) { global = _global; } if (cx == NULL) { cx = _cx; } JSAutoCompartment ac(cx, global); js::RootedScript script(cx); js::RootedObject obj(cx, global); // a) check jsc file first std::string byteCodePath = RemoveFileExt(std::string(path)) + BYTE_CODE_FILE_EXT; long length = 0; unsigned char* data = futil->getFileData(byteCodePath.c_str(), "rb", &length); if (data) { script = JS_DecodeScript(cx, data, length, NULL, NULL); free(data); } // b) no jsc file, check js file if (!script) { /* Clear any pending exception from previous failed decoding. */ ReportException(cx); std::string fullPath = futil->fullPathForFilename(path); JS::CompileOptions options(cx); options.setUTF8(true).setFileAndLine(fullPath.c_str(), 1); #if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) String* content = String::createWithContentsOfFile(path); if (content) { // Not supported in SpiderMonkey 19.0 //JSScript* script = JS_CompileScript(cx, global, (char*)content, contentSize, path, 1); const char* contentCStr = content->getCString(); script = JS::Compile(cx, obj, options, contentCStr, strlen(contentCStr)); } #else script = JS::Compile(cx, obj, options, fullPath.c_str()); #endif } JSBool evaluatedOK = false; if (script) { jsval rval; filename_script[path] = script; JSAutoCompartment ac(cx, global); evaluatedOK = JS_ExecuteScript(cx, global, script, &rval); if (JS_FALSE == evaluatedOK) { cocos2d::log("(evaluatedOK == JS_FALSE)"); JS_ReportPendingException(cx); } } return evaluatedOK; } void ScriptingCore::reset() { cleanup(); start(); } ScriptingCore::~ScriptingCore() { cleanup(); } void ScriptingCore::cleanup() { localStorageFree(); removeAllRoots(_cx); if (_cx) { JS_DestroyContext(_cx); _cx = NULL; } if (_rt) { JS_DestroyRuntime(_rt); _rt = NULL; } JS_ShutDown(); if (_js_log_buf) { free(_js_log_buf); _js_log_buf = NULL; } for (auto iter = _js_global_type_map.begin(); iter != _js_global_type_map.end(); ++iter) { free(iter->second->jsclass); free(iter->second); } _js_global_type_map.clear(); } void ScriptingCore::reportError(JSContext *cx, const char *message, JSErrorReport *report) { js_log("%s:%u:%s\n", report->filename ? report->filename : "", (unsigned int) report->lineno, message); }; JSBool ScriptingCore::log(JSContext* cx, uint32_t argc, jsval *vp) { if (argc > 0) { JSString *string = NULL; JS_ConvertArguments(cx, argc, JS_ARGV(cx, vp), "S", &string); if (string) { JSStringWrapper wrapper(string); js_log("%s", wrapper.get()); } } return JS_TRUE; } void ScriptingCore::removeScriptObjectByObject(Object* pObj) { js_proxy_t* nproxy; js_proxy_t* jsproxy; void *ptr = (void*)pObj; nproxy = jsb_get_native_proxy(ptr); if (nproxy) { JSContext *cx = ScriptingCore::getInstance()->getGlobalContext(); jsproxy = jsb_get_js_proxy(nproxy->obj); JS_RemoveObjectRoot(cx, &jsproxy->obj); jsb_remove_proxy(nproxy, jsproxy); } } JSBool ScriptingCore::setReservedSpot(uint32_t i, JSObject *obj, jsval value) { JS_SetReservedSlot(obj, i, value); return JS_TRUE; } JSBool ScriptingCore::executeScript(JSContext *cx, uint32_t argc, jsval *vp) { if (argc >= 1) { jsval* argv = JS_ARGV(cx, vp); JSString* str = JS_ValueToString(cx, argv[0]); JSStringWrapper path(str); JSBool res = false; if (argc == 2 && argv[1].isString()) { JSString* globalName = JSVAL_TO_STRING(argv[1]); JSStringWrapper name(globalName); // js::RootedObject* rootedGlobal = globals[name]; JSObject* debugObj = ScriptingCore::getInstance()->getDebugGlobal(); if (debugObj) { res = ScriptingCore::getInstance()->runScript(path.get(), debugObj); } else { JS_ReportError(cx, "Invalid global object: %s", name.get()); return JS_FALSE; } } else { JSObject* glob = JS::CurrentGlobalOrNull(cx); res = ScriptingCore::getInstance()->runScript(path.get(), glob); } return res; } return JS_TRUE; } JSBool ScriptingCore::forceGC(JSContext *cx, uint32_t argc, jsval *vp) { JSRuntime *rt = JS_GetRuntime(cx); JS_GC(rt); return JS_TRUE; } //static void dumpNamedRoot(const char *name, void *addr, JSGCRootType type, void *data) //{ // CCLOG("Root: '%s' at %p", name, addr); //} JSBool ScriptingCore::dumpRoot(JSContext *cx, uint32_t argc, jsval *vp) { // JS_DumpNamedRoots is only available on DEBUG versions of SpiderMonkey. // Mac and Simulator versions were compiled with DEBUG. #if DEBUG // JSContext *_cx = ScriptingCore::getInstance()->getGlobalContext(); // JSRuntime *rt = JS_GetRuntime(_cx); // JS_DumpNamedRoots(rt, dumpNamedRoot, NULL); // JS_DumpHeap(rt, stdout, NULL, JSTRACE_OBJECT, NULL, 2, NULL); #endif return JS_TRUE; } JSBool ScriptingCore::addRootJS(JSContext *cx, uint32_t argc, jsval *vp) { if (argc == 1) { JSObject *o = NULL; if (JS_ConvertArguments(cx, argc, JS_ARGV(cx, vp), "o", &o) == JS_TRUE) { if (JS_AddNamedObjectRoot(cx, &o, "from-js") == JS_FALSE) { LOGD("something went wrong when setting an object to the root"); } } return JS_TRUE; } return JS_FALSE; } JSBool ScriptingCore::removeRootJS(JSContext *cx, uint32_t argc, jsval *vp) { if (argc == 1) { JSObject *o = NULL; if (JS_ConvertArguments(cx, argc, JS_ARGV(cx, vp), "o", &o) == JS_TRUE) { JS_RemoveObjectRoot(cx, &o); } return JS_TRUE; } return JS_FALSE; } void ScriptingCore::pauseSchedulesAndActions(js_proxy_t* p) { Array * arr = JSScheduleWrapper::getTargetForJSObject(p->obj); if (! arr) return; Node* node = (Node*)p->ptr; for(unsigned int i = 0; i < arr->count(); ++i) { if (arr->getObjectAtIndex(i)) { node->getScheduler()->pauseTarget(arr->getObjectAtIndex(i)); } } } void ScriptingCore::resumeSchedulesAndActions(js_proxy_t* p) { Array * arr = JSScheduleWrapper::getTargetForJSObject(p->obj); if (!arr) return; Node* node = (Node*)p->ptr; for(unsigned int i = 0; i < arr->count(); ++i) { if (!arr->getObjectAtIndex(i)) continue; node->getScheduler()->resumeTarget(arr->getObjectAtIndex(i)); } } void ScriptingCore::cleanupSchedulesAndActions(js_proxy_t* p) { Array* arr = JSScheduleWrapper::getTargetForJSObject(p->obj); if (arr) { Scheduler* pScheduler = Director::getInstance()->getScheduler(); Object* pObj = NULL; CCARRAY_FOREACH(arr, pObj) { pScheduler->unscheduleAllForTarget(pObj); } JSScheduleWrapper::removeAllTargetsForJSObject(p->obj); } } int ScriptingCore::handleNodeEvent(void* data) { if (NULL == data) return 0; BasicScriptData* basicScriptData = static_cast(data); if (NULL == basicScriptData->nativeObject || NULL == basicScriptData->value) return 0; Node* node = static_cast(basicScriptData->nativeObject); int action = *((int*)(basicScriptData->value)); js_proxy_t * p = jsb_get_native_proxy(node); if (!p) return 0; jsval retval; jsval dataVal = INT_TO_JSVAL(1); if (action == kNodeOnEnter) { executeFunctionWithOwner(OBJECT_TO_JSVAL(p->obj), "onEnter", 1, &dataVal, &retval); resumeSchedulesAndActions(p); } else if (action == kNodeOnExit) { executeFunctionWithOwner(OBJECT_TO_JSVAL(p->obj), "onExit", 1, &dataVal, &retval); pauseSchedulesAndActions(p); } else if (action == kNodeOnEnterTransitionDidFinish) { executeFunctionWithOwner(OBJECT_TO_JSVAL(p->obj), "onEnterTransitionDidFinish", 1, &dataVal, &retval); } else if (action == kNodeOnExitTransitionDidStart) { executeFunctionWithOwner(OBJECT_TO_JSVAL(p->obj), "onExitTransitionDidStart", 1, &dataVal, &retval); } else if (action == kNodeOnCleanup) { cleanupSchedulesAndActions(p); } return 1; } int ScriptingCore::handleMenuClickedEvent(void* data) { if (NULL == data) return 0; BasicScriptData* basicScriptData = static_cast(data); if (NULL == basicScriptData->nativeObject) return 0; MenuItem* menuItem = static_cast(basicScriptData->nativeObject); js_proxy_t * p = jsb_get_native_proxy(menuItem); if (!p) return 0; jsval retval; jsval dataVal; js_proxy_t *proxy = jsb_get_native_proxy(menuItem); dataVal = (proxy ? OBJECT_TO_JSVAL(proxy->obj) : JSVAL_NULL); executeJSFunctionFromReservedSpot(this->_cx, p->obj, dataVal, retval); return 1; } int ScriptingCore::handleTouchesEvent(void* data) { if (NULL == data) return 0; TouchesScriptData* touchesScriptData = static_cast(data); if (NULL == touchesScriptData->nativeObject || touchesScriptData->touches.empty()) return 0; Layer* pLayer = static_cast(touchesScriptData->nativeObject); EventTouch::EventCode eventType = touchesScriptData->actionType; const std::vector& touches = touchesScriptData->touches; std::string funcName = ""; getTouchesFuncName(eventType, funcName); JSObject *jsretArr = JS_NewArrayObject(this->_cx, 0, NULL); JS_AddNamedObjectRoot(this->_cx, &jsretArr, "touchArray"); int count = 0; for (auto& touch : touches) { jsval jsret; getJSTouchObject(this->_cx, touch, jsret); if (!JS_SetElement(this->_cx, jsretArr, count, &jsret)) { break; } ++count; } executeFunctionWithObjectData(pLayer, funcName.c_str(), jsretArr); JS_RemoveObjectRoot(this->_cx, &jsretArr); for (auto& touch : touches) { jsval jsret; removeJSTouchObject(this->_cx, touch, jsret); } return 1; } int ScriptingCore::handleTouchEvent(void* data) { if (NULL == data) return 0; TouchScriptData* touchScriptData = static_cast(data); if (NULL == touchScriptData->nativeObject || NULL == touchScriptData->touch) return 0; Layer* pLayer = static_cast(touchScriptData->nativeObject); EventTouch::EventCode eventType = touchScriptData->actionType; Touch *pTouch = touchScriptData->touch; std::string funcName = ""; getTouchFuncName(eventType, funcName); jsval jsret; getJSTouchObject(this->getGlobalContext(), pTouch, jsret); JSObject *jsObj = JSVAL_TO_OBJECT(jsret); bool retval = executeFunctionWithObjectData(pLayer, funcName.c_str(), jsObj); removeJSTouchObject(this->getGlobalContext(), pTouch, jsret); return retval; } bool ScriptingCore::executeFunctionWithObjectData(Node *self, const char *name, JSObject *obj) { js_proxy_t * p = jsb_get_native_proxy(self); if (!p) return false; jsval retval; jsval dataVal = OBJECT_TO_JSVAL(obj); executeFunctionWithOwner(OBJECT_TO_JSVAL(p->obj), name, 1, &dataVal, &retval); if (JSVAL_IS_NULL(retval)) { return false; } else if (JSVAL_IS_BOOLEAN(retval)) { return JSVAL_TO_BOOLEAN(retval); } return false; } JSBool ScriptingCore::executeFunctionWithOwner(jsval owner, const char *name, uint32_t argc /* = 0 */, jsval *vp /* = NULL */, jsval* retVal /* = NULL */) { JSBool bRet = JS_FALSE; JSBool hasAction; JSContext* cx = this->_cx; JS::RootedValue temp_retval(cx); JSObject* obj = JSVAL_TO_OBJECT(owner); do { JSAutoCompartment ac(cx, obj); if (JS_HasProperty(cx, obj, name, &hasAction) && hasAction) { if (!JS_GetProperty(cx, obj, name, &temp_retval)) { break; } if (temp_retval == JSVAL_VOID) { break; } if (retVal) { bRet = JS_CallFunctionName(cx, obj, name, argc, vp, retVal); } else { jsval jsret; bRet = JS_CallFunctionName(cx, obj, name, argc, vp, &jsret); } } }while(0); return bRet; } int ScriptingCore::handleAccelerometerEvent(void* data) { if (NULL == data) return 0; BasicScriptData* basicScriptData = static_cast(data); if (NULL == basicScriptData->nativeObject || NULL == basicScriptData->value) return 0; Acceleration* accelerationValue = static_cast(basicScriptData->value); Layer* layer = static_cast(basicScriptData->nativeObject); jsval value = ccacceleration_to_jsval(this->getGlobalContext(), *accelerationValue); JS_AddValueRoot(this->getGlobalContext(), &value); executeFunctionWithObjectData(layer, "onAccelerometer", JSVAL_TO_OBJECT(value)); JS_RemoveValueRoot(this->getGlobalContext(), &value); return 1; } int ScriptingCore::handleKeypadEvent(void* data) { if (NULL == data) return 0; KeypadScriptData* keypadScriptData = static_cast(data); if (NULL == keypadScriptData->nativeObject) return 0; EventKeyboard::KeyCode action = keypadScriptData->actionType; js_proxy_t * p = jsb_get_native_proxy(keypadScriptData->nativeObject); if (p) { JSBool ret = JS_FALSE; switch(action) { case EventKeyboard::KeyCode::KEY_BACKSPACE: ret = executeFunctionWithOwner(OBJECT_TO_JSVAL(p->obj), "onBackClicked"); if (!ret) { ret = executeFunctionWithOwner(OBJECT_TO_JSVAL(p->obj), "backClicked"); if (ret) { CCLOG("backClicked will be deprecated, please use onBackClicked instead."); } } break; case EventKeyboard::KeyCode::KEY_MENU: ret = executeFunctionWithOwner(OBJECT_TO_JSVAL(p->obj), "onMenuClicked"); if (!ret) { ret = executeFunctionWithOwner(OBJECT_TO_JSVAL(p->obj), "menuClicked"); if (ret) { CCLOG("menuClicked will be deprecated, please use onMenuClicked instead."); } } break; default: break; } return 1; } return 0; } int ScriptingCore::executeCustomTouchesEvent(EventTouch::EventCode eventType, const std::vector& touches, JSObject *obj) { jsval retval; std::string funcName; getTouchesFuncName(eventType, funcName); JSObject *jsretArr = JS_NewArrayObject(this->_cx, 0, NULL); JS_AddNamedObjectRoot(this->_cx, &jsretArr, "touchArray"); int count = 0; for (auto& touch : touches) { jsval jsret; getJSTouchObject(this->_cx, touch, jsret); if (!JS_SetElement(this->_cx, jsretArr, count, &jsret)) { break; } ++count; } jsval jsretArrVal = OBJECT_TO_JSVAL(jsretArr); executeFunctionWithOwner(OBJECT_TO_JSVAL(obj), funcName.c_str(), 1, &jsretArrVal, &retval); JS_RemoveObjectRoot(this->_cx, &jsretArr); for (auto& touch : touches) { jsval jsret; removeJSTouchObject(this->_cx, touch, jsret); } return 1; } int ScriptingCore::executeCustomTouchEvent(EventTouch::EventCode eventType, Touch *pTouch, JSObject *obj) { JSB_AUTOCOMPARTMENT_WITH_GLOBAL_OBJCET jsval retval; std::string funcName; getTouchFuncName(eventType, funcName); jsval jsTouch; getJSTouchObject(this->_cx, pTouch, jsTouch); executeFunctionWithOwner(OBJECT_TO_JSVAL(obj), funcName.c_str(), 1, &jsTouch, &retval); // Remove touch object from global hash table and unroot it. removeJSTouchObject(this->_cx, pTouch, jsTouch); return 1; } int ScriptingCore::executeCustomTouchEvent(EventTouch::EventCode eventType, Touch *pTouch, JSObject *obj, jsval &retval) { JSB_AUTOCOMPARTMENT_WITH_GLOBAL_OBJCET std::string funcName; getTouchFuncName(eventType, funcName); jsval jsTouch; getJSTouchObject(this->_cx, pTouch, jsTouch); executeFunctionWithOwner(OBJECT_TO_JSVAL(obj), funcName.c_str(), 1, &jsTouch, &retval); // Remove touch object from global hash table and unroot it. removeJSTouchObject(this->_cx, pTouch, jsTouch); return 1; } int ScriptingCore::sendEvent(ScriptEvent* evt) { if (NULL == evt) return 0; JSAutoCompartment ac(_cx, _global); switch (evt->type) { case kNodeEvent: { return handleNodeEvent(evt->data); } break; case kMenuClickedEvent: { return handleMenuClickedEvent(evt->data); } break; case kTouchEvent: { return handleTouchEvent(evt->data); } break; case kTouchesEvent: { return handleTouchesEvent(evt->data); } break; case kKeypadEvent: { return handleKeypadEvent(evt->data); } break; case kAccelerometerEvent: { return handleAccelerometerEvent(evt->data); } break; default: break; } return 0; } #pragma mark - Debug void SimpleRunLoop::update(float dt) { g_qMutex.lock(); size_t size = g_queue.size(); g_qMutex.unlock(); while (size > 0) { g_qMutex.lock(); auto first = g_queue.begin(); std::string str = *first; g_queue.erase(first); size = g_queue.size(); g_qMutex.unlock(); ScriptingCore::getInstance()->debugProcessInput(str); } } void ScriptingCore::debugProcessInput(const std::string& str) { JSAutoCompartment ac(_cx, _debugGlobal); JSString* jsstr = JS_NewStringCopyZ(_cx, str.c_str()); jsval argv = STRING_TO_JSVAL(jsstr); jsval outval; JS_CallFunctionName(_cx, _debugGlobal, "processInput", 1, &argv, &outval); } static bool NS_ProcessNextEvent() { g_qMutex.lock(); size_t size = g_queue.size(); g_qMutex.unlock(); while (size > 0) { g_qMutex.lock(); auto first = g_queue.begin(); std::string str = *first; g_queue.erase(first); size = g_queue.size(); g_qMutex.unlock(); ScriptingCore::getInstance()->debugProcessInput(str); } // std::this_thread::yield(); std::this_thread::sleep_for(std::chrono::milliseconds(10)); return true; } JSBool JSBDebug_enterNestedEventLoop(JSContext* cx, unsigned argc, jsval* vp) { enum { NS_OK = 0, NS_ERROR_UNEXPECTED }; #define NS_SUCCEEDED(v) ((v) == NS_OK) int rv = NS_OK; uint32_t nestLevel = ++s_nestedLoopLevel; while (NS_SUCCEEDED(rv) && s_nestedLoopLevel >= nestLevel) { if (!NS_ProcessNextEvent()) rv = NS_ERROR_UNEXPECTED; } CCASSERT(s_nestedLoopLevel <= nestLevel, "nested event didn't unwind properly"); JS_SET_RVAL(cx, vp, UINT_TO_JSVAL(s_nestedLoopLevel)); return JS_TRUE; } JSBool JSBDebug_exitNestedEventLoop(JSContext* cx, unsigned argc, jsval* vp) { if (s_nestedLoopLevel > 0) { --s_nestedLoopLevel; } else { JS_SET_RVAL(cx, vp, UINT_TO_JSVAL(0)); return JS_TRUE; } JS_SET_RVAL(cx, vp, UINT_TO_JSVAL(s_nestedLoopLevel)); return JS_TRUE; } JSBool JSBDebug_getEventLoopNestLevel(JSContext* cx, unsigned argc, jsval* vp) { JS_SET_RVAL(cx, vp, UINT_TO_JSVAL(s_nestedLoopLevel)); return JS_TRUE; } //#pragma mark - Debugger static void _clientSocketWriteAndClearString(std::string& s) { ::send(clientSocket, s.c_str(), s.length(), 0); s.clear(); } static void processInput(const std::string& data) { std::lock_guard lk(g_qMutex); g_queue.push_back(data); } static void clearBuffers() { std::lock_guard lk(g_rwMutex); // only process input if there's something and we're not locked if (inData.length() > 0) { processInput(inData); inData.clear(); } if (outData.length() > 0) { _clientSocketWriteAndClearString(outData); } } static void serverEntryPoint(void) { // start a server, accept the connection and keep reading data from it struct addrinfo hints, *result = nullptr, *rp = nullptr; int s = 0; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = AF_INET; // IPv4 hints.ai_socktype = SOCK_STREAM; // TCP stream sockets hints.ai_flags = AI_PASSIVE; // fill in my IP for me std::stringstream portstr; portstr << JSB_DEBUGGER_PORT; int err = 0; #if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32) WSADATA wsaData; err = WSAStartup(MAKEWORD(2, 2),&wsaData); #endif if ((err = getaddrinfo(NULL, portstr.str().c_str(), &hints, &result)) != 0) { LOGD("getaddrinfo error : %s\n", gai_strerror(err)); } for (rp = result; rp != NULL; rp = rp->ai_next) { if ((s = socket(rp->ai_family, rp->ai_socktype, 0)) < 0) { continue; } int optval = 1; if ((setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char*)&optval, sizeof(optval))) < 0) { close(s); TRACE_DEBUGGER_SERVER("debug server : error setting socket option SO_REUSEADDR"); return; } #if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS) if ((setsockopt(s, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval))) < 0) { close(s); TRACE_DEBUGGER_SERVER("debug server : error setting socket option SO_NOSIGPIPE"); return; } #endif //(CC_TARGET_PLATFORM == CC_PLATFORM_IOS) if ((::bind(s, rp->ai_addr, rp->ai_addrlen)) == 0) { break; } close(s); s = -1; } if (s < 0 || rp == NULL) { TRACE_DEBUGGER_SERVER("debug server : error creating/binding socket"); return; } freeaddrinfo(result); listen(s, 1); while (true) { clientSocket = accept(s, NULL, NULL); if (clientSocket < 0) { TRACE_DEBUGGER_SERVER("debug server : error on accept"); return; } else { // read/write data TRACE_DEBUGGER_SERVER("debug server : client connected"); inData = "connected"; // process any input, send any output clearBuffers(); char buf[1024] = {0}; int readBytes = 0; while ((readBytes = ::recv(clientSocket, buf, sizeof(buf), 0)) > 0) { buf[readBytes] = '\0'; // TRACE_DEBUGGER_SERVER("debug server : received command >%s", buf); // no other thread is using this inData.append(buf); // process any input, send any output clearBuffers(); } // while(read) close(clientSocket); } } // while(true) } JSBool JSBDebug_BufferWrite(JSContext* cx, unsigned argc, jsval* vp) { if (argc == 1) { jsval* argv = JS_ARGV(cx, vp); JSStringWrapper strWrapper(argv[0]); // this is safe because we're already inside a lock (from clearBuffers) outData.append(strWrapper.get()); _clientSocketWriteAndClearString(outData); } return JS_TRUE; } void ScriptingCore::enableDebugger() { if (_debugGlobal == NULL) { JSAutoCompartment ac0(_cx, _global); JS_SetDebugMode(_cx, JS_TRUE); _debugGlobal = NewGlobalObject(_cx, true); JS_WrapObject(_cx, &_debugGlobal); JSAutoCompartment ac(_cx, _debugGlobal); // these are used in the debug program JS_DefineFunction(_cx, _debugGlobal, "log", ScriptingCore::log, 0, JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(_cx, _debugGlobal, "_bufferWrite", JSBDebug_BufferWrite, 1, JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(_cx, _debugGlobal, "_enterNestedEventLoop", JSBDebug_enterNestedEventLoop, 0, JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(_cx, _debugGlobal, "_exitNestedEventLoop", JSBDebug_exitNestedEventLoop, 0, JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(_cx, _debugGlobal, "_getEventLoopNestLevel", JSBDebug_getEventLoopNestLevel, 0, JSPROP_READONLY | JSPROP_PERMANENT); runScript("jsb_debugger.js", _debugGlobal); // prepare the debugger jsval argv = OBJECT_TO_JSVAL(_global); jsval outval; JSBool ok = JS_CallFunctionName(_cx, _debugGlobal, "_prepareDebugger", 1, &argv, &outval); if (!ok) { JS_ReportPendingException(_cx); } // start bg thread auto t = std::thread(&serverEntryPoint); t.detach(); Scheduler* scheduler = Director::getInstance()->getScheduler(); scheduler->scheduleUpdateForTarget(this->_runLoop, 0, false); } } JSObject* NewGlobalObject(JSContext* cx, bool debug) { JS::CompartmentOptions options; options.setVersion(JSVERSION_LATEST); JS::RootedObject glob(cx, JS_NewGlobalObject(cx, &global_class, NULL, JS::DontFireOnNewGlobalHook, options)); if (!glob) { return NULL; } JSAutoCompartment ac(cx, glob); JSBool ok = JS_TRUE; ok = JS_InitStandardClasses(cx, glob); if (ok) JS_InitReflect(cx, glob); if (ok && debug) ok = JS_DefineDebuggerObject(cx, glob); if (!ok) return NULL; JS_FireOnNewGlobalObject(cx, glob); return glob; } JSBool jsb_set_reserved_slot(JSObject *obj, uint32_t idx, jsval value) { JSClass *klass = JS_GetClass(obj); unsigned int slots = JSCLASS_RESERVED_SLOTS(klass); if ( idx >= slots ) return JS_FALSE; JS_SetReservedSlot(obj, idx, value); return JS_TRUE; } JSBool jsb_get_reserved_slot(JSObject *obj, uint32_t idx, jsval& ret) { JSClass *klass = JS_GetClass(obj); unsigned int slots = JSCLASS_RESERVED_SLOTS(klass); if ( idx >= slots ) return JS_FALSE; ret = JS_GetReservedSlot(obj, idx); return JS_TRUE; } js_proxy_t* jsb_new_proxy(void* nativeObj, JSObject* jsObj) { js_proxy_t* p; JS_NEW_PROXY(p, nativeObj, jsObj); return p; } js_proxy_t* jsb_get_native_proxy(void* nativeObj) { js_proxy_t* p; JS_GET_PROXY(p, nativeObj); return p; } js_proxy_t* jsb_get_js_proxy(JSObject* jsObj) { js_proxy_t* p; JS_GET_NATIVE_PROXY(p, jsObj); return p; } void jsb_remove_proxy(js_proxy_t* nativeProxy, js_proxy_t* jsProxy) { JS_REMOVE_PROXY(nativeProxy, jsProxy); } // JSStringWrapper JSStringWrapper::JSStringWrapper() : _buffer(nullptr) { } JSStringWrapper::JSStringWrapper(JSString* str, JSContext* cx/* = NULL*/) : _buffer(nullptr) { set(str, cx); } JSStringWrapper::JSStringWrapper(jsval val, JSContext* cx/* = NULL*/) : _buffer(nullptr) { set(val, cx); } JSStringWrapper::~JSStringWrapper() { CC_SAFE_DELETE_ARRAY(_buffer); } void JSStringWrapper::set(jsval val, JSContext* cx) { if (val.isString()) { this->set(val.toString(), cx); } else { CC_SAFE_DELETE_ARRAY(_buffer); } } void JSStringWrapper::set(JSString* str, JSContext* cx) { CC_SAFE_DELETE_ARRAY(_buffer); if (!cx) { cx = ScriptingCore::getInstance()->getGlobalContext(); } // JS_EncodeString isn't supported in SpiderMonkey ff19.0. //buffer = JS_EncodeString(cx, string); unsigned short* pStrUTF16 = (unsigned short*)JS_GetStringCharsZ(cx, str); _buffer = cc_utf16_to_utf8(pStrUTF16, -1, NULL, NULL); } const char* JSStringWrapper::get() { return _buffer ? _buffer : ""; }