feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,127 @@
cmake_minimum_required(VERSION 3.4.1)
project(expo-modules-core)
set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_CXX_STANDARD 20)
set(PACKAGE_NAME "expo-modules-core")
set(BUILD_DIR ${CMAKE_SOURCE_DIR}/build)
string(APPEND CMAKE_CXX_FLAGS " -DREACT_NATIVE_TARGET_VERSION=${REACT_NATIVE_TARGET_VERSION}")
if (${NATIVE_DEBUG})
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g")
endif ()
set(SRC_DIR ${CMAKE_SOURCE_DIR}/src)
set(COMMON_DIR ${CMAKE_SOURCE_DIR}/../common/cpp)
file(GLOB sources_android "${SRC_DIR}/main/cpp/*.cpp")
file(GLOB sources_android_types "${SRC_DIR}/main/cpp/types/*.cpp")
file(GLOB sources_android_javaclasses "${SRC_DIR}/main/cpp/javaclasses/*.cpp")
file(GLOB common_sources "${COMMON_DIR}/*.cpp")
# shared
macro(createVarAsBoolToInt name value)
if(${value})
set(${name} "1")
else()
set(${name} "0")
endif()
endmacro()
add_library(CommonSettings INTERFACE)
add_library(
${PACKAGE_NAME}
SHARED
${common_sources}
${sources_android}
${sources_android_types}
${sources_android_javaclasses}
)
if(IS_NEW_ARCHITECTURE_ENABLED)
add_subdirectory("${SRC_DIR}/fabric")
set(NEW_ARCHITECTURE_DEPENDENCIES "fabric")
set(NEW_ARCHITECTURE_COMPILE_OPTIONS -DIS_NEW_ARCHITECTURE_ENABLED=1 -DRN_FABRIC_ENABLED=1)
else()
set(NEW_ARCHITECTURE_DEPENDENCIES "")
set(NEW_ARCHITECTURE_COMPILE_OPTIONS "")
endif()
createVarAsBoolToInt("USE_HERMES_INT" ${USE_HERMES})
createVarAsBoolToInt("UNIT_TEST_INT" ${UNIT_TEST})
target_compile_options(CommonSettings INTERFACE
-O2
-frtti
-fexceptions
-Wall
-fstack-protector-all
-DUSE_HERMES=${USE_HERMES_INT}
-DUNIT_TEST=${UNIT_TEST_INT}
${NEW_ARCHITECTURE_COMPILE_OPTIONS}
)
# tests
if(${UNIT_TEST})
if(${USE_HERMES})
find_package(hermes-engine REQUIRED CONFIG)
set(JSEXECUTOR_LIB hermes-engine::libhermes)
else()
set(JSEXECUTOR_LIB ReactAndroid::jscexecutor)
endif()
else()
set(JSEXECUTOR_LIB "")
endif()
# find libraries
find_library(LOG_LIB log)
find_package(ReactAndroid REQUIRED CONFIG)
find_package(fbjni REQUIRED CONFIG)
# includes
get_target_property(INCLUDE_reactnativejni
ReactAndroid::reactnativejni
INTERFACE_INCLUDE_DIRECTORIES)
target_include_directories(
${PACKAGE_NAME}
PRIVATE
${INCLUDE_reactnativejni}/react
# header only imports from turbomodule, e.g. CallInvokerHolder.h
"${REACT_NATIVE_DIR}/ReactAndroid/src/main/jni/react/turbomodule"
"${COMMON_DIR}"
"${SRC_DIR}/fabric"
)
# linking
include("${REACT_NATIVE_DIR}/ReactAndroid/cmake-utils/folly-flags.cmake")
target_compile_options(
${PACKAGE_NAME}
PRIVATE
${folly_FLAGS}
)
target_link_libraries(
${PACKAGE_NAME}
CommonSettings
${LOG_LIB}
fbjni::fbjni
ReactAndroid::jsi
ReactAndroid::reactnativejni
ReactAndroid::folly_runtime
ReactAndroid::react_nativemodule_core
android
${JSEXECUTOR_LIB}
${NEW_ARCHITECTURE_DEPENDENCIES}
)

View File

@@ -0,0 +1,109 @@
class KotlinExpoModulesCorePlugin implements Plugin<Project> {
void apply(Project project) {
// For compatibility reasons the plugin needs to declare that it provides common build.gradle
// options for the modules
project.rootProject.ext.expoProvidesDefaultConfig = {
true
}
project.ext.safeExtGet = { prop, fallback ->
project.rootProject.ext.has(prop) ? project.rootProject.ext.get(prop) : fallback
}
project.buildscript {
project.ext.kotlinVersion = {
project.rootProject.ext.has("kotlinVersion")
? project.rootProject.ext.get("kotlinVersion")
: "1.9.23"
}
project.ext.kspVersion = {
def kspVersionsMap = [
"1.6.10": "1.6.10-1.0.4",
"1.6.21": "1.6.21-1.0.6",
"1.7.22": "1.7.22-1.0.8",
"1.8.0": "1.8.0-1.0.9",
"1.8.10": "1.8.10-1.0.9",
"1.8.22": "1.8.22-1.0.11",
"1.9.23": "1.9.23-1.0.20"
]
project.rootProject.ext.has("kspVersion")
? project.rootProject.ext.get("kspVersion")
: kspVersionsMap.containsKey(project.ext.kotlinVersion())
? kspVersionsMap.get(project.ext.kotlinVersion())
: "1.9.23-1.0.20"
}
}
}
}
ext.applyKotlinExpoModulesCorePlugin = {
try {
// Tries to apply the kotlin-android plugin if the client project does not apply yet.
// On previous `applyKotlinExpoModulesCorePlugin`, it is inside the `project.buildscript` block.
// We cannot use `project.plugins.hasPlugin()` yet but only to try-catch instead.
apply plugin: 'kotlin-android'
} catch (e) {}
apply plugin: KotlinExpoModulesCorePlugin
}
// Setup build options that are common for all modules
ext.useDefaultAndroidSdkVersions = {
project.android {
compileSdkVersion project.ext.safeExtGet("compileSdkVersion", 34)
defaultConfig {
minSdkVersion project.ext.safeExtGet("minSdkVersion", 23)
targetSdkVersion project.ext.safeExtGet("targetSdkVersion", 34)
}
lintOptions {
abortOnError false
}
}
}
ext.useExpoPublishing = {
if (!project.plugins.hasPlugin('maven-publish')) {
apply plugin: 'maven-publish'
}
project.android {
publishing {
singleVariant("release") {
withSourcesJar()
}
}
}
project.afterEvaluate {
publishing {
publications {
release(MavenPublication) {
from components.release
}
}
repositories {
maven {
url = mavenLocal().url
}
}
}
}
}
ext.useCoreDependencies = {
dependencies {
// Avoids cyclic dependencies
if (!project.project.name.startsWith("expo-modules-core")) {
implementation project.project(':expo-modules-core')
}
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${project.ext.kotlinVersion()}"
}
}
ext.boolish = { value ->
return value.toString().toBoolean()
}

View File

@@ -0,0 +1,191 @@
apply plugin: 'com.android.library'
group = 'host.exp.exponent'
version = '1.12.26'
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useDefaultAndroidSdkVersions()
useExpoPublishing()
def isExpoModulesCoreTests = {
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
if (tskReqStr =~ /:expo-modules-core:connected\w*AndroidTest/) {
def androidTests = project.file("src/androidTest")
return androidTests.exists() && androidTests.isDirectory()
}
return false
}.call()
def REACT_NATIVE_BUILD_FROM_SOURCE = findProject(":packages:react-native:ReactAndroid") != null
def REACT_NATIVE_DIR = REACT_NATIVE_BUILD_FROM_SOURCE
? findProject(":packages:react-native:ReactAndroid").getProjectDir().parent
: file(providers.exec {
workingDir(rootDir)
commandLine("node", "--print", "require.resolve('react-native/package.json')")
}.standardOutput.asText.get().trim()).parent
def reactProperties = new Properties()
file("$REACT_NATIVE_DIR/ReactAndroid/gradle.properties").withInputStream { reactProperties.load(it) }
def REACT_NATIVE_VERSION = reactProperties.getProperty("VERSION_NAME")
def REACT_NATIVE_TARGET_VERSION = REACT_NATIVE_VERSION.split("\\.")[1].toInteger()
def reactNativeArchitectures() {
def value = project.getProperties().get("reactNativeArchitectures")
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}
// HERMES
def USE_HERMES = true
if (findProject(":app")) {
def appProject = project(":app")
USE_HERMES = appProject?.hermesEnabled?.toBoolean() || appProject?.ext?.react?.enableHermes?.toBoolean()
}
// Currently the needs for hermes/jsc are only for androidTest, so we turn on this flag only when `isExpoModulesCoreTests` is true
USE_HERMES = USE_HERMES && isExpoModulesCoreTests
// END HERMES
def isNewArchitectureEnabled = findProperty("newArchEnabled") == "true"
android {
if (rootProject.hasProperty("ndkPath")) {
ndkPath rootProject.ext.ndkPath
}
if (rootProject.hasProperty("ndkVersion")) {
ndkVersion rootProject.ext.ndkVersion
}
namespace "expo.modules"
defaultConfig {
consumerProguardFiles 'proguard-rules.pro'
versionCode 1
versionName "1.12.26"
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled.toString()
testInstrumentationRunner "expo.modules.TestRunner"
externalNativeBuild {
cmake {
abiFilters (*reactNativeArchitectures())
arguments "-DANDROID_STL=c++_shared",
"-DREACT_NATIVE_DIR=${REACT_NATIVE_DIR}",
"-DREACT_NATIVE_TARGET_VERSION=${REACT_NATIVE_TARGET_VERSION}",
"-DUSE_HERMES=${USE_HERMES}",
"-DIS_NEW_ARCHITECTURE_ENABLED=${isNewArchitectureEnabled}",
"-DUNIT_TEST=${isExpoModulesCoreTests}"
}
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
buildFeatures {
buildConfig true
prefab true
}
packagingOptions {
// Gradle will add cmake target dependencies into packaging.
// Theses files are intermediated linking files to build modules-core and should not be in final package.
def sharedLibraries = [
"**/libc++_shared.so",
"**/libfabricjni.so",
"**/libfbjni.so",
"**/libfolly_json.so",
"**/libfolly_runtime.so",
"**/libglog.so",
"**/libhermes.so",
"**/libjscexecutor.so",
"**/libjsi.so",
"**/libreactnativejni.so",
"**/libreact_debug.so",
"**/libreact_nativemodule_core.so",
"**/libreact_render_debug.so",
"**/libreact_render_graphics.so",
"**/libreact_render_core.so",
"**/libreact_render_componentregistry.so",
"**/libreact_render_mapbuffer.so",
"**/librrc_view.so",
"**/libruntimeexecutor.so",
"**/libyoga.so",
]
// Required or mockk will crash
resources {
excludes += [
"META-INF/LICENSE.md",
"META-INF/LICENSE-notice.md"
]
}
// In android (instrumental) tests, we want to package all so files to enable our JSI functionality.
// Otherwise, those files should be excluded, because will be loaded by the application.
if (isExpoModulesCoreTests) {
pickFirsts += sharedLibraries
} else {
excludes += sharedLibraries
}
}
testOptions {
unitTests.includeAndroidResources = true
unitTests.all { test ->
testLogging {
outputs.upToDateWhen { false }
events "passed", "failed", "skipped", "standardError"
showCauses true
showExceptions true
showStandardStreams true
}
}
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinVersion()}"
implementation "org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion()}"
implementation 'androidx.annotation:annotation:1.7.1'
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
api "androidx.core:core-ktx:1.12.0"
implementation("androidx.tracing:tracing-ktx:1.2.0")
implementation 'com.facebook.react:react-android'
compileOnly 'com.facebook.fbjni:fbjni:0.5.1'
testImplementation 'androidx.test:core:1.5.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'io.mockk:mockk:1.13.10'
testImplementation "com.google.truth:truth:1.1.2"
testImplementation "org.robolectric:robolectric:4.11.1"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0"
testImplementation "org.json:json:20230227"
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:core:1.5.0'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation "io.mockk:mockk-android:1.13.10"
androidTestImplementation "com.google.truth:truth:1.1.2"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0"
if (isExpoModulesCoreTests) {
if (USE_HERMES) {
compileOnly "com.facebook.react:hermes-android"
} else {
compileOnly "org.webkit:android-jsc:+"
}
}
}

View File

@@ -0,0 +1,31 @@
-keep @expo.modules.core.interfaces.DoNotStrip class *
-keepclassmembers class * {
@expo.modules.core.interfaces.DoNotStrip *;
}
-keep class * implements expo.modules.kotlin.records.Record {
*;
}
-keep class * extends expo.modules.kotlin.sharedobjects.SharedRef
-keep enum * implements expo.modules.kotlin.types.Enumerable {
*;
}
-keepnames class kotlin.Pair
-keep,allowoptimization,allowobfuscation class * extends expo.modules.kotlin.modules.Module {
public <init>();
public expo.modules.kotlin.modules.ModuleDefinitionData definition();
}
-keepclassmembers class * implements expo.modules.kotlin.views.ExpoView {
public <init>(android.content.Context);
public <init>(android.content.Context, expo.modules.kotlin.AppContext);
}
-keepclassmembers class * {
expo.modules.kotlin.viewevent.ViewEventCallback *;
}
-keepclassmembers class * {
expo.modules.kotlin.viewevent.ViewEventDelegate *;
}

View File

@@ -0,0 +1,50 @@
# Copyright 2018-present 650 Industries. All rights reserved.
set(COMMON_FABRIC_DIR ${COMMON_DIR}/fabric)
file(GLOB SOURCES "*.cpp")
file(GLOB COMMON_FABRIC_SOURCES "${COMMON_FABRIC_DIR}/*.cpp")
add_library(fabric STATIC
${COMMON_FABRIC_SOURCES}
${SOURCES}
)
include("${REACT_NATIVE_DIR}/ReactAndroid/cmake-utils/folly-flags.cmake")
target_compile_options(fabric PRIVATE
"-std=c++20"
${folly_FLAGS}
)
find_package(ReactAndroid REQUIRED CONFIG)
find_package(fbjni REQUIRED CONFIG)
get_target_property(INCLUDE_fabricjni
ReactAndroid::fabricjni
INTERFACE_INCLUDE_DIRECTORIES)
target_include_directories(fabric PRIVATE
"${REACT_NATIVE_DIR}/ReactCommon"
"${COMMON_FABRIC_DIR}"
"${INCLUDE_fabricjni}/react/fabric"
)
target_link_libraries(fabric
CommonSettings
fbjni::fbjni
ReactAndroid::fabricjni
ReactAndroid::folly_runtime
ReactAndroid::glog
ReactAndroid::jsi
ReactAndroid::react_debug
ReactAndroid::react_render_componentregistry
ReactAndroid::react_render_core
ReactAndroid::react_render_debug
ReactAndroid::react_render_graphics
ReactAndroid::react_render_mapbuffer
ReactAndroid::react_utils
ReactAndroid::rrc_view
ReactAndroid::runtimeexecutor
ReactAndroid::yoga
)

View File

@@ -0,0 +1,47 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#include "FabricComponentsRegistry.h"
#include <react/renderer/componentregistry/ComponentDescriptorProvider.h>
#include <CoreComponentsRegistry.h>
#include "ExpoViewComponentDescriptor.h"
namespace jni = facebook::jni;
namespace react = facebook::react;
namespace expo {
// static
void FabricComponentsRegistry::registerNatives() {
registerHybrid({
makeNativeMethod("initHybrid", FabricComponentsRegistry::initHybrid),
makeNativeMethod("registerComponentsRegistry", FabricComponentsRegistry::registerComponentsRegistry),
});
}
// static
jni::local_ref<FabricComponentsRegistry::jhybriddata>
FabricComponentsRegistry::initHybrid(jni::alias_ref<jhybridobject> jThis) {
return makeCxxInstance();
}
void FabricComponentsRegistry::registerComponentsRegistry(
jni::alias_ref<jni::JArrayClass<jni::JString>> componentNames) {
// Inject the component to the CoreComponentsRegistry because we don't want to touch the MainApplicationReactNativeHost
auto providerRegistry = react::CoreComponentsRegistry::sharedProviderRegistry();
size_t size = componentNames->size();
for (size_t i = 0; i < size; ++i) {
auto flavor = std::make_shared<std::string const>(componentNames->getElement(i)->toStdString());
auto componentName = react::ComponentName{flavor->c_str()};
providerRegistry->add(react::ComponentDescriptorProvider {
reinterpret_cast<react::ComponentHandle>(componentName),
componentName,
flavor,
&facebook::react::concreteComponentDescriptorConstructor<expo::ExpoViewComponentDescriptor>
});
}
}
} // namespace expo

View File

@@ -0,0 +1,25 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#pragma once
#include <fbjni/fbjni.h>
namespace expo {
class FabricComponentsRegistry : public facebook::jni::HybridClass<FabricComponentsRegistry> {
public:
static auto constexpr
kJavaDescriptor = "Lexpo/modules/adapters/react/FabricComponentsRegistry;";
static void registerNatives();
FabricComponentsRegistry() {};
void registerComponentsRegistry(
facebook::jni::alias_ref<facebook::jni::JArrayClass<facebook::jni::JString>> componentNames);
private:
static facebook::jni::local_ref<jhybriddata> initHybrid(facebook::jni::alias_ref<jhybridobject> jThis);
};
} // namespace expo

View File

@@ -0,0 +1,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<meta-data
android:name="org.unimodules.core.AppLoader#react-native-headless"
android:value="expo.modules.adapters.react.apploader.RNHeadlessAppLoader" />
<meta-data
android:name="com.facebook.soloader.enabled"
android:value="true"
tools:replace="android:value" />
</application>
</manifest>

View File

@@ -0,0 +1,122 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#include "Exceptions.h"
#include "JSIContext.h"
#include "JSReferencesCache.h"
namespace jni = facebook::jni;
namespace expo {
jni::local_ref<CodedException> CodedException::create(const std::string &message) {
return CodedException::newInstance(jni::make_jstring(message));
}
std::string CodedException::getCode() {
const auto getCode = this->getClass()->getMethod<jni::JString()>("getCode");
const auto code = getCode(this->self());
return code->toStdString();
}
std::optional<std::string> CodedException::getLocalizedMessage() {
const auto getLocalizedMessage = this->getClass()
->getMethod<jni::JString()>("getLocalizedMessage");
const auto message = getLocalizedMessage(this->self());
if (message != nullptr) {
return message->toStdString();
}
return std::nullopt;
}
jni::local_ref<JavaScriptEvaluateException> JavaScriptEvaluateException::create(
const std::string &message,
const std::string &jsStack
) {
return JavaScriptEvaluateException::newInstance(
jni::make_jstring(message),
jni::make_jstring(jsStack)
);
}
jni::local_ref<UnexpectedException> UnexpectedException::create(const std::string &message) {
return UnexpectedException::newInstance(
jni::make_jstring(message)
);
}
jni::local_ref<InvalidArgsNumberException> InvalidArgsNumberException::create(int received, int expected) {
return InvalidArgsNumberException::newInstance(
received,
expected,
expected // number of required arguments
);
}
jsi::Value makeCodedError(
jsi::Runtime &rt,
jsi::String code,
jsi::String message
) {
auto codedErrorConstructor = rt
.global()
.getProperty(rt, "ExpoModulesCore_CodedError")
.asObject(rt)
.asFunction(rt);
return codedErrorConstructor.callAsConstructor(
rt, {
jsi::Value(rt, code),
jsi::Value(rt, message)
}
);
}
void rethrowAsCodedError(
jsi::Runtime &rt,
jni::JniException &jniException
) {
jni::local_ref<jni::JThrowable> unboxedThrowable = jniException.getThrowable();
if (unboxedThrowable->isInstanceOf(CodedException::javaClassLocal())) {
auto codedException = jni::static_ref_cast<CodedException>(unboxedThrowable);
auto code = codedException->getCode();
auto message = codedException->getLocalizedMessage();
auto codedError = makeCodedError(
rt,
jsi::String::createFromUtf8(rt, code),
jsi::String::createFromUtf8(rt, message.value_or(""))
);
throw jsi::JSError(
message.value_or(""),
rt,
std::move(codedError)
);
}
// Rethrow error if we can't wrap it.
throw;
}
void throwPendingJniExceptionAsCppException() {
JNIEnv* env = jni::Environment::current();
if (env->ExceptionCheck() == JNI_FALSE) {
return;
}
auto throwable = env->ExceptionOccurred();
if (!throwable) {
throw std::runtime_error("Unable to get pending JNI exception.");
}
env->ExceptionClear();
throw jni::JniException(jni::adopt_local(throwable));
}
void throwNewJavaException(jthrowable throwable) {
throw jni::JniException(jni::wrap_alias(throwable));
}
} // namespace expo

View File

@@ -0,0 +1,98 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include <fbjni/fbjni.h>
#include <jsi/jsi.h>
#include <optional>
namespace jni = facebook::jni;
namespace jsi = facebook::jsi;
namespace expo {
class JSIContext;
/**
* A convenient wrapper for the Kotlin CodedException.
* It can be used with the `jni::throwNewJavaException` function to throw a cpp exception that
* will be automatically changed to the corresponding Java/Kotlin exception.
* `jni::throwNewJavaException` creates and throws a C++ exception which wraps a Java exception,
* so the C++ flow is interrupted. Then, when translatePendingCppExceptionToJavaException
* is called at the topmost level of the native stack, the wrapped Java exception is thrown to the java caller.
*/
class CodedException : public jni::JavaClass<CodedException, jni::JThrowable> {
public:
static auto constexpr kJavaDescriptor = "Lexpo/modules/kotlin/exception/CodedException;";
static jni::local_ref<CodedException> create(const std::string &message);
std::string getCode();
std::optional<std::string> getLocalizedMessage();
};
/**
* A convenient wrapper for the Kotlin JavaScriptEvaluateException.
*/
class JavaScriptEvaluateException
: public jni::JavaClass<JavaScriptEvaluateException, CodedException> {
public:
static auto constexpr kJavaDescriptor = "Lexpo/modules/kotlin/exception/JavaScriptEvaluateException;";
static jni::local_ref<JavaScriptEvaluateException> create(
const std::string &message,
const std::string &jsStack
);
};
/**
* A convenient wrapper for the Kotlin UnexpectedException.
*/
class UnexpectedException
: public jni::JavaClass<UnexpectedException, CodedException> {
public:
static auto constexpr kJavaDescriptor = "Lexpo/modules/kotlin/exception/UnexpectedException;";
static jni::local_ref<UnexpectedException> create(
const std::string &message
);
};
class InvalidArgsNumberException
: public jni::JavaClass<InvalidArgsNumberException, CodedException> {
public:
static auto constexpr kJavaDescriptor = "Lexpo/modules/kotlin/exception/InvalidArgsNumberException;";
static jni::local_ref<InvalidArgsNumberException> create(
int received,
int expected
);
};
/**
* Tries to rethrow an jni::JniException as a js version of the CodedException
*/
[[noreturn]] void rethrowAsCodedError(
jsi::Runtime &rt,
jni::JniException &jniException
);
jsi::Value makeCodedError(
jsi::Runtime &runtime,
jsi::String code,
jsi::String message
);
/**
* fbjni@0.2.2 is built by ndk r21, its exceptions are not catchable by expo-modules-core built by ndk r23+.
* To catch these excetptions, we copy the `facebook::jni::throwPendingJniExceptionAsCppException` here and throw exceptions on our own.
*/
void throwPendingJniExceptionAsCppException();
/**
* Same as `facebook::jni::throwNewJavaException` but throwing exceptions on our own.
*/
[[noreturn]] void throwNewJavaException(jthrowable throwable);
} // namespace expo

View File

@@ -0,0 +1,82 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#include "ExpoModulesHostObject.h"
#include "LazyObject.h"
#include <folly/dynamic.h>
#include <jsi/JSIDynamic.h>
#include <react/bridging/LongLivedObject.h>
namespace jsi = facebook::jsi;
namespace expo {
ExpoModulesHostObject::ExpoModulesHostObject(JSIContext *installer)
: installer(installer) {}
/**
* Clears jsi references held by JSRegistry and JavaScriptRuntime.
*/
ExpoModulesHostObject::~ExpoModulesHostObject() {
#if REACT_NATIVE_TARGET_VERSION >= 75
auto &runtime = installer->runtimeHolder->get();
facebook::react::LongLivedObjectCollection::get(runtime).clear();
#else
facebook::react::LongLivedObjectCollection::get().clear();
#endif
installer->prepareForDeallocation();
}
jsi::Value ExpoModulesHostObject::get(jsi::Runtime &runtime, const jsi::PropNameID &name) {
auto cName = name.utf8(runtime);
if (!installer->hasModule(cName)) {
modulesCache.erase(cName);
return jsi::Value::undefined();
}
if (UniqueJSIObject &cachedObject = modulesCache[cName]) {
return jsi::Value(runtime, *cachedObject);
}
// Create a lazy object for the specific module. It defers initialization of the final module object.
LazyObject::Shared moduleLazyObject = std::make_shared<LazyObject>(
[this, cName](jsi::Runtime &rt) {
// Check if the installer has been deallocated.
// If so, return nullptr to avoid a "field operation on NULL object" crash.
// As it's probably the best we can do in this case.
if (installer->wasDeallocated()) {
return std::shared_ptr<jsi::Object>(nullptr);
}
auto module = installer->getModule(cName);
return module->cthis()->getJSIObject(rt);
});
// Save the module's lazy host object for later use.
modulesCache[cName] = std::make_unique<jsi::Object>(
jsi::Object::createFromHostObject(runtime, moduleLazyObject));
return jsi::Value(runtime, *modulesCache[cName]);
}
void ExpoModulesHostObject::set(jsi::Runtime &runtime, const jsi::PropNameID &name,
const jsi::Value &value) {
throw jsi::JSError(
runtime,
"RuntimeError: Cannot override the host object for expo module '" + name.utf8(runtime) + "'"
);
}
std::vector<jsi::PropNameID> ExpoModulesHostObject::getPropertyNames(jsi::Runtime &rt) {
auto names = installer->getModulesName();
size_t size = names->size();
std::vector<jsi::PropNameID> result;
result.reserve(size);
for (int i = 0; i < size; i++) {
result.push_back(
jsi::PropNameID::forUtf8(rt, names->getElement(i)->toStdString())
);
}
return result;
}
} // namespace expo

View File

@@ -0,0 +1,39 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include "JSIContext.h"
#include <jsi/jsi.h>
#include <vector>
#import <unordered_map>
namespace jsi = facebook::jsi;
namespace expo {
using UniqueJSIObject = std::unique_ptr<jsi::Object>;
/**
* An entry point to all exported functionalities like modules.
*
* An instance of this class will be added to the JS global object.
*/
class ExpoModulesHostObject : public jsi::HostObject {
public:
ExpoModulesHostObject(JSIContext *installer);
~ExpoModulesHostObject() override;
jsi::Value get(jsi::Runtime &, const jsi::PropNameID &name) override;
void set(jsi::Runtime &, const jsi::PropNameID &name, const jsi::Value &value) override;
std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime &rt) override;
private:
JSIContext *installer;
std::unordered_map<std::string, UniqueJSIObject> modulesCache;
};
} // namespace expo

View File

@@ -0,0 +1,17 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#include "JNIDeallocator.h"
namespace expo {
void JNIDeallocator::addReference(
jni::local_ref<Destructible::javaobject> jniObject
) {
const static auto method = JNIDeallocator::javaClassLocal()
->getMethod<void(jni::local_ref<Destructible>)>(
"addReference"
);
method(self(), std::move(jniObject));
}
} // namespace expo

View File

@@ -0,0 +1,25 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include <fbjni/fbjni.h>
namespace jni = facebook::jni;
namespace expo {
class Destructible : public jni::JavaClass<Destructible> {
public:
static auto constexpr kJavaDescriptor = "Lexpo/modules/kotlin/jni/Destructible;";
};
class JNIDeallocator : public jni::JavaClass<JNIDeallocator> {
public:
static auto constexpr kJavaDescriptor = "Lexpo/modules/kotlin/jni/JNIDeallocator;";
void addReference(
jni::local_ref<Destructible::javaobject> jniObject
);
};
} // namespace expo

View File

@@ -0,0 +1,51 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#include "JNIFunctionBody.h"
#include "Exceptions.h"
#include "JavaReferencesCache.h"
namespace jni = facebook::jni;
namespace react = facebook::react;
namespace expo {
jni::local_ref<jni::JObject>
JNIFunctionBody::invoke(jobjectArray args) {
// Do NOT use getClass here!
// Method obtained from `getClass` will point to the overridden version of the method.
// Because of that, it can't be cached - we will try to invoke the nonexistent method
// if we receive an object of a different class than the one used to obtain the method id.
// The only cacheable method id can be obtain from the base class.
static const auto method = jni::findClassLocal("expo/modules/kotlin/jni/JNIFunctionBody")
->getMethod<jni::local_ref<jni::JObject>(jobjectArray)>(
"invoke",
"([Ljava/lang/Object;)Ljava/lang/Object;"
);
auto result = jni::Environment::current()->CallObjectMethod(this->self(), method.getId(), args);
throwPendingJniExceptionAsCppException();
return jni::adopt_local(static_cast<jni::JniType<jni::JObject>>(result));
}
void JNIAsyncFunctionBody::invoke(
jobjectArray args,
jobject promise
) {
// Do NOT use getClass here!
// Method obtained from `getClass` will point to the overridden version of the method.
// Because of that, it can't be cached - we will try to invoke the nonexistent method
// if we receive an object of a different class than the one used to obtain the method id.
// The only cacheable method id can be obtain from the base class.
static const auto method = jni::findClassLocal("expo/modules/kotlin/jni/JNIAsyncFunctionBody")
->getMethod<
void(jobjectArray , jobject)
>(
"invoke",
"([Ljava/lang/Object;Lexpo/modules/kotlin/jni/PromiseImpl;)V"
);
jni::Environment::current()->CallVoidMethod(this->self(), method.getId(), args, promise);
throwPendingJniExceptionAsCppException();
}
} // namespace expo

View File

@@ -0,0 +1,50 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include <fbjni/fbjni.h>
#include <react/jni/ReadableNativeArray.h>
namespace jni = facebook::jni;
namespace react = facebook::react;
namespace expo {
/**
* A CPP part of the expo.modules.kotlin.jni.JNIFunctionBody class.
* It represents the Kotlin's promise-less function.
*/
class JNIFunctionBody : public jni::JavaClass<JNIFunctionBody> {
public:
static auto constexpr kJavaDescriptor = "Lexpo/modules/kotlin/jni/JNIFunctionBody;";
/**
* Invokes a Kotlin's implementation of this function.
*
* @param args
* @return result of the Kotlin function
*/
jni::local_ref<jni::JObject> invoke(
jobjectArray args
);
};
/**
* A CPP part of the expo.modules.kotlin.jni.JNIAsyncFunctionBody class.
* It represents the Kotlin's promise function.
*/
class JNIAsyncFunctionBody : public jni::JavaClass<JNIAsyncFunctionBody> {
public:
static auto constexpr kJavaDescriptor = "Lexpo/modules/kotlin/jni/JNIAsyncFunctionBody;";
/**
* Invokes a Kotlin's implementation of this async function.
*
* @param args
* @param promise that will be resolve or rejected in the Kotlin's implementation
*/
void invoke(
jobjectArray args,
jobject promise
);
};
} // namespace expo

View File

@@ -0,0 +1,40 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#include "JSIContext.h"
#include "JavaScriptModuleObject.h"
#include "JavaScriptValue.h"
#include "JavaScriptObject.h"
#include "JavaScriptWeakObject.h"
#include "JavaScriptFunction.h"
#include "JavaScriptTypedArray.h"
#include "JavaReferencesCache.h"
#include "JavaCallback.h"
#include "types/FrontendConverterProvider.h"
#if RN_FABRIC_ENABLED
#include "FabricComponentsRegistry.h"
#endif
#include <jni.h>
#include <fbjni/fbjni.h>
// Install all jni bindings
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
return facebook::jni::initialize(vm, [] {
// Loads references to often use Java classes
expo::JavaReferencesCache::instance()->loadJClasses(jni::Environment::current());
expo::FrontendConverterProvider::instance()->createConverters();
expo::JSIContext::registerNatives();
expo::JavaScriptModuleObject::registerNatives();
expo::JavaScriptValue::registerNatives();
expo::JavaScriptObject::registerNatives();
expo::JavaScriptWeakObject::registerNatives();
expo::JavaScriptFunction::registerNatives();
expo::JavaScriptTypedArray::registerNatives();
expo::JavaCallback::registerNatives();
#if RN_FABRIC_ENABLED
expo::FabricComponentsRegistry::registerNatives();
#endif
});
}

View File

