1 /**
2 * Copyright (c) 2021-2025 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 #ifndef PANDA_PLUGINS_ETS_RUNTIME_INTEROP_JS_JS_CONVERT_H
17 #define PANDA_PLUGINS_ETS_RUNTIME_INTEROP_JS_JS_CONVERT_H
18
19 #include "js_convert_base.h"
20 #include "js_convert_stdlib.h"
21
22 namespace ark::ets::interop::js {
23
24 template <typename Cpptype>
25 struct JSConvertNumeric : public JSConvertBase<JSConvertNumeric<Cpptype>, Cpptype> {
26 static constexpr const char *TYPE_NAME = "number";
27
28 template <typename P = Cpptype>
WrapImplJSConvertNumeric29 static std::enable_if_t<std::is_integral_v<P>, napi_value> WrapImpl(napi_env env, Cpptype etsVal)
30 {
31 napi_value jsVal;
32 if constexpr (sizeof(Cpptype) >= sizeof(int32_t)) {
33 NAPI_CHECK_FATAL(napi_create_int64(env, etsVal, &jsVal));
34 } else if constexpr (std::is_signed_v<Cpptype>) {
35 NAPI_CHECK_FATAL(napi_create_int32(env, etsVal, &jsVal));
36 } else {
37 NAPI_CHECK_FATAL(napi_create_uint32(env, etsVal, &jsVal));
38 }
39 return jsVal;
40 }
41
42 template <typename P = Cpptype>
UnwrapImplJSConvertNumeric43 static std::enable_if_t<std::is_integral_v<P>, std::optional<Cpptype>> UnwrapImpl([[maybe_unused]] InteropCtx *ctx,
44 napi_env env, napi_value jsVal)
45 {
46 napi_valuetype valueType = GetValueType(env, jsVal);
47 napi_value result = jsVal;
48 if (valueType == napi_object && !GetValueByValueOf(env, jsVal, CONSTRUCTOR_NAME_NUMBER, &result)) {
49 JSConvertNumeric::TypeCheckFailed();
50 return {};
51 }
52 if (UNLIKELY(GetValueType(env, result) != napi_number)) {
53 JSConvertNumeric::TypeCheckFailed();
54 return {};
55 }
56 Cpptype etsVal;
57 if constexpr (sizeof(Cpptype) >= sizeof(int32_t)) {
58 int64_t val;
59 NAPI_CHECK_FATAL(napi_get_value_int64(env, result, &val));
60 etsVal = static_cast<Cpptype>(val);
61 } else if constexpr (std::is_signed_v<Cpptype>) {
62 int32_t val;
63 NAPI_CHECK_FATAL(napi_get_value_int32(env, result, &val));
64 etsVal = static_cast<Cpptype>(val);
65 } else {
66 uint32_t val;
67 NAPI_CHECK_FATAL(napi_get_value_uint32(env, result, &val));
68 etsVal = static_cast<Cpptype>(val);
69 }
70 return etsVal;
71 }
72
73 template <typename P = Cpptype>
WrapImplJSConvertNumeric74 static std::enable_if_t<std::is_floating_point_v<P>, napi_value> WrapImpl(napi_env env, Cpptype etsVal)
75 {
76 napi_value jsVal;
77 NAPI_CHECK_FATAL(napi_create_double(env, etsVal, &jsVal));
78 return jsVal;
79 }
80
81 template <typename P = Cpptype>
UnwrapImplJSConvertNumeric82 static std::enable_if_t<std::is_floating_point_v<P>, std::optional<Cpptype>> UnwrapImpl(
83 [[maybe_unused]] InteropCtx *ctx, napi_env env, napi_value jsVal)
84 {
85 napi_valuetype valueType = GetValueType(env, jsVal);
86 napi_value result = jsVal;
87 if (valueType == napi_object && !GetValueByValueOf(env, jsVal, CONSTRUCTOR_NAME_NUMBER, &result)) {
88 JSConvertNumeric::TypeCheckFailed();
89 return {};
90 }
91 if (UNLIKELY(GetValueType(env, result) != napi_number)) {
92 JSConvertNumeric::TypeCheckFailed();
93 return {};
94 }
95 double val;
96 NAPI_CHECK_FATAL(napi_get_value_double(env, result, &val));
97 return val;
98 }
99 };
100
101 using JSConvertI8 = JSConvertNumeric<int8_t>;
102 using JSConvertU8 = JSConvertNumeric<uint8_t>;
103 using JSConvertI16 = JSConvertNumeric<int16_t>;
104 using JSConvertI32 = JSConvertNumeric<int32_t>;
105 using JSConvertU32 = JSConvertNumeric<uint32_t>;
106 using JSConvertI64 = JSConvertNumeric<int64_t>;
107 using JSConvertU64 = JSConvertNumeric<uint64_t>;
108 using JSConvertF32 = JSConvertNumeric<float>;
109 using JSConvertF64 = JSConvertNumeric<double>;
110
111 JSCONVERT_DEFINE_TYPE(U1, bool);
JSCONVERT_WRAP(U1)112 JSCONVERT_WRAP(U1)
113 {
114 napi_value jsVal;
115 NAPI_CHECK_FATAL(napi_get_boolean(env, static_cast<bool>(etsVal), &jsVal));
116 return jsVal;
117 }
JSCONVERT_UNWRAP(U1)118 JSCONVERT_UNWRAP(U1)
119 {
120 if (IsUndefined(env, jsVal)) {
121 TypeCheckFailed();
122 return {};
123 }
124 auto objVal = JSConvertStdlibBoolean::UnwrapWithNullCheck(ctx, env, jsVal);
125 if (!objVal || (objVal.has_value() && objVal.value() == nullptr)) {
126 return {};
127 }
128 if (objVal.has_value()) {
129 return EtsBoxPrimitive<EtsBoolean>::FromCoreType(objVal.value())->GetValue();
130 }
131 return {};
132 }
133
134 JSCONVERT_DEFINE_TYPE(U16, char16_t);
JSCONVERT_WRAP(U16)135 JSCONVERT_WRAP(U16)
136 {
137 napi_value jsVal;
138 NAPI_CHECK_FATAL(napi_create_string_utf16(env, &etsVal, 1, &jsVal));
139 return jsVal;
140 }
JSCONVERT_UNWRAP(U16)141 JSCONVERT_UNWRAP(U16)
142 {
143 if (IsUndefined(env, jsVal)) {
144 TypeCheckFailed();
145 return {};
146 }
147 auto objVal = JSConvertStdlibChar::UnwrapWithNullCheck(ctx, env, jsVal);
148 if (!objVal || (objVal.has_value() && objVal.value() == nullptr)) {
149 return {};
150 }
151 if (objVal.has_value()) {
152 return EtsBoxPrimitive<EtsChar>::FromCoreType(objVal.value())->GetValue();
153 }
154 return {};
155 }
156
157 JSCONVERT_DEFINE_TYPE(String, EtsString *);
JSCONVERT_WRAP(String)158 JSCONVERT_WRAP(String)
159 {
160 napi_value jsVal;
161 if (UNLIKELY(etsVal->IsUtf16())) {
162 auto str = reinterpret_cast<char16_t *>(etsVal->GetDataUtf16());
163 NAPI_CHECK_FATAL(napi_create_string_utf16(env, str, etsVal->GetUtf16Length(), &jsVal));
164 } else {
165 auto str = utf::Mutf8AsCString(etsVal->GetDataMUtf8());
166 // -1 for NULL terminated Mutf8 string!!!
167 NAPI_CHECK_FATAL(napi_create_string_utf8(env, str, etsVal->GetMUtf8Length() - 1, &jsVal));
168 }
169 return jsVal;
170 }
JSCONVERT_UNWRAP(String)171 JSCONVERT_UNWRAP(String)
172 {
173 napi_value result = jsVal;
174 napi_valuetype valueType = GetValueType(env, jsVal);
175 if (valueType == napi_object && !GetValueByValueOf(env, jsVal, CONSTRUCTOR_NAME_STRING, &result)) {
176 TypeCheckFailed();
177 return {};
178 }
179 if (UNLIKELY(GetValueType(env, result) != napi_string)) {
180 TypeCheckFailed();
181 return {};
182 }
183 std::string value = GetString(env, result);
184 return EtsString::CreateFromUtf8(value.data(), value.length());
185 }
186
187 JSCONVERT_DEFINE_TYPE(BigInt, EtsBigInt *);
JSCONVERT_WRAP(BigInt)188 JSCONVERT_WRAP(BigInt)
189 {
190 auto size = etsVal->GetBytes()->GetLength();
191 auto data = etsVal->GetBytes();
192
193 std::vector<uint32_t> etsArray;
194 etsArray.reserve(size);
195 for (size_t i = 0; i < size; ++i) {
196 etsArray.emplace_back(static_cast<uint32_t>(data->Get(i)));
197 }
198
199 SmallVector<uint64_t, 4U> jsArray;
200 if (size > 0) {
201 jsArray = ConvertBigIntArrayFromEtsToJs(etsArray);
202 } else {
203 jsArray = {0};
204 }
205
206 napi_value jsVal;
207 int sign = etsVal->GetSign() < 0 ? 1 : 0;
208 NAPI_CHECK_FATAL(napi_create_bigint_words(env, sign, jsArray.size(), jsArray.data(), &jsVal));
209
210 return jsVal;
211 }
JSCONVERT_UNWRAP(BigInt)212 JSCONVERT_UNWRAP(BigInt)
213 {
214 if (UNLIKELY(GetValueType(env, jsVal) != napi_bigint)) {
215 TypeCheckFailed();
216 return {};
217 }
218
219 auto [words, signBit] = GetBigInt(env, jsVal);
220 std::vector<EtsInt> array = ConvertBigIntArrayFromJsToEts(words);
221
222 auto *etsIntArray = EtsIntArray::Create(array.size());
223 ASSERT(etsIntArray != nullptr);
224 for (uint32_t i = 0; i < array.size(); ++i) {
225 etsIntArray->Set(i, array[i]);
226 }
227
228 auto bigintKlass = EtsClass::FromRuntimeClass(ctx->GetBigIntClass());
229 auto bigInt = EtsBigInt::FromEtsObject(EtsObject::Create(bigintKlass));
230 bigInt->SetFieldObject(EtsBigInt::GetBytesOffset(), reinterpret_cast<EtsObject *>(etsIntArray));
231 bigInt->SetFieldPrimitive(EtsBigInt::GetSignOffset(), array.empty() ? 0 : signBit == 0 ? 1 : -1);
232
233 return bigInt;
234 }
235
236 JSCONVERT_DEFINE_TYPE(JSValue, JSValue *);
JSCONVERT_WRAP(JSValue)237 JSCONVERT_WRAP(JSValue)
238 {
239 return etsVal->GetNapiValue(env);
240 }
JSCONVERT_UNWRAP(JSValue)241 JSCONVERT_UNWRAP(JSValue)
242 {
243 return JSValue::Create(EtsCoroutine::GetCurrent(), ctx, jsVal);
244 }
245
246 JSCONVERT_DEFINE_TYPE(EtsObject, EtsObject *);
JSCONVERT_WRAP(EtsObject)247 JSCONVERT_WRAP(EtsObject)
248 {
249 InteropFatal("Wrap of EtsObject should be done with relevant converter");
250 UNREACHABLE();
251 }
252
JSCONVERT_UNWRAP(EtsObject)253 JSCONVERT_UNWRAP(EtsObject)
254 {
255 auto objectConverter = ctx->GetEtsClassWrappersCache()->Lookup(PlatformTypes()->coreObject);
256 return objectConverter->Unwrap(ctx, jsVal);
257 }
258
259 // ESError convertors are supposed to box JSValue objects, do not treat them in any other way
260 JSCONVERT_DEFINE_TYPE(ESError, EtsObject *);
JSCONVERT_WRAP(ESError)261 JSCONVERT_WRAP(ESError)
262 {
263 auto coro = EtsCoroutine::GetCurrent();
264 auto ctx = InteropCtx::Current(coro);
265 if (ctx == nullptr) {
266 ThrowNoInteropContextException();
267 return {};
268 }
269
270 auto klass = etsVal->GetClass();
271 INTEROP_FATAL_IF(klass->GetRuntimeClass() != ctx->GetESErrorClass());
272
273 auto method = klass->GetInstanceMethod("getJsError", nullptr);
274 ASSERT(method != nullptr);
275 std::array args = {Value(etsVal->GetCoreType())};
276 auto val = JSValue::FromCoreType(method->GetPandaMethod()->Invoke(coro, args.data()).GetAs<ObjectHeader *>());
277 INTEROP_FATAL_IF(val == nullptr);
278 return val->GetNapiValue(env);
279 }
JSCONVERT_UNWRAP(ESError)280 JSCONVERT_UNWRAP(ESError)
281 {
282 auto coro = EtsCoroutine::GetCurrent();
283 JSValue *value = nullptr;
284 value = JSValue::Create(coro, ctx, jsVal);
285 if (UNLIKELY(value == nullptr)) {
286 return {};
287 }
288 auto res = ctx->CreateETSCoreESError(coro, value);
289 if (UNLIKELY(res == nullptr)) {
290 return {};
291 }
292 return res;
293 }
294
295 JSCONVERT_DEFINE_TYPE(Promise, EtsPromise *);
296
JSCONVERT_WRAP(Promise)297 JSCONVERT_WRAP(Promise)
298 {
299 auto *coro = EtsCoroutine::GetCurrent();
300 auto ctx = InteropCtx::Current(coro);
301 if (ctx == nullptr) {
302 ThrowNoInteropContextException();
303 return {};
304 }
305 ets_proxy::SharedReferenceStorage *storage = ctx->GetSharedRefStorage();
306 // SharedReferenceStorage uses object's MarkWord to store interop hash.
307 // Also runtime may lock a Promise object (this operation also requires MarkWord modification).
308 // Interop hash and locks cannot co-exist. That is why a separate object (EtsPromiseRef) is used for
309 // mapping between js and ets Promise objects.
310 // When a ets Promise object goes to JS we should get the corresponding EtsPromiseRef object.
311 // So the ets Promise object should know about EtsPromiseRef which is stored in 'interopObject' field.
312 EtsObject *interopObj = etsVal->GetInteropObject(coro);
313 if (interopObj != nullptr && storage->HasReference(interopObj, env)) {
314 return storage->GetJsObject(interopObj, env);
315 }
316
317 [[maybe_unused]] EtsHandleScope s(coro);
318 EtsHandle<EtsPromise> hpromise(coro, etsVal);
319 ASSERT(hpromise.GetPtr() != nullptr);
320 napi_deferred deferred;
321 napi_value jsPromise;
322 NAPI_CHECK_FATAL(napi_create_promise(env, &deferred, &jsPromise));
323
324 hpromise->Lock();
325 // NOTE(alimovilya, #23064) This if should be removed. Only else branch should remain.
326 if (!hpromise->IsPending() && !hpromise->IsLinked()) { // it will never get PENDING again
327 EtsHandle<EtsObject> value(coro, hpromise->GetValue(coro));
328 napi_value completionValue;
329 if (value.GetPtr() == nullptr) {
330 completionValue = GetUndefined(env);
331 } else {
332 auto refconv = JSRefConvertResolve(ctx, value->GetClass()->GetRuntimeClass());
333 completionValue = refconv->Wrap(ctx, value.GetPtr());
334 }
335 if (hpromise->IsResolved()) {
336 NAPI_CHECK_FATAL(napi_resolve_deferred(env, deferred, completionValue));
337 } else {
338 NAPI_CHECK_FATAL(napi_reject_deferred(env, deferred, completionValue));
339 }
340 } else {
341 // connect->Invoke calls EtsPromiseSubmitCallback that acquires the mutex and checks the state again
342 hpromise->Unlock();
343 ASSERT_MANAGED_CODE();
344 Method *connect = ctx->GetPromiseInteropConnectMethod();
345 std::array<Value, 2U> args = {Value(hpromise.GetPtr()), Value(reinterpret_cast<int64_t>(deferred))};
346 connect->Invoke(coro, args.data());
347 hpromise->Lock();
348 }
349 EtsPromiseRef *ref = EtsPromiseRef::Create(coro);
350 ref->SetTarget(coro, hpromise->AsObject());
351 hpromise->SetInteropObject(coro, ref);
352 [[maybe_unused]] auto *sharedRef = storage->CreateETSObjectRef(ctx, ref, jsPromise);
353 ASSERT(sharedRef != nullptr);
354 hpromise->Unlock();
355 return jsPromise;
356 }
357
JSCONVERT_UNWRAP(Promise)358 JSCONVERT_UNWRAP(Promise)
359 {
360 #ifndef NDEBUG
361 bool isPromise = false;
362 NAPI_CHECK_FATAL(napi_is_promise(env, jsVal, &isPromise));
363 ASSERT(isPromise);
364 #endif
365 auto coro = EtsCoroutine::GetCurrent();
366 ets_proxy::SharedReferenceStorage *storage = ctx->GetSharedRefStorage();
367 ets_proxy::SharedReference *sharedRef = storage->GetReference(env, jsVal);
368 if (sharedRef != nullptr) {
369 auto *ref = reinterpret_cast<EtsPromiseRef *>(sharedRef->GetEtsObject());
370 ASSERT(ref->GetTarget(coro) != nullptr);
371 return EtsPromise::FromEtsObject(ref->GetTarget(coro));
372 }
373
374 [[maybe_unused]] EtsHandleScope s(coro);
375 auto *promise = EtsPromise::Create(coro);
376 EtsHandle<EtsPromise> hpromise(coro, promise);
377 ASSERT(hpromise.GetPtr() != nullptr);
378 EtsHandle<EtsPromiseRef> href(coro, EtsPromiseRef::Create(coro));
379 href->SetTarget(coro, hpromise->AsObject());
380 hpromise->SetInteropObject(coro, href.GetPtr());
381 storage->CreateJSObjectRef(ctx, href.GetPtr(), jsVal);
382 hpromise->SetLinkedPromise(coro, href->AsObject());
383 EtsPromise::CreateLink(hpromise->GetLinkedPromise(coro), hpromise.GetPtr());
384 return hpromise.GetPtr();
385 }
386
387 JSCONVERT_DEFINE_TYPE(EtsNull, EtsObject *);
JSCONVERT_WRAP(EtsNull)388 JSCONVERT_WRAP(EtsNull)
389 {
390 return GetNull(env);
391 }
392
JSCONVERT_UNWRAP(EtsNull)393 JSCONVERT_UNWRAP(EtsNull)
394 {
395 if (UNLIKELY(!IsNull(env, jsVal))) {
396 TypeCheckFailed();
397 return {};
398 }
399 return ctx->GetNullValue();
400 }
401
402 #undef JSCONVERT_DEFINE_TYPE
403 #undef JSCONVERT_WRAP
404 #undef JSCONVERT_UNWRAP
405
406 template <typename T>
JSValueGetByName(InteropCtx * ctx,JSValue * jsvalue,const char * name)407 static ALWAYS_INLINE inline std::optional<typename T::cpptype> JSValueGetByName(InteropCtx *ctx, JSValue *jsvalue,
408 const char *name)
409 {
410 auto env = ctx->GetJSEnv();
411 napi_value jsVal = jsvalue->GetNapiValue(env);
412 napi_status jsStatus;
413 auto coro = EtsCoroutine::GetCurrent();
414 {
415 ScopedNativeCodeThread nativeScope(coro);
416 jsStatus = napi_get_named_property(env, jsVal, name, &jsVal);
417 }
418 if (jsStatus != napi_ok) {
419 return {};
420 }
421 return T::UnwrapWithNullCheck(ctx, env, jsVal);
422 }
423
424 template <typename T>
JSValueSetByName(InteropCtx * ctx,JSValue * jsvalue,const char * name,typename T::cpptype etsPropVal)425 ALWAYS_INLINE inline void JSValueSetByName(InteropCtx *ctx, JSValue *jsvalue, const char *name,
426 typename T::cpptype etsPropVal)
427 {
428 auto env = ctx->GetJSEnv();
429 napi_value jsVal = jsvalue->GetNapiValue(env);
430 napi_value jsPropVal = T::WrapWithNullCheck(env, etsPropVal);
431 if (UNLIKELY(jsPropVal == nullptr)) {
432 return;
433 }
434 auto coro = EtsCoroutine::GetCurrent();
435 napi_status jsStatus;
436 {
437 ScopedNativeCodeThread nativeScope(coro);
438 jsStatus = napi_set_named_property(env, jsVal, name, jsPropVal);
439 }
440 if (jsStatus != napi_ok) {
441 ctx->ForwardJSException(coro);
442 }
443 }
444
445 } // namespace ark::ets::interop::js
446
447 #endif // !PANDA_PLUGINS_ETS_RUNTIME_INTEROP_JS_JS_CONVERT_H
448