Skip to content

Commit

Permalink
feat: Support Record<K, V> as Map<K, V> in Kotlin/Android (#80)
Browse files Browse the repository at this point in the history
* feat: Support `Record<K, V>` as `Map<K, V>` in Kotlin/Android

* Update README.md

* fix: Optimize a bit
  • Loading branch information
mrousavy authored Aug 28, 2024
1 parent fd85cf5 commit 9db7852
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 35 deletions.
3 changes: 3 additions & 0 deletions example/src/screens/HybridObjectTestsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
Expand Down
121 changes: 91 additions & 30 deletions packages/nitrogen/src/syntax/kotlin/KotlinCxxBridgedType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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++':
Expand Down Expand Up @@ -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++': {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<T>
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<void>
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<std::promise<${actualCppType}>>();
${parameterName}->cthis()->addOnResolvedListener([=](jni::alias_ref<jni::JObject> boxedResult) {
${indent(resolveBody, ' ')}
});
${parameterName}->cthis()->addOnRejectedListener([=](jni::alias_ref<jni::JString> 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
}
Expand Down Expand Up @@ -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<T>
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<void>
resolveBody = `
promise->set_value();
`.trim()
}
return `
[&]() {
auto promise = std::make_shared<std::promise<${actualCppType}>>();
${parameterName}->cthis()->addOnResolvedListener([=](const jni::global_ref<jni::JObject>& boxedResult) {
${indent(resolveBody, ' ')}
});
${parameterName}->cthis()->addOnRejectedListener([=](const jni::global_ref<jni::JString>& 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
Expand Down
2 changes: 2 additions & 0 deletions packages/nitrogen/src/syntax/types/RecordType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,6 @@ class KotlinTestObject: HybridKotlinTestObjectSpec() {
callback(Person("Marc", 24.0))
}
}
}

override var someRecord: Map<String, String> = mapOf("something" to "else")
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ namespace NitroModules { class AnyMap; }
#include "JPerson.hpp"
#include <NitroModules/ArrayBuffer.hpp>
#include <NitroModules/JArrayBuffer.hpp>
#include <unordered_map>
#include <future>
#include <NitroModules/JPromise.hpp>
#include <NitroModules/AnyMap.hpp>
Expand Down Expand Up @@ -113,7 +114,7 @@ namespace margelo::nitro::image {
size_t size = carCollection.size();
jni::local_ref<jni::JArrayClass<JCar>> array = jni::JArrayClass<JCar>::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;
Expand All @@ -128,17 +129,39 @@ namespace margelo::nitro::image {
static const auto method = _javaPart->getClass()->getMethod<void(jni::alias_ref<JArrayBuffer::javaobject> /* someBuffer */)>("setSomeBuffer");
method(_javaPart, JArrayBuffer::wrap(someBuffer));
}
std::unordered_map<std::string, std::string> JHybridKotlinTestObjectSpec::getSomeRecord() {
static const auto method = _javaPart->getClass()->getMethod<jni::alias_ref<jni::JMap<jni::JString, jni::JString>>()>("getSomeRecord");
auto result = method(_javaPart);
return [&]() {
std::unordered_map<std::string, std::string> 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<std::string, std::string>& someRecord) {
static const auto method = _javaPart->getClass()->getMethod<void(jni::alias_ref<jni::JMap<jni::JString, jni::JString>> /* someRecord */)>("setSomeRecord");
method(_javaPart, [&]() {
auto map = jni::JHashMap<jni::JString, jni::JString>::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<void> JHybridKotlinTestObjectSpec::asyncTest() {
static const auto method = _javaPart->getClass()->getMethod<jni::alias_ref<JPromise::javaobject>()>("asyncTest");
auto result = method(_javaPart);
return [&]() {
auto promise = std::make_shared<std::promise<void>>();
result->cthis()->addOnResolvedListener([=](jni::alias_ref<jni::JObject> boxedResult) {
result->cthis()->addOnResolvedListener([=](const jni::global_ref<jni::JObject>& boxedResult) {
promise->set_value();
});
result->cthis()->addOnRejectedListener([=](jni::alias_ref<jni::JString> message) {
result->cthis()->addOnRejectedListener([=](const jni::global_ref<jni::JString>& message) {
std::runtime_error error(message->toStdString());
promise->set_exception(std::make_exception_ptr(error));
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ namespace margelo::nitro::image {
void setCarCollection(const std::vector<Car>& carCollection) override;
std::shared_ptr<ArrayBuffer> getSomeBuffer() override;
void setSomeBuffer(const std::shared_ptr<ArrayBuffer>& someBuffer) override;
std::unordered_map<std::string, std::string> getSomeRecord() override;
void setSomeRecord(const std::unordered_map<std::string, std::string>& someRecord) override;

public:
// Methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String>

// Methods
@DoNotStrip
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,24 @@ namespace margelo::nitro::image::bridge::swift {
return vector;
}

/**
* Specialized version of `std::unordered_map<std::string, std::string>`.
*/
using std__unordered_map_std__string__std__string_ = std::unordered_map<std::string, std::string>;
inline std::unordered_map<std::string, std::string> create_std__unordered_map_std__string__std__string_(size_t size) {
std::unordered_map<std::string, std::string> map;
map.reserve(size);
return map;
}
inline std::vector<std::string> get_std__unordered_map_std__string__std__string__keys(const std__unordered_map_std__string__std__string_& map) {
std::vector<std::string> keys;
keys.reserve(map.size());
for (const auto& entry : map) {
keys.push_back(entry.first);
}
return keys;
}

/**
* Specialized version of `std::function<void(const Person& / * p * /)>`.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ namespace margelo::nitro::image { struct Person; }
#include <vector>
#include "Car.hpp"
#include <NitroModules/ArrayBuffer.hpp>
#include <unordered_map>
#include <string>
#include <future>
#include <NitroModules/AnyMap.hpp>
#include <functional>
Expand Down Expand Up @@ -66,6 +68,8 @@ namespace margelo::nitro::image {
virtual void setCarCollection(const std::vector<Car>& carCollection) = 0;
virtual std::shared_ptr<ArrayBuffer> getSomeBuffer() = 0;
virtual void setSomeBuffer(const std::shared_ptr<ArrayBuffer>& someBuffer) = 0;
virtual std::unordered_map<std::string, std::string> getSomeRecord() = 0;
virtual void setSomeRecord(const std::unordered_map<std::string, std::string>& someRecord) = 0;

public:
// Methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,6 @@ export interface KotlinTestObject extends HybridObject<{ android: 'kotlin' }> {
createMap(): AnyMap

addOnPersonBornListener(callback: (p: Person) => void): void

someRecord: Record<string, string>
}
2 changes: 1 addition & 1 deletion packages/react-native-nitro-modules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ The following C++ / JS types are supported out of the box:
<td><code>Record&lt;string, T&gt;</code></td>
<td><code>std::unordered_map&lt;std::string, T&gt;</code></td>
<td><code>Dictionary&lt;String, T&gt;</code></td>
<td></td>
<td><code>Map&lt;std::string, T&gt;</code></td>
</tr>
<tr>
<td><code>T?</code></td>
Expand Down

0 comments on commit 9db7852

Please sign in to comment.