@@ -0,0 +1,369 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#include "JSIContext.h"
#include "ExpoModulesHostObject.h"
#include "JavaReferencesCache.h"
#include "JSReferencesCache.h"
#include "SharedObject.h"
#include "NativeModule.h"
#include <fbjni/detail/Meta.h>
#include <fbjni/fbjni.h>
#include <memory>
#if IS_NEW_ARCHITECTURE_ENABLED
#include "BridgelessJSCallInvoker.h"
#endif
namespace jni = facebook::jni;
namespace jsi = facebook::jsi;
namespace expo {
#if IS_NEW_ARCHITECTURE_ENABLED
#include "BridgelessJSCallInvoker.h"
#endif
jni::local_ref<JSIContext::jhybriddata>
JSIContext::initHybrid(jni::alias_ref<jhybridobject> jThis) {
return makeCxxInstance(jThis);
}
void JSIContext::registerNatives() {
registerHybrid({
makeNativeMethod("initHybrid", JSIContext::initHybrid),
makeNativeMethod("installJSI", JSIContext::installJSI),
#if IS_NEW_ARCHITECTURE_ENABLED
makeNativeMethod("installJSIForBridgeless",
JSIContext::installJSIForBridgeless),
#endif
makeNativeMethod("installJSIForTests",
JSIContext::installJSIForTests),
makeNativeMethod("evaluateScript", JSIContext::evaluateScript),
makeNativeMethod("global", JSIContext::global),
makeNativeMethod("createObject", JSIContext::createObject),
makeNativeMethod("drainJSEventLoop", JSIContext::drainJSEventLoop),
makeNativeMethod("setNativeStateForSharedObject",
JSIContext::jniSetNativeStateForSharedObject),
});
}
JSIContext::JSIContext(jni::alias_ref<jhybridobject> jThis)
: javaPart_(jni::make_global(jThis)),
threadSafeJThis(std::make_shared<ThreadSafeJNIGlobalRef<JSIContext::javaobject>>(
jni::Environment::current()->NewGlobalRef(javaPart_.get())
)) {}
JSIContext::~JSIContext() {
if (runtimeHolder) {
unbindJSIContext(runtimeHolder->get());
// The runtime would be deallocated automatically.
// However, we need to enforce the order of deallocations.
// The runtime has to be deallocated before the JNI part.
runtimeHolder.reset();
}
}
void JSIContext::installJSI(
jlong jsRuntimePointer,
jni::alias_ref<JNIDeallocator::javaobject> jniDeallocator,
jni::alias_ref<react::CallInvokerHolder::javaobject> jsInvokerHolder
) {
prepareJSIContext(
jsRuntimePointer,
jniDeallocator,
jsInvokerHolder->cthis()->getCallInvoker()
);
prepareRuntime();
}
#if IS_NEW_ARCHITECTURE_ENABLED
void JSIContext::installJSIForBridgeless(
jlong jsRuntimePointer,
jni::alias_ref<JNIDeallocator::javaobject> jniDeallocator,
jni::alias_ref<react::JRuntimeExecutor::javaobject> runtimeExecutor
) {
prepareJSIContext(
jsRuntimePointer,
jniDeallocator,
std::make_shared<BridgelessJSCallInvoker>(runtimeExecutor->cthis()->get())
);
prepareRuntime();
}
#endif
void JSIContext::installJSIForTests(
jni::alias_ref<JNIDeallocator::javaobject> jniDeallocator
) {
#if !UNIT_TEST
throw std::logic_error("The function is only available when UNIT_TEST is defined.");
#else
this->jniDeallocator = jni::make_global(jniDeallocator);
runtimeHolder = std::make_shared<JavaScriptRuntime>();
jsi::Runtime &jsiRuntime = runtimeHolder->get();
jsRegistry = std::make_unique<JSReferencesCache>(jsiRuntime);
prepareRuntime();
#endif // !UNIT_TEST
}
void JSIContext::prepareJSIContext(
jlong jsRuntimePointer,
jni::alias_ref<JNIDeallocator::javaobject> jniDeallocator,
std::shared_ptr<react::CallInvoker> callInvoker
) {
this->jniDeallocator = jni::make_global(jniDeallocator);
auto runtime = reinterpret_cast<jsi::Runtime *>(jsRuntimePointer);
jsRegistry = std::make_unique<JSReferencesCache>(*runtime);
runtimeHolder = std::make_shared<JavaScriptRuntime>(
runtime,
std::move(callInvoker)
);
}
void JSIContext::prepareRuntime() {
jsi::Runtime &runtime = runtimeHolder->get();
bindJSIContext(runtime, this);
runtimeHolder->installMainObject();
EventEmitter::installClass(runtime);
auto threadSafeRef = std::make_shared<ThreadSafeJNIGlobalRef<JSIContext::javaobject>>(
jni::Environment::current()->NewGlobalRef(javaPart_.get())
);
SharedObject::installBaseClass(
runtime,
// We can't predict the order of deallocation of the JSIContext and the SharedObject.
// So we need to pass a new ref to retain the JSIContext to make sure it's not deallocated before the SharedObject.
[threadSafeRef = std::move(threadSafeRef)](const SharedObject::ObjectId objectId) {
threadSafeRef->use([objectId](jni::alias_ref<JSIContext::javaobject> globalRef) {
JSIContext::deleteSharedObject(globalRef, objectId);
});
}
);
NativeModule::installClass(runtime);
auto expoModules = std::make_shared<ExpoModulesHostObject>(this);
auto expoModulesObject = jsi::Object::createFromHostObject(
runtime,
expoModules
);
// Define the `global.expo.modules` object.
runtimeHolder
->getMainObject()
->setProperty(
runtime,
"modules",
expoModulesObject
);
}
jni::local_ref<JavaScriptModuleObject::javaobject>
JSIContext::callGetJavaScriptModuleObjectMethod(const std::string &moduleName) const {
if (javaPart_ == nullptr) {
throw std::runtime_error(
"getJavaScriptModuleObject: JSIContext was prepared to be deallocated.");
}
const static auto method = expo::JSIContext::javaClassLocal()
->getMethod<jni::local_ref<JavaScriptModuleObject::javaobject>(
std::string)>(
"getJavaScriptModuleObject"
);
return method(javaPart_, moduleName);
}
jni::local_ref<JavaScriptModuleObject::javaobject>
JSIContext::callGetCoreModuleObject() const {
if (javaPart_ == nullptr) {
throw std::runtime_error("getCoreModule: JSIContext was prepared to be deallocated.");
}
const static auto method = expo::JSIContext::javaClassLocal()
->getMethod<jni::local_ref<JavaScriptModuleObject::javaobject>()>(
"getCoreModuleObject"
);
return method(javaPart_);
}
jni::local_ref<jni::JArrayClass<jni::JString>>
JSIContext::callGetJavaScriptModulesNames() const {
if (javaPart_ == nullptr) {
throw std::runtime_error("getJavaScriptModules: JSIContext was prepared to be deallocated.");
}
const static auto method = expo::JSIContext::javaClassLocal()
->getMethod<jni::local_ref<jni::JArrayClass<jni::JString>>()>(
"getJavaScriptModulesName"
);
return method(javaPart_);
}
bool JSIContext::callHasModule(const std::string &moduleName) const {
if (javaPart_ == nullptr) {
throw std::runtime_error("hasModule: JSIContext was prepared to be deallocated.");
}
const static auto method = expo::JSIContext::javaClassLocal()
->getMethod<jboolean(std::string)>(
"hasModule"
);
return (bool) method(javaPart_, moduleName);
}
jni::local_ref<JavaScriptModuleObject::javaobject>
JSIContext::getModule(const std::string &moduleName) const {
return callGetJavaScriptModuleObjectMethod(moduleName);
}
jni::local_ref<JavaScriptModuleObject::javaobject> JSIContext::getCoreModule() const {
return callGetCoreModuleObject();
}
bool JSIContext::hasModule(const std::string &moduleName) const {
return callHasModule(moduleName);
}
jni::local_ref<jni::JArrayClass<jni::JString>> JSIContext::getModulesName() const {
return callGetJavaScriptModulesNames();
}
jni::local_ref<JavaScriptValue::javaobject> JSIContext::evaluateScript(
jni::JString script
) {
return runtimeHolder->evaluateScript(script.toStdString());
}
jni::local_ref<JavaScriptObject::javaobject> JSIContext::global() {
return runtimeHolder->global();
}
jni::local_ref<JavaScriptObject::javaobject> JSIContext::createObject() {
return runtimeHolder->createObject();
}
void JSIContext::drainJSEventLoop() {
runtimeHolder->drainJSEventLoop();
}
void JSIContext::registerSharedObject(
jni::local_ref<jobject> native,
jni::local_ref<JavaScriptObject::javaobject> js
) {
if (javaPart_ == nullptr) {
throw std::runtime_error("registerSharedObject: JSIContext was prepared to be deallocated.");
}
const static auto method = expo::JSIContext::javaClassLocal()
->getMethod<void(jni::local_ref<jobject>, jni::local_ref<JavaScriptObject::javaobject>)>(
"registerSharedObject"
);
method(javaPart_, std::move(native), std::move(js));
}
void JSIContext::deleteSharedObject(
jni::alias_ref<JSIContext::javaobject> javaObject,
int objectId
) {
if (javaObject == nullptr) {
throw std::runtime_error("deleteSharedObject: JSIContext is invalid.");
}
const static auto method = expo::JSIContext::javaClassLocal()
->getMethod<void(int)>(
"deleteSharedObject"
);
method(javaObject, objectId);
}
void JSIContext::registerClass(
jni::local_ref<jclass> native,
jni::local_ref<JavaScriptObject::javaobject> jsClass
) {
if (javaPart_ == nullptr) {
throw std::runtime_error("registerClass: JSIContext was prepared to be deallocated.");
}
const static auto method = expo::JSIContext::javaClassLocal()
->getMethod<void(jni::local_ref<jclass>, jni::local_ref<JavaScriptObject::javaobject>)>(
"registerClass"
);
method(javaPart_, std::move(native), std::move(jsClass));
}
jni::local_ref<JavaScriptObject::javaobject> JSIContext::getJavascriptClass(
jni::local_ref<jclass> native
) {
if (javaPart_ == nullptr) {
throw std::runtime_error("getJavascriptClass: JSIContext was prepared to be deallocated.");
}
const static auto method = expo::JSIContext::javaClassLocal()
->getMethod<jni::local_ref<JavaScriptObject::javaobject>(jni::local_ref<jclass>)>(
"getJavascriptClass"
);
return method(javaPart_, std::move(native));
}
void JSIContext::prepareForDeallocation() {
jsRegistry.reset();
runtimeHolder.reset();
jniDeallocator.reset();
javaPart_.reset();
wasDeallocated_ = true;
}
void JSIContext::jniSetNativeStateForSharedObject(
int id,
jni::alias_ref<JavaScriptObject::javaobject> jsObject
) {
auto nativeState = std::make_shared<expo::SharedObject::NativeState>(
id,
// We can't predict the order of deallocation of the JSIContext and the SharedObject.
// So we need to pass a new ref to retain the JSIContext to make sure it's not deallocated before the SharedObject.
[threadSafeRef = threadSafeJThis](const SharedObject::ObjectId objectId) {
threadSafeRef->use([objectId](jni::alias_ref<JSIContext::javaobject> globalRef) {
JSIContext::deleteSharedObject(globalRef, objectId);
});
}
);
jsObject
->cthis()
->get()
->setNativeState(runtimeHolder->get(), std::move(nativeState));
}
bool JSIContext::wasDeallocated() const {
return wasDeallocated_;
}
thread_local std::unordered_map<uintptr_t, JSIContext *> jsiContexts;
void bindJSIContext(const jsi::Runtime &runtime, JSIContext *jsiContext) {
jsiContexts[reinterpret_cast<uintptr_t>(&runtime)] = jsiContext;
}
void unbindJSIContext(const jsi::Runtime &runtime) {
jsiContexts.erase(reinterpret_cast<uintptr_t>(&runtime));
}
} // namespace expo

View File

@@ -0,0 +1,227 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include "JavaScriptRuntime.h"
#include "JavaScriptModuleObject.h"
#include "JavaScriptValue.h"
#include "JavaScriptObject.h"
#include "JavaReferencesCache.h"
#include "JSReferencesCache.h"
#include "JNIDeallocator.h"
#include "ThreadSafeJNIGlobalRef.h"
#include <fbjni/fbjni.h>
#include <jsi/jsi.h>
#include <ReactCommon/CallInvokerHolder.h>
#include <ReactCommon/CallInvoker.h>
#if IS_NEW_ARCHITECTURE_ENABLED
#include <ReactCommon/RuntimeExecutor.h>
#include <react/jni/JRuntimeExecutor.h>
#endif
#if REACT_NATIVE_TARGET_VERSION >= 73
#include <ReactCommon/NativeMethodCallInvokerHolder.h>
#endif
#include <memory>
namespace jni = facebook::jni;
namespace jsi = facebook::jsi;
namespace react = facebook::react;
namespace expo {
/**
* A JNI wrapper to initialize CPP part of modules and access all data from the module registry.
*/
class JSIContext : public jni::HybridClass<JSIContext> {
public:
static auto constexpr kJavaDescriptor = "Lexpo/modules/kotlin/jni/JSIContext;";
static auto constexpr TAG = "JSIContext";
static jni::local_ref<jhybriddata> initHybrid(jni::alias_ref<jhybridobject> jThis);
static void registerNatives();
/**
* Initializes the `ExpoModulesHostObject` and adds it to the global object.
*/
void installJSI(
jlong jsRuntimePointer,
jni::alias_ref<JNIDeallocator::javaobject> jniDeallocator,
jni::alias_ref<react::CallInvokerHolder::javaobject> jsInvokerHolder
);
#if IS_NEW_ARCHITECTURE_ENABLED
/**
* Initializes the `ExpoModulesHostObject` and adds it to the global object.
*/
void installJSIForBridgeless(
jlong jsRuntimePointer,
jni::alias_ref<JNIDeallocator::javaobject> jniDeallocator,
jni::alias_ref<react::JRuntimeExecutor::javaobject> runtimeExecutor
);
#endif
/**
* Initializes the test runtime. Shouldn't be used in the production.
*/
void installJSIForTests(
jni::alias_ref<JNIDeallocator::javaobject> jniDeallocator
);
/**
* Gets a module for a given name. It will throw an exception if the module doesn't exist.
*
* @param moduleName
* @return An instance of `JavaScriptModuleObject`
*/
jni::local_ref<JavaScriptModuleObject::javaobject> getModule(const std::string &moduleName) const;
bool hasModule(const std::string &moduleName) const;
/**
* Gets names of all available modules.
*/
jni::local_ref<jni::JArrayClass<jni::JString>> getModulesName() const;
/**
* Exposes a `JavaScriptRuntime::evaluateScript` function to Kotlin
*/
jni::local_ref<JavaScriptValue::javaobject> evaluateScript(jni::JString script);
/**
* Exposes a `JavaScriptRuntime::global` function to Kotlin
*/
jni::local_ref<JavaScriptObject::javaobject> global();
/**
* Exposes a `JavaScriptRuntime::createObject` function to Kotlin
*/
jni::local_ref<JavaScriptObject::javaobject> createObject();
/**
* Gets a core module.
*/
jni::local_ref<JavaScriptModuleObject::javaobject> getCoreModule() const;
/**
* Adds a shared object to the internal registry
* @param native part of the shared object
* @param js part of the shared object
*/
void registerSharedObject(
jni::local_ref<jobject> native,
jni::local_ref<JavaScriptObject::javaobject> js
);
static void deleteSharedObject(
jni::alias_ref<JSIContext::javaobject> javaObject,
int objectId
);
/**
* Exposes a `JavaScriptRuntime::drainJSEventLoop` function to Kotlin
*/
void drainJSEventLoop();
std::shared_ptr<JavaScriptRuntime> runtimeHolder;
std::unique_ptr<JSReferencesCache> jsRegistry;
jni::global_ref<JNIDeallocator::javaobject> jniDeallocator;
void registerClass(jni::local_ref<jclass> native,
jni::local_ref<JavaScriptObject::javaobject> jsClass);
jni::local_ref<JavaScriptObject::javaobject> getJavascriptClass(jni::local_ref<jclass> native);
~JSIContext();
void prepareForDeallocation();
bool wasDeallocated() const;
private:
friend HybridBase;
/*
* We store two global references to the Java part of the JSIContext.
* However, one is wrapped in additional abstraction to make it thread-safe,
* which increase the access time. For most operations, we should use the bare reference.
* Only for operations that are executed on different threads that aren't attached to JVM,
* we should use the thread-safe reference.
*/
jni::global_ref<JSIContext::javaobject> javaPart_;
std::shared_ptr<ThreadSafeJNIGlobalRef<JSIContext::javaobject>> threadSafeJThis;
bool wasDeallocated_ = false;
explicit JSIContext(jni::alias_ref<jhybridobject> jThis);
inline jni::local_ref<JavaScriptModuleObject::javaobject>
callGetJavaScriptModuleObjectMethod(const std::string &moduleName) const;
inline jni::local_ref<jni::JArrayClass<jni::JString>> callGetJavaScriptModulesNames() const;
inline jni::local_ref<JavaScriptModuleObject::javaobject> callGetCoreModuleObject() const;
inline bool callHasModule(const std::string &moduleName) const;
void prepareJSIContext(
jlong jsRuntimePointer,
jni::alias_ref<JNIDeallocator::javaobject> jniDeallocator,
std::shared_ptr<react::CallInvoker> callInvoker
);
void prepareRuntime();
void jniSetNativeStateForSharedObject(
int id,
jni::alias_ref<JavaScriptObject::javaobject> jsObject
);
};
/**
* We are binding the JSIContext to the runtime using a thread-local map.
* This is a simplification of how we're accessing the JSIContext from different places.
* We're using a thread-local map to prevent from accessing the wrong JSIContext from a different thread.
* It's much safer than passing around the JSIContext as a parameter.
*/
extern thread_local std::unordered_map<uintptr_t, JSIContext *> jsiContexts;
/**
* Binds the JSIContext to the runtime.
* @param runtime
* @param jsiContext
*/
void bindJSIContext(const jsi::Runtime &runtime, JSIContext *jsiContext);
/**
* Unbinds the JSIContext from the runtime.
* @param runtime
*/
void unbindJSIContext(const jsi::Runtime &runtime);
/**
* Gets the JSIContext for the given runtime.
* @param runtime
* @return JSIContext * - it should never be stored when received from this function.
* It might throw an exception if the JSIContext for the given runtime doesn't exist.
*/
inline JSIContext *getJSIContext(const jsi::Runtime &runtime) {
const auto iterator = jsiContexts.find(reinterpret_cast<uintptr_t>(&runtime));
if (iterator == jsiContexts.end()) {
throw std::invalid_argument("JSIContext for the given runtime doesn't exist");
}
return iterator->second;
}
} // namespace expo

View File

@@ -0,0 +1,44 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include <jsi/jsi.h>
#include <memory>
namespace jsi = facebook::jsi;
namespace expo {
/**
* An interface for classes which wrap `jsi::Object`.
*/
class JSIObjectWrapper {
public:
/**
* @return a pointer to the underlying `jsi::Object`.
*/
virtual std::shared_ptr<jsi::Object> get() = 0;
};
/**
* An interface for classes which wrap `jsi::Value`.
*/
class JSIValueWrapper {
public:
/**
* @return a pointer to the underlying `jsi::Value`.
*/
virtual std::shared_ptr<jsi::Value> get() = 0;
};
/**
* An interface for classes which wrap `jsi::Function`.
*/
class JSIFunctionWrapper {
public:
/**
* @return a pointer to the underlying `jsi::Function`.
*/
virtual std::shared_ptr<jsi::Function> get() = 0;
};
} // namespace expo

View File

@@ -0,0 +1,85 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include "JSIObjectWrapper.h"
#include <fbjni/fbjni.h>
#include <jsi/jsi.h>
#include <type_traits>
namespace jni = facebook::jni;
namespace jsi = facebook::jsi;
namespace expo {
/**
* A base template of the jni to jsi types converter.
* To make this conversion as fast and easy as possible we used the type trait technic.
*/
template<class T, typename = void>
struct jsi_type_converter {
static const bool isDefined = false;
};
/**
* Conversion from jni::alias_ref<T::javaobject> to jsi::Value where T extends JSIValueWrapper, JSIObjectWrapper or JSIFunctionWrapper.
*/
template<class T>
struct jsi_type_converter<
jni::alias_ref<T>,
std::enable_if_t<
// jni::ReprType<T>::HybridType>::value if T looks like `R::javaobject`, it will return R
std::is_base_of<JSIValueWrapper, typename jni::ReprType<T>::HybridType>::value ||
std::is_base_of<JSIObjectWrapper, typename jni::ReprType<T>::HybridType>::value ||
std::is_base_of<JSIValueWrapper, typename jni::ReprType<T>::HybridType>::value
>
> {
static const bool isDefined = true;
inline static jsi::Value convert(
jsi::Runtime &runtime,
jni::alias_ref<T> &value) {
if (value == nullptr) {
return jsi::Value::undefined();
}
return jsi::Value(runtime, *value->cthis()->get());
}
};
/**
* Conversion from primitive types from which jsi::Value can be constructed (like bool, double) to jsi::Value.
*/
template<class T>
struct jsi_type_converter<
T,
std::enable_if_t<std::is_fundamental_v<T> && std::is_constructible_v<jsi::Value, T>>
> {
static const bool isDefined = true;
inline static jsi::Value convert(jsi::Runtime &runtime, T value) {
return jsi::Value(value);
}
};
/**
* Conversion from jni::alias_ref<jstring> to jsi::Value.
*/
template<>
struct jsi_type_converter<jni::alias_ref<jstring>> {
static const bool isDefined = true;
inline static jsi::Value convert(jsi::Runtime &runtime, jni::alias_ref<jstring> &value) {
if (value == nullptr) {
return jsi::Value::undefined();
}
return jsi::Value(jsi::String::createFromUtf8(runtime, value->toStdString()));
}
};
/**
* Helper that checks if the type converter was defined for the given type.
*/
template<class T>
inline constexpr bool is_jsi_type_converter_defined = jsi_type_converter<T>::isDefined;
} // namespace expo

View File

@@ -0,0 +1,26 @@
#include "JSReferencesCache.h"
namespace expo {
JSReferencesCache::JSReferencesCache(jsi::Runtime &runtime) {
jsObjectRegistry.emplace(
JSKeys::PROMISE,
std::make_unique<jsi::Object>(
runtime.global().getPropertyAsFunction(runtime, "Promise")
)
);
}
jsi::PropNameID &JSReferencesCache::getPropNameID(
jsi::Runtime &runtime,
const std::string &name
) {
auto propName = propNameIDRegistry.find(name);
if (propName == propNameIDRegistry.end()) {
auto propNameID = std::make_unique<jsi::PropNameID>(jsi::PropNameID::forAscii(runtime, name));
auto [result, _] = propNameIDRegistry.emplace(name, std::move(propNameID));
return *result->second;
}
return *propName->second;
}
} // namespace expo

View File

@@ -0,0 +1,63 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include "JavaReferencesCache.h"
#include "Exceptions.h"
#include "JavaScriptObject.h"
#include "JavaScriptRuntime.h"
#include "JSIObjectWrapper.h"
#include "WeakRuntimeHolder.h"
#include <jsi/jsi.h>
#include <memory>
#include <unordered_map>
namespace expo {
/**
* Registry used to store references to often used JS objects like Promise.
* The object lifetime should be bound with the JS runtime.
*/
class JSReferencesCache {
public:
enum class JSKeys {
PROMISE
};
JSReferencesCache() = delete;
JSReferencesCache(jsi::Runtime &runtime);
/**
* Gets a cached object.
*/
template<class T, typename std::enable_if_t<std::is_base_of_v<jsi::Object, T>, int> = 0>
T &getObject(JSKeys key) {
return static_cast<T &>(*jsObjectRegistry.at(key));
}
/**
* Gets a cached object if present. Otherwise, returns nullptr.
*/
template<class T, typename std::enable_if_t<std::is_base_of_v<jsi::Object, T>, int> = 0>
T *getOptionalObject(JSKeys key) {
auto result = jsObjectRegistry.find(key);
if (result == jsObjectRegistry.end()) {
return nullptr;
}
jsi::Object &object = *result->second;
return &static_cast<T &>(object);
}
/**
* Gets a cached jsi::PropNameID or creates a new one for the provided string.
*/
jsi::PropNameID &getPropNameID(jsi::Runtime &runtime, const std::string &name);
private:
std::unordered_map<JSKeys, std::unique_ptr<jsi::Object>> jsObjectRegistry;
std::unordered_map<std::string, std::unique_ptr<jsi::PropNameID>> propNameIDRegistry;
};
} // namespace expo

View File

@@ -0,0 +1,327 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#include "JavaCallback.h"
#include "JSIContext.h"
#include "types/JNIToJSIConverter.h"
#include "Exceptions.h"
#include <fbjni/fbjni.h>
#include <fbjni/fbjni.h>
#include <folly/dynamic.h>
#include <functional>
namespace expo {
#if REACT_NATIVE_TARGET_VERSION >= 75
JavaCallback::CallbackContext::CallbackContext(
jsi::Runtime &rt,
std::weak_ptr<react::CallInvoker> jsCallInvokerHolder,
std::optional<jsi::Function> resolveHolder,
std::optional<jsi::Function> rejectHolder
) : react::LongLivedObject(rt),
rt(rt),
jsCallInvokerHolder(std::move(jsCallInvokerHolder)),
resolveHolder(std::move(resolveHolder)),
rejectHolder(std::move(rejectHolder)) {}
#else
JavaCallback::CallbackContext::CallbackContext(
jsi::Runtime &rt,
std::weak_ptr<react::CallInvoker> jsCallInvokerHolder,
std::optional<jsi::Function> resolveHolder,
std::optional<jsi::Function> rejectHolder
) : rt(rt),
jsCallInvokerHolder(std::move(jsCallInvokerHolder)),
resolveHolder(std::move(resolveHolder)),
rejectHolder(std::move(rejectHolder)) {}
#endif
void JavaCallback::CallbackContext::invalidate() {
resolveHolder.reset();
rejectHolder.reset();
allowRelease();
}
JavaCallback::JavaCallback(std::shared_ptr<CallbackContext> callbackContext)
: callbackContext(std::move(callbackContext)) {}
void JavaCallback::registerNatives() {
registerHybrid({
makeNativeMethod("invokeNative", JavaCallback::invoke),
makeNativeMethod("invokeNative", JavaCallback::invokeBool),
makeNativeMethod("invokeNative", JavaCallback::invokeInt),
makeNativeMethod("invokeNative", JavaCallback::invokeDouble),
makeNativeMethod("invokeNative", JavaCallback::invokeFloat),
makeNativeMethod("invokeNative", JavaCallback::invokeString),
makeNativeMethod("invokeNative", JavaCallback::invokeArray),
makeNativeMethod("invokeNative", JavaCallback::invokeMap),
makeNativeMethod("invokeNative", JavaCallback::invokeSharedRef),
makeNativeMethod("invokeNative", JavaCallback::invokeError),
});
}
jni::local_ref<JavaCallback::javaobject> JavaCallback::newInstance(
JSIContext *jsiContext,
std::shared_ptr<CallbackContext> callbackContext
) {
auto object = JavaCallback::newObjectCxxArgs(std::move(callbackContext));
jsiContext->jniDeallocator->addReference(object);
return object;
}
template<typename T>
void JavaCallback::invokeJSFunction(
ArgsConverter<T> argsConverter,
T arg
) {
const auto strongCallbackContext = this->callbackContext.lock();
// The context were deallocated before the callback was invoked.
if (strongCallbackContext == nullptr) {
return;
}
const auto jsInvoker = strongCallbackContext->jsCallInvokerHolder.lock();
// Call invoker is already released, so we cannot invoke the callback.
if (jsInvoker == nullptr) {
return;
}
jsInvoker->invokeAsync(
[
context = callbackContext,
argsConverter = std::move(argsConverter),
arg = std::move(arg)
]() -> void {
auto strongContext = context.lock();
// The context were deallocated before the callback was invoked.
if (strongContext == nullptr) {
return;
}
if (!strongContext->resolveHolder.has_value()) {
throw std::runtime_error(
"JavaCallback was already settled. Cannot invoke it again"
);
}
jsi::Function &jsFunction = strongContext->resolveHolder.value();
jsi::Runtime &rt = strongContext->rt;
argsConverter(rt, jsFunction, std::move(arg));
strongContext->invalidate();
});
}
template<class T>
void JavaCallback::invokeJSFunction(T arg) {
invokeJSFunction<T>(
[](
jsi::Runtime &rt,
jsi::Function &jsFunction,
T arg
) {
jsFunction.call(rt, {jsi::Value(rt, arg)});
},
arg
);
}
void JavaCallback::invoke() {
invokeJSFunction<nullptr_t>(
[](
jsi::Runtime &rt,
jsi::Function &jsFunction,
nullptr_t arg
) {
jsFunction.call(rt, {jsi::Value::null()});
},
nullptr
);
}
void JavaCallback::invokeBool(bool result) {
invokeJSFunction(result);
}
void JavaCallback::invokeInt(int result) {
invokeJSFunction(result);
}
void JavaCallback::invokeDouble(double result) {
invokeJSFunction(result);
}
void JavaCallback::invokeFloat(float result) {
invokeJSFunction(result);
}
void JavaCallback::invokeString(jni::alias_ref<jstring> result) {
invokeJSFunction<std::string>(
[](
jsi::Runtime &rt,
jsi::Function &jsFunction,
std::string arg
) {
std::optional<jsi::Value> extendedString = convertStringToFollyDynamicIfNeeded(
rt,
arg
);
if (extendedString.has_value()) {
const jsi::Value &jsValue = extendedString.value();
jsFunction.call(
rt,
(const jsi::Value *) &jsValue,
(size_t) 1
);
return;
}
jsFunction.call(rt, {jsi::String::createFromUtf8(rt, arg)});
},
result->toStdString()
);
}
void JavaCallback::invokeArray(jni::alias_ref<react::WritableNativeArray::javaobject> result) {
invokeJSFunction<folly::dynamic>(
[](
jsi::Runtime &rt,
jsi::Function &jsFunction,
folly::dynamic arg
) {
jsi::Value convertedArg = jsi::valueFromDynamic(rt, arg);
auto enhancedArg = decorateValueForDynamicExtension(rt, convertedArg);
if (enhancedArg) {
convertedArg = std::move(*enhancedArg);
}
jsFunction.call(
rt,
(const jsi::Value *) &convertedArg,
(size_t) 1
);
},
result->cthis()->consume()
);
}
void JavaCallback::invokeMap(jni::alias_ref<react::WritableNativeMap::javaobject> result) {
invokeJSFunction<folly::dynamic>(
[](
jsi::Runtime &rt,
jsi::Function &jsFunction,
folly::dynamic arg
) {
jsi::Value convertedArg = jsi::valueFromDynamic(rt, arg);
auto enhancedArg = decorateValueForDynamicExtension(rt, convertedArg);
if (enhancedArg) {
convertedArg = std::move(*enhancedArg);
}
jsFunction.call(
rt,
(const jsi::Value *) &convertedArg,
(size_t) 1
);
},
result->cthis()->consume()
);
}
void JavaCallback::invokeSharedRef(jni::alias_ref<SharedRef::javaobject> result) {
invokeJSFunction<jni::global_ref<SharedRef::javaobject>>(
[](
jsi::Runtime &rt,
jsi::Function &jsFunction,
jni::global_ref<SharedRef::javaobject> arg
) {
const auto jsiContext = getJSIContext(rt);
auto native = jni::make_local(arg);
auto jsClass = jsiContext->getJavascriptClass(native->getClass());
auto jsObject = jsClass
->cthis()
->get()
->asFunction(rt)
.callAsConstructor(rt)
.asObject(rt);
auto objSharedPtr = std::make_shared<jsi::Object>(std::move(jsObject));
auto jsObjectInstance = JavaScriptObject::newInstance(
jsiContext,
jsiContext->runtimeHolder,
objSharedPtr
);
jni::local_ref<JavaScriptObject::javaobject> jsRef = jni::make_local(
jsObjectInstance
);
jsiContext->registerSharedObject(native, jsRef);
auto ret = jsi::Value(rt, *objSharedPtr);
jsFunction.call(
rt,
(const jsi::Value *) &ret,
(size_t) 1
);
},
jni::make_global(result)
);
}
void JavaCallback::invokeError(jni::alias_ref<jstring> code, jni::alias_ref<jstring> errorMessage) {
const auto strongCallbackContext = this->callbackContext.lock();
// The context were deallocated before the callback was invoked.
if (strongCallbackContext == nullptr) {
return;
}
const auto jsInvoker = strongCallbackContext->jsCallInvokerHolder.lock();
// Call invoker is already released, so we cannot invoke the callback.
if (jsInvoker == nullptr) {
return;
}
jsInvoker->invokeAsync(
[
context = callbackContext,
code = code->toStdString(),
errorMessage = errorMessage->toStdString()
]() -> void {
auto strongContext = context.lock();
// The context were deallocated before the callback was invoked.
if (strongContext == nullptr) {
return;
}
if (!strongContext->rejectHolder.has_value()) {
throw std::runtime_error(
"JavaCallback was already settled. Cannot invoke it again"
);
}
jsi::Function &jsFunction = strongContext->rejectHolder.value();
jsi::Runtime &rt = strongContext->rt;
auto codedError = makeCodedError(
rt,
jsi::String::createFromUtf8(rt, code),
jsi::String::createFromUtf8(rt, errorMessage)
);
jsFunction.call(
rt,
(const jsi::Value *) &codedError,
(size_t) 1
);
strongContext->invalidate();
});
}
} // namespace expo

View File

@@ -0,0 +1,99 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include "JNIDeallocator.h"
#include <jsi/jsi.h>
#include <fbjni/fbjni.h>
#include <folly/dynamic.h>
#include <variant>
#include <react/jni/WritableNativeArray.h>
#include <react/jni/WritableNativeMap.h>
#include <fbjni/detail/CoreClasses.h>
#include <ReactCommon/CallInvoker.h>
#include <ReactCommon/LongLivedObject.h>
namespace jni = facebook::jni;
namespace react = facebook::react;
namespace jsi = facebook::jsi;
namespace expo {
struct SharedRef : public jni::JavaClass<SharedRef> {
static constexpr const char *kJavaDescriptor = "Lexpo/modules/kotlin/sharedobjects/SharedRef;";
};
class JSIContext;
class JavaCallback : public jni::HybridClass<JavaCallback, Destructible> {
public:
static auto constexpr
kJavaDescriptor = "Lexpo/modules/kotlin/jni/JavaCallback;";
static auto constexpr TAG = "JavaCallback";
class CallbackContext : public react::LongLivedObject {
public:
CallbackContext(
jsi::Runtime &rt,
std::weak_ptr<react::CallInvoker> jsCallInvokerHolder,
std::optional<jsi::Function> resolveHolder,
std::optional<jsi::Function> rejectHolder
);
jsi::Runtime &rt;
std::weak_ptr<react::CallInvoker> jsCallInvokerHolder;
std::optional<jsi::Function> resolveHolder;
std::optional<jsi::Function> rejectHolder;
void invalidate();
};
static void registerNatives();
static jni::local_ref<JavaCallback::javaobject> newInstance(
JSIContext *jsiContext,
std::shared_ptr<CallbackContext> callbackContext
);
private:
std::weak_ptr<CallbackContext> callbackContext;
friend HybridBase;
JavaCallback(std::shared_ptr<CallbackContext> callback);
void invoke();
void invokeBool(bool result);
void invokeInt(int result);
void invokeDouble(double result);
void invokeFloat(float result);
void invokeString(jni::alias_ref<jstring> result);
void invokeArray(jni::alias_ref<react::WritableNativeArray::javaobject> result);
void invokeMap(jni::alias_ref<react::WritableNativeMap::javaobject> result);
void invokeSharedRef(jni::alias_ref<SharedRef::javaobject> result);
void invokeError(jni::alias_ref<jstring> code, jni::alias_ref<jstring> errorMessage);
template<class T>
using ArgsConverter = std::function<void(jsi::Runtime &rt, jsi::Function &jsFunction, T arg)>;
template<class T>
inline void invokeJSFunction(
ArgsConverter<T> argsConverter,
T arg
);
template<class T>
inline void invokeJSFunction(T arg);
};
} // namespace expo

View File

@@ -0,0 +1,108 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#include "JavaReferencesCache.h"
#include <vector>
namespace expo {
std::shared_ptr<JavaReferencesCache> JavaReferencesCache::instance() {
static std::shared_ptr<JavaReferencesCache> singleton{new JavaReferencesCache};
return singleton;
}
void JavaReferencesCache::loadJClasses(JNIEnv *env) {
loadJClass(env, "java/lang/Double", {
{"<init>", "(D)V"}
});
loadJClass(env, "java/lang/Boolean", {
{"<init>", "(Z)V"}
});
loadJClass(env, "java/lang/Integer", {
{"<init>", "(I)V"}
});
loadJClass(env, "java/lang/Long", {
{"<init>", "(J)V"}
});
loadJClass(env, "java/lang/Float", {
{"<init>", "(F)V"}
});
loadJClass(env, "com/facebook/react/bridge/PromiseImpl", {
{"<init>", "(Lcom/facebook/react/bridge/Callback;Lcom/facebook/react/bridge/Callback;)V"}
});
loadJClass(env, "expo/modules/kotlin/jni/PromiseImpl", {
{"<init>", "(Lexpo/modules/kotlin/jni/JavaCallback;)V"}
});
loadJClass(env, "java/lang/Object", {});
loadJClass(env, "java/lang/String", {});
loadJClass(env, "expo/modules/kotlin/jni/JavaScriptObject", {});
loadJClass(env, "expo/modules/kotlin/jni/JavaScriptValue", {});
loadJClass(env, "expo/modules/kotlin/jni/JavaScriptTypedArray", {});
loadJClass(env, "com/facebook/react/bridge/ReadableNativeArray", {});
loadJClass(env, "com/facebook/react/bridge/ReadableNativeMap", {});
loadJClass(env, "com/facebook/react/bridge/WritableNativeArray", {});
loadJClass(env, "com/facebook/react/bridge/WritableNativeMap", {});
loadJClass(env, "expo/modules/kotlin/sharedobjects/SharedObject", {});
}
void JavaReferencesCache::loadJClass(
JNIEnv *env,
const std::string &name,
const std::vector<std::pair<std::string, std::string>> &methodsNames
) {
// Note this clazz variable points to a leaked global reference.
// This is appropriate for classes that are never unloaded which is any class in an Android app.
auto clazz = (jclass) env->NewGlobalRef(env->FindClass(name.c_str()));
MethodHashMap methods;
methods.reserve(methodsNames.size());
for (auto &method: methodsNames) {
methods.insert(
{method, env->GetMethodID(clazz, method.first.c_str(), method.second.c_str())}
);
}
jClassRegistry.insert(
{name, CachedJClass(clazz, std::move(methods))}
);
}
JavaReferencesCache::CachedJClass &JavaReferencesCache::getJClass(
const std::string &className
) {
return jClassRegistry.at(className);
}
JavaReferencesCache::CachedJClass &JavaReferencesCache::getOrLoadJClass(
JNIEnv *env,
const std::string &className
) {
auto result = jClassRegistry.find(className);
if (result == jClassRegistry.end()) {
loadJClass(env, className, {});
return jClassRegistry.at(className);
}
return result->second;
}
jmethodID JavaReferencesCache::CachedJClass::getMethod(
const std::string &name,
const std::string &signature
) {
return methods.at({name, signature});
}
JavaReferencesCache::CachedJClass::CachedJClass(
jclass clazz,
MethodHashMap methods
) : clazz(clazz), methods(std::move(methods)) {}
} // namespace expo

View File

@@ -0,0 +1,95 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include <fbjni/fbjni.h>
#include <memory>
#include <unordered_map>
namespace jni = facebook::jni;
namespace expo {
template <typename T>
inline void hash_combine(std::size_t& seed, const T& v)
{
std::hash<T> hasher;
// Reference from: https://github.com/boostorg/container_hash/blob/boost-1.76.0/include/boost/container_hash/hash.hpp
seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}
struct pairhash {
template <typename A, typename B>
std::size_t operator()(const std::pair<A, B>& v) const {
std::size_t seed = 0;
hash_combine(seed, v.first);
hash_combine(seed, v.second);
return seed;
}
};
using MethodHashMap = std::unordered_map<std::pair<std::string, std::string>, jmethodID, pairhash>;
/**
* Singleton registry used to store references to often used Java classes.
*/
class JavaReferencesCache {
public:
/**
* An entry in the Java class registry.
*/
class CachedJClass {
public:
CachedJClass(jclass clazz, MethodHashMap methods);
/**
* A bare reference to the class object.
*/
jclass clazz;
/**
* Returns a cached method id for provided method name and signature.
*/
jmethodID getMethod(const std::string &name, const std::string &signature);
private:
MethodHashMap methods;
};
JavaReferencesCache(JavaReferencesCache const &) = delete;
JavaReferencesCache &operator=(JavaReferencesCache const &) = delete;
/**
* Gets a singleton instance
*/
static std::shared_ptr<JavaReferencesCache> instance();
/**
* Gets a cached Java class entry.
*/
CachedJClass &getJClass(const std::string &className);
/**
* Gets a cached Java class entry or loads it to the registry.
*/
CachedJClass &getOrLoadJClass(JNIEnv *env, const std::string &className);
/**
* Loads predefined set of Java classes and stores them
*/
void loadJClasses(JNIEnv *env);
private:
JavaReferencesCache() = default;
std::unordered_map<std::string, CachedJClass> jClassRegistry;
void loadJClass(
JNIEnv *env,
const std::string &name,
const std::vector<std::pair<std::string, std::string>> &methods
);
};
} // namespace expo

View File

@@ -0,0 +1,83 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#include "JavaScriptFunction.h"
#include "types/JNIToJSIConverter.h"
#include "types/AnyType.h"
#include "JavaScriptObject.h"
namespace expo {
void JavaScriptFunction::registerNatives() {
registerHybrid({
makeNativeMethod("invoke", JavaScriptFunction::invoke),
});
}
JavaScriptFunction::JavaScriptFunction(
std::weak_ptr<JavaScriptRuntime> runtime,
std::shared_ptr<jsi::Function> jsFunction
) : runtimeHolder(std::move(runtime)), jsFunction(std::move(jsFunction)) {
runtimeHolder.ensureRuntimeIsValid();
}
JavaScriptFunction::JavaScriptFunction(
WeakRuntimeHolder runtime,
std::shared_ptr<jsi::Function> jsFunction
) : runtimeHolder(std::move(runtime)), jsFunction(std::move(jsFunction)) {
runtimeHolder.ensureRuntimeIsValid();
}
std::shared_ptr<jsi::Function> JavaScriptFunction::get() {
return jsFunction;
}
jobject JavaScriptFunction::invoke(
jni::alias_ref<JavaScriptObject::javaobject> jsThis,
jni::alias_ref<jni::JArrayClass<jni::JObject>> args,
jni::alias_ref<ExpectedType::javaobject> expectedReturnType
) {
auto &rt = runtimeHolder.getJSRuntime();
JNIEnv *env = jni::Environment::current();
size_t size = args->size();
std::vector<jsi::Value> convertedArgs;
convertedArgs.reserve(size);
for (size_t i = 0; i < size; i++) {
jni::local_ref<jni::JObject> arg = args->getElement(i);
convertedArgs.push_back(convert(env, rt, std::move(arg)));
}
// TODO(@lukmccall): add better error handling
jsi::Value result = jsThis == nullptr ?
jsFunction->call(
rt,
(const jsi::Value *) convertedArgs.data(),
size
) :
jsFunction->callWithThis(
rt,
*(jsThis->cthis()->get()),
(const jsi::Value *) convertedArgs.data(),
size
);
auto converter = AnyType(jni::make_local(expectedReturnType)).converter;
auto convertedResult = converter->convert(rt, env, result);
return convertedResult;
}
jni::local_ref<JavaScriptFunction::javaobject> JavaScriptFunction::newInstance(
JSIContext *jsiContext,
std::weak_ptr<JavaScriptRuntime> runtime,
std::shared_ptr<jsi::Function> jsFunction
) {
auto function = JavaScriptFunction::newObjectCxxArgs(
std::move(runtime),
std::move(jsFunction)
);
jsiContext->jniDeallocator->addReference(function);
return function;
}
} // namespace expo

View File

@@ -0,0 +1,63 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include "JSIObjectWrapper.h"
#include "JavaScriptRuntime.h"
#include "WeakRuntimeHolder.h"
#include "types/ExpectedType.h"
#include <fbjni/fbjni.h>
#include <jsi/jsi.h>
namespace jni = facebook::jni;
namespace jsi = facebook::jsi;
namespace expo {
class JavaScriptObject;
/**
* Represents any JavaScript function. Its purpose is to expose the `jsi::Function` API back to Kotlin.
*/
class JavaScriptFunction : public jni::HybridClass<JavaScriptFunction, Destructible>, JSIFunctionWrapper {
public:
static auto constexpr
kJavaDescriptor = "Lexpo/modules/kotlin/jni/JavaScriptFunction;";
static auto constexpr TAG = "JavaScriptFunction";
static void registerNatives();
static jni::local_ref<JavaScriptFunction::javaobject> newInstance(
JSIContext *jsiContext,
std::weak_ptr<JavaScriptRuntime> runtime,
std::shared_ptr<jsi::Function> jsFunction
);
JavaScriptFunction(
std::weak_ptr<JavaScriptRuntime> runtime,
std::shared_ptr<jsi::Function> jsFunction
);
JavaScriptFunction(
WeakRuntimeHolder runtime,
std::shared_ptr<jsi::Function> jsFunction
);
std::shared_ptr<jsi::Function> get() override;
private:
friend HybridBase;
WeakRuntimeHolder runtimeHolder;
std::shared_ptr<jsi::Function> jsFunction;
jobject invoke(
jni::alias_ref<jni::HybridClass<JavaScriptObject, Destructible>::javaobject> jsThis,
jni::alias_ref<jni::JArrayClass<jni::JObject>> args,
jni::alias_ref<ExpectedType::javaobject> expectedReturnType
);
};
} // namespace expo

View File

@@ -0,0 +1,390 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#include "JavaScriptModuleObject.h"
#include "JSIContext.h"
#include "JSIUtils.h"
#include "EventEmitter.h"
#include "SharedObject.h"
#include "NativeModule.h"
#include <folly/dynamic.h>
#include <jni.h>
#include <jsi/JSIDynamic.h>
#include <react/jni/ReadableNativeArray.h>
#include <fbjni/detail/Hybrid.h>
#include <ReactCommon/TurboModuleUtils.h>
#include <jni/JCallback.h>
#include <jsi/JSIDynamic.h>
#include <fbjni/fbjni.h>
#include <jsi/jsi.h>
#include <utility>
#include <tuple>
#include <algorithm>
#include <sstream>
namespace jni = facebook::jni;
namespace jsi = facebook::jsi;
namespace react = facebook::react;
namespace expo {
void decorateObjectWithFunctions(
jsi::Runtime &runtime,
jsi::Object *jsObject,
JavaScriptModuleObject *objectData) {
for (auto &[name, method]: objectData->methodsMetadata) {
jsObject->setProperty(
runtime,
jsi::String::createFromUtf8(runtime, name),
jsi::Value(runtime, *method->toJSFunction(runtime))
);
}
}
void decorateObjectWithProperties(
jsi::Runtime &runtime,
jsi::Object *jsObject,
JavaScriptModuleObject *objectData) {
for (auto &[name, property]: objectData->properties) {
auto &[getter, setter] = property;
auto descriptor = JavaScriptObject::preparePropertyDescriptor(runtime,
1 << 1 /* enumerable */);
descriptor.setProperty(
runtime,
"get",
jsi::Value(runtime, *getter->toJSFunction(runtime))
);
descriptor.setProperty(
runtime,
"set",
jsi::Value(runtime, *setter->toJSFunction(runtime))
);
common::defineProperty(runtime, jsObject, name.c_str(), std::move(descriptor));
}
}
void decorateObjectWithConstants(
jsi::Runtime &runtime,
jsi::Object *jsObject,
JavaScriptModuleObject *objectData) {
for (const auto &[name, value]: objectData->constants) {
jsObject->setProperty(
runtime,
jsi::String::createFromUtf8(runtime, name),
jsi::valueFromDynamic(runtime, value)
);
}
}
jni::local_ref<jni::HybridClass<JavaScriptModuleObject>::jhybriddata>
JavaScriptModuleObject::initHybrid(jni::alias_ref<jhybridobject> jThis) {
return makeCxxInstance();
}
void JavaScriptModuleObject::registerNatives() {
registerHybrid({
makeNativeMethod("initHybrid", JavaScriptModuleObject::initHybrid),
makeNativeMethod("exportConstants", JavaScriptModuleObject::exportConstants),
makeNativeMethod("registerSyncFunction",
JavaScriptModuleObject::registerSyncFunction),
makeNativeMethod("registerAsyncFunction",
JavaScriptModuleObject::registerAsyncFunction),
makeNativeMethod("registerProperty",
JavaScriptModuleObject::registerProperty),
makeNativeMethod("registerClass",
JavaScriptModuleObject::registerClass),
makeNativeMethod("registerViewPrototype",
JavaScriptModuleObject::registerViewPrototype),
makeNativeMethod("emitEvent",
JavaScriptModuleObject::emitEvent)
});
}
std::shared_ptr<jsi::Object> JavaScriptModuleObject::getJSIObject(jsi::Runtime &runtime) {
if (auto object = jsiObject.lock()) {
return object;
}
auto moduleObject = std::make_shared<jsi::Object>(NativeModule::createInstance(runtime));
decorate(runtime, moduleObject.get());
jsiObject = moduleObject;
return moduleObject;
}
void JavaScriptModuleObject::decorate(jsi::Runtime &runtime, jsi::Object *moduleObject) {
decorateObjectWithConstants(
runtime,
moduleObject,
this
);
decorateObjectWithProperties(
runtime,
moduleObject,
this
);
decorateObjectWithFunctions(
runtime,
moduleObject,
this
);
if (viewPrototype) {
auto viewPrototypeObject = viewPrototype->cthis();
auto viewPrototypeJSIObject = viewPrototypeObject->getJSIObject(runtime);
moduleObject->setProperty(
runtime,
"ViewPrototype",
jsi::Value(runtime, *viewPrototypeJSIObject)
);
}
for (auto &[name, classInfo]: classes) {
auto &[classRef, constructor, ownerClass] = classInfo;
auto classObject = classRef->cthis();
auto weakConstructor = std::weak_ptr<decltype(constructor)::element_type>(constructor);
auto klass = SharedObject::createClass(
runtime,
name.c_str(),
[classObject, weakConstructor = std::move(weakConstructor)](
jsi::Runtime &runtime,
const jsi::Value &thisValue,
const jsi::Value *args,
size_t count
) -> jsi::Value {
// We need to check if the constructor is still alive.
// If not we can just ignore the call. We're destroying the module.
auto ctr = weakConstructor.lock();
if (ctr == nullptr) {
return jsi::Value::undefined();
}
auto thisObject = std::make_shared<jsi::Object>(thisValue.asObject(runtime));
decorateObjectWithProperties(runtime, thisObject.get(),
classObject);
try {
JNIEnv *env = jni::Environment::current();
/**
* This will push a new JNI stack frame for the LocalReferences in this
* function call. When the stack frame for this lambda is popped,
* all LocalReferences are deleted.
*/
jni::JniLocalScope scope(env, (int) count);
auto result = ctr->callJNISync(
env,
runtime,
thisValue,
args,
count
);
if (result == nullptr) {
return jsi::Value(runtime, thisValue);
}
jobject unpackedResult = result.get();
jclass resultClass = env->GetObjectClass(unpackedResult);
if (env->IsAssignableFrom(
resultClass,
JavaReferencesCache::instance()->getJClass(
"expo/modules/kotlin/sharedobjects/SharedObject").clazz
)) {
JSIContext *jsiContext = getJSIContext(runtime);
auto jsThisObject = JavaScriptObject::newInstance(
jsiContext,
jsiContext->runtimeHolder,
thisObject
);
jsiContext->registerSharedObject(result, jsThisObject);
}
return jsi::Value(runtime, thisValue);
} catch (jni::JniException &jniException) {
rethrowAsCodedError(runtime, jniException);
}
}
);
auto klassSharedPtr = std::make_shared<jsi::Function>(std::move(klass));
JSIContext *jsiContext = getJSIContext(runtime);
auto jsThisObject = JavaScriptObject::newInstance(
jsiContext,
jsiContext->runtimeHolder,
klassSharedPtr
);
if (ownerClass != nullptr) {
jsiContext->registerClass(jni::make_local(ownerClass), jsThisObject);
}
moduleObject->setProperty(
runtime,
jsi::String::createFromUtf8(runtime, name),
jsi::Value(runtime, *klassSharedPtr.get())
);
jsi::PropNameID prototypePropNameId = jsi::PropNameID::forAscii(runtime, "prototype", 9);
jsi::Object klassPrototype = klassSharedPtr
->getProperty(runtime, prototypePropNameId)
.asObject(runtime);
decorateObjectWithFunctions(
runtime,
&klassPrototype,
classObject
);
}
}
void JavaScriptModuleObject::exportConstants(
jni::alias_ref<react::NativeMap::javaobject> constants
) {
auto dynamic = constants->cthis()->consume();
assert(dynamic.isObject());
for (const auto &[key, value]: dynamic.items()) {
this->constants[key.asString()] = value;
}
}
void JavaScriptModuleObject::registerSyncFunction(
jni::alias_ref<jstring> name,
jboolean takesOwner,
jni::alias_ref<jni::JArrayClass<ExpectedType>> expectedArgTypes,
jni::alias_ref<JNIFunctionBody::javaobject> body
) {
std::string cName = name->toStdString();
auto methodMetadata = std::make_shared<MethodMetadata>(
cName,
takesOwner & 0x1, // We're unsure if takesOwner can be greater than 1, so we're using bitwise AND to ensure it's 0 or 1.
false,
jni::make_local(expectedArgTypes),
jni::make_global(body)
);
methodsMetadata.insert_or_assign(cName, std::move(methodMetadata));
}
void JavaScriptModuleObject::registerAsyncFunction(
jni::alias_ref<jstring> name,
jboolean takesOwner,
jni::alias_ref<jni::JArrayClass<ExpectedType>> expectedArgTypes,
jni::alias_ref<JNIAsyncFunctionBody::javaobject> body
) {
std::string cName = name->toStdString();
auto methodMetadata = std::make_shared<MethodMetadata>(
cName,
takesOwner & 0x1, // We're unsure if takesOwner can be greater than 1, so we're using bitwise AND to ensure it's 0 or 1.
true,
jni::make_local(expectedArgTypes),
jni::make_global(body)
);
methodsMetadata.insert_or_assign(cName, std::move(methodMetadata));
}
void JavaScriptModuleObject::registerClass(
jni::alias_ref<jstring> name,
jni::alias_ref<JavaScriptModuleObject::javaobject> classObject,
jboolean takesOwner,
jni::alias_ref<jclass> ownerClass,
jni::alias_ref<jni::JArrayClass<ExpectedType>> expectedArgTypes,
jni::alias_ref<JNIFunctionBody::javaobject> body
) {
std::string cName = name->toStdString();
auto constructor = std::make_shared<MethodMetadata>(
"constructor",
takesOwner & 0x1, // We're unsure if takesOwner can be greater than 1, so we're using bitwise AND to ensure it's 0 or 1.
false,
jni::make_local(expectedArgTypes),
jni::make_global(body)
);
auto classTuple = std::make_tuple(
jni::make_global(classObject),
std::move(constructor),
jni::make_global(ownerClass)
);
classes.try_emplace(
cName,
std::move(classTuple)
);
}
void JavaScriptModuleObject::registerViewPrototype(
jni::alias_ref<JavaScriptModuleObject::javaobject> viewPrototype
) {
this->viewPrototype = jni::make_global(viewPrototype);
}
void JavaScriptModuleObject::registerProperty(
jni::alias_ref<jstring> name,
jboolean getterTakesOwner,
jni::alias_ref<jni::JArrayClass<ExpectedType>> getterExpectedArgsTypes,
jni::alias_ref<JNIFunctionBody::javaobject> getter,
jboolean setterTakesOwner,
jni::alias_ref<jni::JArrayClass<ExpectedType>> setterExpectedArgsTypes,
jni::alias_ref<JNIFunctionBody::javaobject> setter
) {
auto cName = name->toStdString();
auto getterMetadata = make_shared<MethodMetadata>(
cName,
getterTakesOwner & 0x1, // We're unsure if getterTakesOwner can be greater than 1, so we're using bitwise AND to ensure it's 0 or 1.
false,
jni::make_local(getterExpectedArgsTypes),
jni::make_global(getter)
);
auto setterMetadata = make_shared<MethodMetadata>(
cName,
setterTakesOwner & 0x1, // We're unsure if setterTakesOwner can be greater than 1, so we're using bitwise AND to ensure it's 0 or 1.
false,
jni::make_local(setterExpectedArgsTypes),
jni::make_global(setter)
);
auto functions = std::make_pair(
std::move(getterMetadata),
std::move(setterMetadata)
);
properties.insert_or_assign(cName, std::move(functions));
}
void JavaScriptModuleObject::emitEvent(
jni::alias_ref<jni::HybridClass<JSIContext>::javaobject> jsiContextRef,
jni::alias_ref<jstring> eventName,
jni::alias_ref<react::ReadableNativeMap::javaobject> eventBody
) {
const std::string name = eventName->toStdString();
folly::dynamic body;
if (eventBody) {
body = eventBody->cthis()->consume();
}
const JSIContext *jsiContext = jsiContextRef->cthis();
jsiContext->runtimeHolder->jsInvoker->invokeAsync([
jsiContext,
name = std::move(name),
body = std::move(body),
weakThis = jsiObject
]() {
std::shared_ptr<jsi::Object> jsThis = weakThis.lock();
if (!jsThis) {
return;
}
// TODO(@lukmccall): refactor when jsInvoker recieves a runtime as a parameter
jsi::Runtime &rt = jsiContext->runtimeHolder->get();
jsi::Value convertedBody = jsi::valueFromDynamic(rt, body);
std::vector<jsi::Value> args;
args.emplace_back(std::move(convertedBody));
EventEmitter::emitEvent(rt, *jsThis, name, args);
});
}
} // namespace expo

View File

@@ -0,0 +1,192 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include <fbjni/fbjni.h>
#include <jsi/jsi.h>
#include <react/jni/ReadableNativeArray.h>
#include <react/jni/ReadableNativeMap.h>
#include <jni/JCallback.h>
#include <unordered_map>
#include "MethodMetadata.h"
#include "JNIFunctionBody.h"
#include "types/ExpectedType.h"
namespace jni = facebook::jni;
namespace jsi = facebook::jsi;
namespace react = facebook::react;
namespace expo {
class JSIContext;
class JavaScriptModuleObject;
void decorateObjectWithFunctions(
jsi::Runtime &runtime,
jsi::Object *jsObject,
JavaScriptModuleObject *objectData
);
void decorateObjectWithProperties(
jsi::Runtime &runtime,
jsi::Object *jsObject,
JavaScriptModuleObject *objectData
);
void decorateObjectWithConstants(
jsi::Runtime &runtime,
jsi::Object *jsObject,
JavaScriptModuleObject *objectData
);
/**
* A CPP part of the module.
*
* Right now objects of this class are stored by the ModuleHolder to ensure they will live
* as long as the RN context.
*/
class JavaScriptModuleObject : public jni::HybridClass<JavaScriptModuleObject> {
public:
static auto constexpr
kJavaDescriptor = "Lexpo/modules/kotlin/jni/JavaScriptModuleObject;";
static auto constexpr TAG = "JavaScriptModuleObject";
static jni::local_ref<jhybriddata> initHybrid(jni::alias_ref<jhybridobject> jThis);
static void registerNatives();
/**
* Returns a cached instance of jsi::Object representing this module.
* @param runtime
* @return Wrapped instance of JavaScriptModuleObject::HostObject
*/
std::shared_ptr<jsi::Object> getJSIObject(jsi::Runtime &runtime);
/**
* Decorates the given object with properties and functions provided in the module definition.
*/
void decorate(jsi::Runtime &runtime, jsi::Object *moduleObject);
/**
* Exports constants that will be assigned to the underlying HostObject.
*/
void exportConstants(jni::alias_ref<react::NativeMap::javaobject> constants);
/**
* Registers a sync function.
* That function can be called via the `JavaScriptModuleObject.callSyncMethod` method.
*/
void registerSyncFunction(
jni::alias_ref<jstring> name,
jboolean takesOwner,
jni::alias_ref<jni::JArrayClass<ExpectedType>> expectedArgTypes,
jni::alias_ref<JNIFunctionBody::javaobject> body
);
/**
* Registers a async function.
* That function can be called via the `JavaScriptModuleObject.callAsyncMethod` method.
*/
void registerAsyncFunction(
jni::alias_ref<jstring> name,
jboolean takesOwner,
jni::alias_ref<jni::JArrayClass<ExpectedType>> expectedArgTypes,
jni::alias_ref<JNIAsyncFunctionBody::javaobject> body
);
void registerClass(
jni::alias_ref<jstring> name,
jni::alias_ref<JavaScriptModuleObject::javaobject> classObject,
jboolean takesOwner,
jni::alias_ref<jclass> ownerClass,
jni::alias_ref<jni::JArrayClass<ExpectedType>> expectedArgTypes,
jni::alias_ref<JNIFunctionBody::javaobject> body
);
void registerViewPrototype(
jni::alias_ref<JavaScriptModuleObject::javaobject> viewPrototype
);
/**
* Registers a property
* @param name of the property
* @param desiredType of the setter argument
* @param getter body for the get method - can be nullptr
* @param setter body for the set method - can be nullptr
*/
void registerProperty(
jni::alias_ref<jstring> name,
jboolean getterTakesOwner,
jni::alias_ref<jni::JArrayClass<ExpectedType>> getterExpectedArgsTypes,
jni::alias_ref<JNIFunctionBody::javaobject> getter,
jboolean setterTakesOwner,
jni::alias_ref<jni::JArrayClass<ExpectedType>> setterExpectedArgsTypes,
jni::alias_ref<JNIFunctionBody::javaobject> setter
);
/**
* Emits an event using cached jsi::Object with the given name and body.
* @param eventName
* @param eventBody
*/
void emitEvent(
jni::alias_ref<jni::HybridClass<JSIContext>::javaobject> jsiContextRef,
jni::alias_ref<jstring> eventName,
jni::alias_ref<react::ReadableNativeMap::javaobject> eventBody
);
private:
friend HybridBase;
friend void decorateObjectWithFunctions(
jsi::Runtime &runtime,
jsi::Object *jsObject,
JavaScriptModuleObject *objectData
);
friend void decorateObjectWithProperties(
jsi::Runtime &runtime,
jsi::Object *jsObject,
JavaScriptModuleObject *objectData
);
friend void decorateObjectWithConstants(
jsi::Runtime &runtime,
jsi::Object *jsObject,
JavaScriptModuleObject *objectData
);
/**
* A reference to the `jsi::Object`.
* Simple we cached that value to return the same object each time.
* It's a weak reference because the JS runtime holds the actual object.
* Doing that allows the runtime to deallocate jsi::Object if it's not needed anymore.
*/
std::weak_ptr<jsi::Object> jsiObject;
/**
* Metadata map that stores information about all available methods on this module.
*/
std::unordered_map<std::string, std::shared_ptr<MethodMetadata>> methodsMetadata;
/**
* A constants map.
*/
std::unordered_map<std::string, folly::dynamic> constants;
/**
* A registry of properties
* The first MethodMetadata points to the getter and the second one to the setter.
*/
std::map<std::string, std::pair<std::shared_ptr<MethodMetadata>, std::shared_ptr<MethodMetadata>>> properties;
std::map<
std::string,
std::tuple<jni::global_ref<JavaScriptModuleObject::javaobject>, std::shared_ptr<MethodMetadata>, jni::global_ref<jclass>>
> classes;
jni::global_ref<JavaScriptModuleObject::javaobject> viewPrototype;
};
} // namespace expo

View File

@@ -0,0 +1,190 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#include "JavaScriptObject.h"
#include "JavaScriptValue.h"
#include "JavaScriptFunction.h"
#include "JavaScriptRuntime.h"
#include "JavaScriptWeakObject.h"
#include "JSITypeConverter.h"
#include "ObjectDeallocator.h"
#include "JavaReferencesCache.h"
#include "JSIContext.h"
namespace expo {
void JavaScriptObject::registerNatives() {
registerHybrid({
makeNativeMethod("hasProperty", JavaScriptObject::jniHasProperty),
makeNativeMethod("getProperty", JavaScriptObject::jniGetProperty),
makeNativeMethod("getPropertyNames", JavaScriptObject::jniGetPropertyNames),
makeNativeMethod("createWeak", JavaScriptObject::createWeak),
makeNativeMethod("setBoolProperty", JavaScriptObject::setProperty<bool>),
makeNativeMethod("setDoubleProperty", JavaScriptObject::setProperty<double>),
makeNativeMethod("setStringProperty",
JavaScriptObject::setProperty<jni::alias_ref<jstring>>),
makeNativeMethod("setJSValueProperty",
JavaScriptObject::setProperty<jni::alias_ref<JavaScriptValue::javaobject>>),
makeNativeMethod("setJSObjectProperty",
JavaScriptObject::setProperty<jni::alias_ref<JavaScriptObject::javaobject>>),
makeNativeMethod("unsetProperty", JavaScriptObject::unsetProperty),
makeNativeMethod("defineBoolProperty", JavaScriptObject::defineProperty<bool>),
makeNativeMethod("defineDoubleProperty",
JavaScriptObject::defineProperty<double>),
makeNativeMethod("defineStringProperty",
JavaScriptObject::defineProperty<jni::alias_ref<jstring>>),
makeNativeMethod("defineJSValueProperty",
JavaScriptObject::defineProperty<jni::alias_ref<JavaScriptValue::javaobject>>),
makeNativeMethod("defineJSObjectProperty",
JavaScriptObject::defineProperty<jni::alias_ref<JavaScriptObject::javaobject>>),
makeNativeMethod("defineNativeDeallocator",
JavaScriptObject::defineNativeDeallocator),
});
}
JavaScriptObject::JavaScriptObject(
std::weak_ptr<JavaScriptRuntime> runtime,
std::shared_ptr<jsi::Object> jsObject
) : runtimeHolder(std::move(runtime)), jsObject(std::move(jsObject)) {
runtimeHolder.ensureRuntimeIsValid();
}
JavaScriptObject::JavaScriptObject(
WeakRuntimeHolder runtime,
std::shared_ptr<jsi::Object> jsObject
) : runtimeHolder(std::move(runtime)), jsObject(std::move(jsObject)) {
runtimeHolder.ensureRuntimeIsValid();
}
std::shared_ptr<jsi::Object> JavaScriptObject::get() {
return jsObject;
}
bool JavaScriptObject::hasProperty(const std::string &name) {
auto &jsRuntime = runtimeHolder.getJSRuntime();
return jsObject->hasProperty(jsRuntime, name.c_str());
}
jsi::Value JavaScriptObject::getProperty(const std::string &name) {
auto &jsRuntime = runtimeHolder.getJSRuntime();
return jsObject->getProperty(jsRuntime, name.c_str());
}
bool JavaScriptObject::jniHasProperty(jni::alias_ref<jstring> name) {
return hasProperty(name->toStdString());
}
jni::local_ref<JavaScriptValue::javaobject> JavaScriptObject::jniGetProperty(
jni::alias_ref<jstring> name
) {
auto result = std::make_shared<jsi::Value>(getProperty(name->toStdString()));
return JavaScriptValue::newInstance(
runtimeHolder.getJSIContext(),
runtimeHolder,
result
);
}
std::vector<std::string> JavaScriptObject::getPropertyNames() {
auto &jsRuntime = runtimeHolder.getJSRuntime();
jsi::Array properties = jsObject->getPropertyNames(jsRuntime);
auto size = properties.size(jsRuntime);
std::vector<std::string> names(size);
for (size_t i = 0; i < size; i++) {
auto propertyName = properties
.getValueAtIndex(jsRuntime, i)
.asString(jsRuntime)
.utf8(jsRuntime);
names[i] = propertyName;
}
return names;
}
jni::local_ref<jni::JArrayClass<jstring>> JavaScriptObject::jniGetPropertyNames() {
std::vector<std::string> cResult = getPropertyNames();
auto paredResult = jni::JArrayClass<jstring>::newArray(cResult.size());
for (size_t i = 0; i < cResult.size(); i++) {
paredResult->setElement(i, jni::make_jstring(cResult[i]).get());
}
return paredResult;
}
jni::local_ref<jni::HybridClass<JavaScriptWeakObject, Destructible>::javaobject> JavaScriptObject::createWeak() {
return JavaScriptWeakObject::newInstance(
runtimeHolder.getJSIContext(),
runtimeHolder,
get()
);
}
jni::local_ref<JavaScriptFunction::javaobject> JavaScriptObject::jniAsFunction() {
auto &jsRuntime = runtimeHolder.getJSRuntime();
auto jsFuncion = std::make_shared<jsi::Function>(jsObject->asFunction(jsRuntime));
return JavaScriptFunction::newInstance(
runtimeHolder.getJSIContext(),
runtimeHolder,
jsFuncion
);
}
void JavaScriptObject::setProperty(const std::string &name, jsi::Value value) {
auto &jsRuntime = runtimeHolder.getJSRuntime();
jsObject->setProperty(jsRuntime, name.c_str(), value);
}
void JavaScriptObject::unsetProperty(jni::alias_ref<jstring> name) {
auto &jsRuntime = runtimeHolder.getJSRuntime();
auto cName = name->toStdString();
jsObject->setProperty(
jsRuntime,
cName.c_str(),
jsi::Value::undefined()
);
}
jsi::Object JavaScriptObject::preparePropertyDescriptor(
jsi::Runtime &jsRuntime,
int options
) {
jsi::Object descriptor(jsRuntime);
descriptor.setProperty(jsRuntime, "configurable", (bool) ((1 << 0) & options));
descriptor.setProperty(jsRuntime, "enumerable", (bool) ((1 << 1) & options));
if ((bool) (1 << 2 & options)) {
descriptor.setProperty(jsRuntime, "writable", true);
}
return descriptor;
}
jni::local_ref<JavaScriptObject::javaobject> JavaScriptObject::newInstance(
JSIContext *jsiContext,
std::weak_ptr<JavaScriptRuntime> runtime,
std::shared_ptr<jsi::Object> jsObject
) {
auto object = JavaScriptObject::newObjectCxxArgs(std::move(runtime), std::move(jsObject));
jsiContext->jniDeallocator->addReference(object);
return object;
}
void JavaScriptObject::defineNativeDeallocator(
jni::alias_ref<JNIFunctionBody::javaobject> deallocator
) {
auto &rt = runtimeHolder.getJSRuntime();
jni::global_ref<JNIFunctionBody::javaobject> globalRef = jni::make_global(deallocator);
common::setDeallocator(
rt,
jsObject,
[globalRef = std::move(globalRef)]() mutable {
auto args = jni::Environment::ensureCurrentThreadIsAttached()->NewObjectArray(
0,
JavaReferencesCache::instance()->getJClass("java/lang/Object").clazz,
nullptr
);
globalRef->invoke(args);
globalRef.reset();
}
);
}
} // namespace expo

View File

@@ -0,0 +1,145 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include "JSIObjectWrapper.h"
#include "JSITypeConverter.h"
#include "JavaScriptRuntime.h"
#include "WeakRuntimeHolder.h"
#include "JNIFunctionBody.h"
#include "JNIDeallocator.h"
#include "JSIUtils.h"
#include <fbjni/fbjni.h>
#include <jsi/jsi.h>
#include <memory>
namespace jni = facebook::jni;
namespace jsi = facebook::jsi;
namespace expo {
class JavaScriptFunction;
class JavaScriptValue;
class JavaScriptWeakObject;
/**
* Represents any JavaScript object. Its purpose is to exposes `jsi::Object` API back to Kotlin.
*/
class JavaScriptObject : public jni::HybridClass<JavaScriptObject, Destructible>, JSIObjectWrapper {
public:
static auto constexpr
kJavaDescriptor = "Lexpo/modules/kotlin/jni/JavaScriptObject;";
static auto constexpr TAG = "JavaScriptObject";
static void registerNatives();
static jni::local_ref<JavaScriptObject::javaobject> newInstance(
JSIContext *jsiContext,
std::weak_ptr<JavaScriptRuntime> runtime,
std::shared_ptr<jsi::Object> jsObject
);
JavaScriptObject(
std::weak_ptr<JavaScriptRuntime> runtime,
std::shared_ptr<jsi::Object> jsObject
);
JavaScriptObject(
WeakRuntimeHolder runtime,
std::shared_ptr<jsi::Object> jsObject
);
virtual ~JavaScriptObject() = default;
std::shared_ptr<jsi::Object> get() override;
/**
* @return a bool whether the object has a property with the given name
*/
bool hasProperty(const std::string &name);
/**
* @return the property of the object with the given name.
* If the name isn't a property on the object, returns the `jsi::Value::undefined` value.
*/
jsi::Value getProperty(const std::string &name);
/**
* @return a vector consisting of all enumerable property names in the object and its prototype chain.
*/
std::vector<std::string> getPropertyNames();
void setProperty(const std::string &name, jsi::Value value);
static jsi::Object preparePropertyDescriptor(jsi::Runtime &jsRuntime, int options);
void defineNativeDeallocator(
jni::alias_ref<JNIFunctionBody::javaobject> deallocator
);
protected:
WeakRuntimeHolder runtimeHolder;
std::shared_ptr<jsi::Object> jsObject;
private:
friend HybridBase;
bool jniHasProperty(jni::alias_ref<jstring> name);
jni::local_ref<jni::HybridClass<JavaScriptValue, Destructible>::javaobject> jniGetProperty(
jni::alias_ref<jstring> name
);
jni::local_ref<jni::JArrayClass<jstring>> jniGetPropertyNames();
jni::local_ref<jni::HybridClass<JavaScriptWeakObject, Destructible>::javaobject> createWeak();
jni::local_ref<jni::HybridClass<JavaScriptFunction, Destructible>::javaobject> jniAsFunction();
/**
* Unsets property with the given name.
*/
void unsetProperty(jni::alias_ref<jstring> name);
/**
* A template to generate different versions of the `setProperty` method based on the `jsi_type_converter` trait.
* Those generated methods will be exported and visible in the Kotlin codebase.
* On the other hand, we could just make one function that would take a generic Java Object,
* but then we would have to decide what to do with it and how to convert it to jsi::Value
* in cpp. That would be expensive. So it's easier to ensure that
* we call the correct version of `setProperty` in the Kotlin code.
*
* This template will work only if the jsi_type_converter exists for a given type.
*/
template<
class T,
typename = std::enable_if_t<is_jsi_type_converter_defined<T>>
>
void setProperty(jni::alias_ref<jstring> name, T value) {
jsi::Runtime &jsRuntime = runtimeHolder.getJSRuntime();
auto cName = name->toStdString();
jsObject->setProperty(
jsRuntime,
cName.c_str(),
jsi_type_converter<T>::convert(jsRuntime, value)
);
}
template<
class T,
typename = std::enable_if_t<is_jsi_type_converter_defined<T>>
>
void defineProperty(jni::alias_ref<jstring> name, T value, int options) {
jsi::Runtime &jsRuntime = runtimeHolder.getJSRuntime();
auto cName = name->toStdString();
jsi::Object descriptor = preparePropertyDescriptor(jsRuntime, options);
descriptor.setProperty(jsRuntime, "value", jsi_type_converter<T>::convert(jsRuntime, value));
common::defineProperty(jsRuntime, jsObject.get(), cName.c_str(), std::move(descriptor));
}
};
} // namespace expo

View File

@@ -0,0 +1,172 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#include "JavaScriptRuntime.h"
#include "JavaScriptValue.h"
#include "JavaScriptObject.h"
#include "Exceptions.h"
#include "JSIContext.h"
#include "JSIUtils.h"
#if UNIT_TEST
#include "TestingSyncJSCallInvoker.h"
#if USE_HERMES
#include <hermes/hermes.h>
#include <utility>
#else
#include <jsc/JSCRuntime.h>
#endif
#endif // UNIT_TEST
namespace jsi = facebook::jsi;
namespace expo {
JavaScriptRuntime::JavaScriptRuntime() {
#if !UNIT_TEST
throw std::logic_error(
"The JavaScriptRuntime constructor is only available when UNIT_TEST is defined.");
#else
#if USE_HERMES
auto config = ::hermes::vm::RuntimeConfig::Builder()
.withEnableSampleProfiling(false);
runtime = facebook::hermes::makeHermesRuntime(config.build());
// This version of the Hermes uses a Promise implementation that is provided by the RN.
// The `setImmediate` function isn't defined, but is required by the Promise implementation.
// That's why we inject it here.
auto setImmediatePropName = jsi::PropNameID::forUtf8(*runtime, "setImmediate");
runtime->global().setProperty(
*runtime,
setImmediatePropName,
jsi::Function::createFromHostFunction(
*runtime,
setImmediatePropName,
1,
[](jsi::Runtime &rt,
const jsi::Value &thisVal,
const jsi::Value *args,
size_t count) {
args[0].asObject(rt).asFunction(rt).call(rt);
return jsi::Value::undefined();
}
)
);
#else
runtime = facebook::jsc::makeJSCRuntime();
#endif
jsInvoker = std::make_shared<TestingSyncJSCallInvoker>(runtime);
// By default "global" property isn't set.
runtime->global().setProperty(
*runtime,
jsi::PropNameID::forUtf8(*runtime, "global"),
runtime->global()
);
// Mock the CodedError that in a typical scenario will be defined by the `expo-modules-core`.
// Note: we can't use `class` syntax here, because Hermes doesn't support it.
runtime->evaluateJavaScript(
std::make_shared<jsi::StringBuffer>(
"function CodedError(code, message) {\n"
" this.code = code;\n"
" this.message = message;\n"
" this.stack = (new Error).stack;\n"
"}\n"
"CodedError.prototype = new Error;\n"
"global.ExpoModulesCore_CodedError = CodedError"
),
"<<evaluated>>"
);
#endif // !UNIT_TEST
}
JavaScriptRuntime::JavaScriptRuntime(
jsi::Runtime *runtime,
std::shared_ptr<react::CallInvoker> jsInvoker
) : jsInvoker(std::move(jsInvoker)) {
// Creating a shared pointer that points to the runtime but doesn't own it, thus doesn't release it.
// In this code flow, the runtime should be owned by something else like the CatalystInstance.
// See explanation for constructor (8): https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr
this->runtime = std::shared_ptr<jsi::Runtime>(std::shared_ptr<jsi::Runtime>(), runtime);
}
jsi::Runtime &JavaScriptRuntime::get() const {
return *runtime;
}
jni::local_ref<JavaScriptValue::javaobject>
JavaScriptRuntime::evaluateScript(const std::string &script) {
auto scriptBuffer = std::make_shared<jsi::StringBuffer>(script);
try {
return JavaScriptValue::newInstance(
getJSIContext(get()),
weak_from_this(),
std::make_shared<jsi::Value>(runtime->evaluateJavaScript(scriptBuffer, "<<evaluated>>"))
);
} catch (const jsi::JSError &error) {
jni::throwNewJavaException(
JavaScriptEvaluateException::create(
error.getMessage(),
error.getStack()
).get()
);
} catch (const jsi::JSIException &error) {
jni::throwNewJavaException(
JavaScriptEvaluateException::create(
error.what(),
""
).get()
);
}
}
jni::local_ref<JavaScriptObject::javaobject> JavaScriptRuntime::global() {
auto global = std::make_shared<jsi::Object>(runtime->global());
return JavaScriptObject::newInstance(getJSIContext(get()), weak_from_this(), global);
}
jni::local_ref<JavaScriptObject::javaobject> JavaScriptRuntime::createObject() {
auto newObject = std::make_shared<jsi::Object>(*runtime);
return JavaScriptObject::newInstance(getJSIContext(get()), weak_from_this(), newObject);
}
void JavaScriptRuntime::drainJSEventLoop() {
while (!runtime->drainMicrotasks()) {}
}
void JavaScriptRuntime::installMainObject() {
auto coreModule = getJSIContext(get())->getCoreModule();
// As opposed to other modules, the core module is represented by a raw JS object instead of an instance of NativeModule class.
mainObject = std::make_shared<jsi::Object>(*runtime);
// Decorate the core object based on the module definition.
coreModule->cthis()->decorate(*runtime, mainObject.get());
auto global = runtime->global();
jsi::Object descriptor = JavaScriptObject::preparePropertyDescriptor(*runtime, 1 << 1);
descriptor.setProperty(*runtime, "value", jsi::Value(*runtime, *mainObject));
common::defineProperty(
*runtime,
&global,
"expo",
std::move(descriptor)
);
}
std::shared_ptr<jsi::Object> JavaScriptRuntime::getMainObject() {
return mainObject;
}
} // namespace expo

View File

@@ -0,0 +1,84 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include "JNIDeallocator.h"
#include <jsi/jsi.h>
#include <fbjni/fbjni.h>
#include <ReactCommon/CallInvoker.h>
namespace jsi = facebook::jsi;
namespace jni = facebook::jni;
namespace react = facebook::react;
namespace expo {
class JavaScriptValue;
class JavaScriptObject;
class JSIContext;
/**
* A wrapper for the jsi::Runtime.
* This class is used as a bridge between CPP and Kotlin and to encapsulate common runtime helper functions.
*
* Instances of this class should be managed using a shared smart pointer.
* To pass runtime information to all of `JavaScriptValue` and `JavaScriptObject` we use `weak_from_this()`
* that requires that the object is held via a smart pointer. Otherwise, `weak_from_this()` returns `nullptr`.
*/
class JavaScriptRuntime : public std::enable_shared_from_this<JavaScriptRuntime> {
public:
/**
* Initializes a runtime that is independent from React Native and its runtime initialization.
* This flow is mostly intended for tests. The JS call invoker is set to `SyncCallInvoker`.
* See **JavaScriptRuntime.cpp** for the `SyncCallInvoker` implementation.
*/
JavaScriptRuntime();
JavaScriptRuntime(
jsi::Runtime *runtime,
std::shared_ptr<react::CallInvoker> jsInvoker
);
/**
* Returns the underlying runtime object.
*/
jsi::Runtime &get() const;
/**
* Evaluates given JavaScript source code.
* @throws if the input format is unknown, or evaluation causes an error,
* a jni::JniException<JavaScriptEvaluateException> will be thrown.
*/
jni::local_ref<jni::HybridClass<JavaScriptValue, Destructible>::javaobject> evaluateScript(
const std::string &script
);
/**
* Returns the runtime global object for use in Kotlin.
*/
jni::local_ref<jni::HybridClass<JavaScriptObject, Destructible>::javaobject> global();
/**
* Creates a new object for use in Kotlin.
*/
jni::local_ref<jni::HybridClass<JavaScriptObject, Destructible>::javaobject> createObject();
/**
* Drains the JavaScript VM internal Microtask (a.k.a. event loop) queue.
*/
void drainJSEventLoop();
void installMainObject();
std::shared_ptr<react::CallInvoker> jsInvoker;
std::shared_ptr<jsi::Object> getMainObject();
private:
std::shared_ptr<jsi::Runtime> runtime;
std::shared_ptr<jsi::Object> mainObject;
};
} // namespace expo

View File

@@ -0,0 +1,105 @@
#include "JavaScriptTypedArray.h"
#include "JavaScriptRuntime.h"
#include "JSIContext.h"
namespace expo {
JavaScriptTypedArray::JavaScriptTypedArray(
WeakRuntimeHolder runtime,
std::shared_ptr<jsi::Object> jsObject
) : jni::HybridClass<JavaScriptTypedArray, JavaScriptObject>(std::move(runtime),
std::move(jsObject)) {
jsi::Runtime &jsRuntime = runtimeHolder.getJSRuntime();
typedArrayWrapper = std::make_shared<expo::TypedArray>(jsRuntime, *get());
rawPointer = static_cast<char *>(typedArrayWrapper->getRawPointer(jsRuntime));
}
JavaScriptTypedArray::JavaScriptTypedArray(
std::weak_ptr<JavaScriptRuntime> runtime,
std::shared_ptr<jsi::Object> jsObject
) : jni::HybridClass<JavaScriptTypedArray, JavaScriptObject>(std::move(runtime),
std::move(jsObject)) {
jsi::Runtime &jsRuntime = runtimeHolder.getJSRuntime();
typedArrayWrapper = std::make_shared<expo::TypedArray>(jsRuntime, *get());
rawPointer = static_cast<char *>(typedArrayWrapper->getRawPointer(jsRuntime));
}
void JavaScriptTypedArray::registerNatives() {
registerHybrid({
makeNativeMethod("getRawKind", JavaScriptTypedArray::getRawKind),
makeNativeMethod("toDirectBuffer", JavaScriptTypedArray::toDirectBuffer),
makeNativeMethod("read", JavaScriptTypedArray::readBuffer),
makeNativeMethod("write", JavaScriptTypedArray::writeBuffer),
makeNativeMethod("readByte", JavaScriptTypedArray::read<int8_t>),
makeNativeMethod("read2Byte", JavaScriptTypedArray::read<int16_t>),
makeNativeMethod("read4Byte", JavaScriptTypedArray::read<int32_t>),
makeNativeMethod("read8Byte", JavaScriptTypedArray::read<int64_t>),
makeNativeMethod("readFloat", JavaScriptTypedArray::read<float>),
makeNativeMethod("readDouble", JavaScriptTypedArray::read<double>),
makeNativeMethod("writeByte", JavaScriptTypedArray::write<int8_t>),
makeNativeMethod("write2Byte", JavaScriptTypedArray::write<int16_t>),
makeNativeMethod("write4Byte", JavaScriptTypedArray::write<int32_t>),
makeNativeMethod("write8Byte", JavaScriptTypedArray::write<int64_t>),
makeNativeMethod("writeFloat", JavaScriptTypedArray::write<float>),
makeNativeMethod("writeDouble", JavaScriptTypedArray::write<double>),
});
}
int JavaScriptTypedArray::getRawKind() {
jsi::Runtime &jsRuntime = runtimeHolder.getJSRuntime();
return (int) typedArrayWrapper->getKind(jsRuntime);
}
jni::local_ref<jni::JByteBuffer> JavaScriptTypedArray::toDirectBuffer() {
jsi::Runtime &jsRuntime = runtimeHolder.getJSRuntime();
auto byteLength = typedArrayWrapper->byteLength(jsRuntime);
auto byteOffset = typedArrayWrapper->byteOffset(jsRuntime);
auto byteBuffer = jni::JByteBuffer::wrapBytes(
static_cast<uint8_t *>(typedArrayWrapper->getRawPointer(jsRuntime)),
byteLength - byteOffset
);
byteBuffer->order(jni::JByteOrder::nativeOrder());
return byteBuffer;
}
void JavaScriptTypedArray::readBuffer(
jni::alias_ref<jni::JArrayByte> buffer,
int position,
int size
) {
buffer->setRegion(
0,
size,
reinterpret_cast<const signed char *>(rawPointer + position)
);
}
void JavaScriptTypedArray::writeBuffer(
jni::alias_ref<jni::JArrayByte> buffer,
int position,
int size
) {
auto region = buffer->getRegion(0, size);
memcpy(rawPointer + position, region.get(), size);
}
jni::local_ref<JavaScriptTypedArray::javaobject> JavaScriptTypedArray::newInstance(
JSIContext *jSIContext,
std::weak_ptr<JavaScriptRuntime> runtime,
std::shared_ptr<jsi::Object> jsObject
) {
auto object = JavaScriptTypedArray::newObjectCxxArgs(
std::move(runtime),
std::move(jsObject)
);
jSIContext->jniDeallocator->addReference(object);
return object;
}
}

View File

@@ -0,0 +1,76 @@
#pragma once
#include "TypedArray.h"
#include "JavaScriptObject.h"
#include "WeakRuntimeHolder.h"
#include <fbjni/fbjni.h>
#include <fbjni/ByteBuffer.h>
#include <jsi/jsi.h>
#include <memory>
namespace expo {
class JavaScriptRuntime;
namespace jni = facebook::jni;
namespace jsi = facebook::jsi;
class JavaScriptTypedArray : public jni::HybridClass<JavaScriptTypedArray, JavaScriptObject> {
public:
static auto constexpr
kJavaDescriptor = "Lexpo/modules/kotlin/jni/JavaScriptTypedArray;";
static auto constexpr TAG = "JavaScriptTypedArray";
static void registerNatives();
static jni::local_ref<JavaScriptTypedArray::javaobject> newInstance(
JSIContext *jSIContext,
std::weak_ptr<JavaScriptRuntime> runtime,
std::shared_ptr<jsi::Object> jsObject
);
JavaScriptTypedArray(
std::weak_ptr<JavaScriptRuntime> runtime,
std::shared_ptr<jsi::Object> jsObject
);
JavaScriptTypedArray(
WeakRuntimeHolder runtime,
std::shared_ptr<jsi::Object> jsObject
);
/**
* Gets a raw kind of the underlying typed array.
*/
int getRawKind();
/**
* Converts typed array into a direct byte buffer.
*/
jni::local_ref<jni::JByteBuffer> toDirectBuffer();
private:
std::shared_ptr<expo::TypedArray> typedArrayWrapper;
/**
* Cached pointer to the beginning of the byte buffer.
*/
char *rawPointer;
void readBuffer(jni::alias_ref<jni::JArrayByte> buffer, int position, int size);
void writeBuffer(jni::alias_ref<jni::JArrayByte> buffer, int position, int size);
template<class T>
T read(int position) {
return *reinterpret_cast<T *>(rawPointer + position);
}
template<class T>
void write(int position, T value) {
*reinterpret_cast<T *>(rawPointer + position) = value;
}
};
} // namespace expo

View File

@@ -0,0 +1,231 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#include "JavaScriptValue.h"
#include "JavaScriptRuntime.h"
#include "JavaScriptObject.h"
#include "JavaScriptTypedArray.h"
#include "JavaScriptFunction.h"
#include "TypedArray.h"
#include "Exceptions.h"
#include "JSIContext.h"
namespace expo {
void JavaScriptValue::registerNatives() {
registerHybrid({
makeNativeMethod("kind", JavaScriptValue::jniKind),
makeNativeMethod("isNull", JavaScriptValue::isNull),
makeNativeMethod("isUndefined", JavaScriptValue::isUndefined),
makeNativeMethod("isBool", JavaScriptValue::isBool),
makeNativeMethod("isNumber", JavaScriptValue::isNumber),
makeNativeMethod("isString", JavaScriptValue::isString),
makeNativeMethod("isSymbol", JavaScriptValue::isSymbol),
makeNativeMethod("isFunction", JavaScriptValue::isFunction),
makeNativeMethod("isArray", JavaScriptValue::isArray),
makeNativeMethod("isTypedArray", JavaScriptValue::isTypedArray),
makeNativeMethod("isObject", JavaScriptValue::isObject),
makeNativeMethod("getBool", JavaScriptValue::getBool),
makeNativeMethod("getDouble", JavaScriptValue::getDouble),
makeNativeMethod("getString", JavaScriptValue::jniGetString),
makeNativeMethod("getObject", JavaScriptValue::getObject),
makeNativeMethod("getArray", JavaScriptValue::getArray),
makeNativeMethod("getTypedArray", JavaScriptValue::getTypedArray),
makeNativeMethod("jniGetFunction", JavaScriptValue::jniGetFunction),
});
}
JavaScriptValue::JavaScriptValue(
std::weak_ptr<JavaScriptRuntime> runtime,
std::shared_ptr<jsi::Value> jsValue
) : runtimeHolder(std::move(runtime)), jsValue(std::move(jsValue)) {
runtimeHolder.ensureRuntimeIsValid();
}
JavaScriptValue::JavaScriptValue(
WeakRuntimeHolder runtime,
std::shared_ptr<jsi::Value> jsValue
) : runtimeHolder(std::move(runtime)), jsValue(std::move(jsValue)) {
runtimeHolder.ensureRuntimeIsValid();
}
std::shared_ptr<jsi::Value> JavaScriptValue::get() {
return jsValue;
}
std::string JavaScriptValue::kind() {
if (isNull()) {
return "null";
}
if (isUndefined()) {
return "undefined";
}
if (isBool()) {
return "boolean";
}
if (isNumber()) {
return "number";
}
if (isString()) {
return "string";
}
if (isSymbol()) {
return "symbol";
}
if (isFunction()) {
return "function";
}
if (isArray()) {
return "array";
}
if (isObject()) {
return "object";
}
throwNewJavaException(
UnexpectedException::create("Unknown type").get()
);
}
bool JavaScriptValue::isNull() {
return jsValue->isNull();
}
bool JavaScriptValue::isUndefined() {
return jsValue->isUndefined();
}
bool JavaScriptValue::isBool() {
return jsValue->isBool();
}
bool JavaScriptValue::isNumber() {
return jsValue->isNumber();
}
bool JavaScriptValue::isString() {
return jsValue->isString();
}
bool JavaScriptValue::isSymbol() {
return jsValue->isSymbol();
}
bool JavaScriptValue::isFunction() {
if (jsValue->isObject()) {
auto &jsRuntime = runtimeHolder.getJSRuntime();
return jsValue->asObject(jsRuntime).isFunction(jsRuntime);
}
return false;
}
bool JavaScriptValue::isArray() {
if (jsValue->isObject()) {
auto &jsRuntime = runtimeHolder.getJSRuntime();
return jsValue->asObject(jsRuntime).isArray(jsRuntime);
}
return false;
}
bool JavaScriptValue::isObject() {
return jsValue->isObject();
}
bool JavaScriptValue::isTypedArray() {
if (jsValue->isObject()) {
jsi::Runtime &jsRuntime = runtimeHolder.getJSRuntime();
return expo::isTypedArray(jsRuntime, jsValue->getObject(jsRuntime));
}
return false;
}
bool JavaScriptValue::getBool() {
return jsValue->getBool();
}
double JavaScriptValue::getDouble() {
return jsValue->getNumber();
}
std::string JavaScriptValue::getString() {
auto &jsRuntime = runtimeHolder.getJSRuntime();
return jsValue->getString(jsRuntime).utf8(jsRuntime);
}
jni::local_ref<JavaScriptObject::javaobject> JavaScriptValue::getObject() {
auto &jsRuntime = runtimeHolder.getJSRuntime();
auto jsObject = std::make_shared<jsi::Object>(jsValue->getObject(jsRuntime));
return JavaScriptObject::newInstance(
runtimeHolder.getJSIContext(),
runtimeHolder,
jsObject
);
}
jni::local_ref<JavaScriptFunction::javaobject> JavaScriptValue::jniGetFunction() {
auto &jsRuntime = runtimeHolder.getJSRuntime();
auto jsFunction = std::make_shared<jsi::Function>(
jsValue->getObject(jsRuntime).asFunction(jsRuntime));
return JavaScriptFunction::newInstance(
runtimeHolder.getJSIContext(),
runtimeHolder,
jsFunction
);
}
jni::local_ref<jni::JArrayClass<JavaScriptValue::javaobject>> JavaScriptValue::getArray() {
auto &jsRuntime = runtimeHolder.getJSRuntime();
auto moduleRegistry = runtimeHolder.getJSIContext();
auto jsArray = jsValue
->getObject(jsRuntime)
.asArray(jsRuntime);
size_t size = jsArray.size(jsRuntime);
auto result = jni::JArrayClass<JavaScriptValue::javaobject>::newArray(size);
for (size_t i = 0; i < size; i++) {
auto element = JavaScriptValue::newInstance(
moduleRegistry,
runtimeHolder,
std::make_shared<jsi::Value>(jsArray.getValueAtIndex(jsRuntime, i))
);
result->setElement(i, element.release());
}
return result;
}
jni::local_ref<jstring> JavaScriptValue::jniKind() {
auto result = kind();
return jni::make_jstring(result);
}
jni::local_ref<jstring> JavaScriptValue::jniGetString() {
auto result = getString();
return jni::make_jstring(result);
}
jni::local_ref<JavaScriptTypedArray::javaobject> JavaScriptValue::getTypedArray() {
auto &jsRuntime = runtimeHolder.getJSRuntime();
auto jsObject = std::make_shared<jsi::Object>(jsValue->getObject(jsRuntime));
return JavaScriptTypedArray::newInstance(
runtimeHolder.getJSIContext(),
runtimeHolder,
jsObject
);
}
jni::local_ref<JavaScriptValue::javaobject> JavaScriptValue::newInstance(
JSIContext *jsiContext,
std::weak_ptr<JavaScriptRuntime> runtime,
std::shared_ptr<jsi::Value> jsValue
) {
auto value = JavaScriptValue::newObjectCxxArgs(
std::move(runtime),
std::move(jsValue)
);
jsiContext->jniDeallocator->addReference(value);
return value;
}
} // namespace expo

View File

@@ -0,0 +1,102 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include "JSIObjectWrapper.h"
#include "WeakRuntimeHolder.h"
#include "JavaScriptTypedArray.h"
#include "JNIDeallocator.h"
#include <fbjni/fbjni.h>
#include <jsi/jsi.h>
#include <memory>
namespace jni = facebook::jni;
namespace jsi = facebook::jsi;
namespace expo {
class JavaScriptRuntime;
class JavaScriptObject;
class JavaScriptTypedArray;
class JavaScriptFunction;
/**
* Represents any JavaScript value. Its purpose is to expose the `jsi::Value` API back to Kotlin.
*/
class JavaScriptValue : public jni::HybridClass<JavaScriptValue, Destructible>, JSIValueWrapper {
public:
static auto constexpr
kJavaDescriptor = "Lexpo/modules/kotlin/jni/JavaScriptValue;";
static auto constexpr TAG = "JavaScriptValue";
static void registerNatives();
static jni::local_ref<JavaScriptValue::javaobject> newInstance(
JSIContext *jsiContext,
std::weak_ptr<JavaScriptRuntime> runtime,
std::shared_ptr<jsi::Value> jsValue
);
JavaScriptValue(
std::weak_ptr<JavaScriptRuntime> runtime,
std::shared_ptr<jsi::Value> jsValue
);
JavaScriptValue(
WeakRuntimeHolder runtime,
std::shared_ptr<jsi::Value> jsValue
);
std::shared_ptr<jsi::Value> get() override;
std::string kind();
bool isNull();
bool isUndefined();
bool isBool();
bool isNumber();
bool isString();
bool isSymbol();
bool isFunction();
bool isArray();
bool isObject();
bool isTypedArray();
bool getBool();
double getDouble();
std::string getString();
jni::local_ref<jni::HybridClass<JavaScriptObject, Destructible>::javaobject> getObject();
jni::local_ref<jni::JArrayClass<JavaScriptValue::javaobject>> getArray();
jni::local_ref<JavaScriptTypedArray::javaobject> getTypedArray();
jni::local_ref<jni::HybridClass<JavaScriptFunction, Destructible>::javaobject> jniGetFunction();
private:
friend HybridBase;
WeakRuntimeHolder runtimeHolder;
std::shared_ptr<jsi::Value> jsValue;
jni::local_ref<jstring> jniKind();
jni::local_ref<jstring> jniGetString();
};
} // namespace expo

View File

@@ -0,0 +1,49 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#include "JavaScriptWeakObject.h"
#include "JSIContext.h"
namespace expo {
void JavaScriptWeakObject::registerNatives() {
registerHybrid({
makeNativeMethod("lock", JavaScriptWeakObject::lock),
});
}
jni::local_ref<JavaScriptObject::javaobject> JavaScriptWeakObject::lock() {
jsi::Runtime &rt = _runtimeHolder.getJSRuntime();
jsi::Value value = _weakObject->lock(rt);
if (value.isUndefined()) {
return nullptr;
}
std::shared_ptr<jsi::Object> objectPtr =
std::make_shared<jsi::Object>(value.asObject(rt));
if (!objectPtr) {
return nullptr;
}
return JavaScriptObject::newInstance(_runtimeHolder.getJSIContext(),
_runtimeHolder, objectPtr);
}
jni::local_ref<jni::HybridClass<JavaScriptWeakObject, Destructible>::javaobject>
JavaScriptWeakObject::newInstance(
JSIContext *jSIContext,
std::weak_ptr<JavaScriptRuntime> runtime,
std::shared_ptr<jsi::Object> jsObject) {
auto weakObject = JavaScriptWeakObject::newObjectCxxArgs(std::move(runtime),
std::move(jsObject));
jSIContext->jniDeallocator->addReference(weakObject);
return weakObject;
}
JavaScriptWeakObject::JavaScriptWeakObject(
WeakRuntimeHolder runtime, std::shared_ptr<jsi::Object> jsObject)
: _runtimeHolder(std::move(runtime)) {
_runtimeHolder.ensureRuntimeIsValid();
jsi::Runtime &rt = _runtimeHolder.getJSRuntime();
_weakObject = std::make_shared<jsi::WeakObject>(rt, *jsObject);
}
} // namespace expo

View File

@@ -0,0 +1,52 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#pragma once
#include "JNIDeallocator.h"
#include "JavaScriptObject.h"
#include "WeakRuntimeHolder.h"
#include <fbjni/fbjni.h>
#include <jsi/jsi.h>
#include <memory>
namespace jni = facebook::jni;
namespace jsi = facebook::jsi;
namespace expo {
class JavaScriptObject;
/**
* Represents to a jsi::WeakObject.
*/
class JavaScriptWeakObject
: public jni::HybridClass<JavaScriptWeakObject, Destructible> {
public:
static auto constexpr kJavaDescriptor =
"Lexpo/modules/kotlin/jni/JavaScriptWeakObject;";
static auto constexpr TAG = "JavaScriptWeakObject";
static void registerNatives();
static jni::local_ref<
jni::HybridClass<JavaScriptWeakObject, Destructible>::javaobject>
newInstance(JSIContext *jSIContext,
std::weak_ptr<JavaScriptRuntime> runtime,
std::shared_ptr<jsi::Object> jsObject);
jni::local_ref<JavaScriptObject::javaobject> lock();
private:
JavaScriptWeakObject(WeakRuntimeHolder runtime,
std::shared_ptr<jsi::Object> jsObject);
private:
friend HybridBase;
WeakRuntimeHolder _runtimeHolder;
std::shared_ptr<jsi::WeakObject> _weakObject;
};
} // namespace expo

View File

@@ -0,0 +1,386 @@
#include "MethodMetadata.h"
#include "JSIContext.h"
#include "JavaScriptValue.h"
#include "JavaScriptObject.h"
#include "JavaScriptTypedArray.h"
#include "JavaReferencesCache.h"
#include "Exceptions.h"
#include "JavaCallback.h"
#include "types/JNIToJSIConverter.h"
#include "JSReferencesCache.h"
#include <utility>
#include <functional>
#include <unistd.h>
#include <optional>
#include <ReactCommon/LongLivedObject.h>
namespace jni = facebook::jni;
namespace jsi = facebook::jsi;
namespace react = facebook::react;
namespace expo {
jni::local_ref<JavaCallback::JavaPart> createJavaCallback(
jsi::Function &&resolveFunction,
jsi::Function &&rejectFunction,
jsi::Runtime &rt
) {
JSIContext *jsiContext = getJSIContext(rt);
std::shared_ptr<react::CallInvoker> jsInvoker = jsiContext->runtimeHolder->jsInvoker;
std::shared_ptr<JavaCallback::CallbackContext> callbackContext = std::make_shared<JavaCallback::CallbackContext>(
rt,
std::move(jsInvoker),
std::move(resolveFunction),
std::move(rejectFunction)
);
#if REACT_NATIVE_TARGET_VERSION >= 75
facebook::react::LongLivedObjectCollection::get(rt).add(callbackContext);
#else
facebook::react::LongLivedObjectCollection::get().add(callbackContext);
#endif
return JavaCallback::newInstance(jsiContext, std::move(callbackContext));
}
jobjectArray MethodMetadata::convertJSIArgsToJNI(
JNIEnv *env,
jsi::Runtime &rt,
const jsi::Value &thisValue,
const jsi::Value *args,
size_t count
) {
// This function takes the owner, so the args number is higher because we have access to the thisValue.
if (takesOwner) {
count++;
}
// The `count < argTypes.size()` case is handled by the Kotlin part
if (count > argTypes.size()) {
throwNewJavaException(
InvalidArgsNumberException::create(
count,
argTypes.size()
).get()
);
}
auto argumentArray = env->NewObjectArray(
count,
JavaReferencesCache::instance()->getJClass("java/lang/Object").clazz,
nullptr
);
const auto getCurrentArg = [&thisValue, args, takesOwner = takesOwner](
size_t index
) -> const jsi::Value & {
if (!takesOwner) {
return args[index];
}
if (index != 0) {
return args[index - 1];
}
return thisValue;
};
for (size_t argIndex = 0; argIndex < count; argIndex++) {
const jsi::Value &arg = getCurrentArg(argIndex);
auto &type = argTypes[argIndex];
if (type->converter->canConvert(rt, arg)) {
auto converterValue = type->converter->convert(rt, env, arg);
env->SetObjectArrayElement(argumentArray, argIndex, converterValue);
env->DeleteLocalRef(converterValue);
} else if (arg.isNull() || arg.isUndefined()) {
// If value is null or undefined, we just passes a null
// Kotlin code will check if expected type is nullable.
continue;
} else {
auto stringRepresentation = arg.toString(rt).utf8(rt);
throwNewJavaException(
UnexpectedException::create(
"[" + this->name + "] Cannot convert '" + stringRepresentation +
"' to a Kotlin type.").get()
);
}
}
return argumentArray;
}
MethodMetadata::MethodMetadata(
std::string name,
bool takesOwner,
bool isAsync,
jni::local_ref<jni::JArrayClass<ExpectedType>> expectedArgTypes,
jni::global_ref<jobject> &&jBodyReference
) : name(std::move(name)),
takesOwner(takesOwner),
isAsync(isAsync),
jBodyReference(std::move(jBodyReference)) {
size_t argsSize = expectedArgTypes->size();
argTypes.reserve(argsSize);
for (size_t i = 0; i < argsSize; i++) {
auto expectedType = expectedArgTypes->getElement(i);
argTypes.push_back(
std::make_unique<AnyType>(std::move(expectedType))
);
}
}
MethodMetadata::MethodMetadata(
std::string name,
bool takesOwner,
bool isAsync,
std::vector<std::unique_ptr<AnyType>> &&expectedArgTypes,
jni::global_ref<jobject> &&jBodyReference
) : name(std::move(name)),
takesOwner(takesOwner),
isAsync(isAsync),
argTypes(std::move(expectedArgTypes)),
jBodyReference(std::move(jBodyReference)) {
}
std::shared_ptr<jsi::Function> MethodMetadata::toJSFunction(
jsi::Runtime &runtime
) {
if (body == nullptr) {
if (isAsync) {
body = std::make_shared<jsi::Function>(toAsyncFunction(runtime));
} else {
body = std::make_shared<jsi::Function>(toSyncFunction(runtime));
}
}
return body;
}
jsi::Function MethodMetadata::toSyncFunction(
jsi::Runtime &runtime
) {
auto weakThis = weak_from_this();
return jsi::Function::createFromHostFunction(
runtime,
getJSIContext(runtime)->jsRegistry->getPropNameID(runtime, name),
argTypes.size(),
[weakThis = std::move(weakThis)](
jsi::Runtime &rt,
const jsi::Value &thisValue,
const jsi::Value *args,
size_t count
) -> jsi::Value {
try {
auto thisPtr = weakThis.lock();
if (thisPtr == nullptr) {
return jsi::Value::undefined();
}
return thisPtr->callSync(
rt,
thisValue,
args,
count
);
} catch (jni::JniException &jniException) {
rethrowAsCodedError(rt, jniException);
}
});
}
jni::local_ref<jobject> MethodMetadata::callJNISync(
JNIEnv *env,
jsi::Runtime &rt,
const jsi::Value &thisValue,
const jsi::Value *args,
size_t count
) {
if (this->jBodyReference == nullptr) {
return nullptr;
}
auto convertedArgs = convertJSIArgsToJNI(env, rt, thisValue, args, count);
// Cast in this place is safe, cause we know that this function is promise-less.
auto syncFunction = jni::static_ref_cast<JNIFunctionBody>(this->jBodyReference);
auto result = syncFunction->invoke(
convertedArgs
);
env->DeleteLocalRef(convertedArgs);
return result;
}
jsi::Value MethodMetadata::callSync(
jsi::Runtime &rt,
const jsi::Value &thisValue,
const jsi::Value *args,
size_t count
) {
JNIEnv *env = jni::Environment::current();
/**
* This will push a new JNI stack frame for the LocalReferences in this
* function call. When the stack frame for this lambda is popped,
* all LocalReferences are deleted.
*/
jni::JniLocalScope scope(env, (int) count);
auto result = this->callJNISync(env, rt, thisValue, args, count);
return convert(env, rt, std::move(result));
}
jsi::Function MethodMetadata::toAsyncFunction(
jsi::Runtime &runtime
) {
auto weakThis = weak_from_this();
return jsi::Function::createFromHostFunction(
runtime,
getJSIContext(runtime)->jsRegistry->getPropNameID(runtime, name),
argTypes.size(),
[weakThis = std::move(weakThis)](
jsi::Runtime &rt,
const jsi::Value &thisValue,
const jsi::Value *args,
size_t count
) -> jsi::Value {
auto thisPtr = weakThis.lock();
if (thisPtr == nullptr) {
return jsi::Value::undefined();
}
JSIContext *jsiContext = getJSIContext(rt);
JNIEnv *env = jni::Environment::current();
/**
* This will push a new JNI stack frame for the LocalReferences in this
* function call. When the stack frame for this lambda is popped,
* all LocalReferences are deleted.
*/
jni::JniLocalScope scope(env, (int) count);
auto &Promise = jsiContext->jsRegistry->getObject<jsi::Function>(
JSReferencesCache::JSKeys::PROMISE
);
try {
auto convertedArgs = thisPtr->convertJSIArgsToJNI(env, rt, thisValue, args, count);
auto globalConvertedArgs = (jobjectArray) env->NewGlobalRef(convertedArgs);
env->DeleteLocalRef(convertedArgs);
// Creates a JSI promise
jsi::Value promise = Promise.callAsConstructor(
rt,
thisPtr->createPromiseBody(rt, globalConvertedArgs)
);
return promise;
} catch (jni::JniException &jniException) {
jni::local_ref<jni::JThrowable> unboxedThrowable = jniException.getThrowable();
if (!unboxedThrowable->isInstanceOf(CodedException::javaClassLocal())) {
unboxedThrowable = UnexpectedException::create(jniException.what());
}
auto codedException = jni::static_ref_cast<CodedException>(unboxedThrowable);
auto code = codedException->getCode();
auto message = codedException->getLocalizedMessage().value_or("");
jsi::Value promise = Promise.callAsConstructor(
rt,
jsi::Function::createFromHostFunction(
rt,
jsiContext->jsRegistry->getPropNameID(rt, "promiseFn"),
2,
[code, message](
jsi::Runtime &rt,
const jsi::Value &thisVal,
const jsi::Value *promiseConstructorArgs,
size_t promiseConstructorArgCount
) {
if (promiseConstructorArgCount != 2) {
throw std::invalid_argument("Promise fn arg count must be 2");
}
jsi::Function rejectJSIFn = promiseConstructorArgs[1].getObject(rt).getFunction(rt);
rejectJSIFn.call(
rt,
makeCodedError(
rt,
jsi::String::createFromUtf8(rt, code),
jsi::String::createFromUtf8(rt, message)
)
);
return jsi::Value::undefined();
}
)
);
return promise;
}
}
);
}
jsi::Function MethodMetadata::createPromiseBody(
jsi::Runtime &runtime,
jobjectArray globalArgs
) {
return jsi::Function::createFromHostFunction(
runtime,
getJSIContext(runtime)->jsRegistry->getPropNameID(runtime, "promiseFn"),
2,
[this, globalArgs](
jsi::Runtime &rt,
const jsi::Value &thisVal,
const jsi::Value *promiseConstructorArgs,
size_t promiseConstructorArgCount
) {
if (promiseConstructorArgCount != 2) {
throw std::invalid_argument("Promise fn arg count must be 2");
}
jsi::Function resolveJSIFn = promiseConstructorArgs[0].getObject(rt).getFunction(rt);
jsi::Function rejectJSIFn = promiseConstructorArgs[1].getObject(rt).getFunction(rt);
jobject javaCallback = createJavaCallback(
std::move(resolveJSIFn),
std::move(rejectJSIFn),
rt
).release();
JNIEnv *env = jni::Environment::current();
auto &jPromise = JavaReferencesCache::instance()->getJClass(
"expo/modules/kotlin/jni/PromiseImpl");
jmethodID jPromiseConstructor = jPromise.getMethod(
"<init>",
"(Lexpo/modules/kotlin/jni/JavaCallback;)V"
);
// Creates a promise object
jobject promise = env->NewObject(
jPromise.clazz,
jPromiseConstructor,
javaCallback
);
// Cast in this place is safe, cause we know that this function expects promise.
auto asyncFunction = jni::static_ref_cast<JNIAsyncFunctionBody>(this->jBodyReference);
asyncFunction->invoke(
globalArgs,
promise
);
// We have to remove the local reference to the promise object.
// It doesn't mean that the promise will be deallocated, but rather that we move
// the ownership to the `JNIAsyncFunctionBody`.
env->DeleteLocalRef(promise);
env->DeleteGlobalRef(globalArgs);
return jsi::Value::undefined();
}
);
}
} // namespace expo

View File

@@ -0,0 +1,127 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include "types/CppType.h"
#include "types/ExpectedType.h"
#include "types/AnyType.h"
#include <jsi/jsi.h>
#include <fbjni/fbjni.h>
#include <ReactCommon/TurboModuleUtils.h>
#include <react/jni/ReadableNativeArray.h>
#include <memory>
#include <vector>
#include <folly/dynamic.h>
#include <jsi/JSIDynamic.h>
namespace jni = facebook::jni;
namespace jsi = facebook::jsi;
namespace react = facebook::react;
namespace expo {
class JSIContext;
/**
* A class that holds information about the exported function.
*/
class MethodMetadata : public std::enable_shared_from_this<MethodMetadata> {
public:
/**
* Function name
*/
std::string name;
/**
* Whether this function takes owner
*/
bool takesOwner;
/**
* Whether this function is async
*/
bool isAsync;
/**
* Representation of expected argument types.
*/
std::vector<std::unique_ptr<AnyType>> argTypes;
MethodMetadata(
std::string name,
bool takesOwner,
bool isAsync,
jni::local_ref<jni::JArrayClass<ExpectedType>> expectedArgTypes,
jni::global_ref<jobject> &&jBodyReference
);
MethodMetadata(
std::string name,
bool takesOwner,
bool isAsync,
std::vector<std::unique_ptr<AnyType>> &&expectedArgTypes,
jni::global_ref<jobject> &&jBodyReference
);
// We deleted the copy contractor to not deal with transforming the ownership of the `jBodyReference`.
MethodMetadata(const MethodMetadata &) = delete;
MethodMetadata(MethodMetadata &&other) = default;
/**
* Transforms metadata to a jsi::Function.
*
* @param runtime
* @return shared ptr to the jsi::Function that wrapped the underlying Kotlin's function.
*/
std::shared_ptr<jsi::Function> toJSFunction(
jsi::Runtime &runtime
);
/**
* Calls the underlying Kotlin function.
*/
jsi::Value callSync(
jsi::Runtime &rt,
const jsi::Value &thisValue,
const jsi::Value *args,
size_t count
);
jni::local_ref<jobject> callJNISync(
JNIEnv *env,
jsi::Runtime &rt,
const jsi::Value &thisValue,
const jsi::Value *args,
size_t count
);
private:
/**
* Reference to one of two java objects - `JNIFunctionBody` or `JNIAsyncFunctionBody`.
*
* In case when `isAsync` is `true`, this variable will point to `JNIAsyncFunctionBody`.
* Otherwise to `JNIFunctionBody`
*/
jni::global_ref<jobject> jBodyReference;
/**
* To not create a jsi::Function always when we need it, we cached that value.
*/
std::shared_ptr<jsi::Function> body = nullptr;
jsi::Function toSyncFunction(jsi::Runtime &runtime);
jsi::Function toAsyncFunction(jsi::Runtime &runtime);
jsi::Function createPromiseBody(
jsi::Runtime &runtime,
jobjectArray globalArgs
);
jobjectArray convertJSIArgsToJNI(
JNIEnv *env,
jsi::Runtime &rt,
const jsi::Value &thisValue,
const jsi::Value *args,
size_t count
);
};
} // namespace expo

View File

@@ -0,0 +1,49 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include <fbjni/fbjni.h>
#include <android/log.h>
namespace jni = facebook::jni;
namespace expo {
/*
* A wrapper for a global reference that can be deallocated on any thread.
* It should be used with smart pointer. That structure can't be copied or moved.
*/
template<typename T>
class ThreadSafeJNIGlobalRef {
public:
ThreadSafeJNIGlobalRef(jobject globalRef) : globalRef(globalRef) {}
ThreadSafeJNIGlobalRef(const ThreadSafeJNIGlobalRef &other) = delete;
ThreadSafeJNIGlobalRef(ThreadSafeJNIGlobalRef &&other) = delete;
ThreadSafeJNIGlobalRef &operator=(const ThreadSafeJNIGlobalRef &other) = delete;
ThreadSafeJNIGlobalRef &operator=(ThreadSafeJNIGlobalRef &&other) = delete;
void use(std::function<void(jni::alias_ref<T> globalRef)> &&action) {
if (globalRef == nullptr) {
__android_log_print(ANDROID_LOG_WARN, "ExpoModulesCore", "ThreadSafeJNIGlobalRef was used after deallocation.");
return;
}
jni::ThreadScope::WithClassLoader([this, action = std::move(action)]() {
jni::alias_ref<jobject> aliasRef = jni::wrap_alias(globalRef);
jni::alias_ref<T> jsiContextRef = jni::static_ref_cast<T>(aliasRef);
action(jsiContextRef);
});
}
~ThreadSafeJNIGlobalRef() {
if (globalRef != nullptr) {
jni::ThreadScope::WithClassLoader([this] {
jni::Environment::current()->DeleteGlobalRef(this->globalRef);
});
}
}
jobject globalRef;
};
} // namespace expo

View File

@@ -0,0 +1,24 @@
#include "WeakRuntimeHolder.h"
#include "JavaScriptRuntime.h"
#include "JSIContext.h"
namespace expo {
WeakRuntimeHolder::WeakRuntimeHolder(std::weak_ptr<JavaScriptRuntime> runtime)
: std::weak_ptr<JavaScriptRuntime>(std::move(runtime)) {}
jsi::Runtime &WeakRuntimeHolder::getJSRuntime() {
auto runtime = lock();
assert((runtime != nullptr) && "JS Runtime was used after deallocation");
return runtime->get();
}
void WeakRuntimeHolder::ensureRuntimeIsValid() {
assert((!expired()) && "JS Runtime was used after deallocation");
}
JSIContext *WeakRuntimeHolder::getJSIContext() {
auto runtime = lock();
assert((runtime != nullptr) && "JS Runtime was used after deallocation");
return expo::getJSIContext(runtime->get());
}
} // namespace expo

View File

@@ -0,0 +1,40 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include <jsi/jsi.h>
#include <memory>
namespace expo {
namespace jsi = facebook::jsi;
class JavaScriptRuntime;
class JSIContext;
/**
* A convenient class to access underlying jni::Runtime and hold a weak reference to expo::JavaScriptRuntime.
* It's working like std::weak_ptr but can have more helper methods.
*/
class WeakRuntimeHolder : public std::weak_ptr<JavaScriptRuntime> {
public:
WeakRuntimeHolder() = default;
WeakRuntimeHolder(WeakRuntimeHolder const &) = default;
WeakRuntimeHolder(WeakRuntimeHolder &&) = default;
WeakRuntimeHolder(std::weak_ptr<JavaScriptRuntime> runtime);
/**
* @return an reference to the jsi::Runtime.
*/
jsi::Runtime &getJSRuntime();
JSIContext *getJSIContext();
void ensureRuntimeIsValid();
};
} // namespace expo

View File

@@ -0,0 +1,63 @@
#pragma once
#include <fbjni/fbjni.h>
namespace jni = facebook::jni;
namespace expo::java {
template<typename E = jobject>
struct Iterable : public jni::JavaClass<Iterable<E>> {
constexpr static auto kJavaDescriptor = "Ljava/lang/Iterable;";
};
template<typename E = jobject>
struct Collection : public jni::JavaClass<Collection<E>, Iterable<E>> {
constexpr static auto kJavaDescriptor = "Ljava/util/Collection;";
bool add(jni::alias_ref<E> element) {
static auto addMethod = Collection<E>::javaClassStatic()->
template getMethod<jboolean(jni::alias_ref<jni::JObject>)>("add");
return addMethod(this->self(), element);
}
};
template<typename E = jobject>
struct List : public jni::JavaClass<List<E>, Collection<E>> {
constexpr static auto kJavaDescriptor = "Ljava/util/List;";
};
template<typename E = jobject>
struct ArrayList : public jni::JavaClass<ArrayList<E>, List<E>> {
constexpr static auto kJavaDescriptor = "Ljava/util/ArrayList;";
static jni::local_ref<typename ArrayList<E>::javaobject> create(size_t size) {
return ArrayList<E>::newInstance((int) size);
}
};
template<typename K = jobject, typename V = jobject>
struct Map : public jni::JavaClass<Map<K, V>> {
constexpr static auto kJavaDescriptor = "Ljava/util/Map;";
jni::local_ref<V> put(jni::alias_ref<K> key, jni::alias_ref<V> value) {
static auto putMethod = Map<K, V>::javaClassStatic()->
template getMethod<jni::local_ref<V>(jni::alias_ref<K>, jni::alias_ref<V>)>("put");
return putMethod(this->self(), key, value);
}
};
template<typename K = jobject, typename V = jobject>
struct HashMap : public jni::JavaClass<HashMap<K, V>, Map<K, V>> {
constexpr static auto kJavaDescriptor = "Ljava/util/HashMap;";
};
template<typename K = jobject, typename V = jobject>
struct LinkedHashMap : public jni::JavaClass<LinkedHashMap<K, V>, HashMap<K, V>> {
constexpr static auto kJavaDescriptor = "Ljava/util/LinkedHashMap;";
static jni::local_ref<typename LinkedHashMap<K, V>::javaobject> create(size_t size) {
return LinkedHashMap<K, V>::newInstance((int) size);
}
};
} // namespace expo::java

View File

@@ -0,0 +1,11 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#include "AnyType.h"
#include "FrontendConverterProvider.h"
#include "../JSIContext.h"
namespace expo {
AnyType::AnyType(
jni::local_ref<expo::ExpectedType> expectedType
) : converter(FrontendConverterProvider::instance()->obtainConverter(std::move(expectedType))) {}
} // namespace expo

View File

@@ -0,0 +1,27 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include "ExpectedType.h"
#include "FrontendConverter.h"
#include <fbjni/fbjni.h>
namespace jni = facebook::jni;
namespace expo {
class JSIContext;
/**
* Holds information about the expected Kotlin type.
*/
class AnyType {
public:
AnyType(jni::local_ref<ExpectedType> expectedType);
/*
* An instance of convert that should be used to convert from the jsi to the expected JNI type.
*/
std::shared_ptr<FrontendConverter> converter;
};
} // namespace expo

View File

@@ -0,0 +1,32 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
namespace expo {
/**
* A cpp version of the `expo.modules.kotlin.jni.CppType` enum.
* Used to determine which representation of the js value should be sent to the Kotlin.
*/
enum CppType {
NONE = 0,
DOUBLE = 1 << 0,
INT = 1 << 1,
LONG = 1 << 2,
FLOAT = 1 << 3,
BOOLEAN = 1 << 4,
STRING = 1 << 5,
JS_OBJECT = 1 << 6,
JS_VALUE = 1 << 7,
READABLE_ARRAY = 1 << 8,
READABLE_MAP = 1 << 9,
UINT8_TYPED_ARRAY = 1 << 10,
TYPED_ARRAY = 1 << 11,
PRIMITIVE_ARRAY = 1 << 12,
LIST = 1 << 13,
MAP = 1 << 14,
VIEW_TAG = 1 << 15,
SHARED_OBJECT_ID = 1 << 16,
JS_FUNCTION = 1 << 17,
ANY = 1 << 18
};
} // namespace expo

View File

@@ -0,0 +1,99 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#include "ExpectedType.h"
namespace expo {
jni::local_ref<ExpectedType::javaobject> SingleType::getFirstParameterType() {
static const auto method = getClass()->getMethod<jni::local_ref<ExpectedType::javaobject>()>(
"getFirstParameterType");
return method(self());
}
jni::local_ref<ExpectedType::javaobject> SingleType::getSecondParameterType() {
static const auto method = getClass()->getMethod<jni::local_ref<ExpectedType::javaobject>()>(
"getSecondParameterType");
return method(self());
}
CppType SingleType::getCppType() {
static const auto method = getClass()->getMethod<int()>("getCppType");
return static_cast<CppType>(method(self()));
}
CppType ExpectedType::getCombinedTypes() {
static const auto method = getClass()->getMethod<int()>("getCombinedTypes");
return static_cast<CppType>(method(self()));
}
jni::local_ref<SingleType::javaobject> ExpectedType::getFirstType() {
static const auto method = getClass()->getMethod<jni::local_ref<SingleType::javaobject>()>(
"getFirstType");
return method(self());
}
std::string ExpectedType::getJClassString(bool allowsPrimitives) {
CppType type = this->getCombinedTypes();
if (type == CppType::DOUBLE) {
if (allowsPrimitives) {
return "D";
}
return "java/lang/Double";
}
if (type == CppType::BOOLEAN) {
if (allowsPrimitives) {
return "Z";
}
return "java/lang/Boolean";
}
if (type == CppType::INT) {
if (allowsPrimitives) {
return "I";
}
return "java/lang/Integer";
}
if (type == CppType::FLOAT) {
return "java/lang/Float";
}
if (type == CppType::STRING) {
return "java/lang/String";
}
if (type == CppType::JS_OBJECT) {
return "expo/modules/kotlin/jni/JavaScriptObject";
}
if (type == CppType::JS_VALUE) {
return "expo/modules/kotlin/jni/JavaScriptValue";
}
if (type == CppType::READABLE_ARRAY) {
return "com/facebook/react/bridge/ReadableNativeArray";
}
if (type == CppType::READABLE_MAP) {
return "com/facebook/react/bridge/ReadableNativeMap";
}
if (type == CppType::UINT8_TYPED_ARRAY) {
return "[B";
}
if (type == CppType::TYPED_ARRAY) {
return "expo/modules/kotlin/jni/JavaScriptTypedArray";
}
if (type == CppType::PRIMITIVE_ARRAY) {
auto innerType = this->getFirstType()->getFirstParameterType()->getJClassString(true);
if (innerType.size() == 1) {
// is a primitive type
return "[" + innerType;
}
return "[L" + innerType + ";";
}
if (type == CppType::LIST) {
return "java/util/ArrayList";
}
return "java/lang/Object";
}
jni::local_ref<jni::JArrayClass<SingleType>::javaobject> ExpectedType::getPossibleTypes() {
static const auto method = getClass()->getMethod<jni::local_ref<jni::JArrayClass<SingleType>::javaobject>()>(
"getPossibleTypes");
return method(self());
}
} // namespace expo

View File

@@ -0,0 +1,49 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include "CppType.h"
#include <fbjni/fbjni.h>
namespace jni = facebook::jni;
namespace expo {
class ExpectedType;
/**
* A C++ representation of the [expo.modules.kotlin.jni.SingleType] class.
*/
class SingleType : public jni::JavaClass<SingleType> {
public:
static auto constexpr
kJavaDescriptor = "Lexpo/modules/kotlin/jni/SingleType;";
CppType getCppType();
jni::local_ref<jni::JavaClass<ExpectedType>::javaobject> getFirstParameterType();
jni::local_ref<jni::JavaClass<ExpectedType>::javaobject> getSecondParameterType();
};
/**
* A C++ representation of the [expo.modules.kotlin.jni.ExpectedType] class.
*/
class ExpectedType : public jni::JavaClass<ExpectedType> {
public:
static auto constexpr
kJavaDescriptor = "Lexpo/modules/kotlin/jni/ExpectedType;";
CppType getCombinedTypes();
jni::local_ref<SingleType::javaobject> getFirstType();
/**
* Converts [ExpectedType] to a string representing a java class.
* If the allowsPrimitives is set to true type like int will be represented as a primitives.
*/
std::string getJClassString(bool allowsPrimitives = false);
jni::local_ref<jni::JArrayClass<SingleType>::javaobject> getPossibleTypes();
};
} // namespace expo

View File

@@ -0,0 +1,600 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#include "FrontendConverter.h"
#include "ExpectedType.h"
#include "FrontendConverterProvider.h"
#include "../JavaReferencesCache.h"
#include "../Exceptions.h"
#include "../JavaScriptTypedArray.h"
#include "../JSIContext.h"
#include "../JavaScriptObject.h"
#include "../JavaScriptValue.h"
#include "../JavaScriptFunction.h"
#include "../javaclasses/Collections.h"
#include "react/jni/ReadableNativeMap.h"
#include "react/jni/ReadableNativeArray.h"
#include <jsi/JSIDynamic.h>
#include <utility>
#include <algorithm>
namespace jni = facebook::jni;
namespace jsi = facebook::jsi;
namespace react = facebook::react;
namespace expo {
jobject IntegerFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
auto &integerClass = JavaReferencesCache::instance()
->getJClass("java/lang/Integer");
jmethodID integerConstructor = integerClass.getMethod("<init>", "(I)V");
return env->NewObject(integerClass.clazz, integerConstructor,
static_cast<int>(value.asNumber()));
}
bool IntegerFrontendConverter::canConvert(jsi::Runtime &rt, const jsi::Value &value) const {
return value.isNumber();
}
jobject LongFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
auto &longClass = JavaReferencesCache::instance()
->getJClass("java/lang/Long");
jmethodID longConstructor = longClass.getMethod("<init>", "(J)V");
return env->NewObject(longClass.clazz, longConstructor,
static_cast<jlong>(value.asNumber()));
}
bool LongFrontendConverter::canConvert(jsi::Runtime &rt, const jsi::Value &value) const {
return value.isNumber();
}
jobject FloatFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
auto &floatClass = JavaReferencesCache::instance()
->getJClass("java/lang/Float");
jmethodID floatConstructor = floatClass.getMethod("<init>", "(F)V");
return env->NewObject(floatClass.clazz, floatConstructor,
static_cast<float>(value.asNumber()));
}
bool FloatFrontendConverter::canConvert(jsi::Runtime &rt, const jsi::Value &value) const {
return value.isNumber();
}
jobject BooleanFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
auto &booleanClass = JavaReferencesCache::instance()
->getJClass("java/lang/Boolean");
jmethodID booleanConstructor = booleanClass.getMethod("<init>", "(Z)V");
return env->NewObject(booleanClass.clazz, booleanConstructor, value.asBool());
}
bool BooleanFrontendConverter::canConvert(jsi::Runtime &rt, const jsi::Value &value) const {
return value.isBool();
}
jobject DoubleFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
auto &doubleClass = JavaReferencesCache::instance()
->getJClass("java/lang/Double");
jmethodID doubleConstructor = doubleClass.getMethod("<init>", "(D)V");
return env->NewObject(doubleClass.clazz, doubleConstructor, value.asNumber());
}
bool DoubleFrontendConverter::canConvert(jsi::Runtime &rt, const jsi::Value &value) const {
return value.isNumber();
}
jobject StringFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
return env->NewStringUTF(value.asString(rt).utf8(rt).c_str());
}
bool StringFrontendConverter::canConvert(jsi::Runtime &rt, const jsi::Value &value) const {
return value.isString();
}
jobject ReadableNativeArrayFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
auto dynamic = jsi::dynamicFromValue(rt, value);
return react::ReadableNativeArray::newObjectCxxArgs(std::move(dynamic)).release();
}
bool ReadableNativeArrayFrontendConverter::canConvert(
jsi::Runtime &rt,
const jsi::Value &value
) const {
return value.isObject() && value.getObject(rt).isArray(rt);
}
jobject ReadableNativeMapArrayFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
auto dynamic = jsi::dynamicFromValue(rt, value);
return react::ReadableNativeMap::createWithContents(std::move(dynamic)).release();
}
bool ReadableNativeMapArrayFrontendConverter::canConvert(
jsi::Runtime &rt,
const jsi::Value &value) const {
return value.isObject();
}
jobject ByteArrayFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
auto typedArray = TypedArray(rt, value.asObject(rt));
size_t length = typedArray.byteLength(rt);
auto byteArray = jni::JArrayByte::newArray(length);
byteArray->setRegion(0, length, static_cast<const signed char *>(typedArray.getRawPointer(rt)));
return byteArray.release();
}
bool ByteArrayFrontendConverter::canConvert(
jsi::Runtime &rt,
const jsi::Value &value
) const {
if (value.isObject()) {
auto object = value.getObject(rt);
if (isTypedArray(rt, object)) {
auto typedArray = TypedArray(rt, object);
return typedArray.getKind(rt) == TypedArrayKind::Uint8Array;
}
}
return false;
}
jobject TypedArrayFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
JSIContext *jsiContext = getJSIContext(rt);
return JavaScriptTypedArray::newInstance(
jsiContext,
jsiContext->runtimeHolder->weak_from_this(),
std::make_shared<jsi::Object>(value.asObject(rt))
).release();
}
bool TypedArrayFrontendConverter::canConvert(
jsi::Runtime &rt,
const jsi::Value &value
) const {
return value.isObject();
}
jobject JavaScriptValueFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
JSIContext *jsiContext = getJSIContext(rt);
return JavaScriptValue::newInstance(
jsiContext,
jsiContext->runtimeHolder->weak_from_this(),
// TODO(@lukmccall): make sure that copy here is necessary
std::make_shared<jsi::Value>(jsi::Value(rt, value))
).release();
}
bool JavaScriptValueFrontendConverter::canConvert(jsi::Runtime &rt, const jsi::Value &value) const {
return true;
}
jobject JavaScriptObjectFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
JSIContext *jsiContext = getJSIContext(rt);
return JavaScriptObject::newInstance(
jsiContext,
jsiContext->runtimeHolder->weak_from_this(),
std::make_shared<jsi::Object>(value.asObject(rt))
).release();
}
bool JavaScriptObjectFrontendConverter::canConvert(
jsi::Runtime &rt,
const jsi::Value &value
) const {
return value.isObject();
}
jobject JavaScriptFunctionFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
JSIContext *jsiContext = getJSIContext(rt);
return JavaScriptFunction::newInstance(
jsiContext,
jsiContext->runtimeHolder->weak_from_this(),
std::make_shared<jsi::Function>(value.asObject(rt).asFunction(rt))
).release();
}
bool JavaScriptFunctionFrontendConverter::canConvert(
jsi::Runtime &rt,
const jsi::Value &value
) const {
return value.isObject() && value.getObject(rt).isFunction(rt);
}
jobject UnknownFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
auto stringRepresentation = value.toString(rt).utf8(rt);
throwNewJavaException(
UnexpectedException::create(
"Cannot convert '" + stringRepresentation + "' to a Kotlin type.").get()
);
}
bool UnknownFrontendConverter::canConvert(jsi::Runtime &rt, const jsi::Value &value) const {
return true;
}
PolyFrontendConverter::PolyFrontendConverter(
std::vector<std::shared_ptr<FrontendConverter>> converters
) : converters(std::move(converters)) {
}
bool PolyFrontendConverter::canConvert(
jsi::Runtime &rt,
const jsi::Value &value
) const {
// Checks whether any of inner converters can handle the conversion.
return std::any_of(
converters.begin(),
converters.end(),
[&rt = rt, &value = value](const std::shared_ptr<FrontendConverter> &converter) {
return converter->canConvert(rt, value);
}
);
}
jobject PolyFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
for (auto &converter: converters) {
if (converter->canConvert(rt, value)) {
return converter->convert(rt, env, value);
}
}
// That shouldn't happen.
auto stringRepresentation = value.toString(rt).utf8(rt);
throwNewJavaException(
UnexpectedException::create(
"Cannot convert '" + stringRepresentation + "' to a Kotlin type.").get()
);
}
PrimitiveArrayFrontendConverter::PrimitiveArrayFrontendConverter(
jni::local_ref<SingleType::javaobject> expectedType
) {
auto parameterExpectedType = expectedType->getFirstParameterType();
parameterType = parameterExpectedType->getCombinedTypes();
parameterConverter = FrontendConverterProvider::instance()->obtainConverter(
parameterExpectedType
);
javaType = parameterExpectedType->getJClassString();
}
template<typename T, typename A>
jobject createPrimitiveArray(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Array &jsArray,
A (JNIEnv::*arrayConstructor)(jsize),
void (JNIEnv::*setRegion)(A, jsize, jsize, const T *)
) {
size_t size = jsArray.size(rt);
std::vector<T> tmpVector(size);
for (size_t i = 0; i < size; i++) {
tmpVector[i] = (T) jsArray.getValueAtIndex(rt, i).asNumber();
}
auto result = std::invoke(arrayConstructor, env, size);
std::invoke(setRegion, env, result, 0, size, tmpVector.data());
return result;
}
jobject PrimitiveArrayFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
auto jsArray = value.asObject(rt).asArray(rt);
auto _createPrimitiveArray = [&rt, env, &jsArray](
auto arrayConstructor, auto setRegion
) -> jobject {
return createPrimitiveArray(rt, env, jsArray, arrayConstructor, setRegion);
};
if (parameterType == CppType::INT) {
return _createPrimitiveArray(
&JNIEnv::NewIntArray,
&JNIEnv::SetIntArrayRegion
);
}
if (parameterType == CppType::LONG) {
return _createPrimitiveArray(
&JNIEnv::NewLongArray,
&JNIEnv::SetLongArrayRegion
);
}
if (parameterType == CppType::DOUBLE) {
return _createPrimitiveArray(
&JNIEnv::NewDoubleArray,
&JNIEnv::SetDoubleArrayRegion
);
}
if (parameterType == CppType::FLOAT) {
return _createPrimitiveArray(
&JNIEnv::NewFloatArray,
&JNIEnv::SetFloatArrayRegion
);
}
if (parameterType == CppType::BOOLEAN) {
return _createPrimitiveArray(
&JNIEnv::NewBooleanArray,
&JNIEnv::SetBooleanArrayRegion
);
}
size_t size = jsArray.size(rt);
auto result = env->NewObjectArray(
size,
JavaReferencesCache::instance()->getOrLoadJClass(env, javaType).clazz,
nullptr
);
for (size_t i = 0; i < size; i++) {
auto convertedElement = parameterConverter->convert(
rt, env, jsArray.getValueAtIndex(rt, i)
);
env->SetObjectArrayElement(result, i, convertedElement);
env->DeleteLocalRef(convertedElement);
}
return result;
}
bool PrimitiveArrayFrontendConverter::canConvert(jsi::Runtime &rt, const jsi::Value &value) const {
return value.isObject() && value.getObject(rt).isArray(rt);
}
ListFrontendConverter::ListFrontendConverter(
jni::local_ref<SingleType::javaobject> expectedType
) : parameterConverter(
FrontendConverterProvider::instance()->obtainConverter(
expectedType->getFirstParameterType()
)
) {}
jobject ListFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
auto jsArray = value.asObject(rt).asArray(rt);
size_t size = jsArray.size(rt);
auto arrayList = java::ArrayList<jobject>::create(size);
for (size_t i = 0; i < size; i++) {
auto jsValue = jsArray.getValueAtIndex(rt, i);
// TODO(@lukmccall): pass information to CPP if the underlying type is nullable or not.
if (jsValue.isNull() || jsValue.isUndefined()) {
arrayList->add(nullptr);
continue;
}
auto convertedElement = parameterConverter->convert(
rt, env, jsValue
);
arrayList->add(convertedElement);
env->DeleteLocalRef(convertedElement);
}
return arrayList.release();
}
bool ListFrontendConverter::canConvert(jsi::Runtime &rt, const jsi::Value &value) const {
return value.isObject() && value.getObject(rt).isArray(rt);
}
MapFrontendConverter::MapFrontendConverter(
jni::local_ref<SingleType::javaobject> expectedType
) : valueConverter(
FrontendConverterProvider::instance()->obtainConverter(
expectedType->getFirstParameterType()
)
) {}
jobject MapFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
auto jsObject = value.asObject(rt);
auto propertyNames = jsObject.getPropertyNames(rt);
size_t size = propertyNames.size(rt);
auto map = java::LinkedHashMap<jobject, jobject>::create(size);
for (size_t i = 0; i < size; i++) {
auto key = propertyNames.getValueAtIndex(rt, i).getString(rt);
auto jsValue = jsObject.getProperty(rt, key);
auto convertedKey = env->NewStringUTF(key.utf8(rt).c_str());
// TODO(@lukmccall): pass information to CPP if the underlying type is nullable or not.
if (jsValue.isNull() || jsValue.isUndefined()) {
map->put(convertedKey, nullptr);
continue;
}
auto convertedValue = valueConverter->convert(
rt, env, jsValue
);
map->put(convertedKey, convertedValue);
env->DeleteLocalRef(convertedKey);
env->DeleteLocalRef(convertedValue);
}
return map.release();
}
bool MapFrontendConverter::canConvert(
jsi::Runtime &rt,
const jsi::Value &value
) const {
return value.isObject();
}
jobject ViewTagFrontendConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
auto nativeTag = value.asObject(rt).getProperty(rt, "nativeTag");
if (nativeTag.isNull()) {
return nullptr;
}
auto viewTag = (int) nativeTag.getNumber();
auto &integerClass = JavaReferencesCache::instance()
->getJClass("java/lang/Integer");
jmethodID integerConstructor = integerClass.getMethod("<init>", "(I)V");
return env->NewObject(integerClass.clazz, integerConstructor, viewTag);
}
bool ViewTagFrontendConverter::canConvert(jsi::Runtime &rt, const jsi::Value &value) const {
return value.isObject() && value.getObject(rt).hasProperty(rt, "nativeTag");
}
jobject SharedObjectIdConverter::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
auto objectId = value.asObject(rt).getProperty(rt, "__expo_shared_object_id__");
if (objectId.isNull()) {
return nullptr;
}
auto viewTag = (int) objectId.asNumber();
auto &integerClass = JavaReferencesCache::instance()
->getJClass("java/lang/Integer");
jmethodID integerConstructor = integerClass.getMethod("<init>", "(I)V");
return env->NewObject(integerClass.clazz, integerConstructor, viewTag);
}
bool SharedObjectIdConverter::canConvert(jsi::Runtime &rt, const jsi::Value &value) const {
return value.isObject() && value.getObject(rt).hasProperty(rt, "__expo_shared_object_id__");
}
jobject AnyFrontendConvert::convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const {
if (value.isUndefined() || value.isNull()) {
return nullptr;
}
if (booleanConverter.canConvert(rt, value)) {
return booleanConverter.convert(rt, env, value);
}
if (doubleConverter.canConvert(rt, value)) {
return doubleConverter.convert(rt, env, value);
}
if (stringConverter.canConvert(rt, value)) {
return stringConverter.convert(rt, env, value);
}
if (!value.isObject()) {
return nullptr;
}
const jsi::Object &obj = value.asObject(rt);
if (obj.isArray(rt)) {
const jsi::Array &jsArray = obj.asArray(rt);
size_t size = jsArray.size(rt);
auto arrayList = java::ArrayList<jobject>::create(size);
for (size_t i = 0; i < size; i++) {
auto jsValue = jsArray.getValueAtIndex(rt, i);
auto convertedElement = this->convert(
rt, env, jsValue
);
arrayList->add(convertedElement);
env->DeleteLocalRef(convertedElement);
}
return arrayList.release();
}
// it's object, so we're going to convert it to LinkedHashMap
auto propertyNames = obj.getPropertyNames(rt);
size_t size = propertyNames.size(rt);
auto map = java::LinkedHashMap<jobject, jobject>::create(size);
for (size_t i = 0; i < size; i++) {
auto key = propertyNames.getValueAtIndex(rt, i).getString(rt);
auto jsValue = obj.getProperty(rt, key);
auto convertedKey = env->NewStringUTF(key.utf8(rt).c_str());
auto convertedValue = this->convert(
rt, env, jsValue
);
map->put(convertedKey, convertedValue);
env->DeleteLocalRef(convertedKey);
env->DeleteLocalRef(convertedValue);
}
return map.release();
}
bool AnyFrontendConvert::canConvert(jsi::Runtime &rt, const jsi::Value &value) const {
return true;
}
} // namespace expo

View File

@@ -0,0 +1,386 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include "CppType.h"
#include <jsi/jsi.h>
#include <fbjni/fbjni.h>
namespace jni = facebook::jni;
namespace jsi = facebook::jsi;
namespace expo {
class JSIContext;
class SingleType;
/**
* A base interface for all frontend converter classes - converters that cast jsi values into JNI objects.
* Right now, we have two-step arguments conversion. Firstly, we unwrapped the JSI value into selected JNI objects (see CppType).
* Then, we do a more sophisticated conversion like creating records or mapping into enums.
* The second step lives in the Kotlin codebase.
*/
class FrontendConverter {
public:
virtual ~FrontendConverter() = default;
/**
* Checks if the provided value can be converted.
*/
virtual bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const = 0;
/**
* Converts the provided value.
*/
virtual jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const = 0;
};
/**
* Converter from js number to [java.lang.Integer].
*/
class IntegerFrontendConverter : public FrontendConverter {
public:
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
};
/**
* Converter from js number to [java.lang.Long].
*/
class LongFrontendConverter : public FrontendConverter {
public:
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
};
/**
* Converter from js number to [java.lang.Float].
*/
class FloatFrontendConverter : public FrontendConverter {
public:
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
};
/**
* Converter from js bool to [java.lang.Boolean].
*/
class BooleanFrontendConverter : public FrontendConverter {
public:
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
};
/**
* Converter from js number to [java.lang.Double].
*/
class DoubleFrontendConverter : public FrontendConverter {
public:
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
};
/**
* Converter from js string to [java.lang.String].
*/
class StringFrontendConverter : public FrontendConverter {
public:
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
};
/**
* Converter from js array to [com.facebook.react.bridge.ReadableNativeArray].
*/
class ReadableNativeArrayFrontendConverter : public FrontendConverter {
public:
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
};
/**
* Converter from js object to [com.facebook.react.bridge.ReadableNativeMap].
*/
class ReadableNativeMapArrayFrontendConverter : public FrontendConverter {
public:
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
};
/**
* Converter from js Uint8Array to [java.lang.Byte] array.
*/
class ByteArrayFrontendConverter : public FrontendConverter {
public:
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
};
/**
* Converter from js type array to [expo.modules.kotlin.jni.JavaScriptTypedArray].
*/
class TypedArrayFrontendConverter : public FrontendConverter {
public:
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
};
/**
* Converter from any js value to [expo.modules.kotlin.jni.JavaScriptValue].
*/
class JavaScriptValueFrontendConverter : public FrontendConverter {
public:
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
};
/**
* Converter from js object to [expo.modules.kotlin.jni.JavaScriptObject].
*/
class JavaScriptObjectFrontendConverter : public FrontendConverter {
public:
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
};
/**
* Converter from js function to [expo.modules.kotlin.jni.JavaScriptFunction].
*/
class JavaScriptFunctionFrontendConverter : public FrontendConverter {
public:
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
};
/**
* Converter from js view object to int.
*/
class ViewTagFrontendConverter : public FrontendConverter {
public:
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
};
/**
* Converter from js shared object to int.
*/
class SharedObjectIdConverter : public FrontendConverter {
public:
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
};
/**
* Converter that always fails.
* Used to not fail when the function is created.
* TODO(@lukmccall): remove
*/
class UnknownFrontendConverter : public FrontendConverter {
public:
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
};
/**
* Same types like enums can be represented by multiply js types.
* That's why we have a converter that can combine multiple converters into one.
*
* For instance, enum classes will be represented as a PolyFrontendConverter({StringFrontendConverter, IntegerFrontendConverter})
*/
class PolyFrontendConverter : public FrontendConverter {
public:
PolyFrontendConverter(std::vector<std::shared_ptr<FrontendConverter>> converters);
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
private:
std::vector<std::shared_ptr<FrontendConverter>> converters;
};
/**
* Converter from js array object to Java primitive array.
*/
class PrimitiveArrayFrontendConverter : public FrontendConverter {
public:
PrimitiveArrayFrontendConverter(
jni::local_ref<jni::JavaClass<SingleType>::javaobject> expectedType
);
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
private:
/**
* A string representation of desired Java type.
*/
std::string javaType;
/**
* Bare parameter type.
*/
CppType parameterType;
/**
* Converter used to convert array elements.
*/
std::shared_ptr<FrontendConverter> parameterConverter;
};
/**
* Converter from js array object to [java.utils.ArrayList].
*/
class ListFrontendConverter : public FrontendConverter {
public:
ListFrontendConverter(
jni::local_ref<jni::JavaClass<SingleType>::javaobject> expectedType
);
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
private:
/**
* Converter used to convert array elements.
*/
std::shared_ptr<FrontendConverter> parameterConverter;
};
/**
* Converter from js object to [java.utils.LinkedHashMap].
*/
class MapFrontendConverter : public FrontendConverter {
public:
MapFrontendConverter(
jni::local_ref<jni::JavaClass<SingleType>::javaobject> expectedType
);
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
private:
/**
* Converter used to convert values.
*/
std::shared_ptr<FrontendConverter> valueConverter;
};
/**
* Converter from js object to [kotlin.Any] (Boolean, Double, String, Map<Any>, List<Any>).
*/
class AnyFrontendConvert : public FrontendConverter {
public:
jobject convert(
jsi::Runtime &rt,
JNIEnv *env,
const jsi::Value &value
) const override;
bool canConvert(jsi::Runtime &rt, const jsi::Value &value) const override;
private:
BooleanFrontendConverter booleanConverter;
DoubleFrontendConverter doubleConverter;
StringFrontendConverter stringConverter;
};
} // namespace expo

View File

@@ -0,0 +1,109 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#include "FrontendConverterProvider.h"
namespace expo {
std::shared_ptr<FrontendConverterProvider> FrontendConverterProvider::instance() {
static std::shared_ptr<FrontendConverterProvider> singleton{new FrontendConverterProvider};
return singleton;
}
void FrontendConverterProvider::createConverters() {
#define RegisterConverter(type, clazz) simpleConverters.insert({type, std::make_shared<clazz>()})
RegisterConverter(CppType::NONE, UnknownFrontendConverter);
RegisterConverter(CppType::INT, IntegerFrontendConverter);
RegisterConverter(CppType::LONG, LongFrontendConverter);
RegisterConverter(CppType::FLOAT, FloatFrontendConverter);
RegisterConverter(CppType::DOUBLE, DoubleFrontendConverter);
RegisterConverter(CppType::BOOLEAN, BooleanFrontendConverter);
RegisterConverter(CppType::UINT8_TYPED_ARRAY, ByteArrayFrontendConverter);
RegisterConverter(CppType::TYPED_ARRAY, TypedArrayFrontendConverter);
RegisterConverter(CppType::JS_OBJECT, JavaScriptObjectFrontendConverter);
RegisterConverter(CppType::JS_VALUE, JavaScriptValueFrontendConverter);
RegisterConverter(CppType::JS_FUNCTION, JavaScriptFunctionFrontendConverter);
RegisterConverter(CppType::STRING, StringFrontendConverter);
RegisterConverter(CppType::READABLE_MAP, ReadableNativeMapArrayFrontendConverter);
RegisterConverter(CppType::READABLE_ARRAY, ReadableNativeArrayFrontendConverter);
RegisterConverter(CppType::VIEW_TAG, ViewTagFrontendConverter);
RegisterConverter(CppType::SHARED_OBJECT_ID, SharedObjectIdConverter);
RegisterConverter(CppType::ANY, AnyFrontendConvert);
#undef RegisterConverter
auto registerPolyConverter = [this](const std::vector<CppType> &types) {
std::vector<std::shared_ptr<FrontendConverter>> converters;
CppType finalType = CppType::NONE;
for (const auto type: types) {
finalType = (CppType) ((int) finalType | (int) type);
converters.push_back(simpleConverters.at(type));
}
simpleConverters.insert({finalType, std::make_shared<PolyFrontendConverter>(converters)});
};
// Enums
registerPolyConverter({CppType::STRING, CppType::INT});
}
std::shared_ptr<FrontendConverter> FrontendConverterProvider::obtainConverter(
jni::local_ref<ExpectedType::javaobject> expectedType
) {
CppType combinedType = expectedType->getCombinedTypes();
auto result = simpleConverters.find(combinedType);
if (result != simpleConverters.end()) {
return result->second;
}
if (combinedType == CppType::PRIMITIVE_ARRAY) {
return std::make_shared<PrimitiveArrayFrontendConverter>(expectedType->getFirstType());
}
if (combinedType == CppType::LIST) {
return std::make_shared<ListFrontendConverter>(expectedType->getFirstType());
}
if (combinedType == CppType::MAP) {
return std::make_shared<MapFrontendConverter>(expectedType->getFirstType());
}
std::vector<std::shared_ptr<FrontendConverter>> converters;
auto singleTypes = expectedType->getPossibleTypes();
size_t size = singleTypes->size();
for (size_t i = 0; i < size; i++) {
jni::local_ref<SingleType> singleType = singleTypes->getElement(i);
converters.push_back(this->obtainConverterForSingleType(singleType));
}
if (converters.empty()) {
// We don't have a converter for the expected type. That's why we used an UnknownFrontendConverter.
return simpleConverters.at(CppType::NONE);
}
return std::make_shared<PolyFrontendConverter>(converters);
}
std::shared_ptr<FrontendConverter> FrontendConverterProvider::obtainConverterForSingleType(
jni::local_ref<SingleType::javaobject> expectedType
) {
CppType combinedType = expectedType->getCppType();
auto result = simpleConverters.find(combinedType);
if (result != simpleConverters.end()) {
return result->second;
}
if (combinedType == CppType::PRIMITIVE_ARRAY) {
return std::make_shared<PrimitiveArrayFrontendConverter>(expectedType);
}
if (combinedType == CppType::LIST) {
return std::make_shared<ListFrontendConverter>(expectedType);
}
if (combinedType == CppType::MAP) {
return std::make_shared<MapFrontendConverter>(expectedType);
}
// We don't have a converter for the expected type. That's why we used an UnknownFrontendConverter.
return simpleConverters.at(CppType::NONE);
}
} // namespace expo

View File

@@ -0,0 +1,47 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include "CppType.h"
#include "FrontendConverter.h"
#include "ExpectedType.h"
#include <fbjni/fbjni.h>
#include <memory>
#include <unordered_map>
namespace jni = facebook::jni;
namespace expo {
/**
* Singleton registry used to store all basic converters.
*/
class FrontendConverterProvider {
public:
/**
* Gets a singleton instance.
*/
static std::shared_ptr<FrontendConverterProvider> instance();
/**
* Creates converters.
*/
void createConverters();
/**
* Obtains a converter for an expected type.
*/
std::shared_ptr<FrontendConverter> obtainConverter(
jni::local_ref<jni::JavaClass<ExpectedType>::javaobject> expectedType
);
private:
FrontendConverterProvider() = default;
std::shared_ptr<FrontendConverter> obtainConverterForSingleType(
jni::local_ref<jni::JavaClass<SingleType>::javaobject> expectedType
);
std::unordered_map<CppType, std::shared_ptr<FrontendConverter>> simpleConverters;
};
} // namespace expo

View File

@@ -0,0 +1,191 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#include "JNIToJSIConverter.h"
#include "../JavaReferencesCache.h"
#include "ObjectDeallocator.h"
#include <react/jni/ReadableNativeMap.h>
#include <react/jni/ReadableNativeArray.h>
#include <react/jni/WritableNativeArray.h>
#include <react/jni/WritableNativeMap.h>
namespace react = facebook::react;
namespace expo {
// This value should be synced with the value in **FollyDynamicExtensionConverter.kt**
constexpr char DYNAMIC_EXTENSION_PREFIX[] = "__expo_dynamic_extension__#";
/**
* Create an JavaScript Uint8Array instance from Java ByteArray.
*/
jsi::Value createUint8Array(jsi::Runtime &rt, jni::alias_ref<jni::JArrayByte> byteArray) {
auto arrayBufferCtor = rt.global().getPropertyAsFunction(rt, "ArrayBuffer");
auto arrayBufferObject = arrayBufferCtor.callAsConstructor(rt, static_cast<int>(byteArray->size())).getObject(rt);
auto arrayBuffer = arrayBufferObject.getArrayBuffer(rt);
byteArray->getRegion(0, byteArray->size(), reinterpret_cast<signed char *>(arrayBuffer.data(rt)));
auto uint8ArrayCtor = rt.global().getPropertyAsFunction(rt, "Uint8Array");
auto uint8Array = uint8ArrayCtor.callAsConstructor(rt, arrayBufferObject).getObject(rt);
return uint8Array;
}
/**
* Convert a string with FollyDynamicExtensionConverter support.
*/
std::optional<jsi::Value> convertStringToFollyDynamicIfNeeded(jsi::Runtime &rt, const std::string& string) {
if (!string.starts_with(DYNAMIC_EXTENSION_PREFIX)) {
return std::nullopt;
}
auto converterClass = jni::findClassLocal("expo/modules/kotlin/types/folly/FollyDynamicExtensionConverter");
const auto getInstanceMethod = converterClass->getStaticMethod<jni::JObject(std::string)>("get");
jni::local_ref<jni::JObject> instance = getInstanceMethod(converterClass, string);
if (instance->isInstanceOf(jni::JArrayByte::javaClassStatic())) {
return createUint8Array(rt, jni::static_ref_cast<jni::JArrayByte>(instance));
}
return std::nullopt;
}
jsi::Value convert(
JNIEnv *env,
jsi::Runtime &rt,
jni::local_ref<jobject> value
) {
if (value == nullptr) {
return jsi::Value::undefined();
}
auto unpackedValue = value.get();
auto cache = JavaReferencesCache::instance();
if (env->IsInstanceOf(unpackedValue, cache->getJClass("java/lang/Double").clazz)) {
return {jni::static_ref_cast<jni::JDouble>(value)->value()};
}
if (env->IsInstanceOf(unpackedValue, cache->getJClass("java/lang/Integer").clazz)) {
return {jni::static_ref_cast<jni::JInteger>(value)->value()};
}
if (env->IsInstanceOf(unpackedValue, cache->getJClass("java/lang/Long").clazz)) {
return {(double) jni::static_ref_cast<jni::JLong>(value)->value()};
}
if (env->IsInstanceOf(unpackedValue, cache->getJClass("java/lang/String").clazz)) {
std::string string = jni::static_ref_cast<jni::JString>(value)->toStdString();
auto enhancedValue = convertStringToFollyDynamicIfNeeded(rt, string);
return enhancedValue ? std::move(*enhancedValue) : jsi::String::createFromUtf8(rt, string);
}
if (env->IsInstanceOf(unpackedValue, cache->getJClass("java/lang/Boolean").clazz)) {
return {(bool) jni::static_ref_cast<jni::JBoolean>(value)->value()};
}
if (env->IsInstanceOf(unpackedValue, cache->getJClass("java/lang/Float").clazz)) {
return {(double) jni::static_ref_cast<jni::JFloat>(value)->value()};
}
if (env->IsInstanceOf(
unpackedValue,
cache->getJClass("com/facebook/react/bridge/WritableNativeArray").clazz
)) {
auto dynamic = jni::static_ref_cast<react::WritableNativeArray::javaobject>(value)
->cthis()
->consume();
auto arg = jsi::valueFromDynamic(rt, dynamic);
auto enhancedArg = decorateValueForDynamicExtension(rt, arg);
if (enhancedArg) {
arg = std::move(*enhancedArg);
}
return arg;
}
if (env->IsInstanceOf(
unpackedValue,
cache->getJClass("com/facebook/react/bridge/WritableNativeMap").clazz
)) {
auto dynamic = jni::static_ref_cast<react::WritableNativeMap::javaobject>(value)
->cthis()
->consume();
auto arg = jsi::valueFromDynamic(rt, dynamic);
auto enhancedArg = decorateValueForDynamicExtension(rt, arg);
if (enhancedArg) {
arg = std::move(*enhancedArg);
}
return arg;
}
if (env->IsInstanceOf(unpackedValue, JavaScriptModuleObject::javaClassStatic().get())) {
auto anonymousObject = jni::static_ref_cast<JavaScriptModuleObject::javaobject>(value)
->cthis();
auto jsiObject = anonymousObject->getJSIObject(rt);
jni::global_ref<jobject> globalRef = jni::make_global(value);
common::setDeallocator(
rt,
jsiObject,
[globalRef = std::move(globalRef)]() mutable {
globalRef.reset();
}
);
return jsi::Value(rt, *jsiObject);
}
if (env->IsInstanceOf(
unpackedValue,
cache->getJClass(
"expo/modules/kotlin/sharedobjects/SharedObject").clazz
)) {
auto jsObject = std::make_shared<jsi::Object>(jsi::Object(rt));
JSIContext *jsiContext = getJSIContext(rt);
auto jsObjectRef = JavaScriptObject::newInstance(
jsiContext,
jsiContext->runtimeHolder,
jsObject
);
jsiContext->registerSharedObject(jni::make_local(unpackedValue), jsObjectRef);
return jsi::Value(rt, *jsObject);
}
if (env->IsInstanceOf(
unpackedValue,
cache->getJClass("expo/modules/kotlin/jni/JavaScriptTypedArray").clazz
)) {
auto typedArray = jni::static_ref_cast<JavaScriptTypedArray::javaobject>(value);
auto jsTypedArray = typedArray->cthis()->get();
return jsi::Value(rt, *jsTypedArray);
}
return jsi::Value::undefined();
}
std::optional<jsi::Value> decorateValueForDynamicExtension(jsi::Runtime &rt, const jsi::Value &value) {
if (value.isString()) {
std::string string = value.getString(rt).utf8(rt);
return convertStringToFollyDynamicIfNeeded(rt, string);
}
if (value.isObject()) {
auto jsObject = value.getObject(rt);
if (jsObject.isArray(rt)) {
bool changed = false;
auto jsArray = jsObject.getArray(rt);
size_t length = jsArray.length(rt);
for (size_t i = 0; i < length; ++i) {
auto converted = decorateValueForDynamicExtension(rt, jsArray.getValueAtIndex(rt, i));
if (converted) {
jsArray.setValueAtIndex(rt, i, std::move(*converted));
changed = true;
}
}
return changed ? std::make_optional<jsi::Value>(std::move(jsArray)) : std::nullopt;
} else {
bool changed = false;
auto propNames = jsObject.getPropertyNames(rt);
size_t length = propNames.length(rt);
for (size_t i = 0; i < length; ++i) {
auto propName = propNames.getValueAtIndex(rt, i).getString(rt);
auto converted = decorateValueForDynamicExtension(rt, jsObject.getProperty(rt, propName));
if (converted) {
jsObject.setProperty(rt, propName, std::move(*converted));
changed = true;
}
}
return changed ? std::make_optional<jsi::Value>(std::move(jsObject)) : std::nullopt;
}
}
return std::nullopt;
}
} // namespace expo

View File

@@ -0,0 +1,32 @@
// Copyright © 2021-present 650 Industries, Inc. (aka Expo)
#pragma once
#include "../JSIContext.h"
#include <fbjni/fbjni.h>
#include <jsi/jsi.h>
#include <optional>
namespace jni = facebook::jni;
namespace jsi = facebook::jsi;
namespace expo {
jsi::Value convert(
JNIEnv *env,
jsi::Runtime &rt,
jni::local_ref<jobject> value
);
/**
* Convert a string with FollyDynamicExtensionConverter support.
*/
std::optional<jsi::Value> convertStringToFollyDynamicIfNeeded(jsi::Runtime &rt, const std::string& string);
/**
* Decorate jsi::Value with FollyDynamicExtensionConverter support.
*/
std::optional<jsi::Value> decorateValueForDynamicExtension(jsi::Runtime &rt, const jsi::Value &value);
} // namespace expo

View File

@@ -0,0 +1,10 @@
package com.facebook.react.uimanager
import com.facebook.react.bridge.ReadableMap
/**
* Access the package private property declared inside of [ReactStylesDiffMap]
*/
fun ReactStylesDiffMap.getBackingMap(): ReadableMap {
return mBackingMap
}

View File

@@ -0,0 +1,37 @@
// Copyright 2018-present 650 Industries. All rights reserved.
package expo.modules.adapters.react
import com.facebook.jni.HybridData
import com.facebook.react.uimanager.ViewManager
import com.facebook.soloader.SoLoader
import expo.modules.core.interfaces.DoNotStrip
@Suppress("KotlinJniMissingFunction")
@DoNotStrip
class FabricComponentsRegistry(viewManagerList: List<ViewManager<*, *>>) {
private val componentNames: List<String>
@DoNotStrip
private val mHybridData: HybridData
init {
componentNames = viewManagerList.map { it.name }
mHybridData = initHybrid()
registerComponentsRegistry(componentNames.toTypedArray())
}
private external fun initHybrid(): HybridData
private external fun registerComponentsRegistry(componentNames: Array<String>)
@Throws(Throwable::class)
protected fun finalize() {
mHybridData.resetNative()
}
companion object {
init {
SoLoader.loadLibrary("expo-modules-core")
}
}
}

View File

