Skip to content
Open
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
5 changes: 5 additions & 0 deletions docs/docs/types/arrays.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,8 @@ into `Object`s, and provides bulk copy methods. This makes all array operations
- `Array<Double>` -> [`DoubleArray`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double-array/)
- `Array<Boolean>` -> [`BooleanArray`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean-array/)
- `Array<Long>` -> [`LongArray`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-long-array/)
- `Array<ULong>` -> [`ULongArray`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-u-long-array/)

`ULongArray` shares its `LongArray` with JNI, so `UInt64[]` crosses without copying. Kotlin still marks `ULongArray` as experimental, so
Nitrogen's generated `+autolinking.gradle` opts your module in to `kotlin.ExperimentalUnsignedTypes`. Generated code and your
implementations can use it without warnings. Other modules that use these APIs, such as your app, need their own opt-in.
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,3 @@ package com.margelo.nitro.utils
/** Maps object array elements without an intermediate List. */
inline fun <Element, reified Result> Array<Element>.mapToArray(transform: (Element) -> Result): Array<Result> =
Array(size) { transform(this[it]) }

/** Maps primitive LongArray elements to an object array without an intermediate List. */
inline fun <reified Result> LongArray.mapToArray(transform: (Long) -> Result): Array<Result> = Array(size) { transform(this[it]) }

/** Maps object array elements to a primitive LongArray in one allocation. */
inline fun <Element> Array<Element>.mapToLongArray(transform: (Element) -> Long): LongArray = LongArray(size) { transform(this[it]) }
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ ${createFileMetadataString(name)}
// Resolve beside this script so custom Nitrogen output directories work too.
def nitrogenDirectory = buildscript.sourceFile.parentFile
android.sourceSets.main.java.srcDirs += [new File(nitrogenDirectory, 'kotlin')]

