You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
cocos_lib/cocos/bindings/manual/jsb_websocket.cpp

566 lines
22 KiB

/****************************************************************************
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
http://www.cocos.com
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 "jsb_websocket.h"
#include "MappingUtils.h"
#include "base/std/container/unordered_set.h"
#include "cocos/base/DeferredReleasePool.h"
#include "cocos/bindings/jswrapper/SeApi.h"
#include "cocos/bindings/manual/jsb_conversions.h"
#include "cocos/bindings/manual/jsb_global.h"
#include "application/ApplicationManager.h"
#include "base/UTF8.h"
/*
[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;
*/
#define GET_DELEGATE_FN(name) \
_JSDelegate.isObject() && _JSDelegate.toObject()->getProperty(name, &func)
namespace {
se::Class *jsbWebSocketClass = nullptr;
ccstd::unordered_set<JsbWebSocketDelegate *> jsbWebSocketDelegates;
} // namespace
JsbWebSocketDelegate::JsbWebSocketDelegate() {
jsbWebSocketDelegates.insert(this);
}
JsbWebSocketDelegate::~JsbWebSocketDelegate() {
CC_LOG_INFO("In the destructor of JSbWebSocketDelegate(%p)", this);
jsbWebSocketDelegates.erase(this);
}
void JsbWebSocketDelegate::onOpen(cc::network::WebSocket *ws) {
se::ScriptEngine::getInstance()->clearException();
se::AutoHandleScope hs;
if (CC_CURRENT_APPLICATION() == nullptr) {
return;
}
se::Object *wsObj = se::NativePtrToObjectMap::findFirst(ws);
if (!wsObj) {
return;
}
wsObj->setProperty("protocol", se::Value(ws->getProtocol()));
se::HandleObject jsObj(se::Object::createPlainObject());
jsObj->setProperty("type", se::Value("open"));
se::Value target;
native_ptr_to_seval<cc::network::WebSocket>(ws, &target);
jsObj->setProperty("target", target);
se::Value func;
bool ok = GET_DELEGATE_FN("onopen");
if (ok && func.isObject() && func.toObject()->isFunction()) {
se::ValueArray args;
args.push_back(se::Value(jsObj));
func.toObject()->call(args, wsObj);
} else {
SE_REPORT_ERROR("Can't get onopen function!");
}
}
void JsbWebSocketDelegate::onMessage(cc::network::WebSocket *ws, const cc::network::WebSocket::Data &data) {
se::ScriptEngine::getInstance()->clearException();
se::AutoHandleScope hs;
if (CC_CURRENT_APPLICATION() == nullptr) {
return;
}
se::Object *wsObj = se::NativePtrToObjectMap::findFirst(ws);
if (!wsObj) {
return;
}
se::HandleObject jsObj(se::Object::createPlainObject());
jsObj->setProperty("type", se::Value("message"));
se::Value target;
native_ptr_to_seval<cc::network::WebSocket>(ws, &target);
jsObj->setProperty("target", target);
se::Value func;
bool ok = GET_DELEGATE_FN("onmessage");
if (ok && func.isObject() && func.toObject()->isFunction()) {
se::ValueArray args;
args.push_back(se::Value(jsObj));
if (data.isBinary) {
se::HandleObject dataObj(se::Object::createArrayBufferObject(data.bytes, data.len));
jsObj->setProperty("data", se::Value(dataObj));
} else {
se::Value dataVal;
if (strlen(data.bytes) == 0 && data.len > 0) { // String with 0x00 prefix
ccstd::string str(data.bytes, data.len);
dataVal.setString(str);
} else { // Normal string
dataVal.setString(ccstd::string(data.bytes, data.len));
}
if (dataVal.isNullOrUndefined()) {
ws->closeAsync();
} else {
jsObj->setProperty("data", se::Value(dataVal));
}
}
func.toObject()->call(args, wsObj);
} else {
SE_REPORT_ERROR("Can't get onmessage function!");
}
}
void JsbWebSocketDelegate::onClose(cc::network::WebSocket *ws, uint16_t code, const ccstd::string &reason, bool wasClean) {
se::ScriptEngine::getInstance()->clearException();
se::AutoHandleScope hs;
if (CC_CURRENT_APPLICATION() == nullptr) {
return;
}
se::Object *wsObj = se::NativePtrToObjectMap::findFirst(ws);
do {
if (!wsObj) {
CC_LOG_INFO("WebSocket js instance was destroyted, don't need to invoke onclose callback!");
break;
}
se::HandleObject jsObj(se::Object::createPlainObject());
jsObj->setProperty("type", se::Value("close")); // deprecated since v3.6
se::Value target;
native_ptr_to_seval<cc::network::WebSocket>(ws, &target);
jsObj->setProperty("target", target); // deprecated since v3.6
// CloseEvent attributes
jsObj->setProperty("code", se::Value(code));
jsObj->setProperty("reason", se::Value(reason));
jsObj->setProperty("wasClean", se::Value(wasClean));
se::Value func;
bool ok = GET_DELEGATE_FN("onclose");
if (ok && func.isObject() && func.toObject()->isFunction()) {
se::ValueArray args;
args.push_back(se::Value(jsObj));
func.toObject()->call(args, wsObj);
} else {
SE_REPORT_ERROR("Can't get onclose function!");
}
// JS Websocket object now can be GC, since the connection is closed.
wsObj->unroot();
if (_JSDelegate.isObject()) {
_JSDelegate.toObject()->unroot();
}
// Websocket instance is attached to global object in 'WebSocket_close'
// It's safe to detach it here since JS 'onclose' method has been already invoked.
se::ScriptEngine::getInstance()->getGlobalObject()->detachObject(wsObj);
} while (false);
ws->release();
release(); // Release delegate self at last
}
void JsbWebSocketDelegate::onError(cc::network::WebSocket *ws, const cc::network::WebSocket::ErrorCode & /*error*/) {
se::ScriptEngine::getInstance()->clearException();
se::AutoHandleScope hs;
if (CC_CURRENT_APPLICATION() == nullptr) {
return;
}
se::Object *wsObj = se::NativePtrToObjectMap::findFirst(ws);
if (!wsObj) {
return;
}
se::HandleObject jsObj(se::Object::createPlainObject());
jsObj->setProperty("type", se::Value("error"));
se::Value target;
native_ptr_to_seval<cc::network::WebSocket>(ws, &target);
jsObj->setProperty("target", target);
se::Value func;
bool ok = GET_DELEGATE_FN("onerror");
if (ok && func.isObject() && func.toObject()->isFunction()) {
se::ValueArray args;
args.push_back(se::Value(jsObj));
func.toObject()->call(args, wsObj);
} else {
SE_REPORT_ERROR("Can't get onerror function!");
}
}
void JsbWebSocketDelegate::setJSDelegate(const se::Value &jsDelegate) {
CC_ASSERT(jsDelegate.isObject());
_JSDelegate = jsDelegate;
se::ScriptEngine::getInstance()->addBeforeCleanupHook([this]() {
if (jsbWebSocketDelegates.find(this) != jsbWebSocketDelegates.end()) {
_JSDelegate.setUndefined();
}
});
}
static bool webSocketFinalize(se::State &s) {
auto *cobj = static_cast<cc::network::WebSocket *>(s.nativeThisObject());
CC_LOG_INFO("jsbindings: finalizing JS object %p (WebSocket)", cobj);
// Manually close if web socket is not closed
if (cobj->getReadyState() != cc::network::WebSocket::State::CLOSED) {
CC_LOG_INFO("WebSocket (%p) isn't closed, try to close it!", cobj);
cobj->closeAsync();
}
static_cast<JsbWebSocketDelegate *>(cobj->getDelegate())->release();
return true;
}
SE_BIND_FINALIZE_FUNC(webSocketFinalize)
static bool webSocketConstructor(se::State &s) {
const auto &args = s.args();
int argc = static_cast<int>(args.size());
if (argc == 1 || argc == 2 || argc == 3) {
ccstd::string url;
bool ok = sevalue_to_native(args[0], &url);
SE_PRECONDITION2(ok, false, "Error processing url argument");
se::Object *obj = s.thisObject();
cc::network::WebSocket *cobj = nullptr;
if (argc >= 2) {
ccstd::string caFilePath;
ccstd::vector<ccstd::string> protocols;
if (args[1].isString()) {
ccstd::string protocol;
ok = sevalue_to_native(args[1], &protocol);
SE_PRECONDITION2(ok, false, "Error processing protocol string");
protocols.push_back(protocol);
} else if (args[1].isObject() && args[1].toObject()->isArray()) {
se::Object *protocolArr = args[1].toObject();
uint32_t len = 0;
ok = protocolArr->getArrayLength(&len);
SE_PRECONDITION2(ok, false, "getArrayLength failed!");
se::Value tmp;
for (uint32_t i = 0; i < len; ++i) {
if (!protocolArr->getArrayElement(i, &tmp)) {
continue;
}
ccstd::string protocol;
ok = sevalue_to_native(tmp, &protocol);
SE_PRECONDITION2(ok, false, "Error processing protocol object");
protocols.push_back(protocol);
}
}
if (argc > 2) {
ok = sevalue_to_native(args[2], &caFilePath);
SE_PRECONDITION2(ok, false, "Error processing caFilePath");
}
cobj = ccnew cc::network::WebSocket();
auto *delegate = ccnew JsbWebSocketDelegate();
delegate->addRef();
if (cobj->init(*delegate, url, &protocols, caFilePath)) {
delegate->setJSDelegate(se::Value(obj, true));
cobj->addRef(); // release in finalize function and onClose delegate method
delegate->addRef(); // release in finalize function and onClose delegate method
} else {
cobj->release();
delegate->release();
SE_REPORT_ERROR("WebSocket init failed!");
return false;
}
} else {
cobj = ccnew cc::network::WebSocket();
auto *delegate = ccnew JsbWebSocketDelegate();
delegate->addRef();
if (cobj->init(*delegate, url)) {
delegate->setJSDelegate(se::Value(obj, true));
cobj->addRef(); // release in finalize function and onClose delegate method
delegate->addRef(); // release in finalize function and onClose delegate method
} else {
cobj->release();
delegate->release();
SE_REPORT_ERROR("WebSocket init failed!");
return false;
}
}
obj->setProperty("url", args[0]);
// The websocket draft uses lowercase 'url', so 'URL' need to be deprecated.
obj->setProperty("URL", args[0]);
// Initialize protocol property with an empty string, it will be assigned in onOpen delegate.
obj->setProperty("protocol", se::Value(""));
obj->setPrivateData(cobj);
obj->root();
return true;
}
SE_REPORT_ERROR("wrong number of arguments: %d, was expecting 1<= and <=3", argc);
return false;
}
SE_BIND_CTOR(webSocketConstructor, jsbWebSocketClass, webSocketFinalize)
static bool webSocketSend(se::State &s) {
const auto &args = s.args();
int argc = static_cast<int>(args.size());
if (argc == 1) {
auto *cobj = static_cast<cc::network::WebSocket *>(s.nativeThisObject());
bool ok = false;
if (args[0].isString()) {
ccstd::string data;
ok = sevalue_to_native(args[0], &data);
SE_PRECONDITION2(ok, false, "Convert string failed");
// IDEA: We didn't find a way to get the JS string length in JSB2.0.
// if (data.empty() && len > 0)
// {
// CC_LOG_DEBUGWARN("Text message to send is empty, but its length is greater than 0!");
// //IDEA: 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 (args[0].isObject()) {
se::Object *dataObj = args[0].toObject();
uint8_t *ptr = nullptr;
size_t length = 0;
if (dataObj->isArrayBuffer()) {
ok = dataObj->getArrayBufferData(&ptr, &length);
SE_PRECONDITION2(ok, false, "getArrayBufferData failed!");
} else if (dataObj->isTypedArray()) {
ok = dataObj->getTypedArrayData(&ptr, &length);
SE_PRECONDITION2(ok, false, "getTypedArrayData failed!");
} else {
CC_ABORT();
}
cobj->send(ptr, static_cast<unsigned int>(length));
} else {
CC_ABORT();
}
return true;
}
SE_REPORT_ERROR("wrong number of arguments: %d, was expecting 1", argc);
return false;
}
SE_BIND_FUNC(webSocketSend)
static bool webSocketClose(se::State &s) {
const auto &args = s.args();
int argc = static_cast<int>(args.size());
auto *cobj = static_cast<cc::network::WebSocket *>(s.nativeThisObject());
if (argc == 0) {
cobj->closeAsync();
} else if (argc == 1) {
if (args[0].isNumber()) {
int reasonCode{0};
sevalue_to_native(args[0], &reasonCode);
cobj->closeAsync(reasonCode, "no_reason");
} else if (args[0].isString()) {
ccstd::string reasonString;
sevalue_to_native(args[0], &reasonString);
cobj->closeAsync(1005, reasonString);
} else {
CC_ABORT();
}
} else if (argc == 2) {
if (args[0].isNumber()) {
int reasonCode{0};
if (args[1].isString()) {
ccstd::string reasonString;
sevalue_to_native(args[0], &reasonCode);
sevalue_to_native(args[1], &reasonString);
cobj->closeAsync(reasonCode, reasonString);
} else if (args[1].isNullOrUndefined()) {
sevalue_to_native(args[0], &reasonCode);
cobj->closeAsync(reasonCode, "no_reason");
} else {
CC_ABORT();
}
} else if (args[0].isNullOrUndefined()) {
if (args[1].isString()) {
ccstd::string reasonString;
sevalue_to_native(args[1], &reasonString);
cobj->closeAsync(1005, reasonString);
} else if (args[1].isNullOrUndefined()) {
cobj->closeAsync();
} else {
CC_ABORT();
}
} else {
CC_ABORT();
}
} else {
SE_REPORT_ERROR("wrong number of arguments: %d, was expecting <=2", argc);
CC_ABORT();
}
// Attach current WebSocket instance to global object to prevent WebSocket instance
// being garbage collected after "ws.close(); ws = null;"
// There is a state that current WebSocket JS instance is being garbaged but its finalize
// callback has not be invoked. Then in "JSB_WebSocketDelegate::onClose", se::Object is
// still be able to be found and while invoking JS 'onclose' method, crash will happen since
// JS instance is invalid and is going to be collected. This bug is easiler reproduced on iOS
// because JavaScriptCore is more GC sensitive.
// Please note that we need to detach it from global object in "JSB_WebSocketDelegate::onClose".
se::ScriptEngine::getInstance()->getGlobalObject()->attachObject(s.thisObject());
return true;
}
SE_BIND_FUNC(webSocketClose)
static bool webSocketGetReadyState(se::State &s) {
const auto &args = s.args();
int argc = static_cast<int>(args.size());
if (argc == 0) {
auto *cobj = static_cast<cc::network::WebSocket *>(s.nativeThisObject());
s.rval().setInt32(static_cast<int>(cobj->getReadyState()));
return true;
}
SE_REPORT_ERROR("wrong number of arguments: %d, was expecting 0", argc);
return false;
}
SE_BIND_PROP_GET(webSocketGetReadyState)
static bool webSocketGetBufferedAmount(se::State &s) {
const auto &args = s.args();
int argc = static_cast<int>(args.size());
if (argc == 0) {
auto *cobj = static_cast<cc::network::WebSocket *>(s.nativeThisObject());
s.rval().setUint32(static_cast<uint32_t>(cobj->getBufferedAmount()));
return true;
}
SE_REPORT_ERROR("wrong number of arguments: %d, was expecting 0", argc);
return false;
}
SE_BIND_PROP_GET(webSocketGetBufferedAmount)
static bool webSocketGetExtensions(se::State &s) {
const auto &args = s.args();
int argc = static_cast<int>(args.size());
if (argc == 0) {
auto *cobj = static_cast<cc::network::WebSocket *>(s.nativeThisObject());
s.rval().setString(cobj->getExtensions());
return true;
}
SE_REPORT_ERROR("wrong number of arguments: %d, was expecting 0", argc);
return false;
}
SE_BIND_PROP_GET(webSocketGetExtensions)
#define WEBSOCKET_DEFINE_READONLY_INT_FIELD(full_name, value) \
static bool full_name(se::State &s) { \
const auto &args = s.args(); \
int argc = (int)args.size(); \
if (argc == 0) { \
s.rval().setInt32(value); \
return true; \
} \
SE_REPORT_ERROR("wrong number of arguments: %d, was expecting 0", argc); \
return false; \
} \
SE_BIND_PROP_GET(full_name)
WEBSOCKET_DEFINE_READONLY_INT_FIELD(Websocket_CONNECTING, static_cast<int>(cc::network::WebSocket::State::CONNECTING))
WEBSOCKET_DEFINE_READONLY_INT_FIELD(Websocket_OPEN, static_cast<int>(cc::network::WebSocket::State::OPEN))
WEBSOCKET_DEFINE_READONLY_INT_FIELD(Websocket_CLOSING, static_cast<int>(cc::network::WebSocket::State::CLOSING))
WEBSOCKET_DEFINE_READONLY_INT_FIELD(Websocket_CLOSED, static_cast<int>(cc::network::WebSocket::State::CLOSED))
bool register_all_websocket(se::Object *global) { // NOLINT (readability-identifier-naming)
se::Value nsVal;
if (!global->getProperty("jsb", &nsVal, true)) {
se::HandleObject jsobj(se::Object::createPlainObject());
nsVal.setObject(jsobj);
global->setProperty("jsb", nsVal);
}
se::Object *ns = nsVal.toObject();
se::Class *cls = se::Class::create("WebSocket", ns, nullptr, _SE(webSocketConstructor));
cls->defineFinalizeFunction(_SE(webSocketFinalize));
cls->defineFunction("send", _SE(webSocketSend));
cls->defineFunction("close", _SE(webSocketClose));
cls->defineProperty("readyState", _SE(webSocketGetReadyState), nullptr);
cls->defineProperty("bufferedAmount", _SE(webSocketGetBufferedAmount), nullptr);
cls->defineProperty("extensions", _SE(webSocketGetExtensions), nullptr);
cls->defineProperty("CONNECTING", _SE(Websocket_CONNECTING), nullptr);
cls->defineProperty("CLOSING", _SE(Websocket_CLOSING), nullptr);
cls->defineProperty("OPEN", _SE(Websocket_OPEN), nullptr);
cls->defineProperty("CLOSED", _SE(Websocket_CLOSED), nullptr);
cls->install();
se::Value tmp;
ns->getProperty("WebSocket", &tmp);
tmp.toObject()->defineProperty("CONNECTING", _SE(Websocket_CONNECTING), nullptr);
tmp.toObject()->defineProperty("CLOSING", _SE(Websocket_CLOSING), nullptr);
tmp.toObject()->defineProperty("OPEN", _SE(Websocket_OPEN), nullptr);
tmp.toObject()->defineProperty("CLOSED", _SE(Websocket_CLOSED), nullptr);
JSBClassType::registerClass<cc::network::WebSocket>(cls);
jsbWebSocketClass = cls;
se::ScriptEngine::getInstance()->clearException();
return true;
}