Skip to content

Commit 3ad2f17

Browse files
committed
fix(mobile): stabilize Android JSI startup and heap window routing
- jsi heap windows now register dynamically as new pointer regions are exposed to JS, removing the static 4-window assumption that crashed on stack and far-arena pointers; fixes underflow when probe addresses are below 4 GiB - getDependFilePath: object-form alias schema { package, lib? } with auto-derived lib (strip non-alphanumeric), multi-package disambiguation by per-target prebuilt existence, regex/fs caches, prefix boundary fix - android plugin: invoke build_js.js from build.gradle at configuration phase; new-arch autolinking ignores externalNativeBuild, so the script/CMakeLists.txt path never fired and bridges were missing on cold Android builds - RNJsiLibModule: dispatch install() to JS queue thread via runOnJSQueueThread; the previous direct call ran on the native-modules thread and crashed Hermes (thread-affine) non-deterministically on cold start. sleep(5) workaround removed - align ios-iphoneos podspec resource paths with the runtime/buildType target.path convention - bump cpp.js, core-embind-jsi, plugin-react-native to beta.17
1 parent 9862261 commit 3ad2f17

10 files changed

Lines changed: 227 additions & 79 deletions

File tree

cppjs-core/cpp.js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cpp.js",
3-
"version": "2.0.0-beta.16",
3+
"version": "2.0.0-beta.17",
44
"license": "MIT",
55
"homepage": "https://cpp.js.org",
66
"repository": "https://github.com/bugra9/cpp.js.git",
Lines changed: 97 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,106 @@
11
import fs from 'node:fs';
22
import state from '../state/index.js';
33