// UInt64 arrays are Kotlin ULongArrays, which still require an opt-in.
plugins.withId('org.jetbrains.kotlin.android') {
kotlin.sourceSets.configureEach {
languageSettings.optIn('kotlin.ExperimentalUnsignedTypes')
}
}
`.trim()
return {
language: 'gradle',
Expand Down
14 changes: 12 additions & 2 deletions packages/nitrogen/src/syntax/kotlin/KotlinStruct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { capitalizeName, indent } from '../../utils.js'
import { createFileMetadataString } from '../helpers.js'
import type { SourceFile } from '../SourceFile.js'
import type { KotlinStructType } from './types/KotlinStructType.js'
import type { KotlinNamedType } from './types/KotlinNamedType.js'
import { getImportsCode } from '../getImportCode.js'
import { convertJniValue, getJniCode } from './types/KotlinType.js'

Expand Down Expand Up @@ -50,8 +51,17 @@ private fun get${name}_cxx(): ${type} {
})
.join('\n\n')

const equalityComparators = structType.properties.map((p) => `Objects.deepEquals(this.${p.escapedName}, other.${p.escapedName})`)
const propertiesList = structType.properties.map((p) => p.escapedName).join(',\n')
// Values whose boxes compare by identity, such as `ULongArray`, compare by their JNI representation.
const getEqualityValue = (property: KotlinNamedType, value: string): string => {
if (property.isKotlinEqualityCompatible) return value
return property.convert(value, { from: 'kotlin', to: 'c++', in: 'kotlin' })
}
const equalityComparators = structType.properties.map((property) => {
const value = getEqualityValue(property, `this.${property.escapedName}`)
const otherValue = getEqualityValue(property, `other.${property.escapedName}`)
return `Objects.deepEquals(${value}, ${otherValue})`
})
const propertiesList = structType.properties.map((property) => getEqualityValue(property, property.escapedName)).join(',\n')

const code = `
${createFileMetadataString(`${structType.structName}.kt`)}
Expand Down
38 changes: 21 additions & 17 deletions packages/nitrogen/src/syntax/kotlin/types/KotlinArrayType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,32 @@ export class KotlinArrayType extends ArrayType implements KotlinType {
get needsKotlinConversion(): boolean {
return this.itemType.needsKotlinConversion
}
/** `ULongArray` is a value class, so its field holds the `long[]` JNI uses. */
get isKotlinFieldCompatible(): boolean {
return this.isULongArray || !this.needsKotlinConversion
}
/** `ULongArray` boxes compare by identity, so compare their `long[]`s instead. */
get isKotlinEqualityCompatible(): boolean {
return !this.isULongArray && (this.itemType.isKotlinEqualityCompatible ?? true)
}
private get isULongArray(): boolean {
return this.itemType.kind === 'uint64'
}
convert(value: string, conversion: KotlinConversion): string {
switch (conversion.in) {
case 'kotlin': {
if (!this.needsKotlinConversion) return value
if (this.isULongArray) {
// Both reinterpret the same array without copying it.
return conversion.from === 'kotlin' ? `${value}.asLongArray()` : `${value}.asULongArray()`
}
const item = this.itemType.convert('__item', conversion)
const sourceType = conversion.from === 'c++' ? this.itemType : this.type.itemType
const resultType = conversion.from === 'c++' ? this.type.itemType : this.itemType
const resultCode = resultType.getCode('kotlin', {
context: 'type-argument',
})
let conversionFunction: string
if (this.itemType.kind === 'uint64') {
conversionFunction = conversion.from === 'kotlin' ? 'mapToLongArray' : `mapToArray<${resultCode}>`
} else {
const sourceType = conversion.from === 'c++' ? this.itemType : this.type.itemType
const sourceCode = sourceType.getCode('kotlin', { context: 'type-argument' })
conversionFunction = `mapToArray<${sourceCode}, ${resultCode}>`
}
const sourceCode = sourceType.getCode('kotlin', { context: 'type-argument' })
const resultCode = resultType.getCode('kotlin', { context: 'type-argument' })
return `
${value}.${conversionFunction} { __item -> ${item} }
${value}.mapToArray<${sourceCode}, ${resultCode}> { __item -> ${item} }
`.trim()
}
case 'c++': {
Expand Down Expand Up @@ -130,11 +137,8 @@ ${value}.${conversionFunction} { __item -> ${item} }
break
}
case 'kotlin':
if (this.needsKotlinConversion) {
imports.push(
{ language: language, name: 'com.margelo.nitro.utils.mapToArray' },
{ language: language, name: 'com.margelo.nitro.utils.mapToLongArray' }
)
if (this.needsKotlinConversion && !this.isULongArray) {
imports.push({ language: language, name: 'com.margelo.nitro.utils.mapToArray' })
}
imports.push(...this.type.getRequiredImports(language))
break
Expand Down
3 changes: 3 additions & 0 deletions packages/nitrogen/src/syntax/kotlin/types/KotlinNamedType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export class KotlinNamedType extends NamedWrappingType<KotlinType> implements Ko
get isKotlinFieldCompatible(): boolean {
return this.type.isKotlinFieldCompatible ?? !this.type.needsKotlinConversion
}
get isKotlinEqualityCompatible(): boolean {
return this.type.isKotlinEqualityCompatible ?? true
}

get needsKotlinConversion(): boolean {
return this.type.needsKotlinConversion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export class KotlinOptionalType extends OptionalType implements KotlinType {
return this.wrappingType.jniValueKind
}

get isKotlinEqualityCompatible(): boolean {
return this.wrappingType.isKotlinEqualityCompatible ?? true
}

getCppConverter(cppType: Type): KotlinCppConverter {
return new KotlinCxxOptionalType(getTypeAs(cppType, OptionalType), this).getConverter()
}
Expand Down
5 changes: 5 additions & 0 deletions packages/nitrogen/src/syntax/kotlin/types/KotlinType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ export interface KotlinType extends Type {
readonly hasStaticJNIReference?: boolean
/** Whether a generated Kotlin backing field already has this JNI representation. */
readonly isKotlinFieldCompatible?: boolean
/**
* Whether `Objects.deepEquals` and `contentDeepHashCode` compare the public Kotlin value
* by its contents. Otherwise, generated structs compare its JNI representation.
*/
readonly isKotlinEqualityCompatible?: boolean
/** Overrides a native value's printer only when its JVM signature differs. */
getJniCode?(options: GetKotlinCodeOptions): string
/** Adapts a native scalar at the JNI boundary when its JVM ABI differs. */
Expand Down
2 changes: 1 addition & 1 deletion packages/nitrogen/src/syntax/types/ArrayType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class ArrayType implements Type {
case 'int64':
return 'LongArray'
case 'uint64':
return 'Array<ULong>'
return 'ULongArray'
default:
return `Array<${itemCode}>`
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,31 @@ class HybridTestObjectKotlin : HybridTestObjectSwiftKotlinSpec() {
return callback.callSync(value)
}

override fun bounceUnsignedArray(array: ULongArray): ULongArray {
return array
}

override fun bounceNestedUnsignedArray(array: Array<ULongArray>): Array<ULongArray> {
return array
}

override fun bounceOptionalUnsignedArray(array: ULongArray?): ULongArray? {
return array
}

override fun bounceUnsignedArrayAsync(array: ULongArray): Promise<ULongArray> {
return Promise.async {
return@async array
}
}

override fun callUnsignedArrayCallback(
array: ULongArray,
callback: Callback<(array: ULongArray) -> ULongArray>,
): ULongArray {
return callback.callSync(array)
}

override fun bounceHybridObjects(array: Array<HybridChildSpec>): Array<HybridChildSpec> {
return array
}
Expand Down
24 changes: 24 additions & 0 deletions tests/standalone/nitro-test/apple/HybridTestObjectSwift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,30 @@ class HybridTestObjectSwift: HybridTestObjectSwiftKotlinSpec {
return try callback.callSync(value)
}

func bounceUnsignedArray(array: [UInt64]) throws -> [UInt64] {
return array
}

func bounceNestedUnsignedArray(array: [[UInt64]]) throws -> [[UInt64]] {
return array
}

func bounceOptionalUnsignedArray(array: [UInt64]?) throws -> [UInt64]? {
return array
}

func bounceUnsignedArrayAsync(array: [UInt64]) throws -> any Promise<[UInt64]> {
return .async {
return array
}
}

func callUnsignedArrayCallback(
array: [UInt64], callback: any Callback<[UInt64], [UInt64]>
) throws -> [UInt64] {
return try callback.callSync(array)
}

func bounceHybridObjects(array: [(any HybridChildSpec)]) throws -> [(any HybridChildSpec)] {
return array
}
Expand Down
22 changes: 22 additions & 0 deletions tests/standalone/nitro-test/cpp/HybridTestObjectCpp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,28 @@ std::vector<std::vector<std::optional<Person>>> HybridTestObjectCpp::callNestedS
return callback.callSync(value);
}

std::vector<uint64_t> HybridTestObjectCpp::bounceUnsignedArray(const std::vector<uint64_t>& array) {
return array;
}

std::vector<std::vector<uint64_t>> HybridTestObjectCpp::bounceNestedUnsignedArray(const std::vector<std::vector<uint64_t>>& array) {
return array;
}

std::optional<std::vector<uint64_t>> HybridTestObjectCpp::bounceOptionalUnsignedArray(const std::optional<std::vector<uint64_t>>& array) {
return array;
}

Promise<std::vector<uint64_t>> HybridTestObjectCpp::bounceUnsignedArrayAsync(const std::vector<uint64_t>& array) {
return Promise<std::vector<uint64_t>>::async([=]() { return array; });
}

std::vector<uint64_t>
HybridTestObjectCpp::callUnsignedArrayCallback(const std::vector<uint64_t>& array,
const Callback<std::vector<uint64_t>(const std::vector<uint64_t>&)>& callback) {
return callback.callSync(array);
}

std::vector<HybridChildSpecCxx> HybridTestObjectCpp::bounceHybridObjects(const std::vector<HybridChildSpecCxx>& array) {
return array;
}
Expand Down
6 changes: 6 additions & 0 deletions tests/standalone/nitro-test/cpp/HybridTestObjectCpp.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,12 @@ class HybridTestObjectCpp : public HybridTestObjectCppSpec {
const std::vector<std::vector<std::optional<Person>>>& value,
const Callback<std::vector<std::vector<std::optional<Person>>>(const std::vector<std::vector<std::optional<Person>>>&)>& callback)
override;
std::vector<uint64_t> bounceUnsignedArray(const std::vector<uint64_t>& array) override;
std::vector<std::vector<uint64_t>> bounceNestedUnsignedArray(const std::vector<std::vector<uint64_t>>& array) override;
std::optional<std::vector<uint64_t>> bounceOptionalUnsignedArray(const std::optional<std::vector<uint64_t>>& array) override;
Promise<std::vector<uint64_t>> bounceUnsignedArrayAsync(const std::vector<uint64_t>& array) override;
std::vector<uint64_t> callUnsignedArrayCallback(const std::vector<uint64_t>& array,
const Callback<std::vector<uint64_t>(const std::vector<uint64_t>&)>& callback) override;

std::vector<HybridChildSpecCxx> bounceHybridObjects(const std::vector<HybridChildSpecCxx>& array) override;
std::vector<Callback<void()>> bounceFunctions(const std::vector<Callback<void()>>& functions) override;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading