Skip to content

Commit

Permalink
feat: Support awaiting JS Promise in Kotlin and Swift (#355)
Browse files Browse the repository at this point in the history
* feat: Support passing `Promise` to Kotlin

* feat: Add `onResolvedListener`

* feat: Add `.then`, `.catch` and `.await` to Promise!

* feat: Use `await`

* feat: Use `Throwable` instead of `String` for error on Android

* Update Promise.kt

* chore: Remove unused `reject(string)` function now

* Update Promise.hpp

* Update Promise.hpp

* feat: Implement `awaitPromise(..)` on iOS

* feat: Add `RuntimeError` and `.toCpp` to Swift

* fix: Fix duplicate symbol in `RuntimeError`

* feat: Add `ErrorType`

* feat: Add `const&` funcs to `Promise` for Swift

* feat: Add `dependencies` to SwiftCxxBridge

* fix: `error` type

* feat: Add `RuntimeError.from(std.exception)`

* feat: Add `getFunctionCopy()` to `SwiftClosure`

* fix: Account for `Promise<void>` resolver

* fix: Fix `UnsafeMutableRawPointer` being nullable (wrong type)

* nitrogen

* clean

* Update HybridTestObjectSwiftKotlinSpecCxx.swift

* feat: Add actual Promise listeners!!

* fix: Add `addOnResolvedListenerCopy` func for some cases

* chore: Format

* fix: Make `getFunctionCopy()` const

* feat: Support `Promise<void>` from JS!

* feat: Add `awaitAndGet` and `awaitAndGetComplex` promise tests

* feat: Test both new functions

* Slap a good ol' `mutex` on `Promise`

* fix: Fix `Promise<void>` never resolving because args count was `1` (`undefined`)

* fix: Fix Android wrong return type

* Update HybridTestObjectCpp.cpp
  • Loading branch information
mrousavy authored Nov 19, 2024
1 parent b6277ea commit 6da4f98
Show file tree
Hide file tree
Showing 34 changed files with 1,005 additions and 69 deletions.
2 changes: 1 addition & 1 deletion example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1891,7 +1891,7 @@ SPEC CHECKSUMS:
glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a
hermes-engine: 46f1ffbf0297f4298862068dd4c274d4ac17a1fd
NitroImage: 5f785fe73750fe0f44d1914d5360ecf4f722666c
NitroModules: f37dedb0894bb3474aa27a8e034c26b18dc741d0
NitroModules: 0531eae34895d20c8968709d1d36ff7d889d737d
RCT-Folly: bf5c0376ffe4dd2cf438dcf86db385df9fdce648
RCTDeprecation: fde92935b3caa6cb65cbff9fbb7d3a9867ffb259
RCTRequired: 75c6cee42d21c1530a6f204ba32ff57335d19007
Expand Down
40 changes: 38 additions & 2 deletions example/src/getTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -703,15 +703,15 @@ export function getTests(
.didNotThrow()
.equals(true)
),
createTest('JS Promise can be awaited on native side', async () =>
createTest('JS Promise<number> can be awaited on native side', async () =>
(
await it(() => {
return timeoutedPromise(async (complete) => {
let resolve = (_: number) => {}
const promise = new Promise<number>((r) => {
resolve = r
})
const nativePromise = testObject.awaitPromise(promise)
const nativePromise = testObject.awaitAndGetPromise(promise)
resolve(5)
const result = await nativePromise
complete(result)
Expand All @@ -721,6 +721,42 @@ export function getTests(
.didNotThrow()
.equals(5)
),
createTest('JS Promise<Car> can be awaited on native side', async () =>
(
await it(() => {
return timeoutedPromise(async (complete) => {
let resolve = (_: Car) => {}
const promise = new Promise<Car>((r) => {
resolve = r
})
const nativePromise = testObject.awaitAndGetComplexPromise(promise)
resolve(TEST_CAR)
const result = await nativePromise
complete(result)
})
})
)
.didNotThrow()
.equals(TEST_CAR)
),
createTest('JS Promise<void> can be awaited on native side', async () =>
(
await it(() => {
return timeoutedPromise(async (complete) => {
let resolve = () => {}
const promise = new Promise<void>((r) => {
resolve = r
})
const nativePromise = testObject.awaitPromise(promise)
resolve()
const result = await nativePromise
complete(result)
})
})
)
.didNotThrow()
.equals(undefined)
),

// Callbacks
createTest('callCallback(...)', async () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function createSwiftCxxBridge(): SourceFile[] {
})
})
.filter((b) => b != null)
.flatMap((b) => [b, ...b?.dependencies])
.filter(filterDuplicateHelperBridges)
const headerHelperFunctions = bridges
.map((b) => `// pragma MARK: ${b.cxxType}\n${b.cxxHeader.code}`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ ${includes.sort().join('\n')}
#include <NitroModules/ArrayBufferHolder.hpp>
#include <NitroModules/AnyMapHolder.hpp>
#include <NitroModules/HybridContext.hpp>
#include <NitroModules/RuntimeError.hpp>
// Forward declarations of Swift defined types
${swiftForwardDeclares.sort().join('\n')}
Expand Down
30 changes: 27 additions & 3 deletions packages/nitrogen/src/syntax/kotlin/KotlinCxxBridgedType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,30 @@ export class KotlinCxxBridgedType implements BridgedType<'kotlin', 'c++'> {
return parameterName
}
}
case 'promise': {
switch (language) {
case 'c++': {
const promise = getTypeAs(this.type, PromiseType)
const resolvingType = promise.resultingType.getCode('c++')
const bridge = new KotlinCxxBridgedType(promise.resultingType)
return `
[&]() {
jni::local_ref<JPromise::javaobject> __promise = JPromise::create();
${parameterName}->addOnResolvedListener([=](const ${resolvingType}& __result) {
__promise->cthis()->resolve(${indent(bridge.parseFromCppToKotlin('__result', 'c++', true), ' ')});
});
${parameterName}->addOnRejectedListener([=](const std::exception& __error) {
auto __jniError = jni::JCppException::create(__error);
__promise->cthis()->reject(__jniError);
});
return __promise;
}()
`.trim()
}
default:
return parameterName
}
}
default:
// no need to parse anything, just return as is
return parameterName
Expand Down Expand Up @@ -709,9 +733,9 @@ __promise->resolve();
${parameterName}->cthis()->addOnResolvedListener([=](const jni::alias_ref<jni::JObject>& __boxedResult) {
${indent(resolveBody, ' ')}
});
${parameterName}->cthis()->addOnRejectedListener([=](const jni::alias_ref<jni::JString>& __message) {
std::runtime_error __error(__message->toStdString());
__promise->reject(std::move(__error));
${parameterName}->cthis()->addOnRejectedListener([=](const jni::alias_ref<jni::JThrowable>& __throwable) {
jni::JniException __jniError(__throwable);
__promise->reject(std::move(__jniError));
});
return __promise;
}()
Expand Down
70 changes: 63 additions & 7 deletions packages/nitrogen/src/syntax/swift/SwiftCxxBridgedType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import {
import { createSwiftEnumBridge } from './SwiftEnum.js'
import { createSwiftStructBridge } from './SwiftStruct.js'
import { createSwiftVariant, getSwiftVariantCaseName } from './SwiftVariant.js'
import { VoidType } from '../types/VoidType.js'
import { NamedWrappingType } from '../types/NamedWrappingType.js'
import { ErrorType } from '../types/ErrorType.js'

// TODO: Remove enum bridge once Swift fixes bidirectional enums crashing the `-Swift.h` header.

Expand Down Expand Up @@ -323,9 +326,62 @@ export class SwiftCxxBridgedType implements BridgedType<'swift', 'c++'> {
}
}
case 'promise': {
const promise = getTypeAs(this.type, PromiseType)
switch (language) {
case 'c++':
return `[]() -> ${this.getTypeCode('c++')} { throw std::runtime_error("Promise<..> cannot be converted to Swift yet!"); }()`
case 'swift': {
const resolvingTypeBridge = new SwiftCxxBridgedType(
promise.resultingType
)
if (promise.resultingType.kind === 'void') {
// It's void - resolve()
const rejecterFunc = new FunctionType(new VoidType(), [
new NamedWrappingType('error', new ErrorType()),
])
const rejecterFuncBridge = new SwiftCxxBridgedType(rejecterFunc)
return `
{ () -> ${promise.getCode('swift')} in
let __promise = ${promise.getCode('swift')}()
let __resolver = SwiftClosure { __promise.resolve(withResult: ()) }
let __rejecter = { (__error: std.exception) in
__promise.reject(withError: RuntimeError.from(cppError: __error))
}
let __resolverCpp = __resolver.getFunctionCopy()
let __rejecterCpp = ${indent(rejecterFuncBridge.parseFromSwiftToCpp('__rejecter', 'swift'), ' ')}
${cppParameterName}.pointee.addOnResolvedListener(__resolverCpp)
${cppParameterName}.pointee.addOnRejectedListener(__rejecterCpp)
return __promise
}()`.trim()
} else {
// It's resolving to a type - resolve(T)
const resolverFunc = new FunctionType(new VoidType(), [
new NamedWrappingType('result', promise.resultingType),
])
const rejecterFunc = new FunctionType(new VoidType(), [
new NamedWrappingType('error', new ErrorType()),
])
const addResolverName = promise.resultingType
.canBePassedByReference
? 'addOnResolvedListener'
: 'addOnResolvedListenerCopy'
const resolverFuncBridge = new SwiftCxxBridgedType(resolverFunc)
const rejecterFuncBridge = new SwiftCxxBridgedType(rejecterFunc)
return `
{ () -> ${promise.getCode('swift')} in
let __promise = ${promise.getCode('swift')}()
let __resolver = { (__result: ${resolvingTypeBridge.getTypeCode('swift')}) in
__promise.resolve(withResult: ${indent(resolvingTypeBridge.parseFromCppToSwift('__result', 'swift'), ' ')})
}
let __rejecter = { (__error: std.exception) in
__promise.reject(withError: RuntimeError.from(cppError: __error))
}
let __resolverCpp = ${indent(resolverFuncBridge.parseFromSwiftToCpp('__resolver', 'swift'), ' ')}
let __rejecterCpp = ${indent(rejecterFuncBridge.parseFromSwiftToCpp('__rejecter', 'swift'), ' ')}
${cppParameterName}.pointee.${addResolverName}(__resolverCpp)
${cppParameterName}.pointee.addOnRejectedListener(__rejecterCpp)
return __promise
}()`.trim()
}
}
default:
return cppParameterName
}
Expand Down Expand Up @@ -600,7 +656,7 @@ case ${i}:
let __promise = ${makePromise}()
${swiftParameterName}
.then({ __result in __promise.pointee.resolve(${arg}) })
.catch({ __error in __promise.pointee.reject(std.string(String(describing: __error))) })
.catch({ __error in __promise.pointee.reject(__error.toCpp()) })
return __promise
}()`.trim()
default:
Expand Down Expand Up @@ -707,7 +763,7 @@ case ${i}:
.map((p) => `__${p.escapedName}`)
.join(', ')
const cFuncParamsSignature = [
'__closureHolder: UnsafeMutableRawPointer?',
'__closureHolder: UnsafeMutableRawPointer',
...func.parameters.map((p) => {
const bridged = new SwiftCxxBridgedType(p)
return `__${p.escapedName}: ${bridged.getTypeCode('swift')}`
Expand All @@ -728,11 +784,11 @@ case ${i}:
let __closureHolder = Unmanaged.passRetained(ClosureHolder(wrappingClosure: ${swiftParameterName})).toOpaque()
func __callClosure(${cFuncParamsSignature}) -> Void {
let closure = Unmanaged<ClosureHolder>.fromOpaque(__closureHolder!).takeUnretainedValue()
let closure = Unmanaged<ClosureHolder>.fromOpaque(__closureHolder).takeUnretainedValue()
closure.invoke(${indent(cFuncParamsForward, ' ')})
}
func __destroyClosure(_ __closureHolder: UnsafeMutableRawPointer?) -> Void {
Unmanaged<ClosureHolder>.fromOpaque(__closureHolder!).release()
func __destroyClosure(_ __closureHolder: UnsafeMutableRawPointer) -> Void {
Unmanaged<ClosureHolder>.fromOpaque(__closureHolder).release()
}
return ${createFunc}(__closureHolder, __callClosure, __destroyClosure)
Expand Down
27 changes: 26 additions & 1 deletion packages/nitrogen/src/syntax/swift/SwiftCxxTypeHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { FunctionType } from '../types/FunctionType.js'
import { getTypeAs } from '../types/getTypeAs.js'
import { OptionalType } from '../types/OptionalType.js'
import { RecordType } from '../types/RecordType.js'
import type { Type } from '../types/Type.js'
import type { NamedType, Type } from '../types/Type.js'
import { TupleType } from '../types/TupleType.js'
import { escapeComments, indent } from '../../utils.js'
import { PromiseType } from '../types/PromiseType.js'
Expand All @@ -16,6 +16,9 @@ import { getHybridObjectName } from '../getHybridObjectName.js'
import { NitroConfig } from '../../config/NitroConfig.js'
import { getForwardDeclaration } from '../c++/getForwardDeclaration.js'
import { getUmbrellaHeaderName } from '../../autolinking/ios/createSwiftUmbrellaHeader.js'
import { VoidType } from '../types/VoidType.js'
import { NamedWrappingType } from '../types/NamedWrappingType.js'
import { ErrorType } from '../types/ErrorType.js'

export interface SwiftCxxHelper {
cxxHeader: {
Expand All @@ -29,6 +32,7 @@ export interface SwiftCxxHelper {
funcName: string
specializationName: string
cxxType: string
dependencies: SwiftCxxHelper[]
}

export function createSwiftCxxHelpers(type: Type): SwiftCxxHelper | undefined {
Expand Down Expand Up @@ -125,6 +129,7 @@ void* _Nonnull get_${name}(${name} cppType) {
},
],
},
dependencies: [],
}
}

Expand Down Expand Up @@ -158,6 +163,7 @@ inline ${actualType} create_${name}(const ${wrappedBridge.getTypeCode('c++')}& v
...wrappedBridge.getRequiredImports(),
],
},
dependencies: [],
}
}

Expand Down Expand Up @@ -193,6 +199,7 @@ inline ${actualType} create_${name}(size_t size) {
...bridgedType.getRequiredImports(),
],
},
dependencies: [],
}
}

Expand Down Expand Up @@ -237,6 +244,7 @@ inline std::vector<${keyType}> get_${name}_keys(const ${name}& map) {
...bridgedType.getRequiredImports(),
],
},
dependencies: [],
}
}

Expand Down Expand Up @@ -353,6 +361,7 @@ inline std::shared_ptr<${wrapperName}> share_${name}(const ${name}& value) {
...bridgedType.getRequiredImports(),
],
},
dependencies: [],
}
}

Expand Down Expand Up @@ -414,6 +423,7 @@ ${getFunctions.join('\n')}
...bridgedType.getRequiredImports(),
],
},
dependencies: [],
}
}

Expand Down Expand Up @@ -454,6 +464,7 @@ inline ${actualType} create_${name}(${typesSignature}) {
...bridgedType.getRequiredImports(),
],
},
dependencies: [],
}
}

Expand All @@ -464,6 +475,16 @@ function createCxxPromiseSwiftHelper(type: PromiseType): SwiftCxxHelper {
const resultingType = type.resultingType.getCode('c++')
const bridgedType = new SwiftCxxBridgedType(type)
const actualType = `std::shared_ptr<Promise<${resultingType}>>`

const resolverArgs: NamedType[] = []
if (type.resultingType.kind !== 'void') {
resolverArgs.push(new NamedWrappingType('result', type.resultingType))
}
const resolveFunction = new FunctionType(new VoidType(), resolverArgs)
const rejectFunction = new FunctionType(new VoidType(), [
new NamedWrappingType('error', new ErrorType()),
])

const name = escapeCppName(actualType)
return {
cxxType: actualType,
Expand Down Expand Up @@ -493,5 +514,9 @@ inline ${actualType} create_${name}() {
...bridgedType.getRequiredImports(),
],
},
dependencies: [
createCxxFunctionSwiftHelper(resolveFunction),
createCxxFunctionSwiftHelper(rejectFunction),
],
}
}
42 changes: 42 additions & 0 deletions packages/nitrogen/src/syntax/types/ErrorType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Language } from '../../getPlatformSpecs.js'
import { type SourceFile, type SourceImport } from '../SourceFile.js'
import type { Type, TypeKind } from './Type.js'

export class ErrorType implements Type {
constructor() {}

get canBePassedByReference(): boolean {
// It's a exception<..>, pass by reference.
return true
}
get kind(): TypeKind {
return 'error'
}

getCode(language: Language): string {
switch (language) {
case 'c++':
return `std::exception`
case 'swift':
return `std.exception`
case 'kotlin':
return `Throwable`
default:
throw new Error(
`Language ${language} is not yet supported for ThrowableType!`
)
}
}
getExtraFiles(): SourceFile[] {
return []
}
getRequiredImports(): SourceImport[] {
return [
{
language: 'c++',
name: 'exception',
space: 'system',
},
]
}
}
1 change: 1 addition & 0 deletions packages/nitrogen/src/syntax/types/Type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type TypeKind =
| 'bigint'
| 'boolean'
| 'enum'
| 'error'
| 'function'
| 'hybrid-object'
| 'hybrid-object-base'
Expand Down
Loading

0 comments on commit 6da4f98

Please sign in to comment.