Skip to content

Commit 47392e6

Browse files
authored
Merge pull request #21 from callstackincubator/feat/android-additional-support
Feat/android additional support
2 parents e846dca + 1c120ea commit 47392e6

25 files changed

Lines changed: 679 additions & 603 deletions

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ project.xcworkspace
4343
.settings
4444
local.properties
4545
android.iml
46+
apps/*/android/app/src/main/assets/
47+
apps/*/android/app/src/main/res/drawable-*/
48+
apps/*/android/app/src/main/res/raw/
4649

4750
# Cocoapods
4851
#
@@ -54,6 +57,9 @@ vendor/
5457
# node.js
5558
#
5659
node_modules/
60+
node_modules
61+
**/node_modules/
62+
**/node_modules
5763
npm-debug.log
5864
yarn-debug.log
5965
yarn-error.log

apps/demo/ios/Demo.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@
385385
"$(inherited)",
386386
" ",
387387
);
388-
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
388+
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
389389
SDKROOT = iphoneos;
390390
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
391391
USE_HERMES = true;
@@ -462,7 +462,7 @@
462462
"$(inherited)",
463463
" ",
464464
);
465-
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
465+
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
466466
SDKROOT = iphoneos;
467467
USE_HERMES = true;
468468
VALIDATE_PRODUCT = YES;

apps/demo/ios/Podfile.lock

Lines changed: 145 additions & 145 deletions
Large diffs are not rendered by default.

packages/react-native-sandbox/React-Sandbox.podspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ Pod::Spec.new do |s|
1818
s.authors = { "Alex Babrykovich" => "aliaksandr.babrykovich@callstack.com" }
1919
s.platforms = { :ios => "12.4" }
2020
s.source = { :git => "https://github.com/callstackincubator/react-native-sandbox.git", :tag => "#{s.version}" }
21-
s.source_files = "ios/**/*.{h,m,mm,cpp,swift}"
21+
s.source_files = ["ios/**/*.{h,m,mm,cpp,swift}", "cxx/**/*.{h,cpp}"]
2222
install_modules_dependencies(s)
2323
s.dependency "fmt"
2424
s.pod_target_xcconfig = {
25-
"HEADER_SEARCH_PATHS" => header_search_paths,
25+
"HEADER_SEARCH_PATHS" => header_search_paths + ["\"$(PODS_TARGET_SRCROOT)/cxx\""],
2626
# "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
2727
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
2828
}

packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxJSIInstaller.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,13 @@ object SandboxJSIInstaller {
4444
*/
4545
@JvmStatic
4646
external fun nativeDestroy(stateHandle: Long)
47+
48+
/**
49+
* Installs the error handler into the JS runtime. Must be called on the
50+
* JS thread after the bundle has loaded (when ErrorUtils is available).
51+
*
52+
* @param stateHandle Handle returned by nativeInstall
53+
*/
54+
@JvmStatic
55+
external fun nativeInstallErrorHandler(stateHandle: Long)
4756
}

packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxReactNativeDelegate.kt

