Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support Inheritance in Kotlin 🎉 #173

Merged
merged 5 commits into from
Oct 3, 2024
Merged
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
2 changes: 2 additions & 0 deletions example/src/screens/HybridObjectTestsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
HybridTestObjectCpp,
HybridTestObjectSwiftKotlin,
HybridChild,
HybridBase,
} from 'react-native-nitro-image'
import { getTests, type TestRunner } from '../getTests'
import { SafeAreaView } from 'react-native-safe-area-context'
Expand All @@ -20,6 +21,7 @@ import SegmentedControl from '@react-native-segmented-control/segmented-control'
import { NitroModules } from 'react-native-nitro-modules'

logPrototypeChain(HybridChild)
console.log(HybridBase.baseValue)
console.log(HybridChild.baseValue)
console.log(HybridChild.childValue)

Expand Down
62 changes: 53 additions & 9 deletions packages/nitrogen/src/syntax/kotlin/FbjniHybridObject.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
import { NitroConfig } from '../../config/NitroConfig.js'
import { createIndentation, indent } from '../../utils.js'
import { getForwardDeclaration } from '../c++/getForwardDeclaration.js'
import { includeHeader } from '../c++/includeNitroHeader.js'
import { getAllTypes } from '../getAllTypes.js'
import { getHybridObjectName } from '../getHybridObjectName.js'
import { createFileMetadataString, isNotDuplicate } from '../helpers.js'
import type { HybridObjectSpec } from '../HybridObjectSpec.js'
import { Method } from '../Method.js'
import type { Property } from '../Property.js'
import type { SourceFile } from '../SourceFile.js'
import type { SourceFile, SourceImport } from '../SourceFile.js'
import { addJNINativeRegistration } from './JNINativeRegistrations.js'
import { KotlinCxxBridgedType } from './KotlinCxxBridgedType.js'

