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: Variants for Swift and Kotlin 🥳 #148

Merged
merged 24 commits into from
Sep 24, 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
46 changes: 46 additions & 0 deletions packages/nitrogen/src/syntax/kotlin/KotlinCxxBridgedType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ 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 { VariantType } from '../types/VariantType.js'
import { getKotlinBoxedPrimitiveType } from './KotlinBoxedPrimitive.js'
import { createKotlinEnum } from './KotlinEnum.js'
import { createKotlinFunction } from './KotlinFunction.js'
import { createKotlinStruct } from './KotlinStruct.js'
import { createKotlinVariant, getVariantName } from './KotlinVariant.js'

export class KotlinCxxBridgedType implements BridgedType<'kotlin', 'c++'> {
readonly type: Type
Expand Down Expand Up @@ -89,6 +91,15 @@ export class KotlinCxxBridgedType implements BridgedType<'kotlin', 'c++'> {
space: 'system',
})
break
case 'variant':
const variantType = getTypeAs(this.type, VariantType)
const variantName = getVariantName(variantType)
imports.push({
language: 'c++',
name: `J${variantName}.hpp`,
space: 'user',
})
break
case 'hybrid-object': {
const hybridObjectType = getTypeAs(this.type, HybridObjectType)
const name = getHybridObjectName(hybridObjectType.hybridObjectName)
Expand Down Expand Up @@ -134,6 +145,11 @@ export class KotlinCxxBridgedType implements BridgedType<'kotlin', 'c++'> {
const structFiles = createKotlinStruct(structType)
files.push(...structFiles)
break
case 'variant':
const variantType = getTypeAs(this.type, VariantType)
const variantFiles = createKotlinVariant(variantType)
files.push(...variantFiles)
break
case 'function':
const functionType = getTypeAs(this.type, FunctionType)
const funcFiles = createKotlinFunction(functionType)
Expand Down Expand Up @@ -265,6 +281,18 @@ export class KotlinCxxBridgedType implements BridgedType<'kotlin', 'c++'> {
default:
return this.type.getCode(language)
}
case 'variant': {
const variant = getTypeAs(this.type, VariantType)
const name = getVariantName(variant)
switch (language) {
case 'c++':
return `J${name}`
case 'kotlin':
return name
default:
return this.type.getCode(language)
}
}
case 'promise':
switch (language) {
case 'c++':
Expand Down Expand Up @@ -356,6 +384,16 @@ export class KotlinCxxBridgedType implements BridgedType<'kotlin', 'c++'> {
return parameterName
}
}
case 'variant': {
switch (language) {
case 'c++':
const variant = getTypeAs(this.type, VariantType)
const name = getVariantName(variant)
return `J${name}::fromCpp(${parameterName})`
default:
return parameterName
}
}
case 'enum': {
switch (language) {
case 'c++':
Expand Down Expand Up @@ -518,6 +556,14 @@ export class KotlinCxxBridgedType implements BridgedType<'kotlin', 'c++'> {
return parameterName
}
}
case 'variant': {
switch (language) {
case 'c++':
return `${parameterName}->toCpp()`
default:
return parameterName
}
}
case 'hybrid-object': {
switch (language) {
case 'c++':
Expand Down
186 changes: 186 additions & 0 deletions packages/nitrogen/src/syntax/kotlin/KotlinVariant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { NitroConfig } from '../../config/NitroConfig.js'
import { indent } from '../../utils.js'
import { createFileMetadataString, toReferenceType } from '../helpers.js'
import type { SourceFile } from '../SourceFile.js'
import type { Type } from '../types/Type.js'
import type { VariantType } from '../types/VariantType.js'
import { KotlinCxxBridgedType } from './KotlinCxxBridgedType.js'

export function getVariantName(variant: VariantType): string {
const variants = variant.variants.map((v) => v.getCode('kotlin'))
return `Variant_` + variants.join('_')
}

function getVariantInnerName(variantType: Type): string {
return `Some${variantType.getCode('kotlin')}`
}

export function createKotlinVariant(variant: VariantType): SourceFile[] {
const jsName = variant.variants.map((v) => v.getCode('kotlin')).join('|')
const kotlinName = getVariantName(variant)

const innerClasses = variant.variants.map((v) => {
const innerName = getVariantInnerName(v)
return `
@DoNotStrip
data class ${innerName}(val value: ${v.getCode('kotlin')}): ${kotlinName}()
`.trim()
})

const packageName = NitroConfig.getAndroidPackage('java/kotlin')
const getterCases = variant.variants.map((v) => {
const innerName = getVariantInnerName(v)
return `is ${innerName} -> value as? T`
})
const isFunctions = variant.variants.map((v) => {
const innerName = getVariantInnerName(v)
return `
val is${v.getCode('kotlin')}: Boolean
get() = this is ${innerName}
`.trim()
})

const createFunctions = variant.variants.map((v) => {
const innerName = getVariantInnerName(v)
return `
@JvmStatic
fun create(value: ${v.getCode('kotlin')}): ${kotlinName} = ${innerName}(value)
`.trim()
})
const code = `
${createFileMetadataString(`${kotlinName}.kt`)}

package ${packageName}

import com.facebook.proguard.annotations.DoNotStrip

/**
* Represents the TypeScript variant "${jsName}".
*/
@DoNotStrip
sealed class ${kotlinName} {
${indent(innerClasses.join('\n'), ' ')}

inline fun <reified T> getAs(): T? = when (this) {
${indent(getterCases.join('\n'), ' ')}
}

${indent(isFunctions.join('\n'), ' ')}

companion object {
${indent(createFunctions.join('\n'), ' ')}
}
}
`.trim()

const cxxNamespace = NitroConfig.getCxxNamespace('c++')
const jniClassDescriptor = NitroConfig.getAndroidPackage(
'c++/jni',
kotlinName
)
const cppCreateFuncs = variant.variants.map((v) => {
const bridge = new KotlinCxxBridgedType(v)
return `
static jni::local_ref<J${kotlinName}> create(${bridge.asJniReferenceType('alias')} value) {
static const auto method = javaClassStatic()->getStaticMethod<J${kotlinName}(${bridge.asJniReferenceType('alias')})>("create");
return method(javaClassStatic(), value);
}
`.trim()
})
const variantCases = variant.variants.map((v, i) => {
const bridge = new KotlinCxxBridgedType(v)
return `case ${i}: return create(${bridge.parseFromCppToKotlin(`std::get<${i}>(variant)`, 'c++')});`
})
const cppInnerClassesForwardDecl = variant.variants.map((v) => {
const innerName = getVariantInnerName(v)
return `class ${innerName};`
})
const cppGetIfs = variant.variants.map((v) => {
const innerName = getVariantInnerName(v)
const bridge = new KotlinCxxBridgedType(v)
return `
if (isInstanceOf(${innerName}::javaClassStatic())) {
auto jniValue = static_cast<${innerName}*>(this)->get();
return ${bridge.parseFromKotlinToCpp('jniValue', 'c++')};
}
`.trim()
})
const cppInnerClasses = variant.variants.map((v) => {
const bridge = new KotlinCxxBridgedType(v)
const innerName = getVariantInnerName(v)
const descriptor = NitroConfig.getAndroidPackage(
'c++/jni',
`${kotlinName}$${innerName}`
)
return `
class ${innerName}: public jni::JavaClass<${innerName}, J${kotlinName}> {
public:
static auto constexpr kJavaDescriptor = "L${descriptor};";

${bridge.asJniReferenceType('local')} get() {
static const auto field = javaClassStatic()->getField<${bridge.getTypeCode('c++')}>("value");
return getFieldValue(field);
}
};
`.trim()
})
const fbjniCode = `
${createFileMetadataString(`J${kotlinName}.hpp`)}

#pragma once

#include <fbjni/fbjni.h>
#include <variant>

namespace ${cxxNamespace} {

using namespace facebook;

${indent(cppInnerClassesForwardDecl.join('\n'), ' ')}

/**
* The C++ JNI bridge between the C++ std::variant and the Java class "${kotlinName}".
*/
class J${kotlinName}: public jni::JavaClass<J${kotlinName}> {
public:
static auto constexpr kJavaDescriptor = "L${jniClassDescriptor};";

${indent(cppCreateFuncs.join('\n'), ' ')}

static jni::local_ref<J${kotlinName}> fromCpp(${toReferenceType(variant.getCode('c++'))} variant) {
switch (variant.index()) {
${indent(variantCases.join('\n'), ' ')}
default: throw std::runtime_error("Variant holds unknown index! (" + std::to_string(variant.index()) + ")");
}
}

${variant.getCode('c++')} toCpp();
};

${indent(cppInnerClasses.join('\n\n'), ' ')}

${variant.getCode('c++')} J${kotlinName}::toCpp() {
${indent(cppGetIfs.join(' else '), ' ')}
throw std::runtime_error("Variant is unknown Kotlin instance!");
}

} // namespace ${cxxNamespace}
`.trim()

const files: SourceFile[] = []
files.push({
content: code,
language: 'kotlin',
name: `${kotlinName}.kt`,
subdirectory: NitroConfig.getAndroidPackageDirectory(),
platform: 'android',
})
files.push({
content: fbjniCode,
language: 'c++',
name: `J${kotlinName}.hpp`,
subdirectory: [],
platform: 'android',
})
return files
}
17 changes: 8 additions & 9 deletions packages/nitrogen/src/syntax/swift/SwiftCxxBridgedType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,18 +650,17 @@ case ${i}:
}
case 'variant': {
const bridge = this.getBridgeOrThrow()
const makeFunc = NitroConfig.getCxxNamespace('swift', bridge.funcName)
const variant = getTypeAs(this.type, VariantType)
const cases = variant.variants
.map((t) => {
const caseName = getSwiftVariantCaseName(t)
const wrapping = new SwiftCxxBridgedType(t)
const parse = wrapping.parseFromSwiftToCpp('value', 'swift')
return `case .${caseName}(let value):\n return ${makeFunc}(${parse})`
})
.join('\n')
switch (language) {
case 'swift':
const cases = variant.variants
.map((t) => {
const caseName = getSwiftVariantCaseName(t)
const wrapping = new SwiftCxxBridgedType(t)
const parse = wrapping.parseFromSwiftToCpp('value', 'swift')
return `case .${caseName}(let value):\n return bridge.${bridge.funcName}(${parse})`
})
.join('\n')
return `
{ () -> bridge.${bridge.specializationName} in
switch ${swiftParameterName} {
Expand Down
23 changes: 17 additions & 6 deletions packages/nitrogen/src/syntax/swift/SwiftCxxTypeHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,17 +275,17 @@ function createCxxVariantSwiftHelper(type: VariantType): SwiftCxxHelper {
? toReferenceType(t.getCode('c++'))
: t.getCode('c++')
return `
inline ${actualType} create_${name}(${param} value) {
return value;
inline ${name} create_${name}(${param} value) {
return ${name}(value);
}
`.trim()
})
.join('\n')
const getFunctions = type.variants
.map((t, i) => {
return `
inline ${t.getCode('c++')} get_${name}_${i}(const ${actualType}& variant) {
return std::get<${i}>(variant);
inline ${t.getCode('c++')} get_${name}_${i}(const ${name}& variantWrapper) {
return std::get<${i}>(variantWrapper.variant);
}
`.trim()
})
Expand All @@ -304,9 +304,20 @@ inline ${t.getCode('c++')} get_${name}_${i}(const ${actualType}& variant) {
],
cxxCode: `
/**
* Specialized version of \`${escapeComments(actualType)}\`.
* Wrapper struct for \`${escapeComments(actualType)}\`.
* std::variant cannot be used in Swift because of a Swift bug.
* Not even specializing it works. So we create a wrapper struct.
*/
using ${name} = ${actualType};
struct ${name} {
${actualType} variant;
${name}(${actualType} variant): variant(variant) { }
operator ${actualType}() const {
return variant;
}
inline size_t index() const {
return variant.index();
}
};
${createFunctions}
${getFunctions}
`.trim(),
Expand Down
2 changes: 2 additions & 0 deletions packages/nitrogen/src/syntax/types/VariantType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export class VariantType implements Type {
return `std::variant<${types.join(', ')}>`
case 'swift':
return `Variant_${types.join('_')}`
case 'kotlin':
return `Variant_${types.join('_')}`
default:
throw new Error(
`Language ${language} is not yet supported for VariantType!`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ import com.margelo.nitro.core.AnyValue
import com.margelo.nitro.core.ArrayBuffer
import com.margelo.nitro.core.Promise
import kotlinx.coroutines.delay
import java.nio.ByteBuffer
import kotlin.concurrent.thread


class HybridTestObjectKotlin: HybridTestObjectSwiftKotlinSpec() {
override var numberValue: Double = 0.0
Expand All @@ -20,6 +17,7 @@ class HybridTestObjectKotlin: HybridTestObjectSwiftKotlinSpec() {
override var optionalString: String? = null
override val thisObject: HybridTestObjectSwiftKotlinSpec
get() = this
override var someVariantFirst: Variant_String_Double = Variant_String_Double.create(55.05)

override fun simpleFunc() {
// do nothing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import Foundation
import NitroModules

class HybridTestObjectSwift : HybridTestObjectSwiftKotlinSpec {
var someVariantFirst: Variant_String_Double = .someDouble(55)

var numberValue: Double = 0.0

var boolValue: Bool = false
Expand Down
Loading
Loading