From 9db785245e2ce6170f39fd2a3512cfcaffd978ce Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 28 Aug 2024 23:38:05 +0200 Subject: [PATCH] feat: Support `Record` as `Map` in Kotlin/Android (#80) * feat: Support `Record` as `Map` in Kotlin/Android * Update README.md * fix: Optimize a bit --- .../src/screens/HybridObjectTestsScreen.tsx | 3 + .../src/syntax/kotlin/KotlinCxxBridgedType.ts | 121 +++++++++++++----- .../nitrogen/src/syntax/types/RecordType.ts | 2 + .../margelo/nitro/image/KotlinTestObject.kt | 4 +- .../c++/JHybridKotlinTestObjectSpec.cpp | 29 ++++- .../c++/JHybridKotlinTestObjectSpec.hpp | 2 + .../nitro/image/HybridKotlinTestObjectSpec.kt | 6 + .../ios/NitroImage-Swift-Cxx-Bridge.hpp | 18 +++ .../shared/c++/HybridKotlinTestObjectSpec.cpp | 2 + .../shared/c++/HybridKotlinTestObjectSpec.hpp | 4 + .../src/specs/TestObject.nitro.ts | 2 + packages/react-native-nitro-modules/README.md | 2 +- 12 files changed, 160 insertions(+), 35 deletions(-) diff --git a/example/src/screens/HybridObjectTestsScreen.tsx b/example/src/screens/HybridObjectTestsScreen.tsx index a5b78c78f..b0059f354 100644 --- a/example/src/screens/HybridObjectTestsScreen.tsx +++ b/example/src/screens/HybridObjectTestsScreen.tsx @@ -12,6 +12,9 @@ import { logPrototypeChain } from '../logPrototypeChain' logPrototypeChain(HybridTestObject) console.log(HybridKotlinTestObject.createMap()) +console.log(HybridKotlinTestObject.someRecord) +console.log((HybridKotlinTestObject.someRecord = { hello: 'world' })) +console.log(HybridKotlinTestObject.someRecord) HybridKotlinTestObject.addOnPersonBornListener((p) => { console.log(`${p.name} was born! Age: ${p.age}`) diff --git a/packages/nitrogen/src/syntax/kotlin/KotlinCxxBridgedType.ts b/packages/nitrogen/src/syntax/kotlin/KotlinCxxBridgedType.ts index 5b820777f..977409d91 100644 --- a/packages/nitrogen/src/syntax/kotlin/KotlinCxxBridgedType.ts +++ b/packages/nitrogen/src/syntax/kotlin/KotlinCxxBridgedType.ts @@ -10,6 +10,7 @@ import { getTypeAs } from '../types/getTypeAs.js' import { HybridObjectType } from '../types/HybridObjectType.js' import { OptionalType } from '../types/OptionalType.js' import { PromiseType } from '../types/PromiseType.js' +import { RecordType } from '../types/RecordType.js' import { StructType } from '../types/StructType.js' import type { Type } from '../types/Type.js' import { getKotlinBoxedPrimitiveType } from './KotlinBoxedPrimitive.js' @@ -194,6 +195,21 @@ export class KotlinCxxBridgedType implements BridgedType<'kotlin', 'c++'> { default: return this.type.getCode(language) } + case 'record': { + switch (language) { + case 'c++': + const recordType = getTypeAs(this.type, RecordType) + const keyType = new KotlinCxxBridgedType( + recordType.keyType + ).getTypeCode(language) + const valueType = new KotlinCxxBridgedType( + recordType.valueType + ).getTypeCode(language) + return `jni::JMap<${keyType}, ${valueType}>` + default: + return this.type.getCode(language) + } + } case 'enum': switch (language) { case 'c++': @@ -384,6 +400,28 @@ export class KotlinCxxBridgedType implements BridgedType<'kotlin', 'c++'> { return parameterName } } + case 'record': { + switch (language) { + case 'c++': + const record = getTypeAs(this.type, RecordType) + const key = new KotlinCxxBridgedType(record.keyType) + const value = new KotlinCxxBridgedType(record.valueType) + const parseKey = key.parseFromCppToKotlin('entry.first', 'c++') + const parseValue = value.parseFromCppToKotlin('entry.second', 'c++') + const javaMapType = `jni::JHashMap<${key.getTypeCode('c++')}, ${value.getTypeCode('c++')}>` + return ` +[&]() { + auto map = ${javaMapType}::create(${parameterName}.size()); + for (const auto& entry : ${parameterName}) { + map->put(${indent(parseKey, ' ')}, ${indent(parseValue, ' ')}); + } + return map; +}() + `.trim() + default: + return parameterName + } + } case 'array': { switch (language) { case 'c++': { @@ -412,7 +450,7 @@ export class KotlinCxxBridgedType implements BridgedType<'kotlin', 'c++'> { size_t size = ${parameterName}.size(); jni::local_ref<${arrayType}> array = ${arrayType}::newArray(size); for (size_t i = 0; i < size; i++) { - auto element = ${parameterName}[i]; + const auto& element = ${parameterName}[i]; array->setElement(i, *${bridge.parseFromCppToKotlin('element', 'c++')}); } return array; @@ -509,40 +547,25 @@ export class KotlinCxxBridgedType implements BridgedType<'kotlin', 'c++'> { return parameterName } } - case 'promise': { + case 'record': { switch (language) { case 'c++': - const promise = getTypeAs(this.type, PromiseType) - const actualCppType = promise.resultingType.getCode('c++') - const resultingType = new KotlinCxxBridgedType( - promise.resultingType - ) - let resolveBody: string - if (resultingType.hasType) { - // it's a Promise - resolveBody = ` -auto result = jni::static_ref_cast<${resultingType.getTypeCode('c++')}>(boxedResult); -promise->set_value(${resultingType.parseFromKotlinToCpp('result', 'c++', true)}); - `.trim() - } else { - // it's a Promise - resolveBody = ` -promise->set_value(); - `.trim() - } + const record = getTypeAs(this.type, RecordType) + const key = new KotlinCxxBridgedType(record.keyType) + const value = new KotlinCxxBridgedType(record.valueType) + const parseKey = key.parseFromKotlinToCpp('entry.first', 'c++') + const parseValue = value.parseFromKotlinToCpp('entry.second', 'c++') + const cxxType = this.type.getCode('c++') return ` [&]() { - auto promise = std::make_shared>(); - ${parameterName}->cthis()->addOnResolvedListener([=](jni::alias_ref boxedResult) { - ${indent(resolveBody, ' ')} - }); - ${parameterName}->cthis()->addOnRejectedListener([=](jni::alias_ref message) { - std::runtime_error error(message->toStdString()); - promise->set_exception(std::make_exception_ptr(error)); - }); - return promise->get_future(); + ${cxxType} map; + map.reserve(${parameterName}->size()); + for (const auto& entry : *${parameterName}) { + map.emplace(${indent(parseKey, ' ')}, ${indent(parseValue, ' ')}); + } + return map; }() - `.trim() + `.trim() default: return parameterName } @@ -589,6 +612,44 @@ promise->set_value(); return parameterName } } + case 'promise': { + switch (language) { + case 'c++': + const promise = getTypeAs(this.type, PromiseType) + const actualCppType = promise.resultingType.getCode('c++') + const resultingType = new KotlinCxxBridgedType( + promise.resultingType + ) + let resolveBody: string + if (resultingType.hasType) { + // it's a Promise + resolveBody = ` +auto result = jni::static_ref_cast<${resultingType.getTypeCode('c++')}>(boxedResult); +promise->set_value(${resultingType.parseFromKotlinToCpp('result', 'c++', true)}); + `.trim() + } else { + // it's a Promise + resolveBody = ` +promise->set_value(); + `.trim() + } + return ` +[&]() { + auto promise = std::make_shared>(); + ${parameterName}->cthis()->addOnResolvedListener([=](const jni::global_ref& boxedResult) { + ${indent(resolveBody, ' ')} + }); + ${parameterName}->cthis()->addOnRejectedListener([=](const jni::global_ref& message) { + std::runtime_error error(message->toStdString()); + promise->set_exception(std::make_exception_ptr(error)); + }); + return promise->get_future(); +}() + `.trim() + default: + return parameterName + } + } default: // no need to parse anything, just return as is return parameterName diff --git a/packages/nitrogen/src/syntax/types/RecordType.ts b/packages/nitrogen/src/syntax/types/RecordType.ts index df4b69e48..e1c99595b 100644 --- a/packages/nitrogen/src/syntax/types/RecordType.ts +++ b/packages/nitrogen/src/syntax/types/RecordType.ts @@ -28,6 +28,8 @@ export class RecordType implements Type { return `std::unordered_map<${keyCode}, ${valueCode}>` case 'swift': return `Dictionary<${keyCode}, ${valueCode}>` + case 'kotlin': + return `Map<${keyCode}, ${valueCode}>` default: throw new Error( `Language ${language} is not yet supported for RecordType!` diff --git a/packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/KotlinTestObject.kt b/packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/KotlinTestObject.kt index e0be27496..c79cf863e 100644 --- a/packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/KotlinTestObject.kt +++ b/packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/KotlinTestObject.kt @@ -59,4 +59,6 @@ class KotlinTestObject: HybridKotlinTestObjectSpec() { callback(Person("Marc", 24.0)) } } -} \ No newline at end of file + + override var someRecord: Map = mapOf("something" to "else") +} diff --git a/packages/react-native-nitro-image/nitrogen/generated/android/c++/JHybridKotlinTestObjectSpec.cpp b/packages/react-native-nitro-image/nitrogen/generated/android/c++/JHybridKotlinTestObjectSpec.cpp index 94617a2ba..3caff7d1e 100644 --- a/packages/react-native-nitro-image/nitrogen/generated/android/c++/JHybridKotlinTestObjectSpec.cpp +++ b/packages/react-native-nitro-image/nitrogen/generated/android/c++/JHybridKotlinTestObjectSpec.cpp @@ -30,6 +30,7 @@ namespace NitroModules { class AnyMap; } #include "JPerson.hpp" #include #include +#include #include #include #include @@ -113,7 +114,7 @@ namespace margelo::nitro::image { size_t size = carCollection.size(); jni::local_ref> array = jni::JArrayClass::newArray(size); for (size_t i = 0; i < size; i++) { - auto element = carCollection[i]; + const auto& element = carCollection[i]; array->setElement(i, *JCar::fromCpp(element)); } return array; @@ -128,6 +129,28 @@ namespace margelo::nitro::image { static const auto method = _javaPart->getClass()->getMethod /* someBuffer */)>("setSomeBuffer"); method(_javaPart, JArrayBuffer::wrap(someBuffer)); } + std::unordered_map JHybridKotlinTestObjectSpec::getSomeRecord() { + static const auto method = _javaPart->getClass()->getMethod>()>("getSomeRecord"); + auto result = method(_javaPart); + return [&]() { + std::unordered_map map; + map.reserve(result->size()); + for (const auto& entry : *result) { + map.emplace(entry.first->toStdString(), entry.second->toStdString()); + } + return map; + }(); + } + void JHybridKotlinTestObjectSpec::setSomeRecord(const std::unordered_map& someRecord) { + static const auto method = _javaPart->getClass()->getMethod> /* someRecord */)>("setSomeRecord"); + method(_javaPart, [&]() { + auto map = jni::JHashMap::create(someRecord.size()); + for (const auto& entry : someRecord) { + map->put(jni::make_jstring(entry.first), jni::make_jstring(entry.second)); + } + return map; + }()); + } // Methods std::future JHybridKotlinTestObjectSpec::asyncTest() { @@ -135,10 +158,10 @@ namespace margelo::nitro::image { auto result = method(_javaPart); return [&]() { auto promise = std::make_shared>(); - result->cthis()->addOnResolvedListener([=](jni::alias_ref boxedResult) { + result->cthis()->addOnResolvedListener([=](const jni::global_ref& boxedResult) { promise->set_value(); }); - result->cthis()->addOnRejectedListener([=](jni::alias_ref message) { + result->cthis()->addOnRejectedListener([=](const jni::global_ref& message) { std::runtime_error error(message->toStdString()); promise->set_exception(std::make_exception_ptr(error)); }); diff --git a/packages/react-native-nitro-image/nitrogen/generated/android/c++/JHybridKotlinTestObjectSpec.hpp b/packages/react-native-nitro-image/nitrogen/generated/android/c++/JHybridKotlinTestObjectSpec.hpp index a1cd54a3f..791304d24 100644 --- a/packages/react-native-nitro-image/nitrogen/generated/android/c++/JHybridKotlinTestObjectSpec.hpp +++ b/packages/react-native-nitro-image/nitrogen/generated/android/c++/JHybridKotlinTestObjectSpec.hpp @@ -49,6 +49,8 @@ namespace margelo::nitro::image { void setCarCollection(const std::vector& carCollection) override; std::shared_ptr getSomeBuffer() override; void setSomeBuffer(const std::shared_ptr& someBuffer) override; + std::unordered_map getSomeRecord() override; + void setSomeRecord(const std::unordered_map& someRecord) override; public: // Methods diff --git a/packages/react-native-nitro-image/nitrogen/generated/android/kotlin/com/margelo/nitro/image/HybridKotlinTestObjectSpec.kt b/packages/react-native-nitro-image/nitrogen/generated/android/kotlin/com/margelo/nitro/image/HybridKotlinTestObjectSpec.kt index c734a3a37..bb4a170c9 100644 --- a/packages/react-native-nitro-image/nitrogen/generated/android/kotlin/com/margelo/nitro/image/HybridKotlinTestObjectSpec.kt +++ b/packages/react-native-nitro-image/nitrogen/generated/android/kotlin/com/margelo/nitro/image/HybridKotlinTestObjectSpec.kt @@ -63,6 +63,12 @@ abstract class HybridKotlinTestObjectSpec: HybridObject() { @set:DoNotStrip @set:Keep abstract var someBuffer: ArrayBuffer + + @get:DoNotStrip + @get:Keep + @set:DoNotStrip + @set:Keep + abstract var someRecord: Map // Methods @DoNotStrip diff --git a/packages/react-native-nitro-image/nitrogen/generated/ios/NitroImage-Swift-Cxx-Bridge.hpp b/packages/react-native-nitro-image/nitrogen/generated/ios/NitroImage-Swift-Cxx-Bridge.hpp index 601b63a2f..4af33be47 100644 --- a/packages/react-native-nitro-image/nitrogen/generated/ios/NitroImage-Swift-Cxx-Bridge.hpp +++ b/packages/react-native-nitro-image/nitrogen/generated/ios/NitroImage-Swift-Cxx-Bridge.hpp @@ -352,6 +352,24 @@ namespace margelo::nitro::image::bridge::swift { return vector; } + /** + * Specialized version of `std::unordered_map`. + */ + using std__unordered_map_std__string__std__string_ = std::unordered_map; + inline std::unordered_map create_std__unordered_map_std__string__std__string_(size_t size) { + std::unordered_map map; + map.reserve(size); + return map; + } + inline std::vector get_std__unordered_map_std__string__std__string__keys(const std__unordered_map_std__string__std__string_& map) { + std::vector keys; + keys.reserve(map.size()); + for (const auto& entry : map) { + keys.push_back(entry.first); + } + return keys; + } + /** * Specialized version of `std::function`. */ diff --git a/packages/react-native-nitro-image/nitrogen/generated/shared/c++/HybridKotlinTestObjectSpec.cpp b/packages/react-native-nitro-image/nitrogen/generated/shared/c++/HybridKotlinTestObjectSpec.cpp index 19be1d8ce..3885624af 100644 --- a/packages/react-native-nitro-image/nitrogen/generated/shared/c++/HybridKotlinTestObjectSpec.cpp +++ b/packages/react-native-nitro-image/nitrogen/generated/shared/c++/HybridKotlinTestObjectSpec.cpp @@ -25,6 +25,8 @@ namespace margelo::nitro::image { prototype.registerHybridSetter("carCollection", &HybridKotlinTestObjectSpec::setCarCollection); prototype.registerHybridGetter("someBuffer", &HybridKotlinTestObjectSpec::getSomeBuffer); prototype.registerHybridSetter("someBuffer", &HybridKotlinTestObjectSpec::setSomeBuffer); + prototype.registerHybridGetter("someRecord", &HybridKotlinTestObjectSpec::getSomeRecord); + prototype.registerHybridSetter("someRecord", &HybridKotlinTestObjectSpec::setSomeRecord); prototype.registerHybridMethod("asyncTest", &HybridKotlinTestObjectSpec::asyncTest); prototype.registerHybridMethod("createMap", &HybridKotlinTestObjectSpec::createMap); prototype.registerHybridMethod("addOnPersonBornListener", &HybridKotlinTestObjectSpec::addOnPersonBornListener); diff --git a/packages/react-native-nitro-image/nitrogen/generated/shared/c++/HybridKotlinTestObjectSpec.hpp b/packages/react-native-nitro-image/nitrogen/generated/shared/c++/HybridKotlinTestObjectSpec.hpp index cf60d0636..8cfe90def 100644 --- a/packages/react-native-nitro-image/nitrogen/generated/shared/c++/HybridKotlinTestObjectSpec.hpp +++ b/packages/react-native-nitro-image/nitrogen/generated/shared/c++/HybridKotlinTestObjectSpec.hpp @@ -27,6 +27,8 @@ namespace margelo::nitro::image { struct Person; } #include #include "Car.hpp" #include +#include +#include #include #include #include @@ -66,6 +68,8 @@ namespace margelo::nitro::image { virtual void setCarCollection(const std::vector& carCollection) = 0; virtual std::shared_ptr getSomeBuffer() = 0; virtual void setSomeBuffer(const std::shared_ptr& someBuffer) = 0; + virtual std::unordered_map getSomeRecord() = 0; + virtual void setSomeRecord(const std::unordered_map& someRecord) = 0; public: // Methods diff --git a/packages/react-native-nitro-image/src/specs/TestObject.nitro.ts b/packages/react-native-nitro-image/src/specs/TestObject.nitro.ts index 6989b69cb..f46157f4e 100644 --- a/packages/react-native-nitro-image/src/specs/TestObject.nitro.ts +++ b/packages/react-native-nitro-image/src/specs/TestObject.nitro.ts @@ -162,4 +162,6 @@ export interface KotlinTestObject extends HybridObject<{ android: 'kotlin' }> { createMap(): AnyMap addOnPersonBornListener(callback: (p: Person) => void): void + + someRecord: Record } diff --git a/packages/react-native-nitro-modules/README.md b/packages/react-native-nitro-modules/README.md index d61cb6e0d..05baaf6d5 100644 --- a/packages/react-native-nitro-modules/README.md +++ b/packages/react-native-nitro-modules/README.md @@ -161,7 +161,7 @@ The following C++ / JS types are supported out of the box: Record<string, T> std::unordered_map<std::string, T> Dictionary<String, T> - ❌ + Map<std::string, T> T?