Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 219 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
# Polygen Android Full Implementation Plan

This document outlines the steps needed to complete full Android support for Polygen. The current fix (PR #132) makes the Android build succeed, but the module throws `UnsupportedOperationException` at runtime because the C++ WASM runtime isn't wired up yet.

## Current State

### What Works
- Android new architecture build compiles successfully
- Codegen generates correct `NativePolygenSpec` in `com.callstack.polygen`
- `PolygenModule.kt` implements all abstract methods from the spec
- Module registers correctly with React Native's TurboModule system

### What Doesn't Work
- All methods throw `UnsupportedOperationException` at runtime
- No JNI bridge to the C++ `ReactNativePolygen` implementation
- CMake native build is disabled

## Architecture Options

### Option A: Pure C++ TurboModule (Recommended)

Register the C++ `ReactNativePolygen` class directly, matching how iOS works.

**Pros:**
- Consistent with iOS implementation
- No duplicate code in Kotlin
- Direct JSI access, better performance
- The C++ implementation already exists and works

**Cons:**
- Requires understanding React Native's C++ TurboModule registration on Android
- More complex CMake setup

### Option B: Kotlin Bridge with JNI

Keep the Kotlin `PolygenModule` and add JNI methods that call into C++.

**Pros:**
- Standard Android pattern
- Easier to debug Kotlin layer

**Cons:**
- Duplicates method definitions in Kotlin and C++
- Additional JNI marshalling overhead
- More code to maintain

## Implementation Steps (Option A - Recommended)

### Phase 1: CMake Setup

1. **Update `android/CMakeLists.txt`** to compile the C++ ReactNativePolygen module:
```cmake
cmake_minimum_required(VERSION 3.13)
project(Polygen)

set(CMAKE_CXX_STANDARD 17)

# Find React Native packages
find_package(ReactAndroid REQUIRED CONFIG)
find_package(fbjni REQUIRED CONFIG)

# Collect all C++ sources
file(GLOB_RECURSE POLYGEN_SOURCES
"../cpp/ReactNativePolygen/*.cpp"
"../cpp/ReactNativePolygen/*.c"
"../cpp/wasm-rt/*.c"
"src/main/jni/OnLoad.cpp"
)

# Create shared library
add_library(polygen SHARED ${POLYGEN_SOURCES})

target_include_directories(polygen PRIVATE
../cpp
../cpp/ReactNativePolygen
../cpp/wasm-rt
)

target_link_libraries(polygen
ReactAndroid::jsi
ReactAndroid::react_nativemodule_core
ReactAndroid::turbomodulejsijni
fbjni::fbjni
)
```

2. **Re-enable native build in `build.gradle`**:
```gradle
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
```

### Phase 2: C++ Module Registration

3. **Create `android/src/main/jni/OnLoad.cpp`**:
```cpp
#include <fbjni/fbjni.h>
#include <ReactCommon/CxxTurboModuleUtils.h>
#include <ReactNativePolygen/ReactNativePolygen.h>

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
return facebook::jni::initialize(vm, [] {
facebook::react::registerCxxModuleToGlobalModuleMap(
std::string(facebook::react::ReactNativePolygen::kModuleName),
[](std::shared_ptr<facebook::react::CallInvoker> jsInvoker) {
return std::make_shared<facebook::react::ReactNativePolygen>(jsInvoker);
}
);
});
}
```

4. **Update `PolygenPackage.kt`** to mark as CxxModule:
```kotlin
moduleInfos[PolygenModule.NAME] = ReactModuleInfo(
PolygenModule.NAME,
PolygenModule.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
false, // hasConstants
true, // isCxxModule <-- Change to true
isTurboModule
)
```

5. **Load native library to trigger `JNI_OnLoad`** in `PolygenPackage.kt`:
```kotlin
import com.facebook.soloader.SoLoader

class PolygenPackage : TurboReactPackage() {
companion object {
init {
SoLoader.loadLibrary("polygen")
}
}
// ... rest of package
}
```
This ensures the native library is loaded when the package class is initialized,
which triggers `JNI_OnLoad` and registers the C++ TurboModule.

6. **Remove or simplify `PolygenModule.kt`** since the C++ module handles everything.

### Phase 3: Codegen Integration

7. **Update `react-native.config.js`** to point to the CMake file:
```js
android: {
cmakeListsPath: 'android/CMakeLists.txt',
}
```

8. **Ensure codegen header paths are correct** in CMakeLists.txt:
```cmake
target_include_directories(polygen PRIVATE
${CMAKE_CURRENT_BINARY_DIR}/generated/source/codegen/jni
)
```

### Phase 4: Testing & Validation

9. **Test with example app**:
```bash
cd apps/example
yarn android
```

10. **Verify all WASM operations work**:
- Module loading
- Memory allocation
- Global variables
- Table operations
- Function calls

### Phase 5: Documentation

11. **Update README.md** with Android setup instructions
12. **Add Android-specific troubleshooting** to docs

## Files to Modify

| File | Action |
|------|--------|
| `android/CMakeLists.txt` | Rewrite with proper C++ compilation |
| `android/build.gradle` | Re-enable externalNativeBuild |
| `android/src/main/jni/OnLoad.cpp` | Create for C++ module registration |
| `android/src/main/java/.../PolygenPackage.kt` | Set isCxxModule=true, add SoLoader.loadLibrary("polygen") |
| `android/src/main/java/.../PolygenModule.kt` | Remove or simplify |
| `react-native.config.js` | Add cmakeListsPath back |
| `cpp/ReactNativePolygen/ReactNativePolygen.h` | Ensure Android compatibility |

## Dependencies

- React Native 0.75+ (for proper new architecture support)
- NDK 21+ (already configured)
- CMake 3.13+

## Risks & Mitigations

| Risk | Mitigation |
|------|------------|
| C++ code may have iOS-specific assumptions | Review and test all code paths on Android |
| CMake configuration complexity | Start with minimal config, add features incrementally |
| Memory management differences | Use shared_ptr consistently, test for leaks |
| Build time increase | Consider prebuilt binaries for release |

## Timeline Estimate

Not provided - depends on developer availability and familiarity with React Native internals.

## References

- [React Native TurboModules](https://reactnative.dev/docs/the-new-architecture/pillars-turbomodules)
- [C++ TurboModules](https://reactnative.dev/docs/the-new-architecture/cxx-cxxturbomodules)
- [Polygen Issue #132](https://github.com/callstackincubator/polygen/issues/132)
- [Polygen Issue #73](https://github.com/callstackincubator/polygen/issues/73)
80 changes: 80 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# TODO - Polygen Android C++ Integration

## Completed
- [x] Review existing C++ implementation structure (iteration 1)
- [x] Review iOS TurboModule registration pattern (iteration 1)
- [x] Phase 1: Rewrite CMakeLists.txt for C++ compilation (iteration 1)
- [x] Phase 1: Re-enable externalNativeBuild in build.gradle (iteration 1)
- [x] Phase 2: Create OnLoad.cpp for JNI_OnLoad registration (iteration 1)
- [x] Phase 2: Update PolygenPackage.kt with SoLoader.loadLibrary (iteration 1)
- [x] Phase 2: Set isCxxModule=true in ReactModuleInfo (iteration 1)
- [x] Phase 3: Add cmakeListsPath to react-native.config.js (iteration 1)
- [x] Fix C++ code to support Android codegen headers (iteration 2)
- Added #include <string> to w2c.h
- Created ModuleBagStub.cpp for getModuleBag() function
- Added RNPolygenSpecJSI-generated.cpp to JNI sources
- Fixed GLOB to GLOB_RECURSE to include utils/*.cpp
- Added ReactAndroid::folly_runtime to link libraries
- [x] Phase 4: Verify Android build succeeds (iteration 2)
- libpolygen.so successfully built for arm64-v8a
- JNI_OnLoad symbol present
- [x] Phase 4: Test example app on Android emulator (iteration 3)
- App launches successfully via ADB
- UI visible and interactive (BottomTabs Example screen)
- Navigation works (Import Validation screen)
- C++ TurboModule registration working (no UnsatisfiedLinkError)
- "Bridgeless mode is enabled" confirms new architecture active
- [x] Phase 5: Android CMake integration infrastructure
- Created android-cmake.ts plugin in polygen-codegen
- Plugin generates CMakeLists.txt in .polygen-out/host/
- Modified polygen CMakeLists.txt to detect and include generated code
- Passes POLYGEN_APP_ROOT from Gradle to find app's .polygen-out/host/
- Generated code now compiling with correct include paths

## In Progress
- [ ] Phase 5: Fix wasm-rt API mismatch
- Generated C code calls `wasm_rt_allocate_memory` with 5 arguments
- Bundled wasm-rt.h defines function with 4 parameters
- This is a pre-existing wasm2c version mismatch, not Android-specific

## Blocked
- Pre-existing wasm2c API mismatch blocks module compilation
- The wasm2c tool generates code for a newer wasm-rt API than what's bundled
- This affects both iOS and Android builds
- Requires updating the bundled wasm-rt or regenerating code with matching wasm2c version

## Notes
- App package: `com.callstack.polygen.example`
- Main activity: `com.microsoft.reacttestapp.MainActivity`
- libpolygen.so loaded successfully (verified in logcat)
- No UnsatisfiedLinkError or TurboModule registration errors
- iOS codegen generates: `RNPolygenSpecJSI.h` with class `NativePolygenCxxSpecJSI`
- Android codegen also generates: `RNPolygenSpecJSI.h` with same class name

## Module Loading Architecture

### How It Works
1. `polygen generate` creates native C/C++ code in `node_modules/.polygen-out/host/`
2. It generates a `loader.cpp` with `getModuleBag()` containing all precompiled modules
3. The polygen library has `ModuleBagStub.cpp` returning an empty bag (fallback)
4. For modules to load, the app must link generated code instead of the stub

### iOS vs Android
- **iOS**: `ReactNativeWebAssemblyHost.podspec` auto-links generated code via CocoaPods
- **Android**: No equivalent mechanism yet - requires manual CMake integration

### Current Behavior
- C++ TurboModule registration: ✅ Working
- Module compile errors (expected): "module was not precompiled"
- This is correct behavior when generated code isn't linked

## Verification Summary
PR scope (C++ TurboModule wiring) verified:
1. ✅ Android example app launches successfully on emulator
2. ✅ App UI is visible and interactive
3. ✅ libpolygen.so loads without UnsatisfiedLinkError
4. ✅ C++ TurboModule registered via JNI_OnLoad
5. ✅ Module API accessible from JavaScript (returns expected errors for unlinked modules)

Future work needed:
- Android equivalent of ReactNativeWebAssemblyHost.podspec for linking generated code
11 changes: 9 additions & 2 deletions apps/example/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ allprojects {
}


react {
bundleCommand = "webpack-bundle"
// Configure react-native in app subproject after plugin is applied
subprojects {
afterEvaluate { project ->
if (project.plugins.hasPlugin("com.facebook.react")) {
project.react {
bundleCommand = "webpack-bundle"
}
}
}
}
2 changes: 1 addition & 1 deletion apps/example/android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
# Note that this is incompatible with web debugging.
#newArchEnabled=true
newArchEnabled=true
#bridgelessEnabled=true

# Uncomment the line below to build React Native from source.
Expand Down
35 changes: 20 additions & 15 deletions packages/codegen/assets/wasm-rt/wasm-rt-exceptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,38 +31,43 @@ static WASM_RT_THREAD_LOCAL wasm_rt_jmp_buf* g_unwind_target;
void wasm_rt_load_exception(const wasm_rt_tag_t tag,
uint32_t size,
const void* values) {
if (size > MAX_EXCEPTION_SIZE) {
wasm_rt_trap(WASM_RT_TRAP_EXHAUSTION);
}
if (size > MAX_EXCEPTION_SIZE) {
wasm_rt_trap(WASM_RT_TRAP_EXHAUSTION);
}

g_active_exception_tag = tag;
g_active_exception_size = size;
g_active_exception_tag = tag;
g_active_exception_size = size;

if (size) {
memcpy(g_active_exception, values, size);
}
if (size) {
memcpy(g_active_exception, values, size);
}
}

WASM_RT_NO_RETURN void wasm_rt_throw(void) {
WASM_RT_LONGJMP(*g_unwind_target, WASM_RT_TRAP_UNCAUGHT_EXCEPTION);
WASM_RT_LONGJMP(*g_unwind_target, WASM_RT_TRAP_UNCAUGHT_EXCEPTION);
}

WASM_RT_UNWIND_TARGET* wasm_rt_get_unwind_target(void) {
return g_unwind_target;
return g_unwind_target;
}

void wasm_rt_set_unwind_target(WASM_RT_UNWIND_TARGET* target) {
g_unwind_target = target;
g_unwind_target = target;
}

wasm_rt_tag_t wasm_rt_exception_tag(void) {
return g_active_exception_tag;
return g_active_exception_tag;
}

uint32_t wasm_rt_exception_size(void) {
return g_active_exception_size;
return g_active_exception_size;
}

void* wasm_rt_exception(void) {
return g_active_exception;
}
return g_active_exception;
}

// Include table operations for exnref
#define WASM_RT_TABLE_OPS_EXNREF
#include "wasm-rt-impl-tableops.inc"
#undef WASM_RT_TABLE_OPS_EXNREF
Loading