@@ -0,0 +1,145 @@
package expo.modules.adapters.react;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import androidx.annotation.Nullable;
import expo.modules.BuildConfig;
import expo.modules.core.ModuleRegistry;
import expo.modules.core.interfaces.Consumer;
import expo.modules.core.interfaces.InternalModule;
import expo.modules.core.interfaces.Package;
import expo.modules.kotlin.AppContext;
import expo.modules.kotlin.CoreLoggerKt;
import expo.modules.kotlin.ExpoBridgeModule;
import expo.modules.kotlin.KotlinInteropModuleRegistry;
import expo.modules.kotlin.ModulesProvider;
import expo.modules.kotlin.views.ViewWrapperDelegateHolder;
/**
* An adapter over {@link ModuleRegistry}, compatible with React (implementing {@link ReactPackage}).
* Provides React Native with native modules and view managers,
* which in turn are created by packages provided by {@link ReactModuleRegistryProvider}.
*/
public class ModuleRegistryAdapter implements ReactPackage {
protected ReactModuleRegistryProvider mModuleRegistryProvider;
protected ModulesProvider mModulesProvider;
protected ReactAdapterPackage mReactAdapterPackage = new ReactAdapterPackage();
private NativeModulesProxy mModulesProxy;
private void setModulesProxy(@Nullable NativeModulesProxy newProxy) {
mModulesProxy = newProxy;
if (mModulesProxy != null) {
mModulesProxy.getKotlinInteropModuleRegistry().setLegacyModulesProxy(mModulesProxy);
}
}
// We need to save all view holders to update them when the new kotlin module registry will be created.
private List<ViewWrapperDelegateHolder> mWrapperDelegateHolders = null;
private FabricComponentsRegistry mFabricComponentsRegistry = null;
public ModuleRegistryAdapter(List<Package> packageList) {
mModuleRegistryProvider = new ReactModuleRegistryProvider(packageList, null);
}
public ModuleRegistryAdapter(ReactModuleRegistryProvider moduleRegistryProvider) {
mModuleRegistryProvider = moduleRegistryProvider;
}
public ModuleRegistryAdapter(ReactModuleRegistryProvider moduleRegistryProvider, ModulesProvider modulesProvider) {
mModuleRegistryProvider = moduleRegistryProvider;
mModulesProvider = modulesProvider;
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
NativeModulesProxy proxy = getOrCreateNativeModulesProxy(reactContext, null);
ModuleRegistry moduleRegistry = proxy.getModuleRegistry();
for (InternalModule internalModule : mReactAdapterPackage.createInternalModules(reactContext)) {
moduleRegistry.registerInternalModule(internalModule);
}
List<NativeModule> nativeModules = getNativeModulesFromModuleRegistry(reactContext, moduleRegistry, null);
if (mWrapperDelegateHolders != null) {
KotlinInteropModuleRegistry kotlinInteropModuleRegistry = proxy.getKotlinInteropModuleRegistry();
kotlinInteropModuleRegistry.updateModuleHoldersInViewManagers(mWrapperDelegateHolders);
}
return nativeModules;
}
protected List<NativeModule> getNativeModulesFromModuleRegistry(
ReactApplicationContext reactContext,
ModuleRegistry moduleRegistry,
@Nullable Consumer<AppContext> appContextConsumer
) {
List<NativeModule> nativeModulesList = new ArrayList<>(2);
NativeModulesProxy nativeModulesProxy = getOrCreateNativeModulesProxy(reactContext, moduleRegistry);
if (appContextConsumer != null) {
appContextConsumer.apply(nativeModulesProxy.getKotlinInteropModuleRegistry().getAppContext());
}
nativeModulesList.add(nativeModulesProxy);
// Add listener that will notify expo.modules.core.ModuleRegistry when all modules are ready
nativeModulesList.add(new ModuleRegistryReadyNotifier(moduleRegistry));
ReactPackagesProvider reactPackagesProvider = moduleRegistry.getModule(ReactPackagesProvider.class);
for (ReactPackage reactPackage : reactPackagesProvider.getReactPackages()) {
nativeModulesList.addAll(reactPackage.createNativeModules(reactContext));
}
nativeModulesList.add(new ExpoBridgeModule(reactContext, new WeakReference<>(nativeModulesProxy)));
return nativeModulesList;
}
@Override
@SuppressWarnings("unchecked")
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
List<ViewManager> viewManagerList = new ArrayList<>(mModuleRegistryProvider.getReactViewManagers(reactContext));
NativeModulesProxy modulesProxy = Objects.requireNonNull(getOrCreateNativeModulesProxy(reactContext, null));
KotlinInteropModuleRegistry kotlinInteropModuleRegistry = modulesProxy.getKotlinInteropModuleRegistry();
List<ViewManager<?, ?>> kViewManager = kotlinInteropModuleRegistry.exportViewManagers();
// Saves all holders that needs to be in sync with module registry
mWrapperDelegateHolders = kotlinInteropModuleRegistry.extractViewManagersDelegateHolders(kViewManager);
viewManagerList.addAll(kViewManager);
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// Intentionally to only add Sweet API view managers for Fabric support
mFabricComponentsRegistry = new FabricComponentsRegistry(kViewManager);
}
return viewManagerList;
}
private synchronized NativeModulesProxy getOrCreateNativeModulesProxy(
ReactApplicationContext reactContext,
@Nullable ModuleRegistry moduleRegistry
) {
if (mModulesProxy != null && mModulesProxy.getReactContext() != reactContext) {
setModulesProxy(null);
}
if (mModulesProxy == null) {
ModuleRegistry registry = moduleRegistry != null ? moduleRegistry : mModuleRegistryProvider.get(reactContext);
if (mModulesProvider != null) {
setModulesProxy(new NativeModulesProxy(reactContext, registry, mModulesProvider));
} else {
setModulesProxy(new NativeModulesProxy(reactContext, registry));
}
}
if (moduleRegistry != null && moduleRegistry != mModulesProxy.getModuleRegistry()) {
CoreLoggerKt.getLogger().error("❌ NativeModuleProxy was configured with a different instance of the modules registry.", null);
}
return mModulesProxy;
}
}

View File

@@ -0,0 +1,30 @@
package expo.modules.adapters.react;
import com.facebook.react.bridge.BaseJavaModule;
import com.facebook.react.bridge.NativeModule;
import expo.modules.core.ModuleRegistry;
/**
* {@link ModuleRegistryReadyNotifier} is exported as a native module
* to React Native and when {@link com.facebook.react.ReactInstanceManager}
* notifies {@link com.facebook.react.bridge.NativeModule} of being ready
* ({@link NativeModule#initialize()}) it delegates the call to {@link ModuleRegistry}.
*/
public class ModuleRegistryReadyNotifier extends BaseJavaModule {
private ModuleRegistry mModuleRegistry;
public ModuleRegistryReadyNotifier(ModuleRegistry moduleRegistry) {
mModuleRegistry = moduleRegistry;
}
@Override
public String getName() {
return "ModuleRegistryReadyNotifier";
}
@Override
public void initialize() {
mModuleRegistry.ensureIsInitialized();
}
}

View File

@@ -0,0 +1,181 @@
package expo.modules.adapters.react;
import android.util.SparseArray;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable;
import expo.modules.core.ModuleRegistry;
import expo.modules.kotlin.CoreLoggerKt;
import expo.modules.kotlin.ExpoModulesHelper;
import expo.modules.kotlin.KPromiseWrapper;
import expo.modules.kotlin.KotlinInteropModuleRegistry;
import expo.modules.kotlin.ModulesProvider;
/**
* A wrapper/proxy for all {@link expo.modules.kotlin.modules.Module}s, gets exposed as {@link com.facebook.react.bridge.NativeModule},
* so that JS code can call methods of the internal modules.
*/
public class NativeModulesProxy extends ReactContextBaseJavaModule {
private final static String NAME = "NativeUnimoduleProxy";
private final static String VIEW_MANAGERS_METADATA_KEY = "viewManagersMetadata";
private final static String MODULES_CONSTANTS_KEY = "modulesConstants";
private final static String EXPORTED_METHODS_KEY = "exportedMethods";
private final static String METHOD_INFO_KEY = "key";
private final static String METHOD_INFO_NAME = "name";
private final static String UNEXPECTED_ERROR = "E_UNEXPECTED_ERROR";
private final static String UNDEFINED_METHOD_ERROR = "E_UNDEFINED_METHOD";
private ModuleRegistry mModuleRegistry;
private Map<String, Map<String, Integer>> mExportedMethodsKeys;
private Map<String, SparseArray<String>> mExportedMethodsReverseKeys;
private KotlinInteropModuleRegistry mKotlinInteropModuleRegistry;
private Map<String, Object> cachedConstants;
public NativeModulesProxy(ReactApplicationContext context, ModuleRegistry moduleRegistry) {
super(context);
mModuleRegistry = moduleRegistry;
mExportedMethodsKeys = new HashMap<>();
mExportedMethodsReverseKeys = new HashMap<>();
mKotlinInteropModuleRegistry = new KotlinInteropModuleRegistry(
Objects.requireNonNull(ExpoModulesHelper.Companion.getModulesProvider()),
moduleRegistry,
new WeakReference<>(context)
);
}
public NativeModulesProxy(ReactApplicationContext context, ModuleRegistry moduleRegistry, ModulesProvider modulesProvider) {
super(context);
mModuleRegistry = moduleRegistry;
mExportedMethodsKeys = new HashMap<>();
mExportedMethodsReverseKeys = new HashMap<>();
mKotlinInteropModuleRegistry = new KotlinInteropModuleRegistry(
Objects.requireNonNull(modulesProvider),
moduleRegistry,
new WeakReference<>(context)
);
}
public KotlinInteropModuleRegistry getKotlinInteropModuleRegistry() {
return mKotlinInteropModuleRegistry;
}
@Override
public String getName() {
return NAME;
}
@Nullable
@Override
public Map<String, Object> getConstants() {
if (cachedConstants != null) {
return cachedConstants;
}
mModuleRegistry.ensureIsInitialized();
KotlinInteropModuleRegistry kotlinModuleRegistry = getKotlinInteropModuleRegistry();
kotlinModuleRegistry.installJSIInterop();
kotlinModuleRegistry.emitOnCreate();
Map<String, Object> constants = new HashMap<>(3);
constants.put(MODULES_CONSTANTS_KEY, new HashMap<>());
constants.put(EXPORTED_METHODS_KEY, new HashMap<>());
constants.put(VIEW_MANAGERS_METADATA_KEY, mKotlinInteropModuleRegistry.viewManagersMetadata());
CoreLoggerKt.getLogger().info("✅ Constants were exported");
cachedConstants = constants;
return constants;
}
/**
* The only exported {@link ReactMethod} for legacy NativeUnimoduleProxy.
* This is used only when JSI is not available. i.e. the legacy remote debugging.
* JavaScript can call native modules' exported methods ({@link ExpoMethod}) using this method as a proxy.
* For native {@link ExpoMethod} `void put(String key, int value)` in `NativeDictionary` module
* JavaScript could call `NativeModulesProxy.callMethod("NativeDictionary", 2, ["key", 42])`, where the second argument
* is a method's constant key.
*/
@ReactMethod
public void callMethod(String moduleName, int methodKey, ReadableArray arguments, final Promise promise) {
callMethod(moduleName, mExportedMethodsReverseKeys.get(moduleName).get(methodKey), arguments, promise);
}
public void callMethod(String moduleName, String methodName, ReadableArray arguments, final Promise promise) {
if (mKotlinInteropModuleRegistry.hasModule(moduleName)) {
mKotlinInteropModuleRegistry.callMethod(moduleName, methodName, arguments, new KPromiseWrapper(promise));
return;
}
promise.reject(
UNDEFINED_METHOD_ERROR,
"Method " + methodName + " of Java module " + moduleName + " is undefined."
);
}
/**
* Assigns keys to exported method infos and updates {@link #mExportedMethodsKeys} and {@link #mExportedMethodsReverseKeys}.
* Mutates maps in provided list.
*/
private void assignExportedMethodsKeys(String moduleName, List<Map<String, Object>> exportedMethodsInfos) {
if (mExportedMethodsKeys.get(moduleName) == null) {
mExportedMethodsKeys.put(moduleName, new HashMap<String, Integer>());
}
if (mExportedMethodsReverseKeys.get(moduleName) == null) {
mExportedMethodsReverseKeys.put(moduleName, new SparseArray<String>());
}
for (int i = 0; i < exportedMethodsInfos.size(); i++) {
Map<String, Object> methodInfo = exportedMethodsInfos.get(i);
if (methodInfo.get(METHOD_INFO_NAME) == null || !(methodInfo.get(METHOD_INFO_NAME) instanceof String)) {
throw new RuntimeException("No method name in MethodInfo - " + methodInfo.toString());
}
String methodName = (String) methodInfo.get(METHOD_INFO_NAME);
Integer maybePreviousIndex = mExportedMethodsKeys.get(moduleName).get(methodName);
if (maybePreviousIndex == null) {
int key = mExportedMethodsKeys.get(moduleName).values().size();
methodInfo.put(METHOD_INFO_KEY, key);
mExportedMethodsKeys.get(moduleName).put(methodName, key);
mExportedMethodsReverseKeys.get(moduleName).put(key, methodName);
} else {
int key = maybePreviousIndex;
methodInfo.put(METHOD_INFO_KEY, key);
}
}
}
@Override
public void invalidate() {
super.invalidate();
mModuleRegistry.onDestroy();
mKotlinInteropModuleRegistry.onDestroy();
}
ModuleRegistry getModuleRegistry() {
return mModuleRegistry;
}
/* package */ ReactApplicationContext getReactContext() {
return getReactApplicationContext();
}
}

View File

@@ -0,0 +1,36 @@
package expo.modules.adapters.react;
import android.content.Context;
import com.facebook.react.bridge.ReactContext;
import java.util.Arrays;
import java.util.List;
import expo.modules.adapters.react.permissions.PermissionsService;
import expo.modules.adapters.react.services.EventEmitterModule;
import expo.modules.adapters.react.services.FontManagerModule;
import expo.modules.adapters.react.services.RuntimeEnvironmentModule;
import expo.modules.adapters.react.services.UIManagerModuleWrapper;
import expo.modules.core.BasePackage;
import expo.modules.core.interfaces.InternalModule;
import expo.modules.core.interfaces.Package;
/**
* A {@link Package} creating modules provided with the @unimodules/react-native-adapter package.
*/
public class ReactAdapterPackage extends BasePackage {
@Override
public List<InternalModule> createInternalModules(Context context) {
// We can force-cast here, because this package will only be used in React Native context.
ReactContext reactContext = (ReactContext) context;
return Arrays.asList(
new UIManagerModuleWrapper(reactContext),
new EventEmitterModule(reactContext),
new FontManagerModule(),
new RuntimeEnvironmentModule(),
new PermissionsService(reactContext)
);
}
}

View File

@@ -0,0 +1,86 @@
package expo.modules.adapters.react;
import android.content.Context;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.ReactApplicationContext;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import expo.modules.core.ModuleRegistry;
import expo.modules.core.ModuleRegistryProvider;
import expo.modules.core.interfaces.InternalModule;
import expo.modules.core.interfaces.Package;
import expo.modules.core.interfaces.SingletonModule;
/**
* Since React Native v0.55, {@link com.facebook.react.ReactPackage#createViewManagers(ReactApplicationContext)}
* gets called only once per lifetime of {@link com.facebook.react.ReactInstanceManager}.
* <p>
* To make @unimodules/react-native-adapter compatible with this change we have to remember view managers collection
* which is returned in {@link ModuleRegistryAdapter#createViewManagers(ReactApplicationContext)}
* only once (and managers returned this one time will persist "forever").
*/
public class ReactModuleRegistryProvider extends ModuleRegistryProvider {
private Collection<com.facebook.react.uimanager.ViewManager> mReactViewManagers;
private Collection<SingletonModule> mSingletonModules;
public ReactModuleRegistryProvider(List<Package> initialPackages) {
this(initialPackages, null);
}
public ReactModuleRegistryProvider(List<Package> initialPackages, List<SingletonModule> singletonModules) {
super(initialPackages);
mSingletonModules = singletonModules;
}
@Override
public ModuleRegistry get(Context context) {
Collection<InternalModule> internalModules = new ArrayList<>();
ReactPackagesProvider reactPackagesProvider = new ReactPackagesProvider();
for (Package pkg : getPackages()) {
internalModules.addAll(pkg.createInternalModules(context));
if (pkg instanceof ReactPackage) {
reactPackagesProvider.addPackage((ReactPackage) pkg);
}
}
internalModules.add(reactPackagesProvider);
return new ModuleRegistry(internalModules, getSingletonModules(context));
}
private Collection<SingletonModule> getSingletonModules(Context context) {
// If singleton modules were provided to registry provider, then just pass them to module registry.
if (mSingletonModules != null) {
return mSingletonModules;
}
Collection<SingletonModule> singletonModules = new ArrayList<>();
for (Package pkg : getPackages()) {
singletonModules.addAll(pkg.createSingletonModules(context));
}
return singletonModules;
}
// TODO: change access to package private when react-native-adapter was removed.
public Collection<com.facebook.react.uimanager.ViewManager> getReactViewManagers(ReactApplicationContext context) {
if (mReactViewManagers != null) {
return mReactViewManagers;
}
mReactViewManagers = new HashSet<>();
for (Package pkg : getPackages()) {
if (pkg instanceof ReactPackage) {
mReactViewManagers.addAll(((ReactPackage) pkg).createViewManagers(context));
}
}
return mReactViewManagers;
}
}

View File

@@ -0,0 +1,39 @@
package expo.modules.adapters.react;
import com.facebook.react.ReactPackage;
import expo.modules.core.interfaces.InternalModule;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Holder for ReactPackages -- visible only to the adapter.
* <p>
* We want to be able to create platform-specific unimodules.
* Thus, we need a way to pass in ReactPackages via unimodules infrastructure.
* This internal module is populated with ReactPackages by ReactModuleRegistryProvider
* and is used by ModuleRegistryAdapter when it creates native modules list.
*/
public class ReactPackagesProvider implements InternalModule {
private Collection<ReactPackage> mReactPackages;
@Override
public List<? extends Class> getExportedInterfaces() {
return Collections.singletonList(ReactPackagesProvider.class);
}
public ReactPackagesProvider() {
mReactPackages = new ArrayList<>();
}
public void addPackage(ReactPackage reactPackage) {
mReactPackages.add(reactPackage);
}
public Collection<ReactPackage> getReactPackages() {
return mReactPackages;
}
}

View File

@@ -0,0 +1,31 @@
package expo.modules.adapters.react.apploader
import java.lang.ref.WeakReference
interface HeadlessAppLoaderListener {
fun appLoaded(appScopeKey: String)
fun appDestroyed(appScopeKey: String)
}
object HeadlessAppLoaderNotifier {
val listeners: MutableSet<WeakReference<HeadlessAppLoaderListener>> = mutableSetOf()
fun registerListener(listener: HeadlessAppLoaderListener) {
listeners.add(WeakReference(listener))
}
fun notifyAppLoaded(appScopeKey: String?) {
if (appScopeKey != null) {
listeners.forEach { it.get()?.appLoaded(appScopeKey) }
}
}
fun notifyAppDestroyed(appScopeKey: String?) {
if (appScopeKey != null) {
listeners.forEach { it.get()?.appDestroyed(appScopeKey) }
}
}
}

View File

@@ -0,0 +1,70 @@
package expo.modules.adapters.react.apploader
import android.content.Context
import com.facebook.react.ReactApplication
import com.facebook.react.ReactInstanceEventListener
import com.facebook.react.ReactInstanceManager
import com.facebook.react.bridge.ReactContext
import com.facebook.react.common.LifecycleState
import expo.modules.apploader.HeadlessAppLoader
import expo.modules.core.interfaces.Consumer
import expo.modules.core.interfaces.DoNotStrip
private val appRecords: MutableMap<String, ReactInstanceManager> = mutableMapOf()
class RNHeadlessAppLoader @DoNotStrip constructor(private val context: Context) : HeadlessAppLoader {
//region HeadlessAppLoader
override fun loadApp(context: Context, params: HeadlessAppLoader.Params?, alreadyRunning: Runnable?, callback: Consumer<Boolean>?) {
if (params == null || params.appScopeKey == null) {
throw IllegalArgumentException("Params must be set with appScopeKey!")
}
if (context.applicationContext is ReactApplication) {
val reactInstanceManager = (context.applicationContext as ReactApplication).reactNativeHost.reactInstanceManager
if (!appRecords.containsKey(params.appScopeKey)) {
reactInstanceManager.addReactInstanceEventListener(object : ReactInstanceEventListener {
override fun onReactContextInitialized(context: ReactContext) {
HeadlessAppLoaderNotifier.notifyAppLoaded(params.appScopeKey)
callback?.apply(true)
}
})
appRecords[params.appScopeKey] = reactInstanceManager
if (reactInstanceManager.hasStartedCreatingInitialContext()) {
reactInstanceManager.recreateReactContextInBackground()
} else {
reactInstanceManager.createReactContextInBackground()
}
} else {
alreadyRunning?.run()
}
} else {
throw IllegalStateException("Your application must implement ReactApplication")
}
}
override fun invalidateApp(appScopeKey: String?): Boolean {
return if (appRecords.containsKey(appScopeKey) && appRecords[appScopeKey] != null) {
val appRecord: ReactInstanceManager = appRecords[appScopeKey]!!
android.os.Handler(context.mainLooper).post {
// Only destroy the `ReactInstanceManager` if it does not bind with an Activity.
// And The Activity would take over the ownership of `ReactInstanceManager`.
// This case happens when a user clicks a background task triggered notification immediately.
if (appRecord.lifecycleState == LifecycleState.BEFORE_CREATE) {
appRecord.destroy()
}
HeadlessAppLoaderNotifier.notifyAppDestroyed(appScopeKey)
appRecords.remove(appScopeKey)
}
true
} else {
false
}
}
override fun isRunning(appScopeKey: String?): Boolean =
appRecords.contains(appScopeKey) && appRecords[appScopeKey]!!.hasStartedCreatingInitialContext()
//endregion HeadlessAppLoader
}

View File

@@ -0,0 +1,347 @@
package expo.modules.adapters.react.permissions
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.facebook.react.modules.core.PermissionAwareActivity
import com.facebook.react.modules.core.PermissionListener
import expo.modules.interfaces.permissions.Permissions
import expo.modules.interfaces.permissions.PermissionsResponse
import expo.modules.interfaces.permissions.PermissionsResponseListener
import expo.modules.interfaces.permissions.PermissionsStatus
import expo.modules.core.ModuleRegistry
import expo.modules.core.Promise
import expo.modules.core.interfaces.ActivityProvider
import expo.modules.core.interfaces.InternalModule
import expo.modules.core.interfaces.LifecycleEventListener
import expo.modules.core.interfaces.services.UIManager
import java.util.*
import kotlin.collections.HashMap
private const val PERMISSIONS_REQUEST: Int = 13
private const val PREFERENCE_FILENAME = "expo.modules.permissions.asked"
open class PermissionsService(val context: Context) : InternalModule, Permissions, LifecycleEventListener {
private var mActivityProvider: ActivityProvider? = null
// state holders for asking for writing permissions
private var mWriteSettingsPermissionBeingAsked = false // change this directly before calling corresponding startActivity
private var mAskAsyncListener: PermissionsResponseListener? = null
private var mAskAsyncRequestedPermissions: Array<out String>? = null
private val mPendingPermissionCalls: Queue<Pair<Array<out String>, PermissionsResponseListener>> = LinkedList()
private var mCurrentPermissionListener: PermissionsResponseListener? = null
private lateinit var mAskedPermissionsCache: SharedPreferences
private fun didAsk(permission: String): Boolean = mAskedPermissionsCache.getBoolean(permission, false)
private fun addToAskedPermissionsCache(permissions: Array<out String>) {
with(mAskedPermissionsCache.edit()) {
permissions.forEach { putBoolean(it, true) }
apply()
}
}
override fun getExportedInterfaces(): List<Class<out Any>> = listOf(Permissions::class.java)
@Throws(IllegalStateException::class)
override fun onCreate(moduleRegistry: ModuleRegistry) {
mActivityProvider = moduleRegistry.getModule(ActivityProvider::class.java)
?: throw IllegalStateException("Couldn't find implementation for ActivityProvider.")
moduleRegistry.getModule(UIManager::class.java).registerLifecycleEventListener(this)
mAskedPermissionsCache = context.applicationContext.getSharedPreferences(PREFERENCE_FILENAME, Context.MODE_PRIVATE)
}
override fun getPermissionsWithPromise(promise: Promise, vararg permissions: String) {
getPermissions(
PermissionsResponseListener { permissionsMap: MutableMap<String, PermissionsResponse> ->
val areAllGranted = permissionsMap.all { (_, response) -> response.status == PermissionsStatus.GRANTED }
val areAllDenied = permissionsMap.isNotEmpty() && permissionsMap.all { (_, response) -> response.status == PermissionsStatus.DENIED }
val canAskAgain = permissionsMap.all { (_, response) -> response.canAskAgain }
promise.resolve(
Bundle().apply {
putString(PermissionsResponse.EXPIRES_KEY, PermissionsResponse.PERMISSION_EXPIRES_NEVER)
putString(
PermissionsResponse.STATUS_KEY,
when {
areAllGranted -> PermissionsStatus.GRANTED.status
areAllDenied -> PermissionsStatus.DENIED.status
else -> PermissionsStatus.UNDETERMINED.status
}
)
putBoolean(PermissionsResponse.CAN_ASK_AGAIN_KEY, canAskAgain)
putBoolean(PermissionsResponse.GRANTED_KEY, areAllGranted)
}
)
},
*permissions
)
}
override fun askForPermissionsWithPromise(promise: Promise, vararg permissions: String) {
askForPermissions(
PermissionsResponseListener {
getPermissionsWithPromise(promise, *permissions)
},
*permissions
)
}
override fun getPermissions(responseListener: PermissionsResponseListener, vararg permissions: String) {
responseListener.onResult(
parseNativeResult(
permissions,
permissions.map {
if (isPermissionGranted(it)) {
PackageManager.PERMISSION_GRANTED
} else {
PackageManager.PERMISSION_DENIED
}
}.toIntArray()
)
)
}
@Throws(IllegalStateException::class)
override fun askForPermissions(responseListener: PermissionsResponseListener, vararg permissions: String) {
if (permissions.isEmpty()) {
responseListener.onResult(mutableMapOf())
return
}
if (permissions.contains(Manifest.permission.WRITE_SETTINGS)) {
val permissionsToAsk = permissions.toMutableList().apply { remove(Manifest.permission.WRITE_SETTINGS) }.toTypedArray()
val newListener = PermissionsResponseListener {
val status = if (hasWriteSettingsPermission()) {
PackageManager.PERMISSION_GRANTED
} else {
PackageManager.PERMISSION_DENIED
}
it[Manifest.permission.WRITE_SETTINGS] = getPermissionResponseFromNativeResponse(Manifest.permission.WRITE_SETTINGS, status)
responseListener.onResult(it)
}
if (!hasWriteSettingsPermission()) {
if (mAskAsyncListener != null) {
throw IllegalStateException("Another permissions request is in progress. Await the old request and then try again.")
}
mAskAsyncListener = newListener
mAskAsyncRequestedPermissions = permissionsToAsk
addToAskedPermissionsCache(arrayOf(Manifest.permission.WRITE_SETTINGS))
askForWriteSettingsPermissionFirst()
} else {
// User only ask for `WRITE_SETTINGS`, we can already return response
if (permissionsToAsk.isEmpty()) {
newListener.onResult(mutableMapOf())
return
}
askForManifestPermissions(permissionsToAsk, newListener)
}
} else {
askForManifestPermissions(permissions, responseListener)
}
}
override fun hasGrantedPermissions(vararg permissions: String): Boolean {
return permissions.all { isPermissionGranted(it) }
}
/**
* Checks whether given permission is present in AndroidManifest or not.
*/
override fun isPermissionPresentInManifest(permission: String): Boolean {
try {
context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)?.run {
return requestedPermissions.contains(permission)
}
return false
} catch (e: PackageManager.NameNotFoundException) {
return false
}
}
/**
* Checks status for Android built-in permission
*
* @param permission [android.Manifest.permission]
*/
private fun isPermissionGranted(permission: String): Boolean {
return when (permission) {
// we need to handle this permission in different way
Manifest.permission.WRITE_SETTINGS -> hasWriteSettingsPermission()
else -> getManifestPermission(permission) == PackageManager.PERMISSION_GRANTED
}
}
/**
* Gets status for Android built-in permission
*
* @param permission [android.Manifest.permission]
*/
private fun getManifestPermission(permission: String): Int {
mActivityProvider?.currentActivity?.let {
if (it is PermissionAwareActivity) {
return ContextCompat.checkSelfPermission(it, permission)
}
}
// We are in the headless mode. So, we ask current context.
return getManifestPermissionFromContext(permission)
}
protected open fun getManifestPermissionFromContext(permission: String): Int {
return ContextCompat.checkSelfPermission(context, permission)
}
private fun canAskAgain(permission: String): Boolean {
return mActivityProvider?.currentActivity?.let {
ActivityCompat.shouldShowRequestPermissionRationale(it, permission)
} ?: false
}
private fun parseNativeResult(permissionsString: Array<out String>, grantResults: IntArray): Map<String, PermissionsResponse> {
return HashMap<String, PermissionsResponse>().apply {
grantResults.zip(permissionsString).forEach { (result, permission) ->
this[permission] = getPermissionResponseFromNativeResponse(permission, result)
}
}
}
private fun getPermissionResponseFromNativeResponse(permission: String, result: Int): PermissionsResponse {
val status = when {
result == PackageManager.PERMISSION_GRANTED -> PermissionsStatus.GRANTED
didAsk(permission) -> PermissionsStatus.DENIED
else -> PermissionsStatus.UNDETERMINED
}
return PermissionsResponse(
status,
if (status == PermissionsStatus.DENIED) {
canAskAgain(permission)
} else {
true
}
)
}
protected open fun askForManifestPermissions(permissions: Array<out String>, listener: PermissionsResponseListener) {
delegateRequestToActivity(permissions, listener)
}
/**
* Asks for Android built-in permission
* According to Android documentation [android.Manifest.permission.WRITE_SETTINGS] need to be handled in different way
*
* @param permissions [android.Manifest.permission]
*/
protected fun delegateRequestToActivity(permissions: Array<out String>, listener: PermissionsResponseListener) {
addToAskedPermissionsCache(permissions)
val currentActivity = mActivityProvider?.currentActivity
if (currentActivity is PermissionAwareActivity) {
synchronized(this@PermissionsService) {
if (mCurrentPermissionListener != null) {
mPendingPermissionCalls.add(permissions to listener)
} else {
mCurrentPermissionListener = listener
currentActivity.requestPermissions(permissions, PERMISSIONS_REQUEST, createListenerWithPendingPermissionsRequest())
}
}
} else {
listener.onResult(parseNativeResult(permissions, IntArray(permissions.size) { PackageManager.PERMISSION_DENIED }))
}
}
private fun createListenerWithPendingPermissionsRequest(): PermissionListener {
return PermissionListener { requestCode, receivePermissions, grantResults ->
if (requestCode == PERMISSIONS_REQUEST) {
synchronized(this@PermissionsService) {
val currentListener = requireNotNull(mCurrentPermissionListener)
currentListener.onResult(parseNativeResult(receivePermissions, grantResults))
mCurrentPermissionListener = null
mPendingPermissionCalls.poll()?.let { pendingCall ->
val activity = mActivityProvider?.currentActivity as? PermissionAwareActivity
if (activity == null) {
// clear all pending calls, because we don't have access to the activity instance
pendingCall.second.onResult(parseNativeResult(pendingCall.first, IntArray(pendingCall.first.size) { PackageManager.PERMISSION_DENIED }))
mPendingPermissionCalls.forEach {
it.second.onResult(parseNativeResult(it.first, IntArray(it.first.size) { PackageManager.PERMISSION_DENIED }))
}
mPendingPermissionCalls.clear()
return@let
}
mCurrentPermissionListener = pendingCall.second
activity.requestPermissions(pendingCall.first, PERMISSIONS_REQUEST, createListenerWithPendingPermissionsRequest())
return@PermissionListener false
}
return@PermissionListener true
}
}
return@PermissionListener false
}
}
/**
* Asking for [android.provider.Settings.ACTION_MANAGE_WRITE_SETTINGS] via separate activity
* WARNING: has to be asked first among all permissions being asked in request
* Scenario that forces this order:
* 1. user asks for "systemBrightness" (actual [android.provider.Settings.ACTION_MANAGE_WRITE_SETTINGS]) and for some other permission (e.g. [android.Manifest.permission.CAMERA])
* 2. first goes ACTION_MANAGE_WRITE_SETTINGS that moves app into background and launches system-specific fullscreen activity
* 3. upon user action system resumes app and [onHostResume] is being called for the first time and logic for other permission is invoked
* 4. other permission invokes other system-specific activity that is visible as dialog what moves app again into background
* 5. upon user action app is restored and [onHostResume] is being called again, but no further action is invoked and promise is resolved
*/
private fun askForWriteSettingsPermissionFirst() {
Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS).apply {
data = Uri.parse("package:${context.packageName}")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}.let {
mWriteSettingsPermissionBeingAsked = true
context.startActivity(it)
}
}
private fun hasWriteSettingsPermission(): Boolean {
return Settings.System.canWrite(context.applicationContext)
}
override fun onHostResume() {
if (!mWriteSettingsPermissionBeingAsked) {
return
}
mWriteSettingsPermissionBeingAsked = false
// cleanup
val askAsyncListener = mAskAsyncListener!!
val askAsyncRequestedPermissions = mAskAsyncRequestedPermissions!!
mAskAsyncListener = null
mAskAsyncRequestedPermissions = null
if (askAsyncRequestedPermissions.isNotEmpty()) {
// invoke actual asking for permissions
askForManifestPermissions(askAsyncRequestedPermissions, askAsyncListener)
} else {
// user asked only for Manifest.permission.WRITE_SETTINGS
askAsyncListener.onResult(mutableMapOf())
}
}
override fun onHostPause() = Unit
override fun onHostDestroy() = Unit
}

