axmol/cocos/scripting/js-bindings/manual/network/jsb_websocket.cpp

456 lines
17 KiB
C++

/*
* Created by James Chen
* Copyright (c) 2013-2016 Chukong Technologies Inc.
* Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "scripting/js-bindings/manual/network/jsb_websocket.h"
#include "base/ccUTF8.h"
#include "base/CCDirector.h"
#include "network/WebSocket.h"
#include "platform/CCPlatformMacros.h"
#include "scripting/js-bindings/manual/ScriptingCore.h"
#include "scripting/js-bindings/manual/cocos2d_specifics.hpp"
#include "scripting/js-bindings/manual/spidermonkey_specifics.h"
using namespace cocos2d::network;
/*
[Constructor(in DOMString url, in optional DOMString protocols)]
[Constructor(in DOMString url, in optional DOMString[] protocols)]
interface WebSocket {
readonly attribute DOMString url;
// ready state
const unsigned short CONNECTING = 0;
const unsigned short OPEN = 1;
const unsigned short CLOSING = 2;
const unsigned short CLOSED = 3;
readonly attribute unsigned short readyState;
readonly attribute unsigned long bufferedAmount;
// networking
attribute Function onopen;
attribute Function onmessage;
attribute Function onerror;
attribute Function onclose;
readonly attribute DOMString protocol;
void send(in DOMString data);
void close();
};
WebSocket implements EventTarget;
*/
class JSB_WebSocketDelegate : public WebSocket::Delegate
{
public:
JSB_WebSocketDelegate()
{
JSContext* cx = ScriptingCore::getInstance()->getGlobalContext();
_JSDelegate.construct(cx);
}
~JSB_WebSocketDelegate()
{
_JSDelegate.destroyIfConstructed();
}
virtual void onOpen(WebSocket* ws)
{
js_proxy_t * p = jsb_get_native_proxy(ws);
if (!p) return;
if (cocos2d::Director::getInstance() == nullptr || cocos2d::ScriptEngineManager::getInstance() == nullptr)
return;
JSB_AUTOCOMPARTMENT_WITH_GLOBAL_OBJCET
JSContext* cx = ScriptingCore::getInstance()->getGlobalContext();
// Set the protocol which server selects.
JS::RootedValue jsprotocol(cx, std_string_to_jsval(cx, ws->getProtocol()));
JS::RootedObject wsObj(cx, p->obj);
JS_SetProperty(cx, wsObj, "protocol", jsprotocol);
JS::RootedObject jsobj(cx, JS_NewObject(cx, NULL, JS::NullPtr(), JS::NullPtr()));
JS::RootedValue vp(cx);
vp = c_string_to_jsval(cx, "open");
JS_SetProperty(cx, jsobj, "type", vp);
jsval args = OBJECT_TO_JSVAL(jsobj);
ScriptingCore::getInstance()->executeFunctionWithOwner(OBJECT_TO_JSVAL(_JSDelegate.ref()), "onopen", 1, &args);
}
virtual void onMessage(WebSocket* ws, const WebSocket::Data& data)
{
js_proxy_t * p = jsb_get_native_proxy(ws);
if (p == nullptr) return;
if (cocos2d::Director::getInstance() == nullptr || cocos2d::ScriptEngineManager::getInstance() == nullptr)
return;
JSB_AUTOCOMPARTMENT_WITH_GLOBAL_OBJCET
JSContext* cx = ScriptingCore::getInstance()->getGlobalContext();
JS::RootedObject jsobj(cx, JS_NewObject(cx, NULL, JS::NullPtr(), JS::NullPtr()));
JS::RootedValue vp(cx);
vp = c_string_to_jsval(cx, "message");
JS_SetProperty(cx, jsobj, "type", vp);
JS::RootedValue args(cx, OBJECT_TO_JSVAL(jsobj));
if (data.isBinary)
{// data is binary
JS::RootedObject buffer(cx, JS_NewArrayBuffer(cx, static_cast<uint32_t>(data.len)));
if (data.len > 0)
{
uint8_t* bufdata = JS_GetArrayBufferData(buffer);
memcpy((void*)bufdata, (void*)data.bytes, data.len);
}
JS::RootedValue dataVal(cx);
dataVal = OBJECT_TO_JSVAL(buffer);
JS_SetProperty(cx, jsobj, "data", dataVal);
}
else
{// data is string
JS::RootedValue dataVal(cx);
if (strlen(data.bytes) == 0 && data.len > 0)
{// String with 0x00 prefix
dataVal = STRING_TO_JSVAL(JS_NewStringCopyN(cx, data.bytes, data.len));
}
else
{// Normal string
dataVal = c_string_to_jsval(cx, data.bytes);
}
if (dataVal.isNullOrUndefined())
{
ws->closeAsync();
return;
}
JS_SetProperty(cx, jsobj, "data", dataVal);
}
ScriptingCore::getInstance()->executeFunctionWithOwner(OBJECT_TO_JSVAL(_JSDelegate.ref()), "onmessage", 1, args.address());
}
virtual void onClose(WebSocket* ws)
{
js_proxy_t * p = jsb_get_native_proxy(ws);
if (!p) return;
if (cocos2d::Director::getInstance() != nullptr && cocos2d::Director::getInstance()->getRunningScene() && cocos2d::ScriptEngineManager::getInstance() != nullptr)
{
JSB_AUTOCOMPARTMENT_WITH_GLOBAL_OBJCET
JSContext* cx = ScriptingCore::getInstance()->getGlobalContext();
JS::RootedObject jsobj(cx, JS_NewObject(cx, NULL, JS::NullPtr(), JS::NullPtr()));
JS::RootedValue vp(cx);
vp = c_string_to_jsval(cx, "close");
JS_SetProperty(cx, jsobj, "type", vp);
JS::RootedValue args(cx, OBJECT_TO_JSVAL(jsobj));
ScriptingCore::getInstance()->executeFunctionWithOwner(OBJECT_TO_JSVAL(_JSDelegate.ref()), "onclose", 1, args.address());
auto copy = &p->obj;
JS::RemoveObjectRoot(cx, copy);
jsb_remove_proxy(p);
}
// Delete WebSocket instance
CC_SAFE_DELETE(ws);
// Delete self at last while websocket was closed.
delete this;
}
virtual void onError(WebSocket* ws, const WebSocket::ErrorCode& error)
{
js_proxy_t * p = jsb_get_native_proxy(ws);
if (!p) return;
if (cocos2d::Director::getInstance() == nullptr || cocos2d::ScriptEngineManager::getInstance() == nullptr)
return;
JSB_AUTOCOMPARTMENT_WITH_GLOBAL_OBJCET
JSContext* cx = ScriptingCore::getInstance()->getGlobalContext();
JS::RootedObject jsobj(cx, JS_NewObject(cx, NULL, JS::NullPtr(), JS::NullPtr()));
JS::RootedValue vp(cx);
vp = c_string_to_jsval(cx, "error");
JS_SetProperty(cx, jsobj, "type", vp);
JS::RootedValue args(cx, OBJECT_TO_JSVAL(jsobj));
ScriptingCore::getInstance()->executeFunctionWithOwner(OBJECT_TO_JSVAL(_JSDelegate.ref()), "onerror", 1, args.address());
}
void setJSDelegate(JS::HandleObject pJSDelegate)
{
_JSDelegate.ref() = pJSDelegate;
}
private:
mozilla::Maybe<JS::PersistentRootedObject> _JSDelegate;
};
JSClass *js_cocos2dx_websocket_class;
JSObject *js_cocos2dx_websocket_prototype;
void js_cocos2dx_WebSocket_finalize(JSFreeOp *fop, JSObject *obj) {
CCLOG("jsbindings: finalizing JS object %p (WebSocket)", obj);
}
bool js_cocos2dx_extension_WebSocket_send(JSContext *cx, uint32_t argc, jsval *vp)
{
JS::CallArgs argv = JS::CallArgsFromVp(argc, vp);
JS::RootedObject obj(cx, argv.thisv().toObjectOrNull());
js_proxy_t *proxy = jsb_get_js_proxy(obj);
WebSocket* cobj = (WebSocket *)(proxy ? proxy->ptr : NULL);
JSB_PRECONDITION2( cobj, cx, false, "Invalid Native Object");
if(argc == 1)
{
if (argv[0].isString())
{
ssize_t len = JS_GetStringLength(argv[0].toString());
std::string data;
jsval_to_std_string(cx, argv[0], &data);
if (data.empty() && len > 0)
{
CCLOGWARN("Text message to send is empty, but its length is greater than 0!");
//FIXME: Note that this text message contains '0x00' prefix, so its length calcuted by strlen is 0.
// we need to fix that if there is '0x00' in text message,
// since javascript language could support '0x00' inserted at the beginning or the middle of text message
}
cobj->send(data);
}
else if (argv[0].isObject())
{
uint8_t *bufdata = NULL;
uint32_t len = 0;
JS::RootedObject jsobj(cx, argv[0].toObjectOrNull());
if (JS_IsArrayBufferObject(jsobj))
{
bufdata = JS_GetArrayBufferData(jsobj);
len = JS_GetArrayBufferByteLength(jsobj);
}
else if (JS_IsArrayBufferViewObject(jsobj))
{
bufdata = (uint8_t*)JS_GetArrayBufferViewData(jsobj);
len = JS_GetArrayBufferViewByteLength(jsobj);
}
cobj->send(bufdata, len);
}
else
{
JS_ReportError(cx, "data type to be sent is unsupported.");
return false;
}
argv.rval().setUndefined();
return true;
}
JS_ReportError(cx, "wrong number of arguments: %d, was expecting %d", argc, 0);
return true;
}
bool js_cocos2dx_extension_WebSocket_close(JSContext *cx, uint32_t argc, jsval *vp){
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
JS::RootedObject obj(cx, args.thisv().toObjectOrNull());
js_proxy_t *proxy = jsb_get_js_proxy(obj);
WebSocket* cobj = (WebSocket *)(proxy ? proxy->ptr : NULL);
JSB_PRECONDITION2( cobj, cx, false, "Invalid Native Object");
if(argc == 0){
cobj->closeAsync();
args.rval().setUndefined();
return true;
}
JS_ReportError(cx, "wrong number of arguments: %d, was expecting %d", argc, 0);
return false;
}
bool js_cocos2dx_extension_WebSocket_constructor(JSContext *cx, uint32_t argc, jsval *vp)
{
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
if (argc == 1 || argc == 2 || argc == 3)
{
std::string url;
do {
bool ok = jsval_to_std_string(cx, args.get(0), &url);
JSB_PRECONDITION2(ok, cx, false, "Error processing arguments");
} while (0);
JS::RootedObject proto(cx, js_cocos2dx_websocket_prototype);
JS::RootedObject obj(cx, JS_NewObject(cx, js_cocos2dx_websocket_class, proto, JS::NullPtr()));
//JS::RootedObject obj(cx, JS_NewObjectForConstructor(cx, js_cocos2dx_websocket_class, args));
WebSocket* cobj = nullptr;
if (argc >= 2)
{
std::string caFilePath;
std::vector<std::string> protocols;
if (args.get(1).isString())
{
std::string protocol;
do {
bool ok = jsval_to_std_string(cx, args.get(1), &protocol);
JSB_PRECONDITION2(ok, cx, false, "Error processing arguments");
} while (0);
protocols.push_back(protocol);
}
else if (args.get(1).isObject())
{
bool ok = true;
JS::RootedObject arg2(cx, args.get(1).toObjectOrNull());
JSB_PRECONDITION(JS_IsArrayObject(cx, arg2), "Object must be an array");
uint32_t len = 0;
JS_GetArrayLength(cx, arg2, &len);
for (uint32_t i = 0; i< len; i++)
{
JS::RootedValue valarg(cx);
JS_GetElement(cx, arg2, i, &valarg);
std::string protocol;
do {
ok = jsval_to_std_string(cx, valarg, &protocol);
JSB_PRECONDITION2(ok, cx, false, "Error processing arguments");
} while (0);
protocols.push_back(protocol);
}
}
if (argc > 2)
{
do {
bool ok = jsval_to_std_string(cx, args.get(2), &caFilePath);
JSB_PRECONDITION2(ok, cx, false, "Error processing arguments");
} while (0);
}
cobj = new (std::nothrow) WebSocket();
JSB_WebSocketDelegate* delegate = new (std::nothrow) JSB_WebSocketDelegate();
delegate->setJSDelegate(obj);
cobj->init(*delegate, url, &protocols, caFilePath);
}
else
{
cobj = new (std::nothrow) WebSocket();
JSB_WebSocketDelegate* delegate = new (std::nothrow) JSB_WebSocketDelegate();
delegate->setJSDelegate(obj);
cobj->init(*delegate, url);
}
JS_DefineProperty(cx, obj, "url", args.get(0), JSPROP_ENUMERATE | JSPROP_PERMANENT | JSPROP_READONLY);
// The websocket draft uses lowercase 'url', so 'URL' need to be deprecated.
JS_DefineProperty(cx, obj, "URL", args.get(0), JSPROP_ENUMERATE | JSPROP_PERMANENT | JSPROP_READONLY);
// Initialize protocol property with an empty string, it will be assigned in onOpen delegate.
JS::RootedValue jsprotocol(cx, c_string_to_jsval(cx, ""));
JS_DefineProperty(cx, obj, "protocol", jsprotocol, JSPROP_ENUMERATE | JSPROP_PERMANENT);
// link the native object with the javascript object
js_proxy_t *p = jsb_new_proxy(cobj, obj);
JS::AddNamedObjectRoot(cx, &p->obj, "WebSocket");
args.rval().set(OBJECT_TO_JSVAL(obj));
return true;
}
JS_ReportError(cx, "wrong number of arguments: %d, was expecting %d", argc, 0);
return false;
}
static bool js_cocos2dx_extension_WebSocket_get_readyState(JSContext *cx, uint32_t argc, jsval *vp)
{
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
JS::RootedObject jsobj(cx, args.thisv().toObjectOrNull());
js_proxy_t *proxy = jsb_get_js_proxy(jsobj);
WebSocket* cobj = (WebSocket *)(proxy ? proxy->ptr : NULL);
JSB_PRECONDITION2( cobj, cx, false, "Invalid Native Object");
if (cobj) {
args.rval().set(INT_TO_JSVAL((int)cobj->getReadyState()));
return true;
} else {
JS_ReportError(cx, "Error: WebSocket instance is invalid.");
return false;
}
}
void register_jsb_websocket(JSContext *cx, JS::HandleObject global)
{
js_cocos2dx_websocket_class = (JSClass *)calloc(1, sizeof(JSClass));
js_cocos2dx_websocket_class->name = "WebSocket";
js_cocos2dx_websocket_class->addProperty = JS_PropertyStub;
js_cocos2dx_websocket_class->delProperty = JS_DeletePropertyStub;
js_cocos2dx_websocket_class->getProperty = JS_PropertyStub;
js_cocos2dx_websocket_class->setProperty = JS_StrictPropertyStub;
js_cocos2dx_websocket_class->enumerate = JS_EnumerateStub;
js_cocos2dx_websocket_class->resolve = JS_ResolveStub;
js_cocos2dx_websocket_class->convert = JS_ConvertStub;
js_cocos2dx_websocket_class->finalize = js_cocos2dx_WebSocket_finalize;
js_cocos2dx_websocket_class->flags = JSCLASS_HAS_RESERVED_SLOTS(2);
static JSPropertySpec properties[] = {
JS_PSG("readyState", js_cocos2dx_extension_WebSocket_get_readyState, JSPROP_ENUMERATE | JSPROP_PERMANENT),
JS_PS_END
};
static JSFunctionSpec funcs[] = {
JS_FN("send",js_cocos2dx_extension_WebSocket_send, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FN("close",js_cocos2dx_extension_WebSocket_close, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
JS_FS_END
};
static JSFunctionSpec st_funcs[] = {
JS_FS_END
};
js_cocos2dx_websocket_prototype = JS_InitClass(
cx, global,
JS::NullPtr(),
js_cocos2dx_websocket_class,
js_cocos2dx_extension_WebSocket_constructor, 0, // constructor
properties,
funcs,
NULL, // no static properties
st_funcs);
JS::RootedObject jsclassObj(cx, anonEvaluate(cx, global, "(function () { return WebSocket; })()").toObjectOrNull());
JS_DefineProperty(cx, jsclassObj, "CONNECTING", (int)WebSocket::State::CONNECTING, JSPROP_ENUMERATE | JSPROP_PERMANENT | JSPROP_READONLY);
JS_DefineProperty(cx, jsclassObj, "OPEN", (int)WebSocket::State::OPEN, JSPROP_ENUMERATE | JSPROP_PERMANENT | JSPROP_READONLY);
JS_DefineProperty(cx, jsclassObj, "CLOSING", (int)WebSocket::State::CLOSING, JSPROP_ENUMERATE | JSPROP_PERMANENT | JSPROP_READONLY);
JS_DefineProperty(cx, jsclassObj, "CLOSED", (int)WebSocket::State::CLOSED, JSPROP_ENUMERATE | JSPROP_PERMANENT | JSPROP_READONLY);
}