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/napi_env_scope.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 panda::ets::interop::js {
29
InteropCtx(EtsCoroutine * coro,napi_env env)30 InteropCtx::InteropCtx(EtsCoroutine *coro, napi_env env)
31 {
32 EtsJSNapiEnvScope envscope(this, env);
33
34 PandaEtsVM *vm = coro->GetPandaVM();
35 EtsClassLinker *etsClassLinker = vm->GetClassLinker();
36 refstor_ = vm->GetGlobalObjectStorage();
37 linkerCtx_ = etsClassLinker->GetEtsClassLinkerExtension()->GetBootContext();
38
39 JSRuntimeIntrinsicsSetIntrinsicsAPI(GetIntrinsicsAPI());
40 auto *jobQueue = Runtime::GetCurrent()->GetInternalAllocator()->New<JsJobQueue>();
41 vm->InitJobQueue(jobQueue);
42
43 auto cacheClass = [&etsClassLinker](std::string_view descriptor) {
44 auto klass = etsClassLinker->GetClass(descriptor.data())->GetRuntimeClass();
45 ASSERT(klass != nullptr);
46 return klass;
47 };
48
49 namespace descriptors = panda_file_items::class_descriptors;
50
51 jsRuntimeClass_ = cacheClass(descriptors::JS_RUNTIME);
52 jsValueClass_ = cacheClass(descriptors::JS_VALUE);
53 jsErrorClass_ = cacheClass(descriptors::JS_ERROR);
54 objectClass_ = cacheClass(descriptors::OBJECT);
55 stringClass_ = cacheClass(descriptors::STRING);
56 voidClass_ = cacheClass(descriptors::VOID);
57 undefinedClass_ = cacheClass(descriptors::INTERNAL_UNDEFINED);
58 promiseClass_ = cacheClass(descriptors::PROMISE);
59 errorClass_ = cacheClass(descriptors::ERROR);
60 exceptionClass_ = cacheClass(descriptors::EXCEPTION);
61 typeClass_ = cacheClass(descriptors::TYPE);
62
63 boxIntClass_ = cacheClass(descriptors::BOX_INT);
64 boxLongClass_ = cacheClass(descriptors::BOX_LONG);
65
66 arrayClass_ = cacheClass(descriptors::ARRAY);
67 arraybufClass_ = cacheClass(descriptors::ARRAY_BUFFER);
68
69 RegisterBuiltinJSRefConvertors(this);
70
71 {
72 auto method = EtsClass::FromRuntimeClass(jsRuntimeClass_)->GetMethod("createFinalizationQueue");
73 ASSERT(method != nullptr);
74 auto res = method->GetPandaMethod()->Invoke(coro, nullptr);
75 ASSERT(!coro->HasPendingException());
76 auto queue = EtsObject::FromCoreType(res.GetAs<ObjectHeader *>());
77 jsvalueFqueueRef_ = Refstor()->Add(queue->GetCoreType(), mem::Reference::ObjectType::GLOBAL);
78 ASSERT(jsvalueFqueueRef_ != nullptr);
79
80 jsvalueFqueueRegister_ =
81 queue->GetClass()
82 ->GetMethod("register", "Lstd/core/Object;Lstd/core/Object;Lstd/core/Object;:Lstd/core/void;")
83 ->GetPandaMethod();
84 ASSERT(jsvalueFqueueRegister_ != nullptr);
85 }
86
87 etsProxyRefStorage_ = ets_proxy::SharedReferenceStorage::Create();
88 ASSERT(etsProxyRefStorage_.get() != nullptr);
89
90 // Set InteropCtx::DestroyLocalScopeForTopFrame to VM for call it it deoptimization and exception handlers
91 coro->GetPandaVM()->SetClearInteropHandleScopesFunction(
92 [this](Frame *frame) { this->DestroyLocalScopeForTopFrame(frame); });
93 }
94
CreateETSCoreJSError(EtsCoroutine * coro,JSValue * jsvalue)95 EtsObject *InteropCtx::CreateETSCoreJSError(EtsCoroutine *coro, JSValue *jsvalue)
96 {
97 [[maybe_unused]] HandleScope<ObjectHeader *> scope(coro);
98 VMHandle<ObjectHeader> jsvalueHandle(coro, jsvalue->GetCoreType());
99
100 Method::Proto proto(Method::Proto::ShortyVector {panda_file::Type(panda_file::Type::TypeId::VOID),
101 panda_file::Type(panda_file::Type::TypeId::REFERENCE)},
102 Method::Proto::RefTypeVector {utf::Mutf8AsCString(GetJSValueClass()->GetDescriptor())});
103 auto ctorName = utf::CStringAsMutf8(panda_file_items::CTOR.data());
104 auto ctor = GetJSErrorClass()->GetDirectMethod(ctorName, proto);
105 ASSERT(ctor != nullptr);
106
107 auto excObj = ObjectHeader::Create(coro, GetJSErrorClass());
108 if (UNLIKELY(excObj == nullptr)) {
109 return nullptr;
110 }
111 VMHandle<ObjectHeader> excHandle(coro, excObj);
112
113 std::array<Value, 2U> args {Value(excHandle.GetPtr()), Value(jsvalueHandle.GetPtr())};
114 ctor->InvokeVoid(coro, args.data());
115 auto res = EtsObject::FromCoreType(excHandle.GetPtr());
116 if (UNLIKELY(coro->HasPendingException())) {
117 return nullptr;
118 }
119 return res;
120 }
121
ThrowETSError(EtsCoroutine * coro,napi_value val)122 void InteropCtx::ThrowETSError(EtsCoroutine *coro, napi_value val)
123 {
124 auto ctx = Current(coro);
125
126 if (coro->IsUsePreAllocObj()) {
127 coro->SetUsePreAllocObj(false);
128 coro->SetException(coro->GetVM()->GetOOMErrorObject());
129 return;
130 }
131 ASSERT(!coro->HasPendingException());
132
133 if (IsNullOrUndefined(ctx->GetJSEnv(), val)) {
134 ctx->ThrowETSError(coro, "interop/js throws undefined/null");
135 return;
136 }
137
138 // To catch `TypeError` or `UserError extends TypeError`
139 // 1. Frontend puts catch(compat/TypeError) { <instanceof-rethrow? if UserError expected> }
140 // Where js.UserError will be wrapped into compat/TypeError
141 // NOTE(vpukhov): compat: add intrinsic to obtain JSValue from compat/ instances
142
143 auto objRefconv = JSRefConvertResolve(ctx, ctx->GetObjectClass());
144 LocalObjectHandle<EtsObject> etsObj(coro, objRefconv->Unwrap(ctx, val));
145 if (UNLIKELY(etsObj.GetPtr() == nullptr)) {
146 INTEROP_LOG(INFO) << "Something went wrong while unwrapping pending js exception";
147 ASSERT(ctx->SanityETSExceptionPending());
148 return;
149 }
150
151 auto klass = etsObj->GetClass()->GetRuntimeClass();
152 if (LIKELY(ctx->GetErrorClass()->IsAssignableFrom(klass) || ctx->GetExceptionClass()->IsAssignableFrom(klass))) {
153 coro->SetException(etsObj->GetCoreType());
154 return;
155 }
156
157 // NOTE(vpukhov): should throw a special error (JSError?) with cause set
158 auto exc = JSConvertJSError::Unwrap(ctx, ctx->GetJSEnv(), val);
159 if (LIKELY(exc.has_value())) {
160 ASSERT(exc != nullptr);
161 coro->SetException(exc.value()->GetCoreType());
162 } // otherwise exception is already set
163 }
164
ThrowETSError(EtsCoroutine * coro,const char * msg)165 void InteropCtx::ThrowETSError(EtsCoroutine *coro, const char *msg)
166 {
167 ASSERT(!coro->HasPendingException());
168 ets::ThrowEtsException(coro, panda_file_items::class_descriptors::ERROR, msg);
169 }
170
ThrowJSError(napi_env env,const std::string & msg)171 void InteropCtx::ThrowJSError(napi_env env, const std::string &msg)
172 {
173 INTEROP_LOG(INFO) << "ThrowJSError: " << msg;
174 ASSERT(!NapiIsExceptionPending(env));
175 NAPI_CHECK_FATAL(napi_throw_error(env, nullptr, msg.c_str()));
176 }
177
ThrowJSTypeError(napi_env env,const std::string & msg)178 void InteropCtx::ThrowJSTypeError(napi_env env, const std::string &msg)
179 {
180 INTEROP_LOG(INFO) << "ThrowJSTypeError: " << msg;
181 ASSERT(!NapiIsExceptionPending(env));
182 NAPI_CHECK_FATAL(napi_throw_type_error(env, nullptr, msg.c_str()));
183 }
184
ThrowJSValue(napi_env env,napi_value val)185 void InteropCtx::ThrowJSValue(napi_env env, napi_value val)
186 {
187 INTEROP_LOG(INFO) << "ThrowJSValue";
188 ASSERT(!NapiIsExceptionPending(env));
189 NAPI_CHECK_FATAL(napi_throw(env, val));
190 }
191
ForwardEtsException(EtsCoroutine * coro)192 void InteropCtx::ForwardEtsException(EtsCoroutine *coro)
193 {
194 auto env = GetJSEnv();
195 ASSERT(coro->HasPendingException());
196 LocalObjectHandle<ObjectHeader> exc(coro, coro->GetException());
197 coro->ClearException();
198
199 auto klass = exc->ClassAddr<Class>();
200 ASSERT(GetErrorClass()->IsAssignableFrom(klass) || GetExceptionClass()->IsAssignableFrom(klass));
201 JSRefConvert *refconv = JSRefConvertResolve<true>(this, klass);
202 if (UNLIKELY(refconv == nullptr)) {
203 INTEROP_LOG(INFO) << "Exception thrown while forwarding ets exception: " << klass->GetDescriptor();
204 return;
205 }
206 napi_value res = refconv->Wrap(this, EtsObject::FromCoreType(exc.GetPtr()));
207 if (UNLIKELY(res == nullptr)) {
208 return;
209 }
210 ThrowJSValue(env, res);
211 }
212
ForwardJSException(EtsCoroutine * coro)213 void InteropCtx::ForwardJSException(EtsCoroutine *coro)
214 {
215 auto env = GetJSEnv();
216 napi_value excval;
217 ASSERT(NapiIsExceptionPending(env));
218 NAPI_CHECK_FATAL(napi_get_and_clear_last_exception(env, &excval));
219 ThrowETSError(coro, excval);
220 }
221
JSConvertTypeCheckFailed(const char * typeName)222 void JSConvertTypeCheckFailed(const char *typeName)
223 {
224 auto ctx = InteropCtx::Current();
225 auto env = ctx->GetJSEnv();
226 InteropCtx::ThrowJSTypeError(env, typeName + std::string(" expected"));
227 }
228
GetErrorStack(napi_env env,napi_value jsErr)229 static std::optional<std::string> GetErrorStack(napi_env env, napi_value jsErr)
230 {
231 bool isError;
232 if (napi_ok != napi_is_error(env, jsErr, &isError)) {
233 return {};
234 }
235 if (!isError) {
236 return "not an Error instance";
237 }
238 napi_value jsStk;
239 if (napi_ok != napi_get_named_property(env, jsErr, "stack", &jsStk)) {
240 return {};
241 }
242 size_t length;
243 if (napi_ok != napi_get_value_string_utf8(env, jsStk, nullptr, 0, &length)) {
244 return {};
245 }
246 std::string value;
247 value.resize(length);
248 // +1 for NULL terminated string!!!
249 if (napi_ok != napi_get_value_string_utf8(env, jsStk, value.data(), value.size() + 1, &length)) {
250 return {};
251 }
252 return value;
253 }
254
NapiTryDumpStack(napi_env env)255 static std::optional<std::string> NapiTryDumpStack(napi_env env)
256 {
257 bool isPending;
258 if (napi_ok != napi_is_exception_pending(env, &isPending)) {
259 return {};
260 }
261
262 std::string pendingErrorMsg;
263 if (isPending) {
264 napi_value valuePending;
265 if (napi_ok != napi_get_and_clear_last_exception(env, &valuePending)) {
266 return {};
267 }
268 auto resStk = GetErrorStack(env, valuePending);
269 if (resStk.has_value()) {
270 pendingErrorMsg = "\nWith pending exception:\n" + resStk.value();
271 } else {
272 pendingErrorMsg = "\nFailed to stringify pending exception";
273 }
274 }
275
276 std::string stacktraceMsg;
277 {
278 napi_value jsDummyStr;
279 if (napi_ok !=
280 napi_create_string_utf8(env, "probe-stacktrace-not-actual-error", NAPI_AUTO_LENGTH, &jsDummyStr)) {
281 return {};
282 }
283 napi_value jsErr;
284 auto rc = napi_create_error(env, nullptr, jsDummyStr, &jsErr);
285 if (napi_ok != rc) {
286 return {};
287 }
288 auto resStk = GetErrorStack(env, jsErr);
289 stacktraceMsg = resStk.has_value() ? resStk.value() : "failed to stringify probe exception";
290 }
291
292 return stacktraceMsg + pendingErrorMsg;
293 }
294
Fatal(const char * msg)295 [[noreturn]] void InteropCtx::Fatal(const char *msg)
296 {
297 INTEROP_LOG(ERROR) << "InteropCtx::Fatal: " << msg;
298
299 auto coro = EtsCoroutine::GetCurrent();
300 auto ctx = InteropCtx::Current(coro);
301
302 INTEROP_LOG(ERROR) << "======================== ETS stack ============================";
303 auto &istk = ctx->GetInteropFrames();
304 auto istkIt = istk.rbegin();
305
306 for (auto stack = StackWalker::Create(coro); stack.HasFrame(); stack.NextFrame()) {
307 while (istkIt != istk.rend() && stack.GetFp() == istkIt->etsFrame) {
308 INTEROP_LOG(ERROR) << (istkIt->toJs ? "<ets-to-js>" : "<js-to-ets>");
309 istkIt++;
310 }
311
312 Method *method = stack.GetMethod();
313 INTEROP_LOG(ERROR) << method->GetClass()->GetName() << "." << method->GetName().data << " at "
314 << method->GetLineNumberAndSourceFile(stack.GetBytecodePc());
315 }
316 ASSERT(istkIt == istk.rend() || istkIt->etsFrame == nullptr);
317
318 auto env = ctx->jsEnv_;
319 INTEROP_LOG(ERROR) << (env != nullptr ? "<ets-entrypoint>" : "current js env is nullptr!");
320
321 if (coro->HasPendingException()) {
322 auto exc = EtsObject::FromCoreType(coro->GetException());
323 INTEROP_LOG(ERROR) << "With pending exception: " << exc->GetClass()->GetDescriptor();
324 }
325
326 if (env != nullptr) {
327 INTEROP_LOG(ERROR) << "======================== JS stack =============================";
328 std::optional<std::string> jsStk = NapiTryDumpStack(env);
329 if (jsStk.has_value()) {
330 INTEROP_LOG(ERROR) << jsStk.value();
331 } else {
332 INTEROP_LOG(ERROR) << "JS stack print failed";
333 }
334 }
335
336 INTEROP_LOG(ERROR) << "======================== Native stack =========================";
337 PrintStack(Logger::Message(Logger::Level::ERROR, Logger::Component::ETS_INTEROP_JS, false).GetStream());
338 std::abort();
339 }
340
341 } // namespace panda::ets::interop::js
342