Lines changed: 89 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -33,29 +33,21 @@ class SandboxReactNativeDelegate(
3333
) {
3434
companion object {
3535
private const val TAG = "SandboxRNDelegate"
36+
37+
private val sharedHosts = mutableMapOf<String, SharedReactHost>()
38+
39+
private data class SharedReactHost(
40+
val reactHost: ReactHostImpl,
41+
val sandboxContext: Context,
42+
var refCount: Int,
43+
)
3644
}
3745

38-
var origin: String = ""
39-
set(value) {
40-
if (field == value) return
41-
if (field.isNotEmpty()) {
42-
SandboxRegistry.unregister(field)
43-
}
44-
field = value
45-
if (value.isNotEmpty()) {
46-
SandboxRegistry.register(value, this, allowedOrigins)
47-
}
48-
}
46+
@JvmField var origin: String = ""
4947

5048
var jsBundleSource: String = ""
5149
var allowedTurboModules: Set<String> = emptySet()
5250
var allowedOrigins: Set<String> = emptySet()
53-
set(value) {
54-
field = value
55-
if (origin.isNotEmpty()) {
56-
SandboxRegistry.register(origin, this, value)
57-
}
58-
}
5951

6052
@JvmField var hasOnMessageHandler: Boolean = false
6153

@@ -66,6 +58,7 @@ class SandboxReactNativeDelegate(
6658
private var reactSurface: ReactSurface? = null
6759
private var jsiStateHandle: Long = 0
6860
private var sandboxReactContext: ReactContext? = null
61+
private var ownsReactHost = false
6962

7063
@OptIn(UnstableReactNativeAPI::class)
7164
fun loadReactNativeView(
@@ -79,49 +72,75 @@ class SandboxReactNativeDelegate(
7972

8073
val capturedBundleSource = jsBundleSource
8174
val capturedAllowedModules = allowedTurboModules
82-
val sandboxId = System.identityHashCode(this).toString(16)
83-
val sandboxContext = SandboxContextWrapper(context, sandboxId)
8475

8576
try {
86-
val packages: List<ReactPackage> =
87-
listOf(
88-
FilteredReactPackage(MainReactPackage(), capturedAllowedModules),
89-
)
90-
91-
val bundleLoader = createBundleLoader(capturedBundleSource) ?: return null
92-
93-
val tmmDelegateBuilder = DefaultTurboModuleManagerDelegate.Builder()
94-
95-
val bindingsInstaller = SandboxBindingsInstaller.create(this)
96-
97-
val hostDelegate =
98-
DefaultReactHostDelegate(
99-
jsMainModulePath = capturedBundleSource,
100-
jsBundleLoader = bundleLoader,
101-
reactPackages = packages,
102-
jsRuntimeFactory = HermesInstance(),
103-
turboModuleManagerDelegateBuilder = tmmDelegateBuilder,
104-
bindingsInstaller = bindingsInstaller,
105-
)
106-
107-
val componentFactory = ComponentFactory()
108-
DefaultComponentsRegistry.register(componentFactory)
109-
110-
val host =
111-
ReactHostImpl(
112-
sandboxContext,
113-
hostDelegate,
114-
componentFactory,
115-
true,
116-
true,
117-
)
77+
val shared = if (origin.isNotEmpty()) sharedHosts[origin] else null
78+
79+
val host: ReactHostImpl
80+
val sandboxContext: Context
81+
82+
if (shared != null) {
83+
host = shared.reactHost
84+
sandboxContext = shared.sandboxContext
85+
shared.refCount++
86+
ownsReactHost = false
87+
Log.d(TAG, "Reusing shared ReactHost for origin '$origin' (refCount=${shared.refCount})")
88+
} else {
89+
val sandboxId = System.identityHashCode(this).toString(16)
90+
sandboxContext = SandboxContextWrapper(context, sandboxId)
91+
92+
val packages: List<ReactPackage> =
93+
listOf(
94+
FilteredReactPackage(MainReactPackage(), capturedAllowedModules),
95+
)
96+
97+
val bundleLoader = createBundleLoader(capturedBundleSource) ?: return null
98+
99+
val tmmDelegateBuilder = DefaultTurboModuleManagerDelegate.Builder()
100+
101+
val bindingsInstaller = SandboxBindingsInstaller.create(this)
102+
103+
val hostDelegate =
104+
DefaultReactHostDelegate(
105+
jsMainModulePath = capturedBundleSource,
106+
jsBundleLoader = bundleLoader,
107+
reactPackages = packages,
108+
jsRuntimeFactory = HermesInstance(),
109+
turboModuleManagerDelegateBuilder = tmmDelegateBuilder,
110+
bindingsInstaller = bindingsInstaller,
111+
)
112+
113+
val componentFactory = ComponentFactory()
114+
DefaultComponentsRegistry.register(componentFactory)
115+
116+
host =
117+
ReactHostImpl(
118+
sandboxContext,
119+
hostDelegate,
120+
componentFactory,
121+
true,
122+
true,
123+
)
124+
125+
ownsReactHost = true
126+
127+
if (origin.isNotEmpty()) {
128+
sharedHosts[origin] = SharedReactHost(host, sandboxContext, refCount = 1)
129+
Log.d(TAG, "Created shared ReactHost for origin '$origin'")
130+
}
131+
}
118132

119133
reactHost = host
120134

121135
host.addReactInstanceEventListener(
122136
object : ReactInstanceEventListener {
123137
override fun onReactContextInitialized(reactContext: ReactContext) {
124138
sandboxReactContext = reactContext
139+
if (jsiStateHandle != 0L) {
140+
reactContext.runOnJSQueueThread {
141+
SandboxJSIInstaller.nativeInstallErrorHandler(jsiStateHandle)
142+
}
143+
}
125144
}
126145
},
127146
)
@@ -240,7 +259,8 @@ class SandboxReactNativeDelegate(
240259
return false
241260
}
242261

243-
return routeMessage(messageJson, targetOrigin)
262+
// Routing handled entirely in C++ SandboxRegistry (see SandboxJSIInstaller.cpp)
263+
return false
244264
}
245265

246266
@Suppress("unused")
@@ -261,28 +281,6 @@ class SandboxReactNativeDelegate(
261281
}
262282
}
263283

264-
fun routeMessage(
265-
message: String,
266-
targetId: String,
267-
): Boolean {
268-
val target = SandboxRegistry.find(targetId)
269-
Log.d(TAG, "routeMessage from '$origin' to '$targetId': target found=${target != null}")
270-
if (target == null) return false
271-
272-
if (!SandboxRegistry.isPermittedFrom(origin, targetId)) {
273-
Log.w(TAG, "routeMessage DENIED: '$origin' -> '$targetId'")
274-
sandboxView?.emitOnError(
275-
"AccessDeniedError",
276-
"Access denied: Sandbox '$origin' is not permitted to send messages to '$targetId'",
277-
)
278-
return false
279-
}
280-
281-
Log.d(TAG, "routeMessage PERMITTED: '$origin' -> '$targetId', delivering...")
282-
target.postMessage(message)
283-
return true
284-
}
285-
286284
private fun getActivity(): Activity? {
287285
var ctx = context
288286
while (ctx is android.content.ContextWrapper) {
@@ -305,17 +303,28 @@ class SandboxReactNativeDelegate(
305303
}
306304
reactSurface = null
307305

308-
reactHost?.let {
309-
it.onHostDestroy()
310-
it.destroy("sandbox cleanup", null)
306+
val host = reactHost
307+
if (host != null) {
308+
if (origin.isNotEmpty()) {
309+
val shared = sharedHosts[origin]
310+
if (shared != null && shared.reactHost === host) {
311+
shared.refCount--
312+
if (shared.refCount <= 0) {
313+
sharedHosts.remove(origin)
314+
host.onHostDestroy()
315+
host.destroy("sandbox cleanup", null)
316+
}
317+
}
318+
} else if (ownsReactHost) {
319+
host.onHostDestroy()
320+
host.destroy("sandbox cleanup", null)
321+
}
311322
}
312323
reactHost = null
324+
ownsReactHost = false
313325
}
314326

315327
fun destroy() {
316-
if (origin.isNotEmpty()) {
317-
SandboxRegistry.unregister(origin)
318-
}
319328
cleanup()
320329
}
321330

packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxRegistry.kt

Lines changed: 0 additions & 72 deletions
This file was deleted.

packages/react-native-sandbox/android/src/main/jni/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,22 @@ project(rnsandbox)
44
set(CMAKE_CXX_STANDARD 17)
55
set(CMAKE_CXX_STANDARD_REQUIRED ON)
66

7+
# Shared C++ sources
8+
set(CPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../cxx")
9+
710
find_package(fbjni REQUIRED CONFIG)
811
find_package(ReactAndroid REQUIRED CONFIG)
912

1013
add_library(${PROJECT_NAME} SHARED
1114
SandboxJSIInstaller.cpp
1215
SandboxBindingsInstaller.cpp
16+
${CPP_DIR}/SandboxRegistry.cpp
1317
)
1418

1519
target_include_directories(${PROJECT_NAME}
1620
PRIVATE
1721
"${CMAKE_CURRENT_SOURCE_DIR}"
22+
"${CPP_DIR}"
1823
)
1924

2025
target_link_libraries(${PROJECT_NAME}

0 commit comments

Comments
 (0)