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,2 @@
// @generated by expo-module-scripts
module.exports = require('expo-module-scripts/eslintrc.base.js');

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,107 @@
require 'json'
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
reactNativeVersion = '0.0.0'
begin
reactNativeVersion = `node --print "require('react-native/package.json').version"`
rescue
reactNativeVersion = '0.0.0'
end
reactNativeTargetVersion = reactNativeVersion.split('.')[1].to_i
fabric_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'
fabric_compiler_flags = '-DRN_FABRIC_ENABLED -DRCT_NEW_ARCH_ENABLED'
folly_version = '2022.05.16.00'
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -DFOLLY_CFG_NO_COROUTINES=1 -Wno-comma -Wno-shorten-64-to-32'
Pod::Spec.new do |s|
s.name = 'ExpoModulesCore'
s.version = package['version']
s.summary = package['description']
s.description = package['description']
s.license = package['license']
s.author = package['author']
s.homepage = package['homepage']
s.platforms = {
:ios => '13.4',
:osx => '10.15',
:tvos => '13.4'
}
s.swift_version = '5.4'
s.source = { git: 'https://github.com/expo/expo.git' }
s.static_framework = true
s.header_dir = 'ExpoModulesCore'
header_search_paths = [
]
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'USE_HEADERMAP' => 'YES',
'DEFINES_MODULE' => 'YES',
'CLANG_CXX_LANGUAGE_STANDARD' => 'c++20',
'SWIFT_COMPILATION_MODE' => 'wholemodule',
'HEADER_SEARCH_PATHS' => header_search_paths.join(' '),
'OTHER_SWIFT_FLAGS' => "$(inherited) #{fabric_enabled ? fabric_compiler_flags : ''}"
}
user_header_search_paths = [
'"${PODS_CONFIGURATION_BUILD_DIR}/ExpoModulesCore/Swift Compatibility Header"',
'"$(PODS_ROOT)/Headers/Private/Yoga"', # Expo.h -> ExpoModulesCore-umbrella.h -> Fabric ViewProps.h -> Private Yoga headers
]
s.user_target_xcconfig = {
"HEADER_SEARCH_PATHS" => user_header_search_paths,
}
compiler_flags = folly_compiler_flags + ' ' + "-DREACT_NATIVE_TARGET_VERSION=#{reactNativeTargetVersion}"
if ENV['USE_HERMES'] == nil || ENV['USE_HERMES'] == '1'
compiler_flags += ' -DUSE_HERMES'
s.dependency 'hermes-engine'
add_dependency(s, "React-jsinspector", :framework_name => 'jsinspector_modern')
else
s.dependency 'React-jsc'
end
s.dependency 'React-Core'
s.dependency 'ReactCommon/turbomodule/core'
s.dependency 'React-RCTAppDelegate'
s.dependency 'React-NativeModulesApple'
if fabric_enabled
compiler_flags << ' ' << fabric_compiler_flags
s.dependency 'React-RCTFabric'
s.dependency 'RCT-Folly', folly_version
end
unless defined?(install_modules_dependencies)
# `install_modules_dependencies` is defined from react_native_pods.rb.
# when running with `pod ipc spec`, this method is not defined and we have to require manually.
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
end
install_modules_dependencies(s)
if !$ExpoUseSources&.include?(package['name']) && ENV['EXPO_USE_SOURCE'].to_i == 0 && File.exist?("ios/#{s.name}.xcframework") && Gem::Version.new(Pod::VERSION) >= Gem::Version.new('1.10.0')
s.source_files = 'ios/**/*.h', 'common/cpp/**/*.h'
s.vendored_frameworks = "ios/#{s.name}.xcframework"
else
s.source_files = 'ios/**/*.{h,m,mm,swift,cpp}', 'common/cpp/**/*.{h,cpp}'
end
exclude_files = ['ios/Tests/']
if !fabric_enabled
exclude_files.append('ios/Fabric/')
exclude_files.append('common/cpp/fabric/')
end
s.exclude_files = exclude_files
s.compiler_flags = compiler_flags
s.private_header_files = ['ios/**/*+Private.h', 'ios/**/Swift.h']
s.test_spec 'Tests' do |test_spec|
test_spec.dependency 'ExpoModulesTestCore'
test_spec.source_files = 'ios/Tests/**/*.{m,swift}'
end
end

