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 PandaEtsVM *vm = coro->GetPandaVM();
123 EtsClassLinker *etsClassLinker = vm->GetClassLinker();
124 refstor_ = vm->GetGlobalObjectStorage();
125 classLinker_ = Runtime::GetCurrent()->GetClassLinker();
126 linkerCtx_ = etsClassLinker->GetEtsClassLinkerExtension()->GetBootContext();
127
128 auto *jobQueue = Runtime::GetCurrent()->GetInternalAllocator()->New<JsJobQueue>();
129 vm->InitJobQueue(jobQueue);
130
131 CacheClasses(etsClassLinker);
132
133 RegisterBuiltinJSRefConvertors(this);
134
135 {
136 auto method = EtsClass::FromRuntimeClass(jsRuntimeClass_)->GetMethod("createFinalizationRegistry");
137 ASSERT(method != nullptr);
138 auto res = method->GetPandaMethod()->Invoke(coro, nullptr);
139 ASSERT(!coro->HasPendingException());
140 auto queue = EtsObject::FromCoreType(res.GetAs<ObjectHeader *>());
141 jsvalueFregistryRef_ = Refstor()->Add(queue->GetCoreType(), mem::Reference::ObjectType::GLOBAL);
142 ASSERT(jsvalueFregistryRef_ != nullptr);
143
144 jsvalueFregistryRegister_ = queue->GetClass()
145 ->GetMethod("register", "Lstd/core/Object;Lstd/core/Object;Lstd/core/Object;:V")
146 ->GetPandaMethod();
147 ASSERT(jsvalueFregistryRegister_ != nullptr);
148 }
149
150 {
151 EtsClass *promiseInteropClass =
152 EtsClass::FromRuntimeClass(CacheClass(etsClassLinker, "Lstd/interop/js/PromiseInterop;"));
153 ASSERT(promiseInteropClass != nullptr);
154 promiseInteropConnectMethod_ =
155 promiseInteropClass->GetMethod("connectPromise", "Lstd/core/Promise;J:V")->GetPandaMethod();
156 ASSERT(promiseInteropConnectMethod_ != nullptr);
157 }
158
159 etsProxyRefStorage_ = ets_proxy::SharedReferenceStorage::Create();
160 ASSERT(etsProxyRefStorage_.get() != nullptr);
161
162 // Set InteropCtx::DestroyLocalScopeForTopFrame to VM for call it it deoptimization and exception handlers
163 vm->SetClearInteropHandleScopesFunction([this](Frame *frame) { this->DestroyLocalScopeForTopFrame(frame); });
164 vm->SetDestroyExternalDataFunction(Destroy);
165 }
166
CacheClasses(EtsClassLinker * etsClassLinker)167 void InteropCtx::CacheClasses(EtsClassLinker *etsClassLinker)
168 {
169 jsRuntimeClass_ = CacheClass(etsClassLinker, descriptors::JS_RUNTIME);
170 jsValueClass_ = CacheClass(etsClassLinker, descriptors::JS_VALUE);
171 jsErrorClass_ = CacheClass(etsClassLinker, descriptors::JS_ERROR);
172 objectClass_ = CacheClass(etsClassLinker, descriptors::OBJECT);
173 stringClass_ = CacheClass(etsClassLinker, descriptors::STRING);
174 undefinedClass_ = CacheClass(etsClassLinker, descriptors::INTERNAL_UNDEFINED);
175 promiseClass_ = CacheClass(etsClassLinker, descriptors::PROMISE);
176 errorClass_ = CacheClass(etsClassLinker, descriptors::ERROR);
177 exceptionClass_ = CacheClass(etsClassLinker, descriptors::EXCEPTION);
178 typeClass_ = CacheClass(etsClassLinker, descriptors::TYPE);
179
180 boxIntClass_ = CacheClass(etsClassLinker, descriptors::BOX_INT);
181 boxLongClass_ = CacheClass(etsClassLinker, descriptors::BOX_LONG);
182
183 arrayClass_ = CacheClass(etsClassLinker, descriptors::ARRAY);
184 arraybufClass_ = CacheClass(etsClassLinker, descriptors::ARRAY_BUFFER);
185
186 for (auto descr : FUNCTION_INTERFACE_DESCRIPTORS) {
187 functionalInterfaces_.insert(CacheClass(etsClassLinker, descr));
188 }
189 }
190
CreateETSCoreJSError(EtsCoroutine * coro,JSValue * jsvalue)191 EtsObject *InteropCtx::CreateETSCoreJSError(EtsCoroutine *coro, JSValue *jsvalue)
192 {
193 [[maybe_unused]] HandleScope<ObjectHeader *> scope(coro);
194 VMHandle<ObjectHeader> jsvalueHandle(coro, jsvalue->GetCoreType());
195
196 Method::Proto proto(Method::Proto::ShortyVector {panda_file::Type(panda_file::Type::TypeId::VOID),
197 panda_file::Type(panda_file::Type::TypeId::REFERENCE)},
198 Method::Proto::RefTypeVector {utf::Mutf8AsCString(GetJSValueClass()->GetDescriptor())});
199 auto ctorName = utf::CStringAsMutf8(panda_file_items::CTOR.data());
200 auto ctor = GetJSErrorClass()->GetDirectMethod(ctorName, proto);
201 ASSERT(ctor != nullptr);
202
203 auto excObj = ObjectHeader::Create(coro, GetJSErrorClass());
204 if (UNLIKELY(excObj == nullptr)) {
205 return nullptr;
206 }
207 VMHandle<ObjectHeader> excHandle(coro, excObj);
208
209 std::array<Value, 2U> args {Value(excHandle.GetPtr()), Value(jsvalueHandle.GetPtr())};
210 ctor->InvokeVoid(coro, args.data());
211 auto res = EtsObject::FromCoreType(excHandle.GetPtr());
212 if (UNLIKELY(coro->HasPendingException())) {
213 return nullptr;
214 }
215 return res;
216 }
217
ThrowETSError(EtsCoroutine * coro,napi_value val)218 void InteropCtx::ThrowETSError(EtsCoroutine *coro, napi_value val)
219 {
220 auto ctx = Current(coro);
221
222 if (coro->IsUsePreAllocObj()) {
223 coro->SetUsePreAllocObj(false);
224 coro->SetException(coro->GetVM()->GetOOMErrorObject());
225 return;
226 }
227 ASSERT(!coro->HasPendingException());
228
229 if (IsNullOrUndefined(ctx->GetJSEnv(), val)) {
230 ctx->ThrowETSError(coro, "interop/js throws undefined/null");
231 return;
232 }
233
234 // To catch `TypeError` or `UserError extends TypeError`
235 // 1. Frontend puts catch(compat/TypeError) { <instanceof-rethrow? if UserError expected> }
236 // Where js.UserError will be wrapped into compat/TypeError
237 // NOTE(vpukhov): compat: add intrinsic to obtain JSValue from compat/ instances
238
239 auto objRefconv = JSRefConvertResolve(ctx, ctx->GetObjectClass());
240 LocalObjectHandle<EtsObject> etsObj(coro, objRefconv->Unwrap(ctx, val));
241 if (UNLIKELY(etsObj.GetPtr() == nullptr)) {
242 INTEROP_LOG(INFO) << "Something went wrong while unwrapping pending js exception";
243 ASSERT(ctx->SanityETSExceptionPending());
244 return;
245 }
246
247 auto klass = etsObj->GetClass()->GetRuntimeClass();
248 if (LIKELY(ctx->GetErrorClass()->IsAssignableFrom(klass) || ctx->GetExceptionClass()->IsAssignableFrom(klass))) {
249 coro->SetException(etsObj->GetCoreType());
250 return;
251 }
252
253 // NOTE(vpukhov): should throw a special error (JSError?) with cause set
254 auto exc = JSConvertJSError::Unwrap(ctx, ctx->GetJSEnv(), val);
255 if (LIKELY(exc.has_value())) {
256 ASSERT(exc != nullptr);
257 coro->SetException(exc.value()->GetCoreType());
258 } // otherwise exception is already set
259 }
260
ThrowETSError(EtsCoroutine * coro,const char * msg)261 void InteropCtx::ThrowETSError(EtsCoroutine *coro, const char *msg)
262 {
263 ASSERT(!coro->HasPendingException());
264 ets::ThrowEtsException(coro, panda_file_items::class_descriptors::ERROR, msg);
265 }
266
ThrowJSError(napi_env env,const std::string & msg)267 void InteropCtx::ThrowJSError(napi_env env, const std::string &msg)
268 {
269 INTEROP_LOG(INFO) << "ThrowJSError: " << msg;
270 ASSERT(!NapiIsExceptionPending(env));
271 NAPI_CHECK_FATAL(napi_throw_error(env, nullptr, msg.c_str()));
272 }
273
ThrowJSTypeError(napi_env env,const std::string & msg)274 void InteropCtx::ThrowJSTypeError(napi_env env, const std::string &msg)
275 {
276 INTEROP_LOG(INFO) << "ThrowJSTypeError: " << msg;
277 ASSERT(!NapiIsExceptionPending(env));
278 NAPI_CHECK_FATAL(napi_throw_type_error(env, nullptr, msg.c_str()));
279 }
280
ThrowJSValue(napi_env env,napi_value val)281 void InteropCtx::ThrowJSValue(napi_env env, napi_value val)
282 {
283 INTEROP_LOG(INFO) << "ThrowJSValue";
284 ASSERT(!NapiIsExceptionPending(env));
285 NAPI_CHECK_FATAL(napi_throw(env, val));
286 }
287
ForwardEtsException(EtsCoroutine * coro)288 void InteropCtx::ForwardEtsException(EtsCoroutine *coro)
289 {
290 auto env = GetJSEnv();
291 ASSERT(coro->HasPendingException());
292 LocalObjectHandle<ObjectHeader> exc(coro, coro->GetException());
293 coro->ClearException();
294
295 auto klass = exc->ClassAddr<Class>();
296 ASSERT(GetErrorClass()->IsAssignableFrom(klass) || GetExceptionClass()->IsAssignableFrom(klass));
297 JSRefConvert *refconv = JSRefConvertResolve<true>(this, klass);
298 if (UNLIKELY(refconv == nullptr)) {
299 INTEROP_LOG(INFO) << "Exception thrown while forwarding ets exception: " << klass->GetDescriptor();
300 return;
301 }
302 napi_value res = refconv->Wrap(this, EtsObject::FromCoreType(exc.GetPtr()));
303 if (UNLIKELY(res == nullptr)) {
304 return;
305 }
306 ThrowJSValue(env, res);
307 }
308
ForwardJSException(EtsCoroutine * coro)309 void InteropCtx::ForwardJSException(EtsCoroutine *coro)
310 {
311 auto env = GetJSEnv();
312 napi_value excval;
313 ASSERT(NapiIsExceptionPending(env));
314 NAPI_CHECK_FATAL(napi_get_and_clear_last_exception(env, &excval));
315 ThrowETSError(coro, excval);
316 }
317
JSConvertTypeCheckFailed(const char * typeName)318 void JSConvertTypeCheckFailed(const char *typeName)
319 {
320 auto ctx = InteropCtx::Current();
321 auto env = ctx->GetJSEnv();
322 InteropCtx::ThrowJSTypeError(env, typeName + std::string(" expected"));
323 }
324
GetErrorStack(napi_env env,napi_value jsErr)325 static std::optional<std::string> GetErrorStack(napi_env env, napi_value jsErr)
326 {
327 bool isError;
328 if (napi_ok != napi_is_error(env, jsErr, &isError)) {
329 return {};
330 }
331 if (!isError) {
332 return "not an Error instance";
333 }
334 napi_value jsStk;
335 if (napi_ok != napi_get_named_property(env, jsErr, "stack", &jsStk)) {
336 return {};
337 }
338 size_t length;
339 if (napi_ok != napi_get_value_string_utf8(env, jsStk, nullptr, 0, &length)) {
340 return {};
341 }
342 std::string value;
343 value.resize(length);
344 // +1 for NULL terminated string!!!
345 if (napi_ok != napi_get_value_string_utf8(env, jsStk, value.data(), value.size() + 1, &length)) {
346 return {};
347 }
348 return value;
349 }
350
NapiTryDumpStack(napi_env env)351 static std::optional<std::string> NapiTryDumpStack(napi_env env)
352 {
353 bool isPending;
354 if (napi_ok != napi_is_exception_pending(env, &isPending)) {
355 return {};
356 }
357
358 std::string pendingErrorMsg;
359 if (isPending) {
360 napi_value valuePending;
361 if (napi_ok != napi_get_and_clear_last_exception(env, &valuePending)) {
362 return {};
363 }
364 auto resStk = GetErrorStack(env, valuePending);
365 if (resStk.has_value()) {
366 pendingErrorMsg = "\nWith pending exception:\n" + resStk.value();
367 } else {
368 pendingErrorMsg = "\nFailed to stringify pending exception";
369 }
370 }
371
372 std::string stacktraceMsg;
373 {
374 napi_value jsDummyStr;
375 if (napi_ok !=
376 napi_create_string_utf8(env, "probe-stacktrace-not-actual-error", NAPI_AUTO_LENGTH, &jsDummyStr)) {
377 return {};
378 }
379 napi_value jsErr;
380 auto rc = napi_create_error(env, nullptr, jsDummyStr, &jsErr);
381 if (napi_ok != rc) {
382 return {};
383 }
384 auto resStk = GetErrorStack(env, jsErr);
385 stacktraceMsg = resStk.has_value() ? resStk.value() : "failed to stringify probe exception";
386 }
387
388 return stacktraceMsg + pendingErrorMsg;
389 }
390
Fatal(const char * msg)391 [[noreturn]] void InteropCtx::Fatal(const char *msg)
392 {
393 INTEROP_LOG(ERROR) << "InteropCtx::Fatal: " << msg;
394
395 auto coro = EtsCoroutine::GetCurrent();
396 auto ctx = InteropCtx::Current(coro);
397
398 INTEROP_LOG(ERROR) << "======================== ETS stack ============================";
399 auto istk = ctx->interopStk_.GetRecords();
400 auto istkIt = istk.rbegin();
401
402 auto printIstkFrames = [&istkIt, &istk](void *fp) {
403 while (istkIt != istk.rend() && fp == istkIt->etsFrame) {
404 INTEROP_LOG(ERROR) << "<interop> " << (istkIt->descr != nullptr ? istkIt->descr : "unknown");
405 istkIt++;
406 }
407 };
408
409 for (auto stack = StackWalker::Create(coro); stack.HasFrame(); stack.NextFrame()) {
410 printIstkFrames(istkIt->etsFrame);
411 Method *method = stack.GetMethod();
412 INTEROP_LOG(ERROR) << method->GetClass()->GetName() << "." << method->GetName().data << " at "
413 << method->GetLineNumberAndSourceFile(stack.GetBytecodePc());
414 }
415 ASSERT(istkIt == istk.rend() || istkIt->etsFrame == nullptr);
416 printIstkFrames(nullptr);
417
418 auto env = ctx->jsEnv_;
419 INTEROP_LOG(ERROR) << (env != nullptr ? "<ets-entrypoint>" : "current js env is nullptr!");
420
421 if (coro->HasPendingException()) {
422 auto exc = EtsObject::FromCoreType(coro->GetException());
423 INTEROP_LOG(ERROR) << "With pending exception: " << exc->GetClass()->GetDescriptor();
424 }
425
426 if (env != nullptr) {
427 INTEROP_LOG(ERROR) << "======================== JS stack =============================";
428 std::optional<std::string> jsStk = NapiTryDumpStack(env);
429 if (jsStk.has_value()) {
430 INTEROP_LOG(ERROR) << jsStk.value();
431 } else {
432 INTEROP_LOG(ERROR) << "JS stack print failed";
433 }
434 }
435
436 INTEROP_LOG(ERROR) << "======================== Native stack =========================";
437 PrintStack(Logger::Message(Logger::Level::ERROR, Logger::Component::ETS_INTEROP_JS, false).GetStream());
438 std::abort();
439 }
440
Init(EtsCoroutine * coro,napi_env env)441 void InteropCtx::Init(EtsCoroutine *coro, napi_env env)
442 {
443 // Initialize InteropCtx in VM ExternalData
444 new (InteropCtx::Current(coro)) InteropCtx(coro, env);
445 }
446
447 } // namespace ark::ets::interop::js
448