export function createFbjniHybridObject(spec: HybridObjectSpec): SourceFile[] {
const name = getHybridObjectName(spec.name)
const propertiesDecl = spec.properties

// Because we cache JNI methods as `static` inside our method bodies,
// we need to re-create the method bodies per inherited class.
// This way `Child`'s statically cached `someMethod()` JNI reference
// is not the same as `Base`'s statically cached `someMethod()` JNI reference.
const properties = [
...spec.properties,
...spec.baseTypes.flatMap((b) => b.properties),
]
const methods = [...spec.methods, ...spec.baseTypes.flatMap((b) => b.methods)]

const propertiesDecl = properties
.map((p) => p.getCode('c++', { override: true }))
.join('\n')
const methodsDecl = spec.methods
const methodsDecl = methods
.map((p) => p.getCode('c++', { override: true }))
.join('\n')
const jniClassDescriptor = NitroConfig.getAndroidPackage(
Expand All @@ -26,6 +38,32 @@ export function createFbjniHybridObject(spec: HybridObjectSpec): SourceFile[] {
const cxxNamespace = NitroConfig.getCxxNamespace('c++')
const spaces = createIndentation(name.JHybridTSpec.length)

let cppBase = 'JHybridObject'
if (spec.baseTypes.length > 0) {
if (spec.baseTypes.length > 1) {
throw new Error(
`${name.T}: Inheriting from multiple HybridObject bases is not yet supported on Kotlin!`
)
}
cppBase = getHybridObjectName(spec.baseTypes[0]!.name).JHybridTSpec
}
const cppImports: SourceImport[] = []
const cppConstructorCalls = [`HybridObject(${name.HybridTSpec}::TAG)`]
for (const base of spec.baseTypes) {
const { JHybridTSpec } = getHybridObjectName(base.name)
cppConstructorCalls.push('HybridBase(jThis)')
cppImports.push({
language: 'c++',
name: `${JHybridTSpec}.hpp`,
space: 'user',
forwardDeclaration: getForwardDeclaration(
'class',
JHybridTSpec,
NitroConfig.getCxxNamespace('c++')
),
})
}

const cppHeaderCode = `
${createFileMetadataString(`${name.HybridTSpec}.hpp`)}

Expand All @@ -35,21 +73,27 @@ ${createFileMetadataString(`${name.HybridTSpec}.hpp`)}
#include <fbjni/fbjni.h>
#include "${name.HybridTSpec}.hpp"

${cppImports
.map((i) => i.forwardDeclaration)
.filter((f) => f != null)
.join('\n')}
${cppImports.map((i) => includeHeader(i)).join('\n')}

namespace ${cxxNamespace} {

using namespace facebook;

class ${name.JHybridTSpec} final: public jni::HybridClass<${name.JHybridTSpec}, JHybridObject>,
${spaces} public ${name.HybridTSpec} {
class ${name.JHybridTSpec}: public jni::HybridClass<${name.JHybridTSpec}, ${cppBase}>,
${spaces} public virtual ${name.HybridTSpec} {
public:
static auto constexpr kJavaDescriptor = "L${jniClassDescriptor};";
static jni::local_ref<jhybriddata> initHybrid(jni::alias_ref<jhybridobject> jThis);
static void registerNatives();

private:
protected:
// C++ constructor (called from Java via \`initHybrid()\`)
explicit ${name.JHybridTSpec}(jni::alias_ref<jhybridobject> jThis) :
HybridObject(${name.HybridTSpec}::TAG),
${indent(cppConstructorCalls.join(',\n'), ' ')},
_javaPart(jni::make_global(jThis)) {}

public:
Expand Down Expand Up @@ -88,10 +132,10 @@ ${spaces} public ${name.HybridTSpec} {
},
})

const propertiesImpl = spec.properties
const propertiesImpl = properties
.map((m) => getFbjniPropertyForwardImplementation(spec, m))
.join('\n')
const methodsImpl = spec.methods
const methodsImpl = methods
.map((m) => getFbjniMethodForwardImplementation(spec, m))
.join('\n')
const allTypes = getAllTypes(spec)
Expand Down
24 changes: 20 additions & 4 deletions packages/nitrogen/src/syntax/kotlin/KotlinHybridObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ export function createKotlinHybridObject(spec: HybridObjectSpec): SourceFile[] {
const javaPackage = NitroConfig.getAndroidPackage('java/kotlin')
const cppLibName = NitroConfig.getAndroidCxxLibName()

let kotlinBase = 'HybridObject'
if (spec.baseTypes.length > 0) {
if (spec.baseTypes.length > 1) {
throw new Error(
`${name.T}: Inheriting from multiple HybridObject bases is not yet supported in Kotlin!`
)
}
const base = spec.baseTypes[0]!.name
kotlinBase = getHybridObjectName(base).HybridTSpec
}

// 1. Create Kotlin abstract class definition
const abstractClassCode = `
${createFileMetadataString(`${name.HybridTSpec}.kt`)}
Expand All @@ -42,18 +53,23 @@ import com.margelo.nitro.core.*
@DoNotStrip
@Keep
@Suppress("RedundantSuppression", "KotlinJniMissingFunction", "PropertyName", "RedundantUnitReturnType", "unused")
abstract class ${name.HybridTSpec}: HybridObject() {
protected val TAG = "${name.HybridTSpec}"

abstract class ${name.HybridTSpec}: ${kotlinBase}() {
@DoNotStrip
val mHybridData: HybridData = initHybrid()
private var mHybridData: HybridData = initHybrid()

init {
// Pass this \`HybridData\` through to it's base class,
// to represent inheritance to JHybridObject on C++ side
super.updateNative(mHybridData)
}

/**
* Call from a child class to initialize HybridData with a child.
*/
override fun updateNative(hybridData: HybridData) {
mHybridData = hybridData
}

// Properties
${indent(properties, ' ')}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.margelo.nitro.image

class HybridBase: HybridBaseSpec() {
override val baseValue: Double
get() = 10.0

override val memorySize: Long
get() = 0L
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.margelo.nitro.image

class HybridChild: HybridChildSpec() {
override val baseValue: Double
get() = 20.0
override val childValue: Double
get() = 30.0

override val memorySize: Long
get() = 0L
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import com.margelo.nitro.core.Promise
import kotlinx.coroutines.delay

class HybridTestObjectKotlin: HybridTestObjectSwiftKotlinSpec() {
private val TAG = "HybridTestObjectKotlin"

override var numberValue: Double = 0.0
override var boolValue: Boolean = false
override var stringValue: String = ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import android.os.Looper
import android.util.Log

class Image(val bitmap: Bitmap): HybridImageSpec() {
private val TAG = "Image"

override val size: ImageSize
get() {
return ImageSize(bitmap.width.toDouble(), bitmap.height.toDouble())
Expand Down
6 changes: 4 additions & 2 deletions packages/react-native-nitro-image/nitro.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
"kotlin": "HybridTestObjectKotlin"
},
"Base": {
"swift": "HybridBase"
"swift": "HybridBase",
"kotlin": "HybridBase"
},
"Child": {
"swift": "HybridChild"
"swift": "HybridChild",
"kotlin": "HybridChild"
}
},
"ignorePaths": ["node_modules"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ target_sources(
../nitrogen/generated/android/c++/JHybridImageSpec.cpp
../nitrogen/generated/android/c++/JHybridImageFactorySpec.cpp
../nitrogen/generated/android/c++/JHybridTestObjectSwiftKotlinSpec.cpp
../nitrogen/generated/android/c++/JHybridBaseSpec.cpp
../nitrogen/generated/android/c++/JHybridChildSpec.cpp
)

# Add all libraries required by the generated specs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
#include "JHybridTestObjectSwiftKotlinSpec.hpp"
#include "JFunc_void.hpp"
#include "JFunc_void_std__optional_double_.hpp"
#include "JHybridBaseSpec.hpp"
#include "JHybridChildSpec.hpp"
#include <NitroModules/JNISharedPtr.hpp>
#include "HybridTestObjectCpp.hpp"

Expand All @@ -38,6 +40,8 @@ int initialize(JavaVM* vm) {
margelo::nitro::image::JFunc_void::registerNatives();
margelo::nitro::image::JFunc_void::registerNatives();
margelo::nitro::image::JFunc_void_std__optional_double_::registerNatives();
margelo::nitro::image::JHybridBaseSpec::registerNatives();
margelo::nitro::image::JHybridChildSpec::registerNatives();

// Register Nitro Hybrid Objects
HybridObjectRegistry::registerHybridObjectConstructor(
Expand Down Expand Up @@ -81,6 +85,38 @@ int initialize(JavaVM* vm) {
return JNISharedPtr::make_shared_from_jni<JHybridTestObjectSwiftKotlinSpec>(globalRef);
}
);
HybridObjectRegistry::registerHybridObjectConstructor(
"Base",
[]() -> std::shared_ptr<HybridObject> {
static auto javaClass = jni::findClassStatic("com/margelo/nitro/image/HybridBase");
static auto defaultConstructor = javaClass->getConstructor<JHybridBaseSpec::javaobject()>();

auto instance = javaClass->newObject(defaultConstructor);
#ifdef NITRO_DEBUG
if (instance == nullptr) [[unlikely]] {
throw std::runtime_error("Failed to create an instance of \"JHybridBaseSpec\" - the constructor returned null!");
}
#endif
auto globalRef = jni::make_global(instance);
return JNISharedPtr::make_shared_from_jni<JHybridBaseSpec>(globalRef);
}
);
HybridObjectRegistry::registerHybridObjectConstructor(
"Child",
[]() -> std::shared_ptr<HybridObject> {
static auto javaClass = jni::findClassStatic("com/margelo/nitro/image/HybridChild");
static auto defaultConstructor = javaClass->getConstructor<JHybridChildSpec::javaobject()>();

auto instance = javaClass->newObject(defaultConstructor);
#ifdef NITRO_DEBUG
if (instance == nullptr) [[unlikely]] {
throw std::runtime_error("Failed to create an instance of \"JHybridChildSpec\" - the constructor returned null!");
}
#endif
auto globalRef = jni::make_global(instance);
return JNISharedPtr::make_shared_from_jni<JHybridChildSpec>(globalRef);
}
);
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
///
/// JHybridBaseSpec.cpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © 2024 Marc Rousavy @ Margelo
///

#include "JHybridBaseSpec.hpp"





namespace margelo::nitro::image {

jni::local_ref<JHybridBaseSpec::jhybriddata> JHybridBaseSpec::initHybrid(jni::alias_ref<jhybridobject> jThis) {
return makeCxxInstance(jThis);
}

void JHybridBaseSpec::registerNatives() {
registerHybrid({
makeNativeMethod("initHybrid", JHybridBaseSpec::initHybrid),
});
}

size_t JHybridBaseSpec::getExternalMemorySize() noexcept {
static const auto method = _javaPart->getClass()->getMethod<jlong()>("getMemorySize");
return method(_javaPart);
}

// Properties
double JHybridBaseSpec::getBaseValue() {
static const auto method = _javaPart->getClass()->getMethod<double()>("getBaseValue");
auto result = method(_javaPart);
return result;
}

// Methods


} // namespace margelo::nitro::image
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
///
/// HybridBaseSpec.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © 2024 Marc Rousavy @ Margelo
///

#pragma once

#include <NitroModules/JHybridObject.hpp>
#include <fbjni/fbjni.h>
#include "HybridBaseSpec.hpp"




namespace margelo::nitro::image {

using namespace facebook;

class JHybridBaseSpec: public jni::HybridClass<JHybridBaseSpec, JHybridObject>,
public virtual HybridBaseSpec {
public:
static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/image/HybridBaseSpec;";
static jni::local_ref<jhybriddata> initHybrid(jni::alias_ref<jhybridobject> jThis);
static void registerNatives();

protected:
// C++ constructor (called from Java via `initHybrid()`)
explicit JHybridBaseSpec(jni::alias_ref<jhybridobject> jThis) :
HybridObject(HybridBaseSpec::TAG),
_javaPart(jni::make_global(jThis)) {}

public:
size_t getExternalMemorySize() noexcept override;

public:
inline const jni::global_ref<JHybridBaseSpec::javaobject>& getJavaPart() const noexcept {
return _javaPart;
}

public:
// Properties
double getBaseValue() override;

public:
// Methods


private:
friend HybridBase;
using HybridBase::HybridBase;
jni::global_ref<JHybridBaseSpec::javaobject> _javaPart;
};

} // namespace margelo::nitro::image
Loading
Loading