View File

@@ -0,0 +1,98 @@
<p>
<a href="https://docs.expo.dev/modules/">
<img
src="../../.github/resources/expo-modules-core.svg"
alt="expo-modules-core"
height="64" />
</a>
</p>
The core of Expo Modules architecture.
# Installation in managed Expo projects
For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](#api-documentation). If you follow the link and there is no documentation available then this library is not yet usable within managed projects &mdash; it is likely to be included in an upcoming Expo SDK release.
# Installation in bare React Native projects
For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing.
### Add the package to your npm dependencies
```
npm install expo-modules-core
```
### Configure for iOS
Run `npx pod-install` after installing the npm package.
### Configure for Android
No additional set up necessary.
# Importing native dependencies - autolinking
Many React Native libraries come with platform-specific (native) code. This native code has to be linked into the project and, in some cases, configured further. These actions require some modifications to the native project files. One of the steps that has to be done with the native configuration is to enable the autolinking mechanism that takes care of including any supported module's native code into the project. The following configuration is required:
### iOS
> Caution! After you have made the following changes you will need to run `pod install` again.
```ruby
# Podfile
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
require File.join(File.dirname(`node --print "require.resolve('expo-modules-core/package.json')"`), "cocoapods.rb")
require File.join(File.dirname(`node --print "require.resolve('expo-modules-core/package.json')"`), "scripts/autolinking")
# ...
target "TargetName" do
use_unimodules!
config = use_native_modules!
use_react_native!(:path => config["reactNativePath"])
# ...
end
```
### Android
```groovy
// app/build.gradle
apply from: new File(["node", "--print", "require.resolve('expo-modules-core/package.json')"].execute(null, rootDir).text.trim(), "../gradle.groovy")
apply from: new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../react.gradle")
apply from: new File(["node", "--print", "require.resolve('expo-updates/package.json')"].execute(null, rootDir).text.trim(), "../scripts/create-manifest-android.gradle")
// ...
apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle");
applyNativeModulesAppBuildGradle(project)
```
```groovy
// settings.gradle
apply from: new File(["node", "--print", "require.resolve('expo-modules-core/package.json')"].execute(null, rootDir).text.trim(), "../gradle.groovy");
includeUnimodulesProjects()
apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle");
applyNativeModulesSettingsGradle(settings)
```
### Explanation
The scripts that are referenced in Gradle and Cocoapods are usually found the related package inside the project's `node_modules` directory. In the case of monorepos (such as Yarn workspaces) the project directory may not contain `node_modules` at all; instead, the modules are likely to be located at the root of the repository. In order to ensure that both cases are supported, we take advantage of the Node dependency resolution strategy. We invoke a subprocess that spawns the simple JavaScript snippet that tries to locate the desired npm package containing the script we need.
- On iOS, we use [`` ` ` `` (backtick)](https://stackoverflow.com/questions/3159945/running-command-line-commands-within-ruby-script) ([alternative reference](https://ruby-doc.org/core-3.0.2/Kernel.html#method-i-60)).
- On Android, we use [String[]#execute](<http://docs.groovy-lang.org/latest/html/groovy-jdk/java/lang/String[].html#execute()>) to obtain the results from the subprocess' stdout.
The Node process that is spawned runs [`require.resolve`](https://nodejs.org/dist/latest-v14.x/docs/api/modules.html#modules_require_resolve_request_options) to obtain the path to a module's `package.json`(if you look for the module location using solely module name, you'll be given the path to the file pointed by the `main` attribute from the `package.json` and we need to know the location of the module's root directory). We then assemble the path to the desired script and execute it.
You can read more about the scripts and libraries used in the examples above in their official READMEs.
# Contributing
Contributions are very welcome! Please refer to guidelines described in the [contributing guide](https://github.com/expo/expo#contributing).

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();
}

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