4-
export default function getDependFilePath(source, target) {
5-
const headerRegex = new RegExp(`\\.(${state.config.ext.header.join('|')})$`);
6-
const moduleRegex = new RegExp(`\\.(${state.config.ext.module.join('|')})$`);
7-
8-
const dependPackage = state.config.allDependencies.find((d) => source.startsWith(d.package.name));
9-
if (dependPackage) {
10-
const depName = dependPackage.package.name;
11-
const configName = dependPackage.general.name;
12-
const filePath = source.substring(depName.length + 1);
13-
14-
let path;
15-
if (headerRegex.test(source)) {
16-
path = `${dependPackage.paths.output}/prebuilt/${target.path}/include`;
17-
} else if (moduleRegex.test(source)) {
18-
path = `${dependPackage.paths.output}/prebuilt/${target.path}/swig`;
19-
} else {
20-
path = `${dependPackage.paths.output}/prebuilt/${target.path}`;
21-
}
4+
const LAYOUT = { header: 'include', module: 'swig', source: '' };
5+
6+
let extRegexCache = { key: null, header: null, module: null };
7+
function getExtRegex() {
8+
const { header, module } = state.config.ext;
9+
const key = `${header.join(',')}|${module.join(',')}`;
10+
if (extRegexCache.key !== key) {
11+
extRegexCache = {
12+
key,
13+
header: new RegExp(`\\.(${header.join('|')})$`),
14+
module: new RegExp(`\\.(${module.join('|')})$`),
15+
};
16+
}
17+
return extRegexCache;
18+
}
19+
20+
const existsCache = new Map();
21+
function existsCached(p) {
22+
if (existsCache.has(p)) return existsCache.get(p);
23+
const result = fs.existsSync(p);
24+
existsCache.set(p, result);
25+
return result;
26+
}
27+
28+
function getAlias(pkg) {
29+
const a = pkg.general.alias;
30+
if (!a) return null;
31+
if (typeof a !== 'object' || !a.package) {
32+
throw new Error(
33+
`Invalid alias for package "${pkg.package.name}": expected { package, lib? }.`,
34+
);
35+
}
36+
return {
37+
package: a.package,
38+
lib: a.lib ?? a.package.replace(/[^a-zA-Z0-9]/g, ''),
39+
};
40+
}
41+
42+
function getMatchingPackages(source) {
43+
const matches = [];
44+
for (const pkg of state.config.allDependencies) {
45+
const alias = getAlias(pkg);
46+
const prefixes = [pkg.package.name];
47+
if (alias) prefixes.push(alias.package);
2248

23-
if (fs.existsSync(`${path}/${configName}/${filePath}`)) {
24-
path = `${path}/${configName}/${filePath}`;
25-
} else if (fs.existsSync(`${path}/${depName}/${filePath}`)) {
26-
path = `${path}/${depName}/${filePath}`;
27-
} else if (fs.existsSync(`${path}/${filePath}`)) {
28-
path = `${path}/${filePath}`;
29-
} else {
30-
throw new Error(`${source} not found in ${depName} package.`);
49+
for (const prefix of prefixes) {
50+
if (source === prefix || source.startsWith(`${prefix}/`)) {
51+
matches.push({
52+
pkg,
53+
alias,
54+
relativePath: source.slice(prefix.length + 1),
55+
});
56+
break;
57+
}
3158
}
59+
}
60+
return matches;
61+
}
62+
63+
function getLayoutSubdir(source) {
64+
const ext = getExtRegex();
65+
if (ext.header.test(source)) return LAYOUT.header;
66+
if (ext.module.test(source)) return LAYOUT.module;
67+
return LAYOUT.source;
68+
}
69+
70+
function getFolderCandidates(pkg, alias) {
71+
const candidates = [];
72+
if (alias) candidates.push(alias.lib);
73+
candidates.push(pkg.general.name, pkg.package.name, '');
74+
return [...new Set(candidates)];
75+
}
3276

33-
return path;
77+
export default function getDependFilePath(source, target) {
78+
const matches = getMatchingPackages(source);
79+
if (matches.length === 0) return null;
80+
81+
const subdir = getLayoutSubdir(source);
82+
const tried = [];
83+
84+
for (const { pkg, alias, relativePath } of matches) {
85+
const prebuiltRoot = `${pkg.paths.output}/prebuilt/${target.path}`;
86+
// Skip packages that don't produce artifacts for this target — multiple
87+
// packages may share the same alias (one per platform), so this filters
88+
// out the wrong-platform matches without needing explicit metadata.
89+
if (!existsCached(prebuiltRoot)) continue;
90+
91+
const baseDir = subdir ? `${prebuiltRoot}/${subdir}` : prebuiltRoot;
92+
for (const candidate of getFolderCandidates(pkg, alias)) {
93+
const fullPath = candidate
94+
? `${baseDir}/${candidate}/${relativePath}`
95+
: `${baseDir}/${relativePath}`;
96+
tried.push(fullPath);
97+
if (existsCached(fullPath)) return fullPath;
98+
}
3499
}
35100

36-
return null;
101+
const matchNames = matches.map((m) => m.pkg.package.name).join(', ');
102+
throw new Error(
103+
`Could not resolve "${source}" in package(s) [${matchNames}] for target "${target.path}". Tried:\n`
104+
+ tried.map((t) => ` - ${t}`).join('\n'),
105+
);
37106
}

cppjs-core/cppjs-core-embind-jsi/cpp/src/emscripten/bind.cpp

Lines changed: 61 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ namespace emscripten {
3535
facebook::jsi::Runtime* jsRuntime = nullptr;
3636
namespace internal {
3737

38+
// Dynamic heap window registration. JS side keeps multiple ArrayBuffer
39+
// windows over native memory; this ensures every pointer we hand to JS
40+
// is covered by at least one window before being read/written there.
41+
void ensureWindowFor(uint64_t ptr);
42+
void registerHeapWindow(uint64_t offset);
43+
3844
template<typename T>
3945
struct Bugra {
4046
static jsi::Value toValue(T rawValue);
@@ -52,12 +58,16 @@ namespace emscripten {
5258

5359
template<typename T>
5460
jsi::Value Bugra<T>::toValue(T* rawValue) {
55-
return jsi::BigInt::fromUint64(*jsRuntime, reinterpret_cast<uint64_t>(rawValue));
61+
uint64_t ptr = reinterpret_cast<uint64_t>(rawValue);
62+
ensureWindowFor(ptr);
63+
return jsi::BigInt::fromUint64(*jsRuntime, ptr);
5664
}
5765

5866
template<typename T>
5967
jsi::Value Bugra<T>::toValue(T** rawValue) {
60-
return jsi::BigInt::fromUint64(*jsRuntime, reinterpret_cast<uint64_t>(rawValue));
68+
uint64_t ptr = reinterpret_cast<uint64_t>(rawValue);
69+
ensureWindowFor(ptr);
70+
return jsi::BigInt::fromUint64(*jsRuntime, ptr);
6171
}
6272

6373
template<typename T>
@@ -146,7 +156,9 @@ namespace emscripten {
146156
}
147157

148158
template<> inline jsi::Value Bugra<EM_DESTRUCTORS*>::toValue(EM_DESTRUCTORS* rawValue) {
149-
return jsi::BigInt::fromUint64(*jsRuntime, reinterpret_cast<uint64_t>(rawValue));
159+
uint64_t ptr = reinterpret_cast<uint64_t>(rawValue);
160+
ensureWindowFor(ptr);
161+
return jsi::BigInt::fromUint64(*jsRuntime, ptr);
150162
}
151163

152164

@@ -912,48 +924,61 @@ namespace emscripten {
912924
uint64_t offset;
913925
};
914926

927+
// Tracks every heap window currently exposed to JS. Single-threaded
928+
// access assumed (all JSI traffic flows through the JS thread today).
929+
static std::vector<uint64_t> registeredOffsets;
930+
931+
void registerHeapWindow(uint64_t offset) {
932+
auto buf = std::make_shared<FixedBuffer>(offset);
933+
auto arrayBuffer = facebook::jsi::ArrayBuffer(*jsRuntime, buf);
934+
jsRuntime->global()
935+
.getPropertyAsFunction(*jsRuntime, "__cppjs_register_heap_window")
936+
.call(*jsRuntime,
937+
jsi::BigInt::fromUint64(*jsRuntime, offset),
938+
arrayBuffer);
939+
registeredOffsets.push_back(offset);
940+
}
941+
942+
void ensureWindowFor(uint64_t ptr) {
943+
for (uint64_t offset : registeredOffsets) {
944+
if (ptr >= offset && (ptr - offset) < UINT32_MAX) return;
945+
}
946+
// Center the new window on `ptr` so nearby allocations also land in it.
947+
uint64_t newOffset = (ptr > UINT32_MAX / 2)
948+
? (ptr - UINT32_MAX / 2)
949+
: 0;
950+
registerHeapWindow(newOffset);
951+
}
952+
915953

916954
EMSCRIPTEN_KEEPALIVE void _embind_initialize_bindings(jsi::Runtime& rt, std::string path) {
917955
CPPJS_DATA_PATH = path;
918956
CppJS::setEnv("CPPJS_DATA_PATH", path, false);
919957
jsRuntime = &rt;
920958

921-
#ifdef __ANDROID__
922-
sleep(2);
923-
#endif
924959

960+
// Initial seed windows: anchored around .rodata (string literals)
961+
// and the C++ heap. Additional windows are registered on demand
962+
// by ensureWindowFor() as new pointer regions are exposed to JS.
925963
char* name = "M";
926964
uint64_t namePtrNumber = reinterpret_cast<uint64_t>(name);
927-
// uint64_t offset = (namePtrNumber >> 32) << 32;
928-
uint64_t offset = namePtrNumber - UINT32_MAX;
929-
auto buf = std::make_shared<FixedBuffer>(offset);
930-
auto arrayBuffer = facebook::jsi::ArrayBuffer(rt, buf);
931-
rt.global().setProperty(rt, "jsiArrayBuffer", arrayBuffer);
932-
933-
uint64_t bufPtrNumber = reinterpret_cast<uint64_t>(buf.get());
934-
// uint64_t offset2 = (bufPtrNumber >> 32) << 32;
935-
uint64_t offset2 = bufPtrNumber - UINT32_MAX;
936-
auto buf2 = std::make_shared<FixedBuffer>(offset2);
937-
auto arrayBuffer2 = facebook::jsi::ArrayBuffer(rt, buf2);
938-
rt.global().setProperty(rt, "jsiArrayBuffer2", arrayBuffer2);
939-
940-
uint64_t offset3 = offset - UINT32_MAX;
941-
auto buf3 = std::make_shared<FixedBuffer>(offset3);
942-
auto arrayBuffer3 = facebook::jsi::ArrayBuffer(rt, buf3);
943-
rt.global().setProperty(rt, "jsiArrayBuffer3", arrayBuffer3);
944-
945-
uint64_t offset4 = namePtrNumber;
946-
auto buf4 = std::make_shared<FixedBuffer>(offset4);
947-
auto arrayBuffer4 = facebook::jsi::ArrayBuffer(rt, buf4);
948-
rt.global().setProperty(rt, "jsiArrayBuffer4", arrayBuffer4);
949-
950-
jsRuntime->global().getPropertyAsFunction(rt, "updateMemoryViews").call(
951-
*jsRuntime,
952-
jsi::BigInt::fromUint64(rt, reinterpret_cast<uint64_t>(offset)),
953-
jsi::BigInt::fromUint64(rt, reinterpret_cast<uint64_t>(offset2)),
954-
jsi::BigInt::fromUint64(rt, reinterpret_cast<uint64_t>(offset3)),
955-
jsi::BigInt::fromUint64(rt, reinterpret_cast<uint64_t>(offset4))
956-
);
965+
966+
// Reference allocation to discover the heap region.
967+
auto heapProbe = std::make_shared<FixedBuffer>(0);
968+
uint64_t heapPtrNumber = reinterpret_cast<uint64_t>(heapProbe.get());
969+
970+
uint64_t offsetRodataLow = (namePtrNumber > UINT32_MAX)
971+
? (namePtrNumber - UINT32_MAX) : 0;
972+
uint64_t offsetHeapLow = (heapPtrNumber > UINT32_MAX)
973+
? (heapPtrNumber - UINT32_MAX) : 0;
974+
uint64_t offsetRodataLowest = (offsetRodataLow > UINT32_MAX)
975+
? (offsetRodataLow - UINT32_MAX) : 0;
976+
uint64_t offsetRodataHigh = namePtrNumber;
977+
978+
registerHeapWindow(offsetRodataLow);
979+
registerHeapWindow(offsetHeapLow);
980+
registerHeapWindow(offsetRodataLowest);
981+
registerHeapWindow(offsetRodataHigh);
957982

958983
for (auto *f = init_funcs; f; f = f->next) {
959984
f->init_func();

cppjs-core/cppjs-core-embind-jsi/js/embind.js

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -480,18 +480,38 @@ function updateMemoryViews(offset, offset2, offset3, offset4) {
480480
DATA_VIEW.push(new DataView(b4));
481481
}
482482

483+
function __cppjs_register_heap_window(offset, arrayBuffer) {
484+
for (let i = 0; i < HEAP_OFFSET.length; i++) {
485+
if (HEAP_OFFSET[i] === offset) return;
486+
}
487+
HEAP_OFFSET.push(offset);
488+
HEAP8.push(new Int8Array(arrayBuffer));
489+
HEAPU8.push(new Uint8Array(arrayBuffer));
490+
DATA_VIEW.push(new DataView(arrayBuffer));
491+
}
492+
globalThis.__cppjs_register_heap_window = __cppjs_register_heap_window;
493+
494+
const UINT32_MAX_BIGINT = 4294967295n;
495+
483496
function getHeapIndex(ptr) {
484-
let heapIndex = -1;
485-
if (ptr >= HEAP_OFFSET[1]) heapIndex = 1;
486-
else if (ptr >= HEAP_OFFSET[3]) heapIndex = 3;
487-
else if (ptr >= HEAP_OFFSET[0]) heapIndex = 0;
488-
else if (ptr >= HEAP_OFFSET[2]) heapIndex = 2;
497+
let bestIdx = -1;
498+
let bestDelta = UINT32_MAX_BIGINT;
499+
for (let i = 0; i < HEAP_OFFSET.length; i++) {
500+
const offset = HEAP_OFFSET[i];
501+
if (ptr >= offset) {
502+
const delta = ptr - offset;
503+
if (delta < bestDelta) {
504+
bestDelta = delta;
505+
bestIdx = i;
506+
}
507+
}
508+
}
489509

490-
if (heapIndex === -1 || ptr - HEAP_OFFSET[heapIndex] > 4294967295) {
491-
throw (`Heap error !!! pointer: ${ptr}, heap index: ${heapIndex}, HEAPS: ${HEAP_OFFSET}`);
510+
if (bestIdx === -1) {
511+
throw (`Heap error !!! pointer: ${ptr}, no covering window in: ${HEAP_OFFSET}`);
492512
}
493513

494-
return heapIndex;
514+
return bestIdx;
495515
}
496516

497517
function writeToMemoryUsingShift(pointer, signed, shift, value) {

cppjs-core/cppjs-core-embind-jsi/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cpp.js/core-embind-jsi",
3-
"version": "2.0.0-beta.15",
3+
"version": "2.0.0-beta.17",
44
"description": "The Embind JSI integration tool enables seamless C++ integration with React Native and Expo.",
55
"homepage": "https://github.com/bugra9/cpp.js/tree/main/packages/cppjs-core-embind-jsi#readme",
66
"repository": "https://github.com/bugra9/cpp.js.git",

cppjs-packages/cppjs-package-gdal/cppjs-package-gdal-ios/cppjs-package-gdal.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Pod::Spec.new do |s|
1111
s.source = { :http => "https://cpp.js.org" }
1212
s.vendored_frameworks = 'gdal.xcframework', 'expat.xcframework', 'geos.xcframework', 'geotiff.xcframework', 'iconv.xcframework', 'proj.xcframework', 'spatialite.xcframework', 'sqlite3.xcframework', 'tiff.xcframework', 'jpeg.xcframework', 'zstd.xcframework', 'Lerc.xcframework', 'webp.xcframework', 'z.xcframework'
1313
s.library = 'xml2'
14-
s.resources = ['dist/prebuilt/ios-iphoneos/share/gdal']
14+
s.resources = ['dist/prebuilt/ios-iphoneos-mt-release/share/gdal']
1515
# arm64-only iOS simulator slice; drop x86_64 to avoid linker errors on consumer apps.
1616
s.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
1717
end

cppjs-packages/cppjs-package-proj/cppjs-package-proj-ios/cppjs-package-proj.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Pod::Spec.new do |s|
1010
s.author = "Proj Authors"
1111
s.source = { :http => "https://cpp.js.org" }
1212
s.vendored_frameworks = 'proj.xcframework', 'sqlite3.xcframework', 'tiff.xcframework'
13-
s.resources = ['dist/prebuilt/ios-iphoneos/share/proj']
13+
s.resources = ['dist/prebuilt/ios-iphoneos-mt-release/share/proj']
1414
# arm64-only iOS simulator slice; drop x86_64 to avoid linker errors on consumer apps.
1515
s.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
1616
end

cppjs-plugins/cppjs-plugin-react-native/android/build.gradle

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
apply plugin: 'com.android.library'
22

3+
// React Native's new-arch autolinking generates its own CMakeLists for this
4+
// module and ignores the `externalNativeBuild { path "../script/CMakeLists.txt" }`
5+
// block below, so the bridge generator that was wired through that CMakeLists
6+
// never ran when the module was consumed via autolinking. Run build_js.js
7+
// directly at Gradle configuration phase instead: by the time any native
8+
// compile task starts the SWIG bridges are already on disk.
9+
def __cppjsAppRoot = new File(rootProject.projectDir, '..').absolutePath
10+
def __cppjsScript = new File(projectDir, '../script/build_js.js').absolutePath
11+
logger.lifecycle("[cppjs] Generating SWIG bridges :: ${__cppjsScript} (cwd=${__cppjsAppRoot})")
12+
def __cppjsProc = new ProcessBuilder('node', __cppjsScript, 'android')
13+
.directory(new File(__cppjsAppRoot))
14+
.inheritIO()
15+
.start()
16+
def __cppjsExitCode = __cppjsProc.waitFor()
17+
if (__cppjsExitCode != 0) {
18+
throw new GradleException("[cppjs] build_js.js android failed with exit code ${__cppjsExitCode}")
19+
}
20+
321
android {
422
compileSdkVersion 34
523
buildToolsVersion "34.0.0"

0 commit comments

Comments
 (0)