1 /**
2 * Copyright (c) 2023-2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16 #include "plugins/ets/runtime/interop_js/interop_context.h"
17
18 #include "plugins/ets/runtime/ets_exceptions.h"
19 #include "plugins/ets/runtime/ets_class_linker_extension.h"
20 #include "plugins/ets/runtime/ets_vm.h"
21 #include "plugins/ets/runtime/interop_js/js_convert.h"
22 #include "plugins/ets/runtime/interop_js/interop_common.h"
23 #include "plugins/ets/runtime/interop_js/code_scopes.h"
24 #include "plugins/ets/runtime/types/ets_method.h"
25 #include "runtime/include/runtime.h"
26 #include "runtime/mem/local_object_handle.h"
27
28 namespace ark::ets::interop::js {
29
30 namespace descriptors = panda_file_items::class_descriptors;
31
32 // NOLINTNEXTLINE(modernize-avoid-c-arrays)
33 static constexpr std::string_view const FUNCTION_INTERFACE_DESCRIPTORS[] = {
34 descriptors::FUNCTION0, descriptors::FUNCTION1, descriptors::FUNCTION2, descriptors::FUNCTION3,
35 descriptors::FUNCTION4, descriptors::FUNCTION5, descriptors::FUNCTION6, descriptors::FUNCTION7,
36 descriptors::FUNCTION8, descriptors::FUNCTION9, descriptors::FUNCTION10, descriptors::FUNCTION11,
37 descriptors::FUNCTION12, descriptors::FUNCTION13, descriptors::FUNCTION14, descriptors::FUNCTION15,
38 descriptors::FUNCTION16, descriptors::FUNCTIONN,
39 };
40
CacheClass(EtsClassLinker * etsClassLinker,std::string_view descriptor)41 static Class *CacheClass(EtsClassLinker *etsClassLinker, std::string_view descriptor)
42 {
43 auto klass = etsClassLinker->GetClass(descriptor.data())->GetRuntimeClass();
44 ASSERT(klass != nullptr);
45 return klass;
46 }
47
LoadDynamicCallClass(Class * klass)48 void ConstStringStorage::LoadDynamicCallClass(Class *klass)
49 {
50 if (klass == lastLoadedClass_) {
51 return;
52 }
53 lastLoadedClass_ = klass;
54 auto fields = klass->GetStaticFields();
55 ASSERT(fields.Size() == 1);
56 auto startFrom = klass->GetFieldPrimitive<uint32_t>(fields[0]);
57 napi_value jsArr;
58 auto *env = Ctx()->GetJSEnv();
59 if (jsStringBufferRef_ == nullptr) {
60 jsArr = InitBuffer(Ctx()->GetStringBufferSize());
61 } else {
62 jsArr = GetReferenceValue(env, jsStringBufferRef_);
63 if (startFrom >= stringBuffer_.size()) {
64 stringBuffer_.resize(Ctx()->GetStringBufferSize());
65 } else if (stringBuffer_[startFrom] != nullptr) {
66 return;
67 }
68 }
69
70 auto *pf = klass->GetPandaFile();
71 panda_file::ClassDataAccessor cda(*pf, klass->GetFileId());
72 [[maybe_unused]] auto annotationFound = cda.EnumerateAnnotation(
73 "Lets/annotation/DynamicCall;", [this, pf, startFrom, jsArr, env](panda_file::AnnotationDataAccessor &ada) {
74 for (uint32_t i = 0; i < ada.GetCount(); i++) {
75 auto adae = ada.GetElement(i);
76 auto *elemName = pf->GetStringData(adae.GetNameId()).data;
77 if (!utf::IsEqual(utf::CStringAsMutf8("value"), elemName)) {
78 continue;
79 }
80 auto arr = adae.GetArrayValue();
81 auto count = arr.GetCount();
82 for (uint32_t j = 0, bufferIndex = startFrom; j < count; j++, bufferIndex++) {
83 panda_file::File::EntityId stringId(arr.Get<uint32_t>(j));
84 auto data = pf->GetStringData(stringId);
85 napi_value jsStr;
86 NAPI_CHECK_FATAL(
87 napi_create_string_utf8(env, utf::Mutf8AsCString(data.data), data.utf16Length, &jsStr));
88 ASSERT(stringBuffer_[bufferIndex] == nullptr);
89 napi_set_element(env, jsArr, bufferIndex, jsStr);
90 stringBuffer_[bufferIndex] = jsStr;
91 }
92 return true;
93 }
94 UNREACHABLE();
95 });
96 ASSERT(annotationFound.has_value());
97 }
98
GetConstantPool()99 napi_value ConstStringStorage::GetConstantPool()
100 {
101 return GetReferenceValue(Ctx()->GetJSEnv(), jsStringBufferRef_);
102 }
103
InitBuffer(size_t length)104 napi_value ConstStringStorage::InitBuffer(size_t length)
105 {
106 napi_value jsArr;
107 NAPI_CHECK_FATAL(napi_create_array_with_length(Ctx()->GetJSEnv(), length, &jsArr));
108 NAPI_CHECK_FATAL(napi_create_reference(Ctx()->GetJSEnv(), jsArr, 1, &jsStringBufferRef_));
109 stringBuffer_.resize(Ctx()->GetStringBufferSize());
110 return jsArr;
111 }
112
Ctx()113 InteropCtx *ConstStringStorage::Ctx()
114 {
115 return InteropCtx::FromConstStringStorage(this);
116 }
117
InteropCtx(EtsCoroutine * coro,napi_env env)118 InteropCtx::InteropCtx(EtsCoroutine *coro, napi_env env)
119 {
120 JSNapiEnvScope envscope(this, env);
121
122 jsEnvForEventLoopCallbacks_ = env;
123 PandaEtsVM *vm = coro->GetPandaVM();
124 EtsClassLinker *etsClassLinker = vm->GetClassLinker();
125 refstor_ = vm->GetGlobalObjectStorage();
126 classLinker_ = Runtime::GetCurrent()->GetClassLinker();
127 linkerCtx_ = etsClassLinker->GetEtsClassLinkerExtension()->GetBootContext();
128
129 auto *jobQueue = Runtime::GetCurrent()->GetInternalAllocator()->New<JsJobQueue>();
130 vm->InitJobQueue(jobQueue);
131
132 CacheClasses(etsClassLinker);
133
134 RegisterBuiltinJSRefConvertors(this);
135
136 {
137 auto method = EtsClass::FromRuntimeClass(jsRuntimeClass_)->GetMethod("createFinalizationRegistry");
138 ASSERT(method != nullptr);
139 auto res = method->GetPandaMethod()->Invoke(coro, nullptr);
140 ASSERT(!coro->HasPendingException());
141 auto queue = EtsObject::FromCoreType(res.GetAs<ObjectHeader *>());
142 jsvalueFregistryRef_ = Refstor()->Add(queue->GetCoreType(), mem::Reference::ObjectType::GLOBAL);
143 ASSERT(jsvalueFregistryRef_ != nullptr);
144
145 jsvalueFregistryRegister_ = queue->GetClass()
146 ->GetMethod("register", "Lstd/core/Object;Lstd/core/Object;Lstd/core/Object;:V")
147 ->GetPandaMethod();
148 ASSERT(jsvalueFregistryRegister_ != nullptr);
149 }
150
151 {
152 EtsClass *promiseInteropClass =
153 EtsClass::FromRuntimeClass(CacheClass(etsClassLinker, "Lstd/interop/js/PromiseInterop;"));
154 ASSERT(promiseInteropClass != nullptr);
155 promiseInteropConnectMethod_ =
156 promiseInteropClass->GetMethod("connectPromise", "Lstd/core/Promise;J:V")->GetPandaMethod();
157 ASSERT(promiseInteropConnectMethod_ != nullptr);
158 }
159
160 etsProxyRefStorage_ = ets_proxy::SharedReferenceStorage::Create();
161 ASSERT(etsProxyRefStorage_.get() != nullptr);
162
163 // Set InteropCtx::DestroyLocalScopeForTopFrame to VM for call it it deoptimization and exception handlers
164 vm->SetClearInteropHandleScopesFunction([this](Frame *frame) { this->DestroyLocalScopeForTopFrame(frame); });
165 vm->SetDestroyExternalDataFunction(Destroy);
166 }
167
CacheClasses(EtsClassLinker * etsClassLinker)168 void InteropCtx::CacheClasses(EtsClassLinker *etsClassLinker)
169 {
170 jsRuntimeClass_ = CacheClass(etsClassLinker, descriptors::JS_RUNTIME);
171 jsValueClass_ = CacheClass(etsClassLinker, descriptors::JS_VALUE);
172 jsErrorClass_ = CacheClass(etsClassLinker, descriptors::JS_ERROR);
173 objectClass_ = CacheClass(etsClassLinker, descriptors::OBJECT);
174 stringClass_ = CacheClass(etsClassLinker, descriptors::STRING);
175 bigintClass_ = CacheClass(etsClassLinker, descriptors::BIG_INT);
176 undefinedClass_ = CacheClass(etsClassLinker, descriptors::INTERNAL_UNDEFINED);
177 promiseClass_ = CacheClass(etsClassLinker, descriptors::PROMISE);
178 errorClass_ = CacheClass(etsClassLinker, descriptors::ERROR);
179 exceptionClass_ = CacheClass(etsClassLinker, descriptors::EXCEPTION);
180 typeClass_ = CacheClass(etsClassLinker, descriptors::TYPE);
181
182 boxIntClass_ = CacheClass(etsClassLinker, descriptors::BOX_INT);
183 boxLongClass_ = CacheClass(etsClassLinker, descriptors::BOX_LONG);
184
185 arrayClass_ = CacheClass(etsClassLinker, descriptors::ARRAY);
186 arraybufClass_ = CacheClass(etsClassLinker, descriptors::ARRAY_BUFFER);
187
188 arrayAsListIntClass_ = CacheClass(etsClassLinker, descriptors::ARRAY_AS_LIST_INT);
189
190 for (auto descr : FUNCTION_INTERFACE_DESCRIPTORS) {
191 functionalInterfaces_.insert(CacheClass(etsClassLinker, descr));
192 }
193 }
194
CreateETSCoreJSError(EtsCoroutine * coro,JSValue * jsvalue)195 EtsObject *InteropCtx::CreateETSCoreJSError(EtsCoroutine *coro, JSValue *jsvalue)
196 {
197 [[maybe_unused]] HandleScope<ObjectHeader *> scope(coro);
198 VMHandle<ObjectHeader> jsvalueHandle(coro, jsvalue->GetCoreType());
199
200 Method::Proto proto(Method::Proto::ShortyVector {panda_file::Type(panda_file::Type::TypeId::VOID),
201 panda_file::Type(panda_file::Type::TypeId::REFERENCE)},
202 Method::Proto::RefTypeVector {utf::Mutf8AsCString(GetJSValueClass()->GetDescriptor())});
203 auto ctorName = utf::CStringAsMutf8(panda_file_items::CTOR.data());
204 auto ctor = GetJSErrorClass()->GetDirectMethod(ctorName, proto);
205 ASSERT(ctor != nullptr);
206
207 auto excObj = ObjectHeader::Create(coro, GetJSErrorClass());
208 if (UNLIKELY(excObj == nullptr)) {
209 return nullptr;
210 }
211 VMHandle<ObjectHeader> excHandle(coro, excObj);
212
213 std::array<Value, 2U> args {Value(excHandle.GetPtr()), Value(jsvalueHandle.GetPtr())};
214 ctor->InvokeVoid(coro, args.data());
215 auto res = EtsObject::FromCoreType(excHandle.GetPtr());
216 if (UNLIKELY(coro->HasPendingException())) {
217 return nullptr;
218 }
219 return res;
220 }
221
ThrowETSError(EtsCoroutine * coro,napi_value val)222 void InteropCtx::ThrowETSError(EtsCoroutine *coro, napi_value val)
223 {
224 auto ctx = Current(coro);
225
226 if (coro->IsUsePreAllocObj()) {
227 coro->SetUsePreAllocObj(false);
228 coro->SetException(coro->GetVM()->GetOOMErrorObject());
229 return;
230 }
231 ASSERT(!coro->HasPendingException());
232
233 if (IsNullOrUndefined(ctx->GetJSEnv(), val)) {
234 ctx->ThrowETSError(coro, "interop/js throws undefined/null");
235 return;
236 }
237
238 // To catch `TypeError` or `UserError extends TypeError`
239 // 1. Frontend puts catch(compat/TypeError) { <instanceof-rethrow? if UserError expected> }
240 // Where js.UserError will be wrapped into compat/TypeError
241 // NOTE(vpukhov): compat: add intrinsic to obtain JSValue from compat/ instances
242
243 auto objRefconv = JSRefConvertResolve(ctx, ctx->GetObjectClass());
244 LocalObjectHandle<EtsObject> etsObj(coro, objRefconv->Unwrap(ctx, val));
245 if (UNLIKELY(etsObj.GetPtr() == nullptr)) {
246 INTEROP_LOG(INFO) << "Something went wrong while unwrapping pending js exception";
247 ASSERT(ctx->SanityETSExceptionPending());
248 return;
249 }
250
251 auto klass = etsObj->GetClass()->GetRuntimeClass();
252 if (LIKELY(ctx->GetErrorClass()->IsAssignableFrom(klass) || ctx->GetExceptionClass()->IsAssignableFrom(klass))) {
253 coro->SetException(etsObj->GetCoreType());
254 return;
255 }
256
257 // NOTE(vpukhov): should throw a special error (JSError?) with cause set
258 auto exc = JSConvertJSError::Unwrap(ctx, ctx->GetJSEnv(), val);
259 if (LIKELY(exc.has_value())) {
260 ASSERT(exc != nullptr);
261 coro->SetException(exc.value()->GetCoreType());
262 } // otherwise exception is already set
263 }
264
ThrowETSError(EtsCoroutine * coro,const char * msg)265 void InteropCtx::ThrowETSError(EtsCoroutine *coro, const char *msg)
266 {
267 ASSERT(!coro->HasPendingException());
268 ets::ThrowEtsException(coro, panda_file_items::class_descriptors::ERROR, msg);
269 }
270
ThrowJSError(napi_env env,const std::string & msg)271 void InteropCtx::ThrowJSError(napi_env env, const std::string &msg)
272 {
273 INTEROP_LOG(INFO) << "ThrowJSError: " << msg;
274 ASSERT(!NapiIsExceptionPending(env));
275 NAPI_CHECK_FATAL(napi_throw_error(env, nullptr, msg.c_str()));
276 }
277
ThrowJSTypeError(napi_env env,const std::string & msg)278 void InteropCtx::ThrowJSTypeError(napi_env env, const std::string &msg)
279 {
280 INTEROP_LOG(INFO) << "ThrowJSTypeError: " << msg;
281 ASSERT(!NapiIsExceptionPending(env));
282 NAPI_CHECK_FATAL(napi_throw_type_error(env, nullptr, msg.c_str()));
283 }
284
ThrowJSValue(napi_env env,napi_value val)285 void InteropCtx::ThrowJSValue(napi_env env, napi_value val)
286 {
287 INTEROP_LOG(INFO) << "ThrowJSValue";
288 ASSERT(!NapiIsExceptionPending(env));
289 NAPI_CHECK_FATAL(napi_throw(env, val));
290 }
291
ForwardEtsException(EtsCoroutine * coro)292 void InteropCtx::ForwardEtsException(EtsCoroutine *coro)
293 {
294 auto env = GetJSEnv();
295 ASSERT(coro->HasPendingException());
296 LocalObjectHandle<ObjectHeader> exc(coro, coro->GetException());
297 coro->ClearException();
298
299 auto klass = exc->ClassAddr<Class>();
300 ASSERT(GetErrorClass()->IsAssignableFrom(klass) || GetExceptionClass()->IsAssignableFrom(klass));
301 JSRefConvert *refconv = JSRefConvertResolve<true>(this, klass);
302 if (UNLIKELY(refconv == nullptr)) {
303 INTEROP_LOG(INFO) << "Exception thrown while forwarding ets exception: " << klass->GetDescriptor();
304 return;
305 }
306 napi_value res = refconv->Wrap(this, EtsObject::FromCoreType(exc.GetPtr()));
307 if (UNLIKELY(res == nullptr)) {
308 return;
309 }
310 ThrowJSValue(env, res);
311 }
312
ForwardJSException(EtsCoroutine * coro)313 void InteropCtx::ForwardJSException(EtsCoroutine *coro)
314 {
315 auto env = GetJSEnv();
316 napi_value excval;
317 ASSERT(NapiIsExceptionPending(env));
318 NAPI_CHECK_FATAL(napi_get_and_clear_last_exception(env, &excval));
319 ThrowETSError(coro, excval);
320 }
321
JSConvertTypeCheckFailed(const char * typeName)322 void JSConvertTypeCheckFailed(const char *typeName)
323 {
324 auto ctx = InteropCtx::Current();
325 auto env = ctx->GetJSEnv();
326 InteropCtx::ThrowJSTypeError(env, typeName + std::string(" expected"));
327 }
328
GetErrorStack(napi_env env,napi_value jsErr)329 static std::optional<std::string> GetErrorStack(napi_env env, napi_value jsErr)
330 {
331 bool isError;
332 if (napi_ok != napi_is_error(env, jsErr, &isError)) {
333 return {};
334 }
335 if (!isError) {
336 return "not an Error instance";
337 }
338 napi_value jsStk;
339 if (napi_ok != napi_get_named_property(env, jsErr, "stack", &jsStk)) {
340 return {};
341 }
342 size_t length;
343 if (napi_ok != napi_get_value_string_utf8(env, jsStk, nullptr, 0, &length)) {
344 return {};
345 }
346 std::string value;
347 value.resize(length);
348 // +1 for NULL terminated string!!!
349 if (napi_ok != napi_get_value_string_utf8(env, jsStk, value.data(), value.size() + 1, &length)) {
350 return {};
351 }
352 return value;
353 }
354
NapiTryDumpStack(napi_env env)355 static std::optional<std::string> NapiTryDumpStack(napi_env env)
356 {
357 bool isPending;
358 if (napi_ok != napi_is_exception_pending(env, &isPending)) {
359 return {};
360 }
361
362 std::string pendingErrorMsg;
363 if (isPending) {
364 napi_value valuePending;
365 if (napi_ok != napi_get_and_clear_last_exception(env, &valuePending)) {
366 return {};
367 }
368 auto resStk = GetErrorStack(env, valuePending);
369 if (resStk.has_value()) {
370 pendingErrorMsg = "\nWith pending exception:\n" + resStk.value();
371 } else {
372 pendingErrorMsg = "\nFailed to stringify pending exception";
373 }
374 }
375
376 std::string stacktraceMsg;
377 {
378 napi_value jsDummyStr;
379 if (napi_ok !=
380 napi_create_string_utf8(env, "probe-stacktrace-not-actual-error", NAPI_AUTO_LENGTH, &jsDummyStr)) {
381 return {};
382 }
383 napi_value jsErr;
384 auto rc = napi_create_error(env, nullptr, jsDummyStr, &jsErr);
385 if (napi_ok != rc) {
386 return {};
387 }
388 auto resStk = GetErrorStack(env, jsErr);
389 stacktraceMsg = resStk.has_value() ? resStk.value() : "failed to stringify probe exception";
390 }
391
392 return stacktraceMsg + pendingErrorMsg;
393 }
394
Fatal(const char * msg)395 [[noreturn]] void InteropCtx::Fatal(const char *msg)
396 {
397 INTEROP_LOG(ERROR) << "InteropCtx::Fatal: " << msg;
398
399 auto coro = EtsCoroutine::GetCurrent();
400 auto ctx = InteropCtx::Current(coro);
401
402 INTEROP_LOG(ERROR) << "======================== ETS stack ============================";
403 auto istk = ctx->interopStk_.GetRecords();
404 auto istkIt = istk.rbegin();
405
406 auto printIstkFrames = [&istkIt, &istk](void *fp) {
407 while (istkIt != istk.rend() && fp == istkIt->etsFrame) {
408 INTEROP_LOG(ERROR) << "<interop> " << (istkIt->descr != nullptr ? istkIt->descr : "unknown");
409 istkIt++;
410 }
411 };
412
413 for (auto stack = StackWalker::Create(coro); stack.HasFrame(); stack.NextFrame()) {
414 printIstkFrames(istkIt->etsFrame);
415 Method *method = stack.GetMethod();
416 INTEROP_LOG(ERROR) << method->GetClass()->GetName() << "." << method->GetName().data << " at "
417 << method->GetLineNumberAndSourceFile(stack.GetBytecodePc());
418 }
419 ASSERT(istkIt == istk.rend() || istkIt->etsFrame == nullptr);
420 printIstkFrames(nullptr);
421
422 auto env = ctx->jsEnv_;
423 INTEROP_LOG(ERROR) << (env != nullptr ? "<ets-entrypoint>" : "current js env is nullptr!");
424
425 if (coro->HasPendingException()) {
426 auto exc = EtsObject::FromCoreType(coro->GetException());
427 INTEROP_LOG(ERROR) << "With pending exception: " << exc->GetClass()->GetDescriptor();
428 }
429
430 if (env != nullptr) {
431 INTEROP_LOG(ERROR) << "======================== JS stack =============================";
432 std::optional<std::string> jsStk = NapiTryDumpStack(env);
433 if (jsStk.has_value()) {
434 INTEROP_LOG(ERROR) << jsStk.value();
435 } else {
436 INTEROP_LOG(ERROR) << "JS stack print failed";
437 }
438 }
439
440 INTEROP_LOG(ERROR) << "======================== Native stack =========================";
441 PrintStack(Logger::Message(Logger::Level::ERROR, Logger::Component::ETS_INTEROP_JS, false).GetStream());
442 std::abort();
443 }
444
Init(EtsCoroutine * coro,napi_env env)445 void InteropCtx::Init(EtsCoroutine *coro, napi_env env)
446 {
447 // Initialize InteropCtx in VM ExternalData
448 new (InteropCtx::Current(coro)) InteropCtx(coro, env);
449 }
450
451 } // namespace ark::ets::interop::js
452