View File

@@ -0,0 +1,91 @@
package expo.modules.adapters.react.services;
import android.os.Bundle;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import java.util.Collections;
import java.util.List;
import expo.modules.adapters.react.views.ViewManagerAdapterUtils;
import expo.modules.core.interfaces.InternalModule;
import expo.modules.core.interfaces.services.EventEmitter;
public class EventEmitterModule implements EventEmitter, InternalModule {
private ReactContext mReactContext;
public EventEmitterModule(ReactContext reactContext) {
mReactContext = reactContext;
}
@Override
public void emit(String eventName, Bundle eventBody) {
mReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, Arguments.fromBundle(eventBody));
}
@Override
public void emit(final int viewId, final Event event) {
final EventDispatcher dispatcher = UIManagerHelper.getEventDispatcherForReactTag(mReactContext, viewId);
dispatcher.dispatchEvent(getReactEventFromEvent(viewId, event));
}
@Override
public void emit(final int viewId, final String eventName, final Bundle eventBody) {
final EventDispatcher dispatcher = UIManagerHelper.getEventDispatcherForReactTag(mReactContext, viewId);
dispatcher.dispatchEvent(new com.facebook.react.uimanager.events.Event(viewId) {
@Override
public String getEventName() {
return ViewManagerAdapterUtils.normalizeEventName(eventName);
}
@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
rctEventEmitter.receiveEvent(viewId, getEventName(), eventBody != null ? Arguments.fromBundle(eventBody) : null);
}
@Override
public boolean canCoalesce() {
return false;
}
@Override
public short getCoalescingKey() {
return 0;
}
});
}
@Override
public List<Class> getExportedInterfaces() {
return Collections.singletonList((Class) EventEmitter.class);
}
private static com.facebook.react.uimanager.events.Event getReactEventFromEvent(final int viewId, final Event event) {
return new com.facebook.react.uimanager.events.Event(viewId) {
@Override
public String getEventName() {
return ViewManagerAdapterUtils.normalizeEventName(event.getEventName());
}
@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
rctEventEmitter.receiveEvent(viewId, getEventName(), Arguments.fromBundle(event.getEventBody()));
}
@Override
public boolean canCoalesce() {
return event.canCoalesce();
}
@Override
public short getCoalescingKey() {
return event.getCoalescingKey();
}
};
}
}

View File

@@ -0,0 +1,24 @@
package expo.modules.adapters.react.services;
import android.graphics.Typeface;
import com.facebook.react.views.text.ReactFontManager;
import java.util.Collections;
import java.util.List;
import expo.modules.core.interfaces.InternalModule;
import expo.modules.interfaces.font.FontManagerInterface;
public class FontManagerModule implements FontManagerInterface, InternalModule {
@Override
public List<Class> getExportedInterfaces() {
return Collections.<Class>singletonList(FontManagerInterface.class);
}
@Override
public void setTypeface(String fontFamilyName, int style, Typeface typeface) {
ReactFontManager.getInstance().setTypeface(fontFamilyName, style, typeface);
}
}

View File

@@ -0,0 +1,50 @@
package expo.modules.adapters.react.services;
import com.facebook.react.modules.systeminfo.ReactNativeVersion;
import expo.modules.core.interfaces.InternalModule;
import expo.modules.core.interfaces.RuntimeEnvironmentInterface;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class RuntimeEnvironmentModule implements InternalModule, RuntimeEnvironmentInterface {
@Override
public List<? extends Class> getExportedInterfaces() {
return Collections.singletonList(RuntimeEnvironmentInterface.class);
}
@Override
public String platformName() {
return "React Native";
}
@Override
public RuntimeEnvironmentInterface.PlatformVersion platformVersion() {
final Map<String, Object> version = ReactNativeVersion.VERSION;
return new RuntimeEnvironmentInterface.PlatformVersion() {
@Override
public int major() {
return (int) version.get("major");
}
@Override
public int minor() {
return (int) version.get("minor");
}
@Override
public int patch() {
return (int) version.get("patch");
}
@Override
public String prerelease() {
return (String) version.get("prerelease");
}
};
}
}

View File

@@ -0,0 +1,236 @@
package expo.modules.adapters.react.services;
import android.app.Activity;
import android.content.Intent;
import android.util.Log;
import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.turbomodule.core.CallInvokerHolderImpl;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.UIManagerModule;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import expo.modules.core.interfaces.ActivityEventListener;
import expo.modules.core.interfaces.ActivityProvider;
import expo.modules.core.interfaces.InternalModule;
import expo.modules.core.interfaces.JavaScriptContextProvider;
import expo.modules.core.interfaces.LifecycleEventListener;
import expo.modules.core.interfaces.services.UIManager;
public class UIManagerModuleWrapper implements
ActivityProvider,
InternalModule,
JavaScriptContextProvider,
UIManager {
private ReactContext mReactContext;
private Map<LifecycleEventListener, com.facebook.react.bridge.LifecycleEventListener> mLifecycleListenersMap = new WeakHashMap<>();
private Map<ActivityEventListener, com.facebook.react.bridge.ActivityEventListener> mActivityEventListenersMap = new WeakHashMap<>();
public UIManagerModuleWrapper(ReactContext reactContext) {
mReactContext = reactContext;
}
protected ReactContext getContext() {
return mReactContext;
}
@Override
public List<Class> getExportedInterfaces() {
return Arrays.<Class>asList(
ActivityProvider.class,
JavaScriptContextProvider.class,
UIManager.class
);
}
@Override
public <T> void addUIBlock(final int tag, final UIBlock<T> block, final Class<T> tClass) {
getContext().getNativeModule(UIManagerModule.class).addUIBlock(new com.facebook.react.uimanager.UIBlock() {
@Override
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
View view = nativeViewHierarchyManager.resolveView(tag);
if (view == null) {
block.reject(new IllegalArgumentException("Expected view for this tag not to be null."));
} else {
try {
if (tClass.isInstance(view)) {
block.resolve(tClass.cast(view));
} else {
block.reject(new IllegalStateException(
"Expected view to be of " + tClass + "; found " + view.getClass() + " instead"));
}
} catch (Exception e) {
block.reject(e);
}
}
}
});
}
@Override
public void addUIBlock(final GroupUIBlock block) {
getContext().getNativeModule(UIManagerModule.class).addUIBlock(new com.facebook.react.uimanager.UIBlock() {
@Override
public void execute(final NativeViewHierarchyManager nativeViewHierarchyManager) {
block.execute(new ViewHolder() {
@Override
public View get(Object key) {
if (key instanceof Number) {
try {
return nativeViewHierarchyManager.resolveView(((Number) key).intValue());
} catch (IllegalViewOperationException e) {
return null;
}
} else {
Log.w("E_INVALID_TAG", "Provided tag is of class " + key.getClass() + " whereas React expects tags to be integers. Are you sure you're providing proper argument to addUIBlock?");
}
return null;
}
});
}
});
}
@Nullable
@Override
public View resolveView(int viewTag) {
final com.facebook.react.bridge.UIManager uiManager = UIManagerHelper.getUIManagerForReactTag(getContext(), viewTag);
if (uiManager == null) {
return null;
}
return uiManager.resolveView(viewTag);
}
@Override
public void runOnUiQueueThread(Runnable runnable) {
if (getContext().isOnUiQueueThread()) {
runnable.run();
} else {
getContext().runOnUiQueueThread(runnable);
}
}
@Override
public void runOnClientCodeQueueThread(Runnable runnable) {
if (getContext().isOnJSQueueThread()) {
runnable.run();
} else {
getContext().runOnJSQueueThread(runnable);
}
}
public void runOnNativeModulesQueueThread(Runnable runnable) {
if (mReactContext.isOnNativeModulesQueueThread()) {
runnable.run();
} else {
mReactContext.runOnNativeModulesQueueThread(runnable);
}
}
@Override
public void registerLifecycleEventListener(final LifecycleEventListener listener) {
final WeakReference<LifecycleEventListener> weakListener = new WeakReference<>(listener);
mLifecycleListenersMap.put(listener, new com.facebook.react.bridge.LifecycleEventListener() {
@Override
public void onHostResume() {
LifecycleEventListener listener = weakListener.get();
if (listener != null) {
listener.onHostResume();
}
}
@Override
public void onHostPause() {
LifecycleEventListener listener = weakListener.get();
if (listener != null) {
listener.onHostPause();
}
}
@Override
public void onHostDestroy() {
LifecycleEventListener listener = weakListener.get();
if (listener != null) {
listener.onHostDestroy();
}
}
});
mReactContext.addLifecycleEventListener(mLifecycleListenersMap.get(listener));
}
@Override
public void onDestroy() {
// We need to create a copy to avoid ConcurrentModificationException
ArrayList<com.facebook.react.bridge.LifecycleEventListener> tmpList = new ArrayList<>(mLifecycleListenersMap.values());
for (com.facebook.react.bridge.LifecycleEventListener listener : tmpList) {
listener.onHostDestroy();
}
for (com.facebook.react.bridge.LifecycleEventListener listener : mLifecycleListenersMap.values()) {
mReactContext.removeLifecycleEventListener(listener);
}
mLifecycleListenersMap.clear();
}
@Override
public void unregisterLifecycleEventListener(LifecycleEventListener listener) {
getContext().removeLifecycleEventListener(mLifecycleListenersMap.get(listener));
mLifecycleListenersMap.remove(listener);
}
@Override
public void registerActivityEventListener(final ActivityEventListener activityEventListener) {
final WeakReference<ActivityEventListener> weakListener = new WeakReference<>(activityEventListener);
mActivityEventListenersMap.put(activityEventListener, new com.facebook.react.bridge.ActivityEventListener() {
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
ActivityEventListener listener = weakListener.get();
if (listener != null) {
listener.onActivityResult(activity, requestCode, resultCode, data);
}
}
@Override
public void onNewIntent(Intent intent) {
ActivityEventListener listener = weakListener.get();
if (listener != null) {
listener.onNewIntent(intent);
}
}
});
mReactContext.addActivityEventListener(mActivityEventListenersMap.get(activityEventListener));
}
@Override
public void unregisterActivityEventListener(final ActivityEventListener activityEventListener) {
getContext().removeActivityEventListener(mActivityEventListenersMap.get(activityEventListener));
mActivityEventListenersMap.remove(activityEventListener);
}
public long getJavaScriptContextRef() {
return mReactContext.getJavaScriptContextHolder().get();
}
public CallInvokerHolderImpl getJSCallInvokerHolder() {
return (CallInvokerHolderImpl) mReactContext.getCatalystInstance().getJSCallInvokerHolder();
}
@Override
public Activity getCurrentActivity() {
return getContext().getCurrentActivity();
}
}

View File

@@ -0,0 +1,10 @@
package expo.modules.adapters.react.views;
public class ViewManagerAdapterUtils {
public static String normalizeEventName(final String eventName) {
if (eventName.startsWith("on")) {
return "top" + eventName.substring(2);
}
return eventName;
}
}

View File

@@ -0,0 +1,17 @@
package expo.modules.apploader;
import java.util.List;
import expo.modules.core.interfaces.Package;
public interface AppLoaderPackagesProviderInterface<ReactPackageType> {
/**
* Returns a list of React Native packages to load.
*/
List<ReactPackageType> getPackages();
/**
* Returns a list of Expo packages to load.
*/
List<Package> getExpoPackages();
}

View File

@@ -0,0 +1,50 @@
package expo.modules.apploader;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;
import java.util.HashMap;
import java.util.Map;
public class AppLoaderProvider {
private static Map<String, HeadlessAppLoader> loaders = new HashMap<>();
public static HeadlessAppLoader getLoader(String name, Context context) {
if (!loaders.containsKey(name)) {
try {
createLoader(name, context);
} catch (Exception e) {
Log.e("Expo", "Cannot initialize app loader. " + e.getMessage());
e.printStackTrace();
return null;
}
}
return loaders.get(name);
}
@SuppressWarnings("unchecked")
private static void createLoader(String name, Context context) throws ClassNotFoundException, IllegalAccessException, InstantiationException, java.lang.reflect.InvocationTargetException, NoSuchMethodException {
Class<? extends HeadlessAppLoader> loaderClass;
try {
String loaderClassName = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA).metaData.getString("org.unimodules.core.AppLoader#" + name);
if (loaderClassName != null) {
loaderClass = (Class<? extends HeadlessAppLoader>)Class.forName(loaderClassName);
loaders.put(name, (HeadlessAppLoader) loaderClass.getDeclaredConstructor(Context.class).newInstance(context));
} else {
throw new IllegalStateException("Unable to instantiate AppLoader!");
}
} catch (PackageManager.NameNotFoundException e) {
throw new IllegalStateException("Unable to instantiate AppLoader!", e);
}
}
public static abstract class Callback {
public void onComplete(boolean success, Exception exception) {
// nothing
}
}
}

View File

@@ -0,0 +1,45 @@
package expo.modules.apploader;
import android.content.Context;
import expo.modules.core.interfaces.Consumer;
public interface HeadlessAppLoader {
class AppConfigurationError extends Exception {
public AppConfigurationError(String message) {
super(message);
}
public AppConfigurationError(String message, Exception cause) {
super(message, cause);
}
}
void loadApp(Context context, Params params, Runnable alreadyRunning, Consumer<Boolean> callback) throws AppConfigurationError;
boolean invalidateApp(String appScopeKey);
boolean isRunning(String appScopeKey);
final class Params {
private final String appScopeKey;
private final String appUrl;
public Params(String appScopeKey, String appUrl) {
this.appScopeKey = appScopeKey;
this.appUrl = appUrl;
}
public String getAppScopeKey() {
return appScopeKey;
}
public String getAppUrl() {
return appUrl;
}
}
}

View File

@@ -0,0 +1,48 @@
package expo.modules.core;
import android.content.Context;
import expo.modules.core.interfaces.ApplicationLifecycleListener;
import expo.modules.core.interfaces.InternalModule;
import expo.modules.core.interfaces.Package;
import expo.modules.core.interfaces.ReactActivityHandler;
import expo.modules.core.interfaces.ReactActivityLifecycleListener;
import expo.modules.core.interfaces.ReactNativeHostHandler;
import expo.modules.core.interfaces.SingletonModule;
import java.util.Collections;
import java.util.List;
// This class should not be used. Implement expo.modules.core.interfaces.Package instead of extending this class
// Remove once no one extends it.
public class BasePackage implements Package {
@Override
public List<InternalModule> createInternalModules(Context context) {
return Collections.emptyList();
}
@Override
public List<SingletonModule> createSingletonModules(Context context) {
return Collections.emptyList();
}
@Override
public List<ApplicationLifecycleListener> createApplicationLifecycleListeners(Context context) {
return Collections.emptyList();
}
@Override
public List<? extends ReactNativeHostHandler> createReactNativeHostHandlers(Context context) {
return Collections.emptyList();
}
@Override
public List<ReactActivityLifecycleListener> createReactActivityLifecycleListeners(Context activityContext) {
return Collections.emptyList();
}
@Override
public List<ReactActivityHandler> createReactActivityHandlers(Context activityContext) {
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,142 @@
package expo.modules.core;
import java.util.List;
import java.util.Map;
import expo.modules.core.interfaces.Arguments;
public class MapHelper implements Arguments {
private Map mMap;
public MapHelper(Map map) {
super();
mMap = map;
}
@Override
public boolean containsKey(String key) {
return mMap.containsKey(key);
}
@Override
public Object get(String key) {
return mMap.get(key);
}
@Override
public boolean getBoolean(String key) {
return getBoolean(key, false);
}
@Override
public boolean getBoolean(String key, boolean defaultValue) {
Object value = mMap.get(key);
if (value instanceof Boolean) {
return (Boolean) value;
}
return defaultValue;
}
@Override
public double getDouble(String key) {
return getDouble(key, 0);
}
@Override
public double getDouble(String key, double defaultValue) {
Object value = mMap.get(key);
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
return defaultValue;
}
@Override
public int getInt(String key) {
return getInt(key, 0);
}
@Override
public int getInt(String key, int defaultValue) {
Object value = mMap.get(key);
if (value instanceof Number) {
return ((Number) value).intValue();
}
return defaultValue;
}
@Override
public long getLong(String key) {
return getLong(key, 0);
}
@Override
public long getLong(String key, long defaultValue) {
Object value = mMap.get(key);
if (value instanceof Number) {
return ((Number) value).longValue();
}
return defaultValue;
}
@Override
public String getString(String key) {
return getString(key, null);
}
@Override
public String getString(String key, String defaultValue) {
Object value = mMap.get(key);
if (value instanceof String) {
return (String) value;
}
return defaultValue;
}
@Override
public List getList(String key) {
return getList(key, null);
}
@Override
public List getList(String key, List defaultValue) {
Object value = mMap.get(key);
if (value instanceof List) {
return (List) value;
}
return defaultValue;
}
@Override
public Map getMap(String key) {
return getMap(key, null);
}
@Override
public Map getMap(String key, Map defaultValue) {
Object value = mMap.get(key);
if (value instanceof Map) {
return (Map) value;
}
return defaultValue;
}
@Override
public boolean isEmpty() {
return mMap.isEmpty();
}
@Override
public int size() {
return mMap.size();
}
@Override
public Arguments getArguments(String key) {
Map value = getMap(key);
if (value != null) {
return new MapHelper(value);
}
return null;
}
}

View File

@@ -0,0 +1,26 @@
package expo.modules.core
/**
* This class determines the order of the following handlers/listeners
* - {@link ReactNativeHostHandler}
* - {@link ApplicationLifecycleListener}
* - {@link ReactActivityLifecycleListener}
* - {@link ReactActivityHandler}
*
* The priority is only for internal use and we maintain a pre-defined {@link SUPPORTED_MODULES} map.
*/
object ModulePriorities {
fun get(packageName: String?): Int {
return packageName?.let {
return SUPPORTED_MODULES[it] ?: 0
} ?: 0
}
private val SUPPORTED_MODULES = mapOf(
// {key} to {value}
// key: full qualified class name
// value: priority value, the higher value takes precedence
"expo.modules.splashscreen.SplashScreenPackage" to 11,
"expo.modules.updates.UpdatesPackage" to 10
)
}

View File

@@ -0,0 +1,123 @@
package expo.modules.core;
import expo.modules.core.interfaces.InternalModule;
import expo.modules.core.interfaces.RegistryLifecycleListener;
import expo.modules.core.interfaces.SingletonModule;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ModuleRegistry {
private final Map<Class, InternalModule> mInternalModulesMap = new HashMap<>();
private final Map<String, SingletonModule> mSingletonModulesMap = new HashMap<>();
private final List<WeakReference<RegistryLifecycleListener>> mExtraRegistryLifecycleListeners = new ArrayList<>();
private volatile boolean mIsInitialized = false;
public ModuleRegistry(
Collection<InternalModule> internalModules,
Collection<SingletonModule> singletonModules) {
for (InternalModule internalModule : internalModules) {
registerInternalModule(internalModule);
}
for (SingletonModule singleton : singletonModules) {
registerSingletonModule(singleton);
}
}
/********************************************************
*
* Getting registered modules
*
*******************************************************/
@SuppressWarnings("unchecked")
public <T> T getModule(Class<T> interfaceClass) {
return (T) mInternalModulesMap.get(interfaceClass);
}
public <T> T getSingletonModule(String singletonName, Class<T> singletonClass) {
return (T) mSingletonModulesMap.get(singletonName);
}
/********************************************************
*
* Registering modules
*
*******************************************************/
public void registerInternalModule(InternalModule module) {
for (Class exportedInterface : module.getExportedInterfaces()) {
mInternalModulesMap.put(exportedInterface, module);
}
}
public InternalModule unregisterInternalModule(Class exportedInterface) {
return mInternalModulesMap.remove(exportedInterface);
}
public void registerSingletonModule(SingletonModule singleton) {
String singletonName = singleton.getName();
mSingletonModulesMap.put(singletonName, singleton);
}
public void registerExtraListener(RegistryLifecycleListener outerListener) {
mExtraRegistryLifecycleListeners.add(new WeakReference<>(outerListener));
}
/********************************************************
*
* Managing registry consumers
*
*******************************************************/
/**
* Register a {@link ModuleRegistryConsumer} as interested of
* when {@link ModuleRegistry} will be ready, i.e. will have
* all the {@link InternalModule} instances registered.
*/
/**
* Call this when all the modules are initialized and registered
* in this {@link ModuleRegistry}, so its consumers can access
* all the needed instances.
*/
public synchronized void ensureIsInitialized() {
if (!mIsInitialized) {
initialize();
mIsInitialized = true;
}
}
public void initialize() {
List<RegistryLifecycleListener> lifecycleListeners = new ArrayList<>(mInternalModulesMap.values());
for (WeakReference<RegistryLifecycleListener> ref : mExtraRegistryLifecycleListeners) {
if (ref.get() != null) {
lifecycleListeners.add(ref.get());
}
}
for (RegistryLifecycleListener lifecycleListener : lifecycleListeners) {
lifecycleListener.onCreate(this);
}
}
public void onDestroy() {
List<RegistryLifecycleListener> lifecycleListeners = new ArrayList<>(mInternalModulesMap.values());
for (WeakReference<RegistryLifecycleListener> ref : mExtraRegistryLifecycleListeners) {
if (ref.get() != null) {
lifecycleListeners.add(ref.get());
}
}
for (RegistryLifecycleListener lifecycleListener : lifecycleListeners) {
lifecycleListener.onDestroy();
}
}
}

View File

@@ -0,0 +1,50 @@
package expo.modules.core;
import android.content.Context;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import expo.modules.core.interfaces.InternalModule;
import expo.modules.core.interfaces.Package;
import expo.modules.core.interfaces.SingletonModule;
/**
* Builder for {@link ModuleRegistry}. Override this class to add some custom
* modules from outside of {@link Package} ecosystem.
*/
public class ModuleRegistryProvider {
private List<Package> mPackages;
public ModuleRegistryProvider(List<Package> initialPackages) {
mPackages = initialPackages;
}
protected List<Package> getPackages() {
return mPackages;
}
public ModuleRegistry get(Context context) {
return new ModuleRegistry(
createInternalModules(context),
createSingletonModules(context)
);
}
public Collection<InternalModule> createInternalModules(Context context) {
Collection<InternalModule> internalModules = new ArrayList<>();
for (Package pkg : getPackages()) {
internalModules.addAll(pkg.createInternalModules(context));
}
return internalModules;
}
public Collection<SingletonModule> createSingletonModules(Context context) {
Collection<SingletonModule> singletonModules = new ArrayList<>();
for (Package pkg : getPackages()) {
singletonModules.addAll(pkg.createSingletonModules(context));
}
return singletonModules;
}
}

View File

@@ -0,0 +1,31 @@
package expo.modules.core;
import expo.modules.core.interfaces.CodedThrowable;
import kotlin.Deprecated;
@Deprecated(message = "AsyncFunction will crash when called. Use expo.modules.kotlin.Promise instead")
public interface Promise {
String UNKNOWN_ERROR = "E_UNKNOWN_ERROR";
void resolve(Object value);
void reject(String code, String message, Throwable e);
// Obsolete methods, however nice-to-have when porting React Native modules to Expo modules.
default void reject(Throwable e) {
if (e instanceof CodedThrowable) {
CodedThrowable codedThrowable = (CodedThrowable) e;
reject(codedThrowable.getCode(), codedThrowable.getMessage(), e);
} else {
reject(UNKNOWN_ERROR, e);
}
}
default void reject(String code, String message) {
reject(code, message, null);
}
default void reject(String code, Throwable e) {
reject(code, e.getMessage(), e);
}
}

View File

@@ -0,0 +1,97 @@
package expo.modules.core.arguments;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MapArguments implements ReadableArguments {
private Map<String, Object> mMap;
public MapArguments() {
mMap = new HashMap<>();
}
public MapArguments(Map<String, Object> map) {
mMap = map;
}
@Override
public Collection<String> keys() {
return mMap.keySet();
}
@Override
public boolean containsKey(String key) {
return mMap.containsKey(key);
}
@Override
public Object get(String key) {
return mMap.get(key);
}
@Override
public boolean getBoolean(String key, boolean defaultValue) {
Object value = mMap.get(key);
if (value instanceof Boolean) {
return (Boolean) value;
}
return defaultValue;
}
@Override
public double getDouble(String key, double defaultValue) {
Object value = mMap.get(key);
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
return defaultValue;
}
@Override
public int getInt(String key, int defaultValue) {
Object value = mMap.get(key);
if (value instanceof Number) {
return ((Number) value).intValue();
}
return defaultValue;
}
@Override
public String getString(String key, String defaultValue) {
Object value = mMap.get(key);
if (value instanceof String) {
return (String) value;
}
return defaultValue;
}
@Override
public List getList(String key, List defaultValue) {
Object value = mMap.get(key);
if (value instanceof List) {
return (List) value;
}
return defaultValue;
}
@Override
public Map getMap(String key, Map defaultValue) {
Object value = mMap.get(key);
if (value instanceof Map) {
return (Map) value;
}
return defaultValue;
}
@Override
public boolean isEmpty() {
return mMap.isEmpty();
}
@Override
public int size() {
return mMap.size();
}
}

View File

@@ -0,0 +1,94 @@
package expo.modules.core.arguments;
import android.os.Bundle;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public interface ReadableArguments {
Collection<String> keys();
boolean containsKey(String key);
Object get(String key);
default boolean getBoolean(String key) {
return getBoolean(key, false);
}
boolean getBoolean(String key, boolean defaultValue);
default double getDouble(String key) {
return getDouble(key, 0);
}
double getDouble(String key, double defaultValue);
default int getInt(String key) {
return getInt(key, 0);
}
int getInt(String key, int defaultValue);
default String getString(String key) {
return getString(key, null);
}
String getString(String key, String defaultValue);
default List getList(String key) {
return getList(key, null);
}
List getList(String key, List defaultValue);
default Map getMap(String key) {
return getMap(key, null);
}
Map getMap(String key, Map defaultValue);
@SuppressWarnings("unchecked")
default ReadableArguments getArguments(String key) {
Object value = get(key);
if (value instanceof Map) {
return new MapArguments((Map) value);
}
return null;
}
boolean isEmpty();
int size();
default Bundle toBundle() {
Bundle bundle = new Bundle();
for (String key : keys()) {
Object value = get(key);
if (value == null) {
bundle.putString(key, null);
} else if (value instanceof String) {
bundle.putString(key, (String) value);
} else if (value instanceof Integer) {
bundle.putInt(key, (Integer) value);
} else if (value instanceof Double) {
bundle.putDouble(key, (Double) value);
} else if (value instanceof Long) {
bundle.putLong(key, (Long) value);
} else if (value instanceof Boolean) {
bundle.putBoolean(key, (Boolean) value);
} else if (value instanceof ArrayList) {
bundle.putParcelableArrayList(key, (ArrayList) value);
} else if (value instanceof Map) {
bundle.putBundle(key, new MapArguments((Map) value).toBundle());
} else if (value instanceof Bundle) {
bundle.putBundle(key, (Bundle) value);
} else {
throw new UnsupportedOperationException("Could not put a value of " + value.getClass() + " to bundle.");
}
}
return bundle;
}
}

View File

@@ -0,0 +1,26 @@
package expo.modules.core.errors;
import expo.modules.core.interfaces.CodedThrowable;
/**
* Base class that can be extended to create coded errors that promise.reject
* can handle.
*/
public abstract class CodedException extends Exception implements CodedThrowable {
public CodedException(String message) {
super(message);
}
public CodedException(Throwable cause) {
super(cause);
}
public CodedException(String message, Throwable cause) {
super(message, cause);
}
public String getCode() {
return "ERR_UNSPECIFIED_ANDROID_EXCEPTION";
}
}

View File

@@ -0,0 +1,26 @@
package expo.modules.core.errors;
import expo.modules.core.interfaces.CodedThrowable;
/**
* Base class that can be extended to create coded runtime errors that
* promise.reject can handle.
*/
public abstract class CodedRuntimeException extends RuntimeException implements CodedThrowable {
public CodedRuntimeException(String message) {
super(message);
}
public CodedRuntimeException(Throwable cause) {
super(cause);
}
public CodedRuntimeException(String message, Throwable cause) {
super(message, cause);
}
public String getCode() {
return "ERR_UNSPECIFIED_ANDROID_EXCEPTION";
}
}

View File

@@ -0,0 +1,7 @@
package expo.modules.core.errors
import kotlinx.coroutines.CancellationException
private const val DEFAULT_MESSAGE = "App context destroyed. All coroutines are cancelled."
class ContextDestroyedException(message: String = DEFAULT_MESSAGE) : CancellationException(message)

View File

@@ -0,0 +1,14 @@
package expo.modules.core.errors;
import expo.modules.core.interfaces.CodedThrowable;
public class CurrentActivityNotFoundException extends CodedException implements CodedThrowable {
public CurrentActivityNotFoundException() {
super("Current activity not found. Make sure to call this method while your application is in foreground.");
}
@Override
public String getCode() {
return "E_CURRENT_ACTIVITY_NOT_FOUND";
}
}

View File

@@ -0,0 +1,26 @@
package expo.modules.core.errors;
/**
* Exception for mismatched host-to-native interfaces. Compared to a Java-only
* program, these modules are more susceptible to mismatched interfaces, and
* this class helps harden those interfaces.
*/
public class InvalidArgumentException extends CodedRuntimeException {
public InvalidArgumentException(String message) {
super(message);
}
public InvalidArgumentException(Throwable cause) {
super(cause);
}
public InvalidArgumentException(String message, Throwable cause) {
super(message, cause);
}
@Override
public String getCode() {
return "ERR_INVALID_ARGUMENT";
}
}

View File

@@ -0,0 +1,22 @@
package expo.modules.core.errors
import kotlinx.coroutines.CancellationException
private const val DEFAULT_MESSAGE = "Module destroyed. All coroutines are cancelled."
/**
* Can be passed as parameter to [kotlinx.coroutines.cancel] when module is destroyed
* in order to cancel all coroutines in [kotlinx.coroutines.CoroutineScope]
*
* Example usage:
* ```
* override fun onDestroy() {
* try {
* moduleCoroutineScope.cancel(ModuleDestroyedException())
* } catch (e: IllegalStateException) {
* Log.w(TAG, "The scope does not have a job in it")
* }
* }
* ```
*/
class ModuleDestroyedException(message: String = DEFAULT_MESSAGE) : CancellationException(message)

View File

@@ -0,0 +1,14 @@
package expo.modules.core.errors;
import expo.modules.core.interfaces.CodedThrowable;
public class ModuleNotFoundException extends CodedException implements CodedThrowable {
public ModuleNotFoundException(String moduleName) {
super("Module '" + moduleName + "' not found. Are you sure all modules are linked correctly?");
}
@Override
public String getCode() {
return "E_MODULE_NOT_FOUND";
}
}

View File

@@ -0,0 +1,13 @@
package expo.modules.core.interfaces;
import android.app.Activity;
import android.content.Intent;
import androidx.annotation.Nullable;
public interface ActivityEventListener {
public void onActivityResult(Activity activity, int requestCode, int resultCode, @Nullable Intent data);
// Called when a new intent is passed to the activity
public void onNewIntent(Intent intent);
}

View File

@@ -0,0 +1,7 @@
package expo.modules.core.interfaces;
import android.app.Activity;
public interface ActivityProvider {
Activity getCurrentActivity();
}

View File

@@ -0,0 +1,10 @@
package expo.modules.core.interfaces;
import android.app.Application;
import android.content.res.Configuration;
public interface ApplicationLifecycleListener {
default void onCreate(Application application) {}
default void onConfigurationChanged(Configuration newConfig) {}
}

View File

@@ -0,0 +1,44 @@
package expo.modules.core.interfaces;
import java.util.List;
import java.util.Map;
public interface Arguments {
boolean containsKey(String key);
Object get(String key);
boolean getBoolean(String key);
boolean getBoolean(String key, boolean defaultValue);
double getDouble(String key);
double getDouble(String key, double defaultValue);
int getInt(String key);
int getInt(String key, int defaultValue);
long getLong(String key);
long getLong(String key, long defaultValue);
String getString(String key);
String getString(String key, String defaultValue);
List getList(String key);
List getList(String key, List defaultValue);
Map getMap(String key);
Map getMap(String key, Map defaultValue);
Arguments getArguments(String key);
boolean isEmpty();
int size();
}

View File

@@ -0,0 +1,12 @@
package expo.modules.core.interfaces;
/**
* Helper interface to make errors easier to handle. The promise.reject
* implementation should know about this interface and be able to get the code
* itself when passed an object which implements it.
*/
public interface CodedThrowable {
String getCode();
String getMessage();
}

View File

@@ -0,0 +1,7 @@
package expo.modules.core.interfaces;
public interface Consumer<T> {
void apply(T t);
}

View File

@@ -0,0 +1,18 @@
package expo.modules.core.interfaces;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.CLASS;
/**
* Add this annotation to a class, method, or field to instruct Proguard to not strip it out.
*
* This is useful for methods called via reflection that could appear as unused to Proguard.
*/
@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR })
@Retention(CLASS)
public @interface DoNotStrip {
}

View File

@@ -0,0 +1,7 @@
package expo.modules.core.interfaces;
public interface Function<T, R> {
R apply(T t);
}

Some files were not shown because too many files have changed in this diff Show More