* Copyright (c) 2025 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <set>
#include <string>
#include <unistd.h>
#include <uv.h>
#include "nlohmann/json.hpp"
#include "common_utils.h"
#include "callback_code_napi.h"
namespace OHOS::perftest {
using namespace nlohmann;
using namespace std;
static constexpr size_t NAPI_MAX_BUF_LEN = 1024;
void ThreadLock::LockRelease(string errorMessage)
{
errMsg = errorMessage;
ready = true;
condition.notify_all();
}
static map<string, napi_ref> g_jsRefs;
static size_t g_incJsCbId = 0;
CallbackCodeNapi &CallbackCodeNapi::Get()
{
static CallbackCodeNapi instance;
return instance;
}
static void GetFunctionFromParam(napi_env env, napi_value jsParam, json& param, string funcName)
{
if (param.contains(funcName)) {
LOG_D("%{public}s has been defined", funcName.c_str());
return;
}
bool funcExisted;
NAPI_CALL_RETURN_VOID(env, napi_has_named_property(env, jsParam, funcName.c_str(), &funcExisted));
if (funcExisted) {
napi_value funcValue = nullptr;
NAPI_CALL_RETURN_VOID(env, napi_get_named_property(env, jsParam, funcName.c_str(), &funcValue));
napi_valuetype valueType = napi_undefined;
NAPI_CALL_RETURN_VOID(env, napi_typeof(env, funcValue, &valueType));
if (valueType != napi_function) {
LOG_E("GetFunctionFromParam failed, invalid callback function argument");
return;
}
const auto jsCbId = string("js_callback#") + to_string(++g_incJsCbId);
napi_ref ref = nullptr;
NAPI_CALL_RETURN_VOID(env, napi_create_reference(env, funcValue, 1, &ref));
LOG_I("CbId = %{public}s, CbRef = %{public}p", jsCbId.c_str(), ref);
g_jsRefs.insert({ jsCbId, ref });
param[funcName] = jsCbId;
}
}
void CallbackCodeNapi::PreprocessCallback(napi_env env, ApiCallInfo &call, napi_value jsThis,
napi_value jsParam, ApiCallErr &err)
{
DCHECK(env != nullptr);
DCHECK(jsThis != nullptr);
napi_valuetype paramType = napi_undefined;
NAPI_CALL_RETURN_VOID(env, napi_typeof(env, jsParam, ¶mType));
if (paramType != napi_object) {
err = ApiCallErr(ERR_INVALID_INPUT, "PreprocessCallback failed, Invalid type of param");
return;
}
auto ¶mList = call.paramList_;
if (paramList.size() < 1 || paramList.at(0).type() != detail::value_t::object) {
err = ApiCallErr(ERR_INVALID_INPUT, "Missing PerfTestStrategy argument");
return;
}
GetFunctionFromParam(env, jsParam, paramList.at(0), ACTION_CODE_FUNC_NAME);
GetFunctionFromParam(env, jsParam, paramList.at(0), RESET_CODE_FUNC_NAME);
}
void CallbackCodeNapi::InitCallbackContext(napi_env env, const ApiCallInfo &in, ApiReplyInfo &out,
shared_ptr<CodeCallbackContext> ctx)
{
LOG_I("Handler api callback: %{public}s", in.apiId_.c_str());
DCHECK(env != nullptr);
DCHECK(in.paramList_.size() == TWO);
DCHECK(in.paramList_.at(INDEX_ZERO).type() == detail::value_t::string);
DCHECK(in.paramList_.at(INDEX_ONE).type() == detail::value_t::number_unsigned);
auto callbackId = in.paramList_.at(INDEX_ZERO).get<string>();
auto timeout = in.paramList_.at(INDEX_ONE).get<uint32_t>();
LOG_I("Begin to callback function: callback=%{public}s", callbackId.c_str());
auto findCallback = g_jsRefs.find(callbackId);
if (findCallback == g_jsRefs.end()) {
out.exception_ = ApiCallErr(ERR_CALLBACK_FAILED, "JsCallbackFunction is not referenced: " + callbackId);
LOG_E("%{public}s", out.exception_.message_.c_str());
return;
}
auto cb = [](napi_env env, napi_callback_info info) -> napi_value {
LOG_D("ExecuteCallback jscallback return start");
size_t argc_in = 1;
napi_value argv_in[1] = {nullptr};
void* param_in = nullptr;
NAPI_CALL_BASE(env, napi_get_cb_info(env, info, &argc_in, argv_in, nullptr, ¶m_in), nullptr);
ThreadLock* threadLock = reinterpret_cast<ThreadLock*>(param_in);
if (threadLock == nullptr) {
LOG_E("ExecuteCallback get threadLock failed");
return nullptr;
}
NAPI_CALL_BASE(env, napi_get_value_bool(env, argv_in[0], &threadLock->res), nullptr);
threadLock->LockRelease("");
LOG_I("ExecuteCallback jscallback return end, res = %{public}d", threadLock->res);
return nullptr;
};
ctx->env = env;
ctx->callbackId = callbackId;
ctx->callbackRef = findCallback->second;
ctx->timeout = timeout;
ctx->cb = cb;
}
void CallbackCodeNapi::BindPromiseCallbacks(napi_env env, napi_value promise, ThreadLock* threadLock)
{
bool is_promise = false;
NAPI_CALL_RETURN_VOID(env, napi_is_promise(env, promise, &is_promise));
if (!is_promise) {
return;
}
napi_value catch_property;
NAPI_CALL_RETURN_VOID(env, napi_get_named_property(env, promise, "catch", &catch_property));
auto catch_handle = [](napi_env env, napi_callback_info info) -> napi_value {
size_t argc_in = 1;
napi_value argv_in[1] = {nullptr};
void* param_in = nullptr;
NAPI_CALL_BASE(env, napi_get_cb_info(env, info, &argc_in, argv_in, nullptr, ¶m_in), nullptr);
napi_value error_str;
napi_coerce_to_string(env, argv_in[0], &error_str);
size_t buf_size = 0;
char buf[NAPI_MAX_BUF_LEN] = {0};
napi_get_value_string_utf8(env, error_str, buf, NAPI_MAX_BUF_LEN, &buf_size);
ThreadLock* threadLock = reinterpret_cast<ThreadLock*>(param_in);
if (threadLock == nullptr) {
LOG_E("ExecuteCallback get threadLock failed");
return nullptr;
}
threadLock->LockRelease("Exception raised during call js_cb_function: " + string(buf, buf_size));
return nullptr;
};
napi_value catch_callback;
NAPI_CALL_RETURN_VOID(env, napi_create_function(env, "CatchCallack", NAPI_AUTO_LENGTH, catch_handle,
threadLock, &catch_callback));
NAPI_CALL_RETURN_VOID(env, napi_call_function(env, promise, catch_property, 1, &catch_callback, nullptr));
}
void CallbackCodeNapi::WaitforCallbackFinish(napi_env env, shared_ptr<CodeCallbackContext> context,
ThreadLock* threadLock, ApiReplyInfo &out)
{
unique_lock<mutex> lock(threadLock->mutex);
const auto timeout = chrono::milliseconds(context->timeout);
if (!threadLock->condition.wait_for(lock, timeout, [&threadLock] { return threadLock->ready; })) {
LOG_E("Jscallback call timeout, has been waiting for %{public}d ms", context->timeout);
out.exception_ = ApiCallErr(ERR_CALLBACK_FAILED, "Code execution has been timeout.");
return;
}
LOG_I("Jscallback call has finished, res = %{public}d", threadLock->res);
if (!threadLock->errMsg.empty()) {
out.exception_ = ApiCallErr(ERR_CALLBACK_FAILED, threadLock->errMsg);
LOG_E("%{public}s", out.exception_.message_.c_str());
} else if (!threadLock->res) {
out.exception_ = ApiCallErr(ERR_CALLBACK_FAILED, "Callback execution return false");
}
delete threadLock;
threadLock = nullptr;
}
void CallbackCodeNapi::ExecuteCallback(napi_env env, const ApiCallInfo &in, ApiReplyInfo &out)
{
shared_ptr<CodeCallbackContext> context = make_shared<CodeCallbackContext>();
InitCallbackContext(env, in, out, context);
if (out.exception_.code_ != NO_ERROR) {
LOG_W("InitCallbackContext failed, cannot perform callback");
return;
}
ThreadLock* threadLock = new (nothrow) ThreadLock();
auto task = [context, threadLock, this]() {
napi_value argv[1] = { nullptr };
napi_value jsCallback = nullptr;
napi_get_reference_value(context->env, context->callbackRef, &jsCallback);
napi_create_function(context->env, "Complete", NAPI_AUTO_LENGTH, context->cb, threadLock, &argv[0]);
napi_value promise;
napi_call_function(context->env, nullptr, jsCallback, 1, argv, &promise);
auto hasError = false;
napi_is_exception_pending(context->env, &hasError);
if (hasError) {
napi_value exception;
napi_get_and_clear_last_exception(context->env, &exception);
napi_value error_str;
napi_coerce_to_string(context->env, exception, &error_str);
size_t buf_size = 0;
char buf[NAPI_MAX_BUF_LEN] = {0};
napi_get_value_string_utf8(context->env, error_str, buf, NAPI_MAX_BUF_LEN, &buf_size);
if (threadLock == nullptr) {
return;
}
threadLock->LockRelease("Exception raised during call js_cb_function: " + string(buf, buf_size));
return;
}
this->BindPromiseCallbacks(context->env, promise, threadLock);
};
if (napi_send_event(env, task, napi_eprio_immediate) != napi_status::napi_ok) {
LOG_E("Exception raised during call js_cb_function!");
}
WaitforCallbackFinish(env, context, threadLock, out);
}
void CallbackCodeNapi::DestroyCallbacks(napi_env env, const ApiCallInfo &in, ApiReplyInfo &out)
{
DCHECK(in.paramList_.size() == ONE);
DCHECK(in.paramList_.at(INDEX_ZERO).type() == detail::value_t::array);
for (auto& callbackId : in.paramList_.at(INDEX_ZERO)) {
auto findCallback = g_jsRefs.find(callbackId);
if (findCallback == g_jsRefs.end()) {
continue;
}
napi_delete_reference(env, findCallback->second);
g_jsRefs.erase(callbackId);
}
}
void CallbackCodeNapi::HandleCallbackEvent(napi_env env, const ApiCallInfo &in, ApiReplyInfo &out)
{
if (in.apiId_ == "PerfTest.run") {
ExecuteCallback(env, in, out);
} else if (in.apiId_ == "PerfTest.destroy") {
DestroyCallbacks(env, in, out);
} else {
out.exception_ = ApiCallErr(ERR_INTERNAL, "Api dose not support callback: " + in.apiId_);
LOG_E("%{public}s", out.exception_.message_.c_str());
}
}
}