1 /**
2 * Copyright (c) 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/call/call.h"
17 #include "plugins/ets/runtime/interop_js/call/arg_convertors.h"
18 #include "plugins/ets/runtime/interop_js/call/proto_reader.h"
19 #include "plugins/ets/runtime/interop_js/code_scopes.h"
20
21 namespace ark::ets::interop::js {
22
CreateProxyBridgeArgReader(uint8_t * args,uint8_t * inStackArgs)23 static ALWAYS_INLINE inline arch::ArgReader<RUNTIME_ARCH> CreateProxyBridgeArgReader(uint8_t *args,
24 uint8_t *inStackArgs)
25 {
26 Span<uint8_t> inGprArgs(args, arch::ExtArchTraits<RUNTIME_ARCH>::GP_ARG_NUM_BYTES);
27 Span<uint8_t> inFprArgs(inGprArgs.end(), arch::ExtArchTraits<RUNTIME_ARCH>::FP_ARG_NUM_BYTES);
28 return arch::ArgReader<RUNTIME_ARCH>(inGprArgs, inFprArgs, inStackArgs);
29 }
30
31 class CallJSHandler {
32 public:
CallJSHandler(EtsCoroutine * coro,Method * method,uint8_t * args,uint8_t * inStackArgs)33 ALWAYS_INLINE CallJSHandler(EtsCoroutine *coro, Method *method, uint8_t *args, uint8_t *inStackArgs)
34 : coro_(coro),
35 ctx_(InteropCtx::Current(coro_)),
36 protoReader_(method, ctx_->GetClassLinker(), ctx_->LinkerCtx()),
37 argReader_(CreateProxyBridgeArgReader(args, inStackArgs))
38 {
39 }
40
SetupArgreader(bool isInstance)41 ALWAYS_INLINE ObjectHeader *SetupArgreader(bool isInstance)
42 {
43 auto method = protoReader_.GetMethod();
44 protoReader_.Advance(); // skip return type
45 argReader_.template Read<Method *>(); // skip method
46 ASSERT(isInstance == !method->IsStatic());
47 numArgs_ = method->GetNumArgs() - static_cast<uint32_t>(isInstance);
48 return isInstance ? argReader_.Read<ObjectHeader *>() : nullptr;
49 }
50
51 template <panda_file::Type::TypeId PF_TYPEID, typename T>
ReadFixedArg()52 ALWAYS_INLINE T ReadFixedArg()
53 {
54 ASSERT(protoReader_.GetType() == panda_file::Type(PF_TYPEID));
55 protoReader_.Advance();
56 numArgs_--;
57 return argReader_.Read<T>();
58 }
59
60 template <typename T>
ReadFixedRefArg(Class * expected)61 ALWAYS_INLINE T *ReadFixedRefArg([[maybe_unused]] Class *expected)
62 {
63 ASSERT(expected == nullptr || protoReader_.GetClass() == expected);
64 return ReadFixedArg<panda_file::Type::TypeId::REFERENCE, T *>();
65 }
66
SetupJSCallee(napi_value jsThis,napi_value jsFn)67 ALWAYS_INLINE void SetupJSCallee(napi_value jsThis, napi_value jsFn)
68 {
69 jsThis_ = jsThis;
70 jsFn_ = jsFn;
71 }
72
73 template <bool IS_NEWCALL, typename ArgSetup>
HandleImpl(Method * method,uint8_t * args,uint8_t * inStackArgs)74 static ALWAYS_INLINE uint64_t HandleImpl(Method *method, uint8_t *args, uint8_t *inStackArgs)
75 {
76 CallJSHandler st(EtsCoroutine::GetCurrent(), method, args, inStackArgs);
77 return st.Handle<IS_NEWCALL, ArgSetup>();
78 }
79
GetMethod()80 ALWAYS_INLINE Method *GetMethod()
81 {
82 return protoReader_.GetMethod();
83 }
84
85 ~CallJSHandler() = default;
86
87 private:
ForwardException(InteropCtx * ctx,EtsCoroutine * coro)88 static uint64_t __attribute__((noinline)) ForwardException(InteropCtx *ctx, EtsCoroutine *coro)
89 {
90 if (NapiIsExceptionPending(ctx->GetJSEnv())) {
91 ctx->ForwardJSException(coro);
92 }
93 ASSERT(ctx->SanityETSExceptionPending());
94 return 0;
95 }
96
97 template <bool IS_NEWCALL, typename ArgSetup>
98 ALWAYS_INLINE uint64_t Handle();
99
100 template <bool IS_NEWCALL>
101 ALWAYS_INLINE std::optional<napi_value> ConvertArgsAndCall();
102
103 template <bool IS_NEWCALL>
104 ALWAYS_INLINE std::optional<Value> ConvertRetval(napi_value jsRet);
105
106 NO_COPY_SEMANTIC(CallJSHandler);
107 NO_MOVE_SEMANTIC(CallJSHandler);
108
109 EtsCoroutine *const coro_;
110 InteropCtx *const ctx_;
111
112 ProtoReader protoReader_;
113 arch::ArgReader<RUNTIME_ARCH> argReader_;
114 uint32_t numArgs_ {};
115 napi_value jsThis_ {};
116 napi_value jsFn_ {};
117 };
118
119 template <bool IS_NEWCALL, typename ArgSetup>
Handle()120 ALWAYS_INLINE inline uint64_t CallJSHandler::Handle()
121 {
122 [[maybe_unused]] InteropCodeScopeETS codeScope(coro_, __PRETTY_FUNCTION__);
123 napi_env env = ctx_->GetJSEnv();
124 NapiScope jsHandleScope(env);
125
126 if (UNLIKELY(!ArgSetup()(ctx_, this))) {
127 return ForwardException(ctx_, coro_);
128 }
129 if (UNLIKELY(GetValueType(env, jsFn_) != napi_function)) {
130 ctx_->ThrowJSTypeError(env, "call target is not a function");
131 return ForwardException(ctx_, coro_);
132 }
133
134 std::optional<napi_value> jsRes = ConvertArgsAndCall<IS_NEWCALL>();
135 if (UNLIKELY(!jsRes.has_value())) {
136 return ForwardException(ctx_, coro_);
137 }
138 std::optional<Value> etsRes = ConvertRetval<IS_NEWCALL>(jsRes.value());
139 if (UNLIKELY(!etsRes.has_value())) {
140 return ForwardException(ctx_, coro_);
141 }
142 return static_cast<uint64_t>(etsRes.value().GetAsLong());
143 }
144
145 template <bool IS_NEWCALL>
ConvertArgsAndCall()146 ALWAYS_INLINE inline std::optional<napi_value> CallJSHandler::ConvertArgsAndCall()
147 {
148 napi_env env = ctx_->GetJSEnv();
149 auto jsargs = ctx_->GetTempArgs<napi_value>(numArgs_);
150
151 for (uint32_t argIdx = 0; argIdx < numArgs_; ++argIdx, protoReader_.Advance()) {
152 auto readVal = [this](auto typeTag) { return argReader_.template Read<typename decltype(typeTag)::type>(); };
153 if (UNLIKELY(!ConvertArgToJS(ctx_, protoReader_, &jsargs[argIdx], readVal))) {
154 return std::nullopt;
155 }
156 }
157
158 napi_value jsRetval;
159 napi_status jsStatus;
160 {
161 ScopedNativeCodeThread nativeScope(coro_);
162 if constexpr (IS_NEWCALL) {
163 jsStatus = napi_new_instance(env, jsFn_, jsargs->size(), jsargs->data(), &jsRetval);
164 } else {
165 jsStatus = napi_call_function(env, jsThis_, jsFn_, jsargs->size(), jsargs->data(), &jsRetval);
166 }
167 }
168 if (UNLIKELY(jsStatus != napi_ok)) {
169 INTEROP_FATAL_IF(jsStatus != napi_pending_exception);
170 return std::nullopt;
171 }
172 return jsRetval;
173 }
174
175 template <bool IS_NEWCALL>
ConvertRetval(napi_value jsRet)176 ALWAYS_INLINE inline std::optional<Value> CallJSHandler::ConvertRetval(napi_value jsRet)
177 {
178 napi_env env = ctx_->GetJSEnv();
179 Value etsRet;
180 protoReader_.Reset();
181
182 if constexpr (IS_NEWCALL) {
183 ASSERT(protoReader_.GetClass() == ctx_->GetJSValueClass());
184 auto res = JSConvertJSValue::Unwrap(ctx_, env, jsRet);
185 if (UNLIKELY(!res.has_value())) {
186 return std::nullopt;
187 }
188 etsRet = Value(res.value()->GetCoreType());
189 } else {
190 auto store = [&etsRet](auto val) { etsRet = Value(val); };
191 if (UNLIKELY(!ConvertArgToEts(ctx_, protoReader_, store, jsRet))) {
192 return std::nullopt;
193 }
194 }
195 return etsRet;
196 }
197
ResolveQualifiedReceiverTarget(napi_env env,napi_value jsVal,coretypes::String * qnameStr)198 static std::optional<std::pair<napi_value, napi_value>> ResolveQualifiedReceiverTarget(napi_env env, napi_value jsVal,
199 coretypes::String *qnameStr)
200 {
201 napi_value jsThis {};
202 ASSERT(qnameStr->IsMUtf8());
203 auto qname = std::string(utf::Mutf8AsCString(qnameStr->GetDataMUtf8()), qnameStr->GetMUtf8Length());
204
205 auto resolveName = [&jsThis, &jsVal, &env](const std::string &name) -> bool {
206 jsThis = jsVal;
207 INTEROP_LOG(DEBUG) << "JSRuntimeCallJS: resolve name: " << name;
208 napi_status rc = napi_get_named_property(env, jsVal, name.c_str(), &jsVal);
209 if (UNLIKELY(rc == napi_object_expected || NapiThrownGeneric(rc))) {
210 ASSERT(NapiIsExceptionPending(env));
211 return false;
212 }
213 return true;
214 };
215 jsThis = jsVal;
216 if (UNLIKELY(!WalkQualifiedName(qname, resolveName))) {
217 return std::nullopt;
218 }
219 return std::make_pair(jsThis, jsVal);
220 }
221
222 template <bool IS_NEWCALL>
JSRuntimeCallJSQNameBase(Method * method,uint8_t * args,uint8_t * inStackArgs)223 static ALWAYS_INLINE inline uint64_t JSRuntimeCallJSQNameBase(Method *method, uint8_t *args, uint8_t *inStackArgs)
224 {
225 struct ArgSetup {
226 ALWAYS_INLINE bool operator()(InteropCtx *ctx, CallJSHandler *st)
227 {
228 st->SetupArgreader(false);
229 napi_env env = ctx->GetJSEnv();
230
231 napi_value jsVal = JSConvertJSValue::Wrap(env, st->ReadFixedRefArg<JSValue>(ctx->GetJSValueClass()));
232 auto qnameStr = st->ReadFixedRefArg<coretypes::String>(ctx->GetStringClass());
233
234 auto res = ResolveQualifiedReceiverTarget(env, jsVal, qnameStr);
235 if (UNLIKELY(!res.has_value())) {
236 ASSERT(NapiIsExceptionPending(env));
237 return false;
238 }
239
240 st->SetupJSCallee(res->first, res->second);
241 return true;
242 }
243 };
244 return CallJSHandler::HandleImpl<IS_NEWCALL, ArgSetup>(method, args, inStackArgs);
245 }
246
JSRuntimeCallJSQName(Method * method,uint8_t * args,uint8_t * inStackArgs)247 extern "C" uint64_t JSRuntimeCallJSQName(Method *method, uint8_t *args, uint8_t *inStackArgs)
248 {
249 return JSRuntimeCallJSQNameBase<false>(method, args, inStackArgs); // IS_NEWCALL is false
250 }
251 extern "C" void JSRuntimeCallJSQNameBridge(Method *method, ...);
252
JSRuntimeNewCallJSQName(Method * method,uint8_t * args,uint8_t * inStackArgs)253 extern "C" uint64_t JSRuntimeNewCallJSQName(Method *method, uint8_t *args, uint8_t *inStackArgs)
254 {
255 return JSRuntimeCallJSQNameBase<true>(method, args, inStackArgs); // IS_NEWCALL is true
256 }
257 extern "C" void JSRuntimeNewCallJSQNameBridge(Method *method, ...);
258
GetClassQnameOffset(InteropCtx * ctx,Method * method)259 static uint32_t GetClassQnameOffset(InteropCtx *ctx, Method *method)
260 {
261 auto klass = method->GetClass();
262 ctx->GetConstStringStorage()->LoadDynamicCallClass(klass);
263 auto fields = klass->GetStaticFields();
264 ASSERT(fields.size() == 1);
265 return klass->GetFieldPrimitive<uint32_t>(fields[0]);
266 }
267
268 template <bool IS_NEWCALL>
JSRuntimeCallJSBase(Method * method,uint8_t * args,uint8_t * inStackArgs)269 static ALWAYS_INLINE inline uint64_t JSRuntimeCallJSBase(Method *method, uint8_t *args, uint8_t *inStackArgs)
270 {
271 struct ArgSetup {
272 ALWAYS_INLINE bool operator()(InteropCtx *ctx, CallJSHandler *st)
273 {
274 st->SetupArgreader(false);
275 napi_env env = ctx->GetJSEnv();
276
277 napi_value jsVal = JSConvertJSValue::Wrap(env, st->ReadFixedRefArg<JSValue>(ctx->GetJSValueClass()));
278
279 auto classQnameOffset = GetClassQnameOffset(ctx, st->GetMethod());
280 auto qnameStart = st->ReadFixedArg<panda_file::Type::TypeId::I32, int32_t>() + classQnameOffset;
281 auto qnameLen = st->ReadFixedArg<panda_file::Type::TypeId::I32, int32_t>();
282 napi_value jsThis {};
283
284 auto success = ctx->GetConstStringStorage()->EnumerateStrings(
285 qnameStart, qnameLen, [&jsThis, &jsVal, env](napi_value jsStr) {
286 jsThis = jsVal;
287 napi_status rc = napi_get_property(env, jsVal, jsStr, &jsVal);
288 if (UNLIKELY(rc == napi_object_expected || NapiThrownGeneric(rc))) {
289 ASSERT(NapiIsExceptionPending(env));
290 return false;
291 }
292 return true;
293 });
294
295 if (!success) {
296 ASSERT(NapiIsExceptionPending(env));
297 return false;
298 }
299 st->SetupJSCallee(jsThis, jsVal);
300 return true;
301 }
302 };
303 return CallJSHandler::HandleImpl<IS_NEWCALL, ArgSetup>(method, args, inStackArgs);
304 }
305
JSRuntimeCallJS(Method * method,uint8_t * args,uint8_t * inStackArgs)306 extern "C" uint64_t JSRuntimeCallJS(Method *method, uint8_t *args, uint8_t *inStackArgs)
307 {
308 return JSRuntimeCallJSBase<false>(method, args, inStackArgs); // IS_NEWCALL is false
309 }
310 extern "C" void JSRuntimeCallJSBridge(Method *method, ...);
311
JSRuntimeNewCallJS(Method * method,uint8_t * args,uint8_t * inStackArgs)312 extern "C" uint64_t JSRuntimeNewCallJS(Method *method, uint8_t *args, uint8_t *inStackArgs)
313 {
314 return JSRuntimeCallJSBase<true>(method, args, inStackArgs); // IS_NEWCALL is true
315 }
316 extern "C" void JSRuntimeNewCallJSBridge(Method *method, ...);
317
JSRuntimeCallJSByValue(Method * method,uint8_t * args,uint8_t * inStackArgs)318 extern "C" uint64_t JSRuntimeCallJSByValue(Method *method, uint8_t *args, uint8_t *inStackArgs)
319 {
320 struct ArgSetup {
321 ALWAYS_INLINE bool operator()(InteropCtx *ctx, CallJSHandler *st)
322 {
323 st->SetupArgreader(false);
324 napi_env env = ctx->GetJSEnv();
325
326 napi_value jsFn = JSConvertJSValue::Wrap(env, st->ReadFixedRefArg<JSValue>(ctx->GetJSValueClass()));
327 napi_value jsThis = JSConvertJSValue::Wrap(env, st->ReadFixedRefArg<JSValue>(ctx->GetJSValueClass()));
328
329 st->SetupJSCallee(jsThis, jsFn);
330 return true;
331 }
332 };
333 return CallJSHandler::HandleImpl<false, ArgSetup>(method, args, inStackArgs);
334 }
335 extern "C" void JSRuntimeCallJSByValueBridge(Method *method, ...);
336
CallJSProxy(Method * method,uint8_t * args,uint8_t * inStackArgs)337 extern "C" uint64_t CallJSProxy(Method *method, uint8_t *args, uint8_t *inStackArgs)
338 {
339 struct ArgSetup {
340 ALWAYS_INLINE bool operator()(InteropCtx *ctx, CallJSHandler *st)
341 {
342 ObjectHeader *etsThis = st->SetupArgreader(true);
343 napi_env env = ctx->GetJSEnv();
344
345 auto refconv = JSRefConvertResolve(ctx, etsThis->ClassAddr<Class>());
346 napi_value jsThis = refconv->Wrap(ctx, EtsObject::FromCoreType(etsThis));
347 ASSERT(GetValueType(env, jsThis) == napi_object);
348 const char *methodName = utf::Mutf8AsCString(st->GetMethod()->GetName().data);
349 napi_value jsFn;
350 napi_status rc = napi_get_named_property(env, jsThis, methodName, &jsFn);
351 if (UNLIKELY(rc == napi_object_expected || NapiThrownGeneric(rc))) {
352 ASSERT(NapiIsExceptionPending(env));
353 return false;
354 }
355
356 st->SetupJSCallee(jsThis, jsFn);
357 return true;
358 }
359 };
360 return CallJSHandler::HandleImpl<false, ArgSetup>(method, args, inStackArgs);
361 }
362 extern "C" void CallJSProxyBridge(Method *method, ...);
363
SelectCallJSEntrypoint(InteropCtx * ctx,Method * method)364 static void *SelectCallJSEntrypoint(InteropCtx *ctx, Method *method)
365 {
366 ASSERT(method->IsStatic());
367 ProtoReader protoReader(method, ctx->GetClassLinker(), ctx->LinkerCtx());
368
369 // skip return type
370 protoReader.Advance();
371
372 ASSERT(protoReader.GetClass() == ctx->GetJSValueClass());
373 protoReader.Advance();
374
375 if (protoReader.GetType().IsPrimitive()) {
376 if (protoReader.GetType().GetId() != panda_file::Type::TypeId::I32) {
377 return nullptr;
378 }
379 protoReader.Advance();
380
381 if (protoReader.GetType().GetId() != panda_file::Type::TypeId::I32) {
382 return nullptr;
383 }
384 return reinterpret_cast<void *>(JSRuntimeCallJSBridge);
385 }
386 if (protoReader.GetClass() == ctx->GetStringClass()) {
387 return reinterpret_cast<void *>(JSRuntimeCallJSQNameBridge);
388 }
389 if (protoReader.GetClass() == ctx->GetJSValueClass()) {
390 return reinterpret_cast<void *>(JSRuntimeCallJSByValueBridge);
391 }
392 InteropFatal("Bad jscall signature");
393 }
394
SelectNewCallJSEntrypoint(InteropCtx * ctx,Method * method)395 static void *SelectNewCallJSEntrypoint(InteropCtx *ctx, Method *method)
396 {
397 ASSERT(method->IsStatic());
398 ProtoReader protoReader(method, ctx->GetClassLinker(), ctx->LinkerCtx());
399
400 if (protoReader.GetClass() != ctx->GetJSValueClass()) {
401 return nullptr;
402 }
403 protoReader.Advance();
404
405 if (protoReader.GetClass() != ctx->GetJSValueClass()) {
406 return nullptr;
407 }
408 protoReader.Advance();
409
410 if (protoReader.GetType().IsPrimitive()) {
411 if (protoReader.GetType().GetId() != panda_file::Type::TypeId::I32) {
412 return nullptr;
413 }
414 protoReader.Advance();
415
416 if (protoReader.GetType().GetId() != panda_file::Type::TypeId::I32) {
417 return nullptr;
418 }
419 return reinterpret_cast<void *>(JSRuntimeNewCallJSBridge);
420 }
421 if (protoReader.GetClass() != ctx->GetStringClass()) {
422 return nullptr;
423 }
424 return reinterpret_cast<void *>(JSRuntimeNewCallJSQNameBridge);
425 }
426
GetQnameCount(Class * klass)427 static std::optional<uint32_t> GetQnameCount(Class *klass)
428 {
429 auto pf = klass->GetPandaFile();
430 panda_file::ClassDataAccessor cda(*pf, klass->GetFileId());
431 auto qnameCount =
432 cda.EnumerateAnnotation("Lets/annotation/DynamicCall;", [pf](panda_file::AnnotationDataAccessor &ada) {
433 for (uint32_t i = 0; i < ada.GetCount(); i++) {
434 auto adae = ada.GetElement(i);
435 auto *elemName = pf->GetStringData(adae.GetNameId()).data;
436 if (utf::IsEqual(utf::CStringAsMutf8("value"), elemName)) {
437 return adae.GetArrayValue().GetCount();
438 }
439 }
440 UNREACHABLE();
441 });
442 return qnameCount;
443 }
444
InitCallJSClass(coretypes::String * descriptorStr,bool isNewCall)445 static uint8_t InitCallJSClass(coretypes::String *descriptorStr, bool isNewCall)
446 {
447 auto coro = EtsCoroutine::GetCurrent();
448 auto ctx = InteropCtx::Current(coro);
449 auto *klass = ctx->GetClassLinker()->GetClass(descriptorStr->GetDataMUtf8(), true, ctx->LinkerCtx());
450 INTEROP_FATAL_IF(klass == nullptr);
451 INTEROP_LOG(DEBUG) << "Bind bridge call methods for " << utf::Mutf8AsCString(klass->GetDescriptor());
452
453 for (auto &method : klass->GetMethods()) {
454 if (method.IsConstructor()) {
455 continue;
456 }
457 void *ep = nullptr;
458 if (method.IsStatic()) {
459 ep = isNewCall ? SelectNewCallJSEntrypoint(ctx, &method) : SelectCallJSEntrypoint(ctx, &method);
460 }
461 if (ep == nullptr) {
462 InteropFatal("Bad interop call bridge signature");
463 }
464 method.SetCompiledEntryPoint(ep);
465 method.SetNativePointer(nullptr);
466 }
467
468 auto qnameCount = GetQnameCount(klass);
469 // JSCallClass which was manually created in test will not have the required annotation and field
470 if (qnameCount.has_value()) {
471 auto fields = klass->GetStaticFields();
472 INTEROP_FATAL_IF(fields.Size() != 1);
473 INTEROP_FATAL_IF(klass->GetFieldPrimitive<uint32_t>(fields[0]) != 0);
474 klass->SetFieldPrimitive<uint32_t>(fields[0], ctx->AllocateSlotsInStringBuffer(*qnameCount));
475 }
476 return 1;
477 }
478
JSRuntimeInitJSCallClass(EtsString * clsStr)479 uint8_t JSRuntimeInitJSCallClass(EtsString *clsStr)
480 {
481 return InitCallJSClass(clsStr->GetCoreType(), false);
482 }
483
JSRuntimeInitJSNewClass(EtsString * clsStr)484 uint8_t JSRuntimeInitJSNewClass(EtsString *clsStr)
485 {
486 return InitCallJSClass(clsStr->GetCoreType(), true);
487 }
488
489 } // namespace ark::ets::interop::js
490