From b7856f8e273bfab36ddd6b26fb7c8b4520e97f40 Mon Sep 17 00:00:00 2001 From: Paulo Pinheiro Date: Fri, 26 May 2023 20:00:33 +0200 Subject: [PATCH] Add Kotlin multiplatform support (#7969) * [Kotlin] Introduction to Kotlin Multiplaform The first implementation of the Kotlin code generation was made years ago at the time Kotlin Multiplaform was not stable and Kotlin is mostly used on JVM-based targets. For this reason the generated code uses java based runtime. That design decision comes with many drawbacks, leaving the code generated more java-like and making it impossible to use more advanced features of the Kotlin language. In this change we are adding two parts: A pure, multi-plaform, Kotlin runtime and a new code generator to accompany it. * [Kotlin] Remove scalar sign cast from code generation Now that we have a new runtime the accepts unsigned types, we don't need to code generate casting back and from signed scalars. This MR removes this from both code generations and adds the necessary API to the runtime. * [Kotlin] Use offset on public API to represent buffer position Currently, kotlin was following Java's approach of representing objects, vectors, tables as "Int" (the position of it in the buffer). This change replaces naked Int with Offset, offering a type-safe API. So, instead of fun Table.createTable(b: FlatBufferBuilder, subTable: Int) We will have fun Table.createTable(b: FlatBufferBuilder, subTable: Offset) Making impossible to accidentally switch parameters. The performance should be similar to use Int as we are using value class for Offset and ArrayOffset, which most of the time translate to Int in the bytecode. * [Kotlin] Add builder for tables Add builder constructor to make create of table more ergonomic. For example the movie sample for the test set could be written as: Movie.createMovie(fbb, mainCharacterType = Character_.MuLan, mainCharacter = att) { charactersType = charsType this.characters = characters } instead of: Movie.startMovie(fbb) Movie.addMainCharacterType(fbb, Character_.MuLan) Movie.addMainCharacter(fbb, att as Offset) Movie.addCharactersType(fbb, charsType) Movie.addCharacters(fbb, charsVec) Movie.endMovie(fbb) * [Kotlin] Move enum types to value class Moving to flatbuffer enums to value class adds type safety for parameters with minimum to no performance impact. * [Kotlin] Simplify Union parameters to avoid naked casting Just a small change on the APIs that receive union as parameters, creating a typealias UnionOffset to avoid using Offset. To "convert" an table offset to an union, one just call Offset.toUnion(). * [Kotlin] Apply clang-format on kotlin code generators * [Kotlin] Update kotlin generator to follow official naming conventions Updating directory, package and enum naming to follow Kotlin official convention. https://kotlinlang.org/docs/coding-conventions.html#naming-rules * [Kotlin] Add fixes to improve performance 1 - Add benchmark comparing serialization between Java & Kotlin 2 - ReadWriteBuffer does not auto-grow (thus avoid check size in every op) 3 - Add specialized add functions on FlatBufferBuilder to avoid boxing offsets. 4 - Remove a few Kotlin syntax sugar that generated performance penalties. * [Kotlin] Remove builder from Kotlin KMP and add some optimizations to avoid boxing of Offset classes --------- Co-authored-by: Derek Bailey --- .github/labeler.yml | 1 + .github/workflows/build.yml | 12 +- .gitignore | 2 + CMakeLists.txt | 1 + android/.project | 2 +- include/flatbuffers/idl.h | 6 +- kotlin/benchmark/build.gradle.kts | 89 +- kotlin/benchmark/monster_test_java.fbs | 37 + kotlin/benchmark/monster_test_kotlin.fbs | 37 + kotlin/benchmark/src/jvmMain/java | 1 - .../kotlin/benchmark/FlatbufferBenchmark.kt | 68 + .../kotlin/benchmark/FlexBuffersBenchmark.kt | 2 + .../kotlin/benchmark/JsonBenchmark.kt | 7 +- .../kotlin/benchmark/UTF8Benchmark.kt | 14 +- kotlin/build.gradle.kts | 25 +- kotlin/flatbuffers-kotlin/build.gradle.kts | 124 +- .../com/google/flatbuffers/kotlin/Buffers.kt | 253 ++- .../google/flatbuffers/kotlin/ByteArray.kt | 36 +- .../flatbuffers/kotlin/FlatBufferBuilder.kt | 1105 +++++++++++ .../google/flatbuffers/kotlin/Flatbuffers.kt | 367 ++++ .../google/flatbuffers/kotlin/FlexBuffers.kt | 30 +- .../flatbuffers/kotlin/FlexBuffersBuilder.kt | 89 +- .../kotlin/FlexBuffersInternals.kt | 20 +- .../com/google/flatbuffers/kotlin/Utf8.kt | 62 +- .../flatbuffers/kotlin/{JSON.kt => json.kt} | 112 +- .../com/google/flatbuffers/kotlin/Asserts.kt | 55 + .../google/flatbuffers/kotlin/BuffersTest.kt | 78 + .../flatbuffers/kotlin/ByteArrayTest.kt | 53 +- .../kotlin/FlatBufferBuilderTest.kt | 575 ++++++ .../flatbuffers/kotlin/FlexBuffersTest.kt | 1 + .../google/flatbuffers/kotlin/ByteArray.kt | 1 + .../com/google/flatbuffers/kotlin/Utf8Test.kt | 9 +- .../google/flatbuffers/kotlin/ByteArray.kt | 1 + kotlin/gradle.properties | 12 +- kotlin/gradle/libs.versions.toml | 14 +- src/BUILD.bazel | 1 + src/flatc_main.cpp | 5 + src/idl_gen_kotlin.h | 2 + src/idl_gen_kotlin_kmp.cpp | 1623 +++++++++++++++++ src/idl_namer.h | 5 +- src/idl_parser.cpp | 9 +- src/namer.h | 8 +- tests/KotlinTest.sh | 2 +- 43 files changed, 4597 insertions(+), 359 deletions(-) create mode 100644 kotlin/benchmark/monster_test_java.fbs create mode 100644 kotlin/benchmark/monster_test_kotlin.fbs delete mode 120000 kotlin/benchmark/src/jvmMain/java create mode 100644 kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/FlatbufferBenchmark.kt create mode 100644 kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/FlatBufferBuilder.kt create mode 100644 kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Flatbuffers.kt rename kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/{JSON.kt => json.kt} (90%) create mode 100644 kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/Asserts.kt create mode 100644 kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/BuffersTest.kt create mode 100644 kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/FlatBufferBuilderTest.kt create mode 100644 src/idl_gen_kotlin_kmp.cpp diff --git a/.github/labeler.yml b/.github/labeler.yml index a3667ae56..e3da18c1e 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -48,6 +48,7 @@ java: kotlin: - '**/*.kt' - src/idl_gen_kotlin.cpp + - src/idl_gen_kotlin_kmp.cpp lua: - '**/*.lua' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dcbf6260b..dd59a2438 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -422,9 +422,14 @@ jobs: with: distribution: 'temurin' java-version: '11' + - name: Build flatc + run: | + cmake -DFLATBUFFERS_BUILD_TESTS=OFF -DFLATBUFFERS_BUILD_FLATLIB=OFF -DFLATBUFFERS_BUILD_FLATHASH=OFF . + make -j + echo "${PWD}" >> $GITHUB_PATH - name: Build working-directory: kotlin - run: ./gradlew clean iosX64Test macosX64Test + run: ./gradlew clean iosSimulatorArm64Test macosX64Test macosArm64Test build-kotlin-linux: name: Build Kotlin Linux @@ -437,6 +442,11 @@ jobs: distribution: 'temurin' java-version: '11' - uses: gradle/wrapper-validation-action@v1.0.5 + - name: Build flatc + run: | + cmake -DFLATBUFFERS_BUILD_TESTS=OFF -DFLATBUFFERS_BUILD_FLATLIB=OFF -DFLATBUFFERS_BUILD_FLATHASH=OFF . + make -j + echo "${PWD}" >> $GITHUB_PATH - name: Build working-directory: kotlin # we are using docker's version of gradle diff --git a/.gitignore b/.gitignore index 4d83964e5..828ca1d61 100644 --- a/.gitignore +++ b/.gitignore @@ -151,3 +151,5 @@ flatbuffers.pc # https://cmake.org/cmake/help/latest/module/FetchContent.html#variable:FETCHCONTENT_BASE_DIR cmake-build-debug/ _deps/ +**/.gradle/** +kotlin/**/generated diff --git a/CMakeLists.txt b/CMakeLists.txt index a895a340e..15db900bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -160,6 +160,7 @@ set(FlatBuffers_Compiler_SRCS src/idl_gen_csharp.cpp src/idl_gen_dart.cpp src/idl_gen_kotlin.cpp + src/idl_gen_kotlin_kmp.cpp src/idl_gen_go.cpp src/idl_gen_java.cpp src/idl_gen_ts.cpp diff --git a/android/.project b/android/.project index 3ed7298f8..17f0659d4 100644 --- a/android/.project +++ b/android/.project @@ -10,7 +10,7 @@ - 1677235311958 + 1672434305228 30 diff --git a/include/flatbuffers/idl.h b/include/flatbuffers/idl.h index ad45d3115..542169449 100644 --- a/include/flatbuffers/idl.h +++ b/include/flatbuffers/idl.h @@ -342,7 +342,10 @@ struct FieldDef : public Definition { bool Deserialize(Parser &parser, const reflection::Field *field); bool IsScalarOptional() const { - return IsScalar(value.type.base_type) && IsOptional(); + return IsScalar() && IsOptional(); + } + bool IsScalar() const { + return ::flatbuffers::IsScalar(value.type.base_type); } bool IsOptional() const { return presence == kOptional; } bool IsRequired() const { return presence == kRequired; } @@ -725,6 +728,7 @@ struct IDLOptions { kSwift = 1 << 16, kNim = 1 << 17, kProto = 1 << 18, + kKotlinKmp = 1 << 19, kMAX }; diff --git a/kotlin/benchmark/build.gradle.kts b/kotlin/benchmark/build.gradle.kts index 976cb7bb8..4cddf5825 100644 --- a/kotlin/benchmark/build.gradle.kts +++ b/kotlin/benchmark/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.ir.backend.js.compile - plugins { kotlin("multiplatform") id("org.jetbrains.kotlinx.benchmark") @@ -27,7 +25,7 @@ benchmark { iterationTime = 300 iterationTimeUnit = "ms" // uncomment for benchmarking JSON op only - include(".*JsonBenchmark.*") + include(".*FlatbufferBenchmark.*") } } targets { @@ -36,24 +34,34 @@ benchmark { } kotlin { - jvm() + jvm { + compilations { + val main by getting { } + // custom benchmark compilation + val benchmarks by compilations.creating { + defaultSourceSet { + dependencies { + // Compile against the main compilation's compile classpath and outputs: + implementation(main.compileDependencyFiles + main.output.classesDirs) + } + } + } + } + } sourceSets { - - all { - languageSettings.enableLanguageFeature("InlineClasses") - } - val jvmMain by getting { dependencies { implementation(kotlin("stdlib-common")) implementation(project(":flatbuffers-kotlin")) implementation(libs.kotlinx.benchmark.runtime) - implementation("com.google.flatbuffers:flatbuffers-java:2.0.3") + implementation("com.google.flatbuffers:flatbuffers-java:23.5.9") // json serializers implementation(libs.moshi.kotlin) implementation(libs.gson) } + kotlin.srcDir("src/jvmMain/generated/kotlin/") + kotlin.srcDir("src/jvmMain/generated/java/") } } } @@ -67,3 +75,64 @@ tasks.register("downloadMultipleFi dest(File("${project.projectDir.absolutePath}/src/jvmMain/resources")) overwrite(false) } + +abstract class GenerateFBTestClasses : DefaultTask() { + @get:InputFiles + abstract val inputFiles: ConfigurableFileCollection + + @get:Input + abstract val includeFolder: Property + + @get:Input + abstract val outputFolder: Property + + @get:Input + abstract val variants: ListProperty + + @Inject + protected open fun getExecActionFactory(): org.gradle.process.internal.ExecActionFactory? { + throw UnsupportedOperationException() + } + + init { + includeFolder.set("") + } + + @TaskAction + fun compile() { + val execAction = getExecActionFactory()!!.newExecAction() + val sources = inputFiles.asPath.split(":") + val langs = variants.get().map { "--$it" } + val args = mutableListOf("flatc","-o", outputFolder.get(), *langs.toTypedArray()) + if (includeFolder.get().isNotEmpty()) { + args.add("-I") + args.add(includeFolder.get()) + } + args.addAll(sources) + println(args) + execAction.commandLine = args + print(execAction.execute()) + } +} + +// Use the default greeting +tasks.register("generateFBTestClassesKt") { + inputFiles.setFrom("$projectDir/monster_test_kotlin.fbs") + includeFolder.set("$rootDir/../tests/include_test") + outputFolder.set("${projectDir}/src/jvmMain/generated/kotlin/") + variants.addAll("kotlin-kmp") +} + +tasks.register("generateFBTestClassesJava") { + inputFiles.setFrom("$projectDir/monster_test_java.fbs") + includeFolder.set("$rootDir/../tests/include_test") + outputFolder.set("${projectDir}/src/jvmMain/generated/java/") + variants.addAll("kotlin") +} + +project.tasks.forEach { + if (it.name.contains("compileKotlin")) { + it.dependsOn("generateFBTestClassesKt") + it.dependsOn("generateFBTestClassesJava") + } +} diff --git a/kotlin/benchmark/monster_test_java.fbs b/kotlin/benchmark/monster_test_java.fbs new file mode 100644 index 000000000..700731007 --- /dev/null +++ b/kotlin/benchmark/monster_test_java.fbs @@ -0,0 +1,37 @@ +// Example IDL file for our monster's schema. + +namespace jmonster; + +enum JColor:byte { Red = 0, Green, Blue = 2 } + +union JEquipment { JWeapon } // Optionally add more tables. + +struct JVec3 { + x:float; + y:float; + z:float; +} + +table JMonster { + pos:JVec3; + mana:short = 150; + hp:short = 100; + name:string; + friendly:bool = false (deprecated); + inventory:[ubyte]; + color:JColor = Blue; + weapons:[JWeapon]; + equipped:JEquipment; + path:[JVec3]; +} + +table JWeapon { + name:string; + damage:short; +} + +table JAllMonsters { + monsters: [JMonster]; +} + +root_type JAllMonsters; diff --git a/kotlin/benchmark/monster_test_kotlin.fbs b/kotlin/benchmark/monster_test_kotlin.fbs new file mode 100644 index 000000000..2513fd3ec --- /dev/null +++ b/kotlin/benchmark/monster_test_kotlin.fbs @@ -0,0 +1,37 @@ +// Example IDL file for our monster's schema. + +namespace monster; + +enum Color:byte { Red = 0, Green, Blue = 2 } + +union Equipment { Weapon } // Optionally add more tables. + +struct Vec3 { + x:float; + y:float; + z:float; +} + +table Monster { + pos:Vec3; + mana:short = 150; + hp:short = 100; + name:string; + friendly:bool = false (deprecated); + inventory:[ubyte]; + color:Color = Blue; + weapons:[Weapon]; + equipped:Equipment; + path:[Vec3]; +} + +table Weapon { + name:string; + damage:short; +} + +table AllMonsters { + monsters: [Monster]; +} + +root_type AllMonsters; diff --git a/kotlin/benchmark/src/jvmMain/java b/kotlin/benchmark/src/jvmMain/java deleted file mode 120000 index fd62a87c5..000000000 --- a/kotlin/benchmark/src/jvmMain/java +++ /dev/null @@ -1 +0,0 @@ -../../../../java/src/main/java \ No newline at end of file diff --git a/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/FlatbufferBenchmark.kt b/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/FlatbufferBenchmark.kt new file mode 100644 index 000000000..5c37b95f1 --- /dev/null +++ b/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/FlatbufferBenchmark.kt @@ -0,0 +1,68 @@ +@file:OptIn(ExperimentalUnsignedTypes::class) + +package com.google.flatbuffers.kotlin.benchmark + + +import com.google.flatbuffers.kotlin.FlatBufferBuilder +import jmonster.JAllMonsters +import jmonster.JMonster +import jmonster.JVec3 +import monster.AllMonsters.Companion.createAllMonsters +import monster.AllMonsters.Companion.createMonstersVector +import monster.Monster +import monster.Monster.Companion.createInventoryVector +import monster.MonsterOffsetArray +import monster.Vec3 +import org.openjdk.jmh.annotations.* +import java.util.concurrent.TimeUnit + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Measurement(iterations = 20, time = 1, timeUnit = TimeUnit.NANOSECONDS) +open class FlatbufferBenchmark { + + val repetition = 1000000 + val fbKotlin = FlatBufferBuilder(1024 * repetition) + val fbJava = com.google.flatbuffers.FlatBufferBuilder(1024 * repetition) + + @OptIn(ExperimentalUnsignedTypes::class) + @Benchmark + fun monstersKotlin() { + fbKotlin.clear() + val monsterName = fbKotlin.createString("MonsterName"); + val items = ubyteArrayOf(0u, 1u, 2u, 3u, 4u) + val inv = createInventoryVector(fbKotlin, items) + val monsterOffsets: MonsterOffsetArray = MonsterOffsetArray(repetition) { + Monster.startMonster(fbKotlin) + Monster.addName(fbKotlin, monsterName) + Monster.addPos(fbKotlin, Vec3.createVec3(fbKotlin, 1.0f, 2.0f, 3.0f)) + Monster.addHp(fbKotlin, 80) + Monster.addMana(fbKotlin, 150) + Monster.addInventory(fbKotlin, inv) + Monster.endMonster(fbKotlin) + } + val monsters = createMonstersVector(fbKotlin, monsterOffsets) + val allMonsters = createAllMonsters(fbKotlin, monsters) + fbKotlin.finish(allMonsters) + } + + @Benchmark + fun monstersjava() { + fbJava.clear() + val monsterName = fbJava.createString("MonsterName"); + val inv = JMonster.createInventoryVector(fbJava, byteArrayOf(0, 1, 2, 3, 4).asUByteArray()) + val monsters = JAllMonsters.createMonstersVector(fbJava, IntArray(repetition) { + JMonster.startJMonster(fbJava) + JMonster.addName(fbJava, monsterName) + JMonster.addPos(fbJava, JVec3.createJVec3(fbJava, 1.0f, 2.0f, 3.0f)) + JMonster.addHp(fbJava, 80) + JMonster.addMana(fbJava, 150) + JMonster.addInventory(fbJava, inv) + JMonster.endJMonster(fbJava) + }) + val allMonsters = JAllMonsters.createJAllMonsters(fbJava, monsters) + fbJava.finish(allMonsters) + } + +} diff --git a/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/FlexBuffersBenchmark.kt b/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/FlexBuffersBenchmark.kt index 99088aa03..03788285d 100644 --- a/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/FlexBuffersBenchmark.kt +++ b/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/FlexBuffersBenchmark.kt @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(ExperimentalUnsignedTypes::class) + package com.google.flatbuffers.kotlin.benchmark import com.google.flatbuffers.ArrayReadWriteBuf import com.google.flatbuffers.FlexBuffers diff --git a/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/JsonBenchmark.kt b/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/JsonBenchmark.kt index ad7688e86..e39b29ff1 100644 --- a/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/JsonBenchmark.kt +++ b/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/JsonBenchmark.kt @@ -53,9 +53,10 @@ open class JsonBenchmark { val fbParser = JSONParser() - final val twitterData = this.javaClass.classLoader.getResourceAsStream("twitter.json")!!.readBytes() - final val canadaData = this.javaClass.classLoader.getResourceAsStream("canada.json")!!.readBytes() - final val citmData = this.javaClass.classLoader.getResourceAsStream("citm_catalog.json")!!.readBytes() + final val classLoader = this.javaClass.classLoader + final val twitterData = classLoader.getResourceAsStream("twitter.json")!!.readBytes() + final val canadaData = classLoader.getResourceAsStream("canada.json")!!.readBytes() + final val citmData = classLoader.getResourceAsStream("citm_catalog.json")!!.readBytes() val fbCitmRef = JSONParser().parse(ArrayReadBuffer(citmData)) val moshiCitmRef = moshi.adapter(Map::class.java).fromJson(citmData.decodeToString()) diff --git a/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/UTF8Benchmark.kt b/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/UTF8Benchmark.kt index 6fa2882e2..426253882 100644 --- a/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/UTF8Benchmark.kt +++ b/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/UTF8Benchmark.kt @@ -35,14 +35,14 @@ import kotlin.random.Random @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) @Measurement(iterations = 100, time = 1, timeUnit = TimeUnit.MICROSECONDS) -class UTF8Benchmark { +open class UTF8Benchmark { - private final val sampleSize = 5000 - private final val stringSize = 25 - final var sampleSmallUtf8 = (0..sampleSize).map { populateUTF8(stringSize) }.toList() - final var sampleSmallUtf8Decoded = sampleSmallUtf8.map { it.encodeToByteArray() }.toList() - final var sampleSmallAscii = (0..sampleSize).map { populateAscii(stringSize) }.toList() - final var sampleSmallAsciiDecoded = sampleSmallAscii.map { it.encodeToByteArray() }.toList() + private val sampleSize = 5000 + private val stringSize = 25 + private var sampleSmallUtf8 = (0..sampleSize).map { populateUTF8(stringSize) }.toList() + private var sampleSmallUtf8Decoded = sampleSmallUtf8.map { it.encodeToByteArray() }.toList() + private var sampleSmallAscii = (0..sampleSize).map { populateAscii(stringSize) }.toList() + private var sampleSmallAsciiDecoded = sampleSmallAscii.map { it.encodeToByteArray() }.toList() @Setup fun setUp() { diff --git a/kotlin/build.gradle.kts b/kotlin/build.gradle.kts index 8778c8663..0daf4e362 100644 --- a/kotlin/build.gradle.kts +++ b/kotlin/build.gradle.kts @@ -1,5 +1,7 @@ -group = "com.google.flatbuffers" -version = "2.0.0-SNAPSHOT" +import org.gradle.internal.impldep.org.testng.ITestResult.STARTED +import org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import java.nio.charset.StandardCharsets buildscript { repositories { @@ -21,3 +23,22 @@ allprojects { mavenCentral() } } + +tasks.withType>().configureEach { + kotlinOptions { + freeCompilerArgs += "-progressive" // https://kotlinlang.org/docs/whatsnew13.html#progressive-mode + } +} + +tasks.withType().configureEach { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + freeCompilerArgs += "-Xjvm-default=all" + } +} + +tasks.withType { + options.encoding = StandardCharsets.UTF_8.toString() + sourceCompatibility = JavaVersion.VERSION_1_8.toString() + targetCompatibility = JavaVersion.VERSION_1_8.toString() +} diff --git a/kotlin/flatbuffers-kotlin/build.gradle.kts b/kotlin/flatbuffers-kotlin/build.gradle.kts index f384320f2..d40865372 100644 --- a/kotlin/flatbuffers-kotlin/build.gradle.kts +++ b/kotlin/flatbuffers-kotlin/build.gradle.kts @@ -1,29 +1,37 @@ +import org.gradle.internal.impldep.org.fusesource.jansi.AnsiRenderer.test +import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework +import org.jetbrains.kotlin.cli.common.toBooleanLenient +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType +import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFrameworkConfig + plugins { kotlin("multiplatform") } + +val libName = "Flatbuffers" group = "com.google.flatbuffers.kotlin" version = "2.0.0-SNAPSHOT" kotlin { explicitApi() jvm() - js { + js(IR) { browser { - testTask { - useKarma { - useChromeHeadless() - } + testTask { + enabled = false } } binaries.executable() } macosX64() - iosArm32() + macosArm64() iosArm64() - iosX64() + iosSimulatorArm64() sourceSets { + val commonMain by getting { dependencies { implementation(kotlin("stdlib-common")) @@ -34,47 +42,33 @@ kotlin { dependencies { implementation(kotlin("test")) } + + kotlin.srcDir("src/commonTest/generated/kotlin/") } val jvmTest by getting { dependencies { implementation(kotlin("test-junit")) + implementation("com.google.flatbuffers:flatbuffers-java:2.0.3") } } val jvmMain by getting { - kotlin.srcDir("java") } - val jsMain by getting { - dependsOn(commonMain) - } - val jsTest by getting { - dependsOn(commonTest) - dependencies { - implementation(kotlin("test-js")) - } - } + val macosX64Main by getting + val macosArm64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val nativeMain by creating { - dependsOn(commonMain) - } - val nativeTest by creating { + // this sourceSet will hold common cold for all iOS targets dependsOn(commonMain) - } - val macosX64Main by getting { - dependsOn(nativeMain) - } - - val iosArm32Main by getting { - dependsOn(nativeMain) - } - val iosArm64Main by getting { - dependsOn(nativeMain) - } - val iosX64Main by getting { - dependsOn(nativeMain) + macosArm64Main.dependsOn(this) + macosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) } all { - languageSettings.enableLanguageFeature("InlineClasses") languageSettings.optIn("kotlin.ExperimentalUnsignedTypes") } } @@ -83,4 +77,66 @@ kotlin { // Fixes JS issue: https://youtrack.jetbrains.com/issue/KT-49109 rootProject.plugins.withType { rootProject.the().nodeVersion = "16.0.0" + +} + +// Use the default greeting +tasks.register("generateFBTestClassesKt") { + inputFiles.setFrom("$rootDir/../tests/monster_test.fbs", + "$rootDir/../tests/dictionary_lookup.fbs", +// @todo Seems like nesting code generation is broken for all generators. +// disabling test for now. +// "$rootDir/../tests/namespace_test/namespace_test1.fbs", +// "$rootDir/../tests/namespace_test/namespace_test2.fbs", + "$rootDir/../tests/union_vector/union_vector.fbs", + "$rootDir/../tests/optional_scalars.fbs") + includeFolder.set("$rootDir/../tests/include_test") + outputFolder.set("${projectDir}/src/commonTest/generated/kotlin/") + variant.set("kotlin-kmp") +} + + +project.tasks.forEach { + if (it.name.contains("compileKotlin")) + it.dependsOn("generateFBTestClassesKt") +} + +fun String.intProperty() = findProperty(this).toString().toInt() + +abstract class GenerateFBTestClasses : DefaultTask() { + @get:InputFiles + abstract val inputFiles: ConfigurableFileCollection + + @get:Input + abstract val includeFolder: Property + + @get:Input + abstract val outputFolder: Property + + @get:Input + abstract val variant: Property + + @Inject + protected open fun getExecActionFactory(): org.gradle.process.internal.ExecActionFactory? { + throw UnsupportedOperationException() + } + + init { + includeFolder.set("") + } + + @TaskAction + fun compile() { + val execAction = getExecActionFactory()!!.newExecAction() + val sources = inputFiles.asPath.split(":") + val args = mutableListOf("flatc","-o", outputFolder.get(), "--${variant.get()}") + if (includeFolder.get().isNotEmpty()) { + args.add("-I") + args.add(includeFolder.get()) + } + args.addAll(sources) + println(args) + execAction.commandLine = args + print(execAction.execute()) + } } diff --git a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Buffers.kt b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Buffers.kt index 9851d90db..e10037a0d 100644 --- a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Buffers.kt +++ b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Buffers.kt @@ -110,16 +110,23 @@ public interface ReadBuffer { public fun getDouble(index: Int): Double /** - * Read an UTF-8 string from the buffer. + * Read a UTF-8 string from the buffer. * @param start initial element of the string * @param size size of the string in bytes. * @return a `String` */ - public fun getString(start: Int, size: Int): String + public fun getString(start: Int = 0, size: Int = limit): String + + /** + * Read a ByteArray from the buffer. + * @param start position from the [ReadBuffer] to be read + * @param length maximum number of bytes to be written in the buffer + */ + public fun getBytes(array: ByteArray, start: Int, length: Int = array.size) /** * Expose [ReadBuffer] as an array of bytes. - * This method is meant to be as efficient as possible, so for a array-backed [ReadBuffer], it should + * This method is meant to be as efficient as possible, so for an array-backed [ReadBuffer], it should * return its own internal data. In case access to internal data is not possible, * a copy of the data into an array of bytes might occur. * @return [ReadBuffer] as an array of bytes @@ -151,6 +158,29 @@ public interface ReadWriteBuffer : ReadBuffer { */ public fun clear() + /** + * Request capacity of the buffer relative to [writePosition]. In case buffer is already larger + * than the requested, this method will just return true. Otherwise, + * It might try to resize the buffer. In case of being unable to allocate + * enough memory, an exception will be thrown. + * @param additional capacity in bytes to be added on top of [writePosition] + * @param copyAtEnd copy current data at the end of new underlying buffer + * @return new capacity in bytes + */ + public fun requestAdditionalCapacity(additional: Int, copyAtEnd: Boolean = false): Int = + requestCapacity(writePosition + additional, copyAtEnd) + + /** + * Request capacity of the buffer in absolute values. In case buffer is already larger + * than the requested the method is a no-op. Otherwise, + * It might try to resize the buffer. In case of being unable to allocate + * enough memory, an exception will be thrown. + * @param capacity new capacity + * @param copyAtEnd copy current data at the end of new underlying buffer + * @return new capacity in bytes + */ + public fun requestCapacity(capacity: Int, copyAtEnd: Boolean = false): Int + /** * Put a [Boolean] into the buffer at [writePosition] . Booleans as stored as single byte. * Write position will be incremented. @@ -164,7 +194,15 @@ public interface ReadWriteBuffer : ReadBuffer { * @param start initial position on value to be copied * @param length amount of bytes to be copied */ - public fun put(value: ByteArray, start: Int, length: Int) + public fun put(value: ByteArray, start: Int = 0, length: Int = value.size) + + /** + * Put an array of bytes into the buffer at [writePosition]. Write position will be incremented. + * @param value [ReadBuffer] the data to be copied + * @param start initial position on value to be copied + * @param length amount of bytes to be copied + */ + public fun put(value: ReadBuffer, start: Int = 0, length: Int = value.limit - start) /** * Write a [Byte] into the buffer at [writePosition]. Write position will be incremented. @@ -182,7 +220,7 @@ public interface ReadWriteBuffer : ReadBuffer { public fun put(value: Short) /** - * Writea [UShort] into in the buffer at [writePosition]. Write position will be incremented. + * Write a [UShort] into in the buffer at [writePosition]. Write position will be incremented. */ public fun put(value: UShort) @@ -224,7 +262,7 @@ public interface ReadWriteBuffer : ReadBuffer { * Write a [String] encoded as UTF-8 into the buffer at [writePosition]. Write position will be incremented. * @return size in bytes of the encoded string */ - public fun put(value: String, encodedLength: Int = -1): Int + public fun put(value: CharSequence, encodedLength: Int = -1): Int /** * Write an array of bytes into the buffer. @@ -233,7 +271,16 @@ public interface ReadWriteBuffer : ReadBuffer { * @param srcStart initial position on [src] that will be copied. * @param srcLength amount of bytes to be copied */ - public operator fun set(dstIndex: Int, src: ByteArray, srcStart: Int, srcLength: Int) + public fun set(dstIndex: Int, src: ByteArray, srcStart: Int = 0, srcLength: Int = src.size) + + /** + * Write an array of bytes into the buffer. + * @param dstIndex initial position where [src] will be copied into. + * @param src the data to be copied. + * @param srcStart initial position on [src] that will be copied. + * @param srcLength amount of bytes to be copied + */ + public operator fun set(dstIndex: Int, src: ReadBuffer, srcStart: Int = 0, srcLength: Int) /** * Write [Boolean] into a given position [index] on the buffer. Booleans as stored as single byte. @@ -301,63 +348,95 @@ public interface ReadWriteBuffer : ReadBuffer { */ public fun set(index: Int, value: Double) + public fun fill(value: Byte, start: Int, end: Int) + /** * Current position of the buffer to be written. It will be automatically updated on [put] operations. */ public var writePosition: Int /** - * Defines the size of the message in the buffer. It also determines last position that buffer - * can be read or write. Last byte to be accessed is in position `limit() -1`. - * @return indicate last position + * Creates a new [ReadWriteBuffer] point to a region of the current buffer, starting at [offset] with size [size]. + * @param offset starting position of the [ReadWriteBuffer] + * @param size in bytes of the [ReadWriteBuffer] + * @return [ReadWriteBuffer] slice. */ - override val limit: Int + public fun writeSlice(offset: Int, size: Int): ReadWriteBuffer /** - * Request capacity of the buffer. In case buffer is already larger - * than the requested, this method will just return true. Otherwise - * It might try to resize the buffer. In case of being unable to allocate - * enough memory, an exception will be thrown. + * Special operation where we increase the backed buffer size to [capacity] + * and shift all already written data to the end of the buffer. + * + * This function is mostly used when creating a Flatbuffer message, as + * data is written from the end of the buffer towards index 0. + * @param capacity required in bytes + * @return new capacity in bytes */ - public fun requestCapacity(capacity: Int) + public fun moveWrittenDataToEnd(capacity: Int): Int + + /** + * Maximum size in bytes that the backed buffer supports. + */ + public val capacity: Int + + /** + * Defines last relative position of the backed buffer that can be written. + * Any addition to the buffer that goes beyond will throw an exception + * instead of regrow the buffer (default behavior). + */ + public val writeLimit: Int } -public open class ArrayReadBuffer(protected var buffer: ByteArray, override val limit: Int = buffer.size) : ReadBuffer { +public open class ArrayReadBuffer(protected var buffer: ByteArray, + // offsets writePosition against backed buffer e.g. offset = 1, writePosition = 1 + // will write first byte at position 2 of the backed buffer + internal val offset: Int = 0, + override val limit: Int = buffer.size - offset) : ReadBuffer { + override fun findFirst(value: Byte, start: Int, end: Int): Int { val e = min(end, limit) - val s = max(0, start) + val s = max(0, this.offset + start) for (i in s until e) if (buffer[i] == value) return i return -1 } - override fun getBoolean(index: Int): Boolean = buffer[index] != 0.toByte() + override fun getBoolean(index: Int): Boolean = buffer[offset + index] != 0.toByte() - override operator fun get(index: Int): Byte = buffer[index] + override operator fun get(index: Int): Byte = buffer[offset + index] - override fun getUByte(index: Int): UByte = buffer.getUByte(index) + override fun getUByte(index: Int): UByte = buffer.getUByte(offset + index) - override fun getShort(index: Int): Short = buffer.getShort(index) + override fun getShort(index: Int): Short = buffer.getShort(offset + index) - override fun getUShort(index: Int): UShort = buffer.getUShort(index) + override fun getUShort(index: Int): UShort = buffer.getUShort(offset + index) - override fun getInt(index: Int): Int = buffer.getInt(index) + override fun getInt(index: Int): Int = buffer.getInt(offset + index) - override fun getUInt(index: Int): UInt = buffer.getUInt(index) + override fun getUInt(index: Int): UInt = buffer.getUInt(offset + index) - override fun getLong(index: Int): Long = buffer.getLong(index) + override fun getLong(index: Int): Long = buffer.getLong(offset + index) - override fun getULong(index: Int): ULong = buffer.getULong(index) + override fun getULong(index: Int): ULong = buffer.getULong(offset + index) - override fun getFloat(index: Int): Float = buffer.getFloat(index) + override fun getFloat(index: Int): Float = buffer.getFloat(offset + index) - override fun getDouble(index: Int): Double = buffer.getDouble(index) + override fun getDouble(index: Int): Double = buffer.getDouble(offset + index) - override fun getString(start: Int, size: Int): String = buffer.decodeToString(start, start + size) + override fun getString(start: Int, size: Int): String = buffer.decodeToString(this.offset + start, + this.offset + start + size) + + override fun getBytes(array: ByteArray, start: Int, length: Int) { + val end = min(this.offset + start + length, buffer.size) + var j = 0 + for (i in this.offset + start until end) { + array[j++] = buffer[i] + } + } override fun data(): ByteArray = buffer - override fun slice(start: Int, size: Int): ReadBuffer = ArrayReadBuffer(buffer, limit) + override fun slice(start: Int, size: Int): ReadBuffer = ArrayReadBuffer(buffer, this.offset + start, size) } /** * Implements `[ReadWriteBuffer]` using [ByteArray] as backing buffer. Using array of bytes are @@ -365,14 +444,20 @@ public open class ArrayReadBuffer(protected var buffer: ByteArray, override val * * This class is not thread-safe, meaning that * it must operate on a single thread. Operating from - * multiple thread leads into a undefined behavior + * multiple thread leads into an undefined behavior * - * All operations assumes Little Endian byte order. + * All operations assume Little Endian byte order. */ + public class ArrayReadWriteBuffer( buffer: ByteArray, - override var writePosition: Int = 0 -) : ArrayReadBuffer(buffer, writePosition), ReadWriteBuffer { + offset: Int = 0, + // Defines last position of the backed buffer that can be written. + // Any addition to the buffer that goes beyond will throw an exception + // instead of regrow the buffer (default behavior). + public override val writeLimit: Int = -1, + override var writePosition: Int = offset +) : ArrayReadBuffer(buffer, offset, writePosition), ReadWriteBuffer { public constructor(initialCapacity: Int = 10) : this(ByteArray(initialCapacity)) @@ -390,6 +475,11 @@ public class ArrayReadWriteBuffer( writePosition += length } + override fun put(value: ReadBuffer, start: Int, length: Int) { + set(writePosition, value, start, length) + writePosition += length + } + override fun put(value: Byte) { set(writePosition, value) writePosition++ @@ -440,50 +530,87 @@ public class ArrayReadWriteBuffer( writePosition += 8 } - override fun put(value: String, encodedLength: Int): Int { + override fun put(value: CharSequence, encodedLength: Int): Int { val length = if (encodedLength != -1) encodedLength else Utf8.encodedLength(value) - withCapacity(writePosition + length) { - writePosition = setString(writePosition, value) - } + writePosition = buffer.setCharSequence(writePosition, value) return length } override fun set(index: Int, value: Boolean) { - set(index, if (value) 1.toByte() else 0.toByte()) + buffer[index] = if (value) 1.toByte() else 0.toByte() } - override operator fun set(dstIndex: Int, src: ByteArray, srcStart: Int, srcLength: Int) { - withCapacity(dstIndex + (srcLength + srcStart)) { - src.copyInto(buffer, dstIndex, srcStart, srcStart + srcLength) - } + override fun set(dstIndex: Int, src: ByteArray, srcStart: Int, srcLength: Int) { + src.copyInto(buffer, dstIndex, srcStart, srcStart + srcLength) } - override operator fun set(index: Int, value: Byte): Unit = withCapacity(index + 1) { set(index, value) } - override operator fun set(index: Int, value: UByte): Unit = withCapacity(index + 1) { setUByte(index, value) } - override operator fun set(index: Int, value: Short): Unit = withCapacity(index + 2) { setShort(index, value) } - override operator fun set(index: Int, value: UShort): Unit = withCapacity(index + 2) { setUShort(index, value) } - override operator fun set(index: Int, value: Int): Unit = withCapacity(index + 4) { setInt(index, value) } - override operator fun set(index: Int, value: UInt): Unit = withCapacity(index + 4) { setUInt(index, value) } - override operator fun set(index: Int, value: Long): Unit = withCapacity(index + 8) { setLong(index, value) } - override operator fun set(index: Int, value: ULong): Unit = withCapacity(index + 8) { setULong(index, value) } - override operator fun set(index: Int, value: Float): Unit = withCapacity(index + 4) { setFloat(index, value) } - override operator fun set(index: Int, value: Double): Unit = withCapacity(index + 8) { setDouble(index, value) } + override operator fun set(dstIndex: Int, src: ReadBuffer, srcStart: Int, srcLength: Int) { + when(src) { + is ArrayReadBuffer -> { + src.data().copyInto(buffer, dstIndex, src.offset + srcStart, src.offset + srcStart + srcLength) + } + else -> { + for (i in 0 until srcLength) { + buffer[dstIndex + i] = src[srcStart + i] + } + } + } + } - override fun requestCapacity(capacity: Int) { + override operator fun set(index: Int, value: Byte) { buffer[index] = value } + override operator fun set(index: Int, value: UByte) { buffer.setUByte(index, value) } + override operator fun set(index: Int, value: Short) { buffer.setShort(index, value) } + override operator fun set(index: Int, value: UShort) { buffer.setUShort(index, value) } + override operator fun set(index: Int, value: Int) { buffer.setInt(index, value) } + override operator fun set(index: Int, value: UInt) { buffer.setUInt(index, value) } + override operator fun set(index: Int, value: Long) { buffer.setLong(index, value) } + override operator fun set(index: Int, value: ULong) { buffer.setULong(index, value) } + override operator fun set(index: Int, value: Float) { buffer.setFloat(index, value) } + override operator fun set(index: Int, value: Double) { buffer.setDouble(index, value) } + override fun fill(value: Byte, start: Int, end: Int) { buffer.fill(value, start, end) } + + /** + * Request capacity of the buffer. In case buffer is already larger + * than the requested, it is a no-op. Otherwise, + * It might try to resize the buffer. In case of being unable to allocate + * enough memory, an exception will be thrown. + * @param capacity new capacity + * @param copyAtEnd copy current data at the end of new underlying buffer + */ + override fun requestCapacity(capacity: Int, copyAtEnd: Boolean): Int { if (capacity < 0) error("Capacity may not be negative (likely a previous int overflow)") - if (buffer.size >= capacity) return + if (buffer.size >= capacity) return buffer.size + + if (writeLimit > 0 && writeLimit + offset >= buffer.size) error("Buffer in writeLimit mode. In writeLimit mode" + + " the buffer does not grow automatically and any write beyond writeLimit will throw exception. " + + "(writeLimit: $writeLimit, newCapacity: $capacity") // implemented in the same growing fashion as ArrayList val oldCapacity = buffer.size - var newCapacity = oldCapacity + (oldCapacity shr 1) - if (newCapacity < capacity) { // Note: this also catches newCapacity int overflow - newCapacity = capacity + if (oldCapacity == Int.MAX_VALUE - 8) { // Ensure we don't grow beyond what fits in an int. + error("FlatBuffers: cannot grow buffer beyond 2 gigabytes.") } - buffer = buffer.copyOf(newCapacity) + //(old_buf_size & 0xC0000000) != 0 ? MAX_BUFFER_SIZE : old_buf_size << 1; + var newCapacity = 8 + while (newCapacity < capacity) { // Note: this also catches newCapacity int overflow + newCapacity = if (newCapacity and -0x40000000 != 0) Int.MAX_VALUE - 8 else newCapacity shl 1 + } + val newBuffer = ByteArray(newCapacity) + + buffer.copyInto(newBuffer, if (copyAtEnd) newBuffer.size - buffer.size else 0) + buffer = newBuffer + return newCapacity } - private inline fun withCapacity(size: Int, crossinline action: ByteArray.() -> Unit) { - requestCapacity(size) - buffer.action() + override fun writeSlice(offset: Int, size: Int): ReadWriteBuffer { + return ArrayReadWriteBuffer(this.buffer, offset=offset, writeLimit=size) } + + override fun moveWrittenDataToEnd(capacity: Int): Int = requestCapacity(capacity, true) + + override val capacity: Int + get() = buffer.size + } + +public val emptyBuffer: ReadWriteBuffer = ArrayReadWriteBuffer(ByteArray(1)) diff --git a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/ByteArray.kt b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/ByteArray.kt index 68fd0f301..e851f5d2a 100644 --- a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/ByteArray.kt +++ b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/ByteArray.kt @@ -14,13 +14,14 @@ * limitations under the License. */ @file:Suppress("NOTHING_TO_INLINE") + package com.google.flatbuffers.kotlin import kotlin.experimental.and internal fun ByteArray.getString(index: Int, size: Int): String = Utf8.decodeUtf8Array(this, index, size) -internal fun ByteArray.setString(index: Int, value: String): Int = +internal fun ByteArray.setCharSequence(index: Int, value: CharSequence): Int = Utf8.encodeUtf8Array(value, this, index, this.size - index) // List of functions that needs to be implemented on all platforms. @@ -35,7 +36,7 @@ internal expect inline fun ByteArray.getFloat(index: Int): Float internal expect inline fun ByteArray.getDouble(index: Int): Double internal expect inline fun ByteArray.setUByte(index: Int, value: UByte) -internal expect inline fun ByteArray.setShort(index: Int, value: Short) +public expect inline fun ByteArray.setShort(index: Int, value: Short) internal expect inline fun ByteArray.setUShort(index: Int, value: UShort) internal expect inline fun ByteArray.setInt(index: Int, value: Int) internal expect inline fun ByteArray.setUInt(index: Int, value: UInt) @@ -102,43 +103,20 @@ public object ByteArrayOps { public inline fun setUInt(ary: ByteArray, index: Int, value: UInt): Unit = setInt(ary, index, value.toInt()) public inline fun setLong(ary: ByteArray, index: Int, value: Long) { - var idx = index var i = value.toInt() - ary[idx++] = (i and 0xff).toByte() - ary[idx++] = (i shr 8 and 0xff).toByte() - ary[idx++] = (i shr 16 and 0xff).toByte() - ary[idx++] = (i shr 24 and 0xff).toByte() + setInt(ary, index, i) i = (value shr 32).toInt() - ary[idx++] = (i and 0xff).toByte() - ary[idx++] = (i shr 8 and 0xff).toByte() - ary[idx++] = (i shr 16 and 0xff).toByte() - ary[idx] = (i shr 24 and 0xff).toByte() + setInt(ary, index + 4, i) } public inline fun setULong(ary: ByteArray, index: Int, value: ULong): Unit = setLong(ary, index, value.toLong()) public inline fun setFloat(ary: ByteArray, index: Int, value: Float) { - var idx = index - val iValue: Int = value.toRawBits() - ary[idx++] = (iValue and 0xff).toByte() - ary[idx++] = (iValue shr 8 and 0xff).toByte() - ary[idx++] = (iValue shr 16 and 0xff).toByte() - ary[idx] = (iValue shr 24 and 0xff).toByte() + setInt(ary, index, value.toRawBits()) } public inline fun setDouble(ary: ByteArray, index: Int, value: Double) { - var idx = index - val lValue: Long = value.toRawBits() - var i = lValue.toInt() - ary[idx++] = (i and 0xff).toByte() - ary[idx++] = (i shr 8 and 0xff).toByte() - ary[idx++] = (i shr 16 and 0xff).toByte() - ary[idx++] = (i shr 24 and 0xff).toByte() - i = (lValue shr 32).toInt() - ary[idx++] = (i and 0xff).toByte() - ary[idx++] = (i shr 8 and 0xff).toByte() - ary[idx++] = (i shr 16 and 0xff).toByte() - ary[idx] = (i shr 24 and 0xff).toByte() + setLong(ary, index, value.toRawBits()) } public inline fun getFloat(ary: ByteArray, index: Int): Float = Float.fromBits(getInt(ary, index)) diff --git a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/FlatBufferBuilder.kt b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/FlatBufferBuilder.kt new file mode 100644 index 000000000..28ab2ca98 --- /dev/null +++ b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/FlatBufferBuilder.kt @@ -0,0 +1,1105 @@ +/* + * Copyright 2021 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.flatbuffers.kotlin + +import kotlin.jvm.JvmOverloads + + +/** + * Class that helps you build a FlatBuffer. See the section + * "Use in Kotlin" in the main FlatBuffers documentation. + */ +public class FlatBufferBuilder @JvmOverloads constructor( + private val initialSize: Int = 1024, + private var buffer: ReadWriteBuffer = ArrayReadWriteBuffer(initialSize) +) { + // Remaining space in the ByteBuffer. + private var space: Int = buffer.capacity + + // Minimum alignment encountered so far. + private var minalign: Int = 1 + + // The vtable for the current table. + private var vtable: IntArray = IntArray(16) + + // The amount of fields we're actually using. + private var vtableInUse: Int = 0 + + // Whether we are currently serializing a table. + private var nested: Boolean = false + + // Whether the buffer is finished. + private var finished: Boolean = false + + // Starting offset of the current struct/table. + private var objectStart: Int = 0 + + // List of offsets of all vtables. + private var vtables = IntArray(16) + + // Number of entries in `vtables` in use. + private var numVtables = 0 + + // For the current vector being built. + private var vectorNumElems = 0 + + // False omits default values from the serialized data. + private var forceDefaults = false + + // map used to cache shared strings. + private var stringPool: MutableMap>? = null + + /** + * Reset the FlatBufferBuilder by purging all data that it holds. + */ + public fun clear() { + space = buffer.capacity + buffer.clear() + minalign = 1 + vtable.fill(0, 0, vtableInUse) + vtableInUse = 0 + nested = false + finished = false + objectStart = 0 + numVtables = 0 + vectorNumElems = 0 + stringPool?.clear() + } + + /** + * Offset relative to the end of the buffer. + * + * @return Offset relative to the end of the buffer. + */ + public fun offset(): Int = buffer.capacity - space + + /** + * Add zero valued bytes to prepare a new entry to be added. + * + * @param byteSize Number of bytes to add. + */ + public fun pad(byteSize: Int) { + for (i in 0 until byteSize) buffer[--space] = 0.toByte() + } + + /** + * Prepare to write an element of `size` after `additional_bytes` + * have been written, e.g. if you write a string, you need to align such + * the int length field is aligned to [com.google.flatbuffers.Int.SIZE_BYTES], and + * the string data follows it directly. If all you need to do is alignment, `additional_bytes` + * will be 0. + * + * @param size This is the of the new element to write. + * @param additionalBytes The padding size. + */ + public fun prep(size: Int, additionalBytes: Int) { + // Track the biggest thing we've ever aligned to. + if (size > minalign) minalign = size + // Find the amount of alignment needed such that `size` is properly + // aligned after `additional_bytes` + + val alignSize: Int = ((buffer.capacity - space + additionalBytes).inv() + 1).and(size - 1) + // Reallocate the buffer if needed. + while (space < alignSize + size + additionalBytes) { + val oldBufSize: Int = buffer.capacity + val newBufSize = buffer.moveWrittenDataToEnd(oldBufSize + alignSize + size + additionalBytes) + space += newBufSize - oldBufSize + } + if (alignSize > 0) { + pad(alignSize) + } + } + + /** + * Add a `boolean` to the buffer, backwards from the current location. Doesn't align nor + * check for space. + * + * @param x A `boolean` to put into the buffer. + */ + public fun put(x: Boolean) { + space -= Byte.SIZE_BYTES + buffer[space] = (if (x) 1 else 0).toByte() + } + + /** + * Add a [UByte] to the buffer, backwards from the current location. Doesn't align nor + * check for space. + * + * @param x A [UByte] to put into the buffer. + */ + public fun put(x: UByte): Unit = put(x.toByte()) + + /** + * Add a [Byte] to the buffer, backwards from the current location. Doesn't align nor + * check for space. + * + * @param x A [Byte] to put into the buffer. + */ + public fun put(x: Byte) { + space -= Byte.SIZE_BYTES + buffer[space] = x + } + + /** + * Add a [UShort] to the buffer, backwards from the current location. Doesn't align nor + * check for space. + * + * @param x A [UShort] to put into the buffer. + */ + public fun put(x: UShort): Unit = put(x.toShort()) + + /** + * Add a [Short] to the buffer, backwards from the current location. Doesn't align nor + * check for space. + * + * @param x A [Short] to put into the buffer. + */ + public fun put(x: Short) { + space -= Short.SIZE_BYTES + buffer.set(space, x) + } + + /** + * Add an [UInt] to the buffer, backwards from the current location. Doesn't align nor + * check for space. + * + * @param x An [UInt] to put into the buffer. + */ + public fun put(x: UInt): Unit = put(x.toInt()) + + /** + * Add an [Int] to the buffer, backwards from the current location. Doesn't align nor + * check for space. + * + * @param x An [Int] to put into the buffer. + */ + public fun put(x: Int){ + space -= Int.SIZE_BYTES + buffer.set(space, x) + } + + /** + * Add a [ULong] to the buffer, backwards from the current location. Doesn't align nor + * check for space. + * + * @param x A [ULong] to put into the buffer. + */ + public fun put(x: ULong): Unit = put(x.toLong()) + + /** + * Add a [Long] to the buffer, backwards from the current location. Doesn't align nor + * check for space. + * + * @param x A [Long] to put into the buffer. + */ + public fun put(x: Long) { + space -= Long.SIZE_BYTES + buffer.set(space, x) + } + + /** + * Add a [Float] to the buffer, backwards from the current location. Doesn't align nor + * check for space. + * + * @param x A [Float] to put into the buffer. + */ + public fun put(x: Float) { + space -= Float.SIZE_BYTES + buffer.set(space, x) + } + + /** + * Add a [Double] to the buffer, backwards from the current location. Doesn't align nor + * check for space. + * + * @param x A [Double] to put into the buffer. + */ + public fun put(x: Double) { + space -= Double.SIZE_BYTES + buffer.set(space, x) + } + + /** + * Add a [Boolean] to the buffer, properly aligned, and grows the buffer (if necessary). + * + * @param x A [Boolean] to put into the buffer. + */ + public fun add(x: Boolean) { + prep(Byte.SIZE_BYTES, 0) + put(x) + } + + /** + * Add a [UByte] to the buffer, properly aligned, and grows the buffer (if necessary). + * + * @param x A [UByte] to put into the buffer. + */ + public fun add(x: UByte): Unit = add(x.toByte()) + + /** + * Add a [Byte] to the buffer, properly aligned, and grows the buffer (if necessary). + * + * @param x A [Byte] to put into the buffer. + */ + public fun add(x: Byte) { + prep(Byte.SIZE_BYTES, 0) + put(x) + } + + /** + * Add a [UShort] to the buffer, properly aligned, and grows the buffer (if necessary). + * + * @param x A [UShort] to put into the buffer. + */ + public fun add(x: UShort): Unit = add(x.toShort()) + + /** + * Add a [Short] to the buffer, properly aligned, and grows the buffer (if necessary). + * + * @param x A [Short] to put into the buffer. + */ + public fun add(x: Short) { + prep(Short.SIZE_BYTES, 0) + put(x) + } + + /** + * Add an [Unit] to the buffer, properly aligned, and grows the buffer (if necessary). + * + * @param x An [Unit] to put into the buffer. + */ + public fun add(x: UInt): Unit = add(x.toInt()) + + /** + * Add an [Int] to the buffer, properly aligned, and grows the buffer (if necessary). + * + * @param x An [Int] to put into the buffer. + */ + public fun add(x: Int) { + prep(Int.SIZE_BYTES, 0) + put(x) + } + + /** + * Add a [ULong] to the buffer, properly aligned, and grows the buffer (if necessary). + * + * @param x A [ULong] to put into the buffer. + */ + public fun add(x: ULong): Unit = add(x.toLong()) + + /** + * Add a `long` to the buffer, properly aligned, and grows the buffer (if necessary). + * + * @param x A `long` to put into the buffer. + */ + public fun add(x: Long) { + prep(Long.SIZE_BYTES, 0) + put(x) + } + + /** + * Add a [Float] to the buffer, properly aligned, and grows the buffer (if necessary). + * + * @param x A [Float] to put into the buffer. + */ + public fun add(x: Float) { + prep(Float.SIZE_BYTES, 0) + put(x) + } + + /** + * Add a [Double] to the buffer, properly aligned, and grows the buffer (if necessary). + * + * @param x A [Double] to put into the buffer. + */ + public fun add(x: Double) { + prep(Double.SIZE_BYTES, 0) + put(x) + } + + /** + * Adds on offset, relative to where it will be written. + * + * @param off The offset to add. + */ + public fun add(off: Offset<*>): Unit = addOffset(off.value) + public fun add(off: VectorOffset<*>): Unit = addOffset(off.value) + private fun addOffset(off: Int) { + prep(Int.SIZE_BYTES, 0) // Ensure alignment is already done. + put(buffer.capacity - space - off + Int.SIZE_BYTES) + } + + /** + * Start a new array/vector of objects. Users usually will not call + * this directly. The `FlatBuffers` compiler will create a start/end + * method for vector types in generated code. + * + * + * The expected sequence of calls is: + * + * 1. Start the array using this method. + * 1. Call [.addOffset] `num_elems` number of times to set + * the offset of each element in the array. + * 1. Call [.endVector] to retrieve the offset of the array. + * + * + * + * For example, to create an array of strings, do: + *
`// Need 10 strings
+   * FlatBufferBuilder builder = new FlatBufferBuilder(existingBuffer);
+   * int[] offsets = new int[10];
+   *
+   * for (int i = 0; i < 10; i++) {
+   * offsets[i] = fbb.createString(" " + i);
+   * }
+   *
+   * // Have the strings in the buffer, but don't have a vector.
+   * // Add a vector that references the newly created strings:
+   * builder.startVector(4, offsets.length, 4);
+   *
+   * // Add each string to the newly created vector
+   * // The strings are added in reverse order since the buffer
+   * // is filled in back to front
+   * for (int i = offsets.length - 1; i >= 0; i--) {
+   * builder.addOffset(offsets[i]);
+   * }
+   *
+   * // Finish off the vector
+   * int offsetOfTheVector = fbb.endVector();
+   `
* + * + * @param elemSize The size of each element in the array. + * @param numElems The number of elements in the array. + * @param alignment The alignment of the array. + */ + public fun startVector(elemSize: Int, numElems: Int, alignment: Int) { + notNested() + vectorNumElems = numElems + prep(Int.SIZE_BYTES, elemSize * numElems) + prep(alignment, elemSize * numElems) // Just in case alignment > int. + nested = true + } + public fun startString(numElems: Int): Unit = startVector(1, numElems, 1) + + /** + * Finish off the creation of an array and all its elements. The array + * must be created with [.startVector]. + * + * @return The offset at which the newly created array starts. + * @see .startVector + */ + public fun endVector(): VectorOffset { + if (!nested) throw AssertionError("FlatBuffers: endVector called without startVector") + nested = false + put(vectorNumElems) + return VectorOffset(offset()) + } + + public fun endString(): Offset { + if (!nested) throw AssertionError("FlatBuffers: endString called without startString") + nested = false + put(vectorNumElems) + return Offset(offset()) + } + + private fun endVector(): Int { + if (!nested) throw AssertionError("FlatBuffers: endVector called without startVector") + nested = false + put(vectorNumElems) + return offset() + } + + /** + * Create a new array/vector and return a ByteBuffer to be filled later. + * Call [endVector] after this method to get an offset to the beginning + * of vector. + * + * @param elemSize the size of each element in bytes. + * @param numElems number of elements in the vector. + * @param alignment byte alignment. + * @return ByteBuffer with position and limit set to the space allocated for the array. + */ + public fun createUnintializedVector(elemSize: Int, numElems: Int, alignment: Int): ReadWriteBuffer { + val length = elemSize * numElems + startVector(elemSize, numElems, alignment) + space -= length + buffer.writePosition = space + return buffer.writeSlice(buffer.writePosition, length) + } + + /** + * Create a vector of tables. + * + * @param offsets Offsets of the tables. + * @return Returns offset of the vector. + */ + public fun createVectorOfTables(offsets: Array>): VectorOffset { + notNested() + startVector(Int.SIZE_BYTES, offsets.size, Int.SIZE_BYTES) + for (i in offsets.indices.reversed()) add(offsets[i]) + return VectorOffset(endVector()) + } + + /** + * Create a vector of sorted by the key tables. + * + * @param obj Instance of the table subclass. + * @param offsets Offsets of the tables. + * @return Returns offset of the sorted vector. + */ + public fun createSortedVectorOfTables(obj: T, offsets: Array>): VectorOffset { + obj.sortTables(offsets, buffer) + return createVectorOfTables(offsets) + } + + /** + * Encode the String `s` in the buffer using UTF-8. If a String with + * this exact contents has already been serialized using this method, + * instead simply returns the offset of the existing String. + * + * Usage of the method will incur into additional allocations, + * so it is advisable to use it only when it is known upfront that + * your message will have several repeated strings. + * + * @param s The String to encode. + * @return The offset in the buffer where the encoded String starts. + */ + public fun createSharedString(s: CharSequence): Offset { + if (stringPool == null) { + stringPool = HashMap() + val offset = createString(s) + stringPool!![s] = offset + return offset + } + var offset = stringPool!![s] + if (offset == null) { + offset = createString(s) + stringPool?.put(s, offset) + } + return offset + } + + /** + * Encode the [CharSequence] `s` in the buffer using UTF-8. + * @param s The [CharSequence] to encode. + * @return The offset in the buffer where the encoded string starts. + */ + public fun createString(s: CharSequence): Offset { + val length: Int = Utf8.encodedLength(s) + add(0.toByte()) + startString(length) + space -= length + buffer.writePosition = space + buffer.put(s, length) + return endString() + } + + /** + * Create a string in the buffer from an already encoded UTF-8 string in a ByteBuffer. + * + * @param s An already encoded UTF-8 string as a `ByteBuffer`. + * @return The offset in the buffer where the encoded string starts. + */ + public fun createString(s: ReadBuffer): Offset { + val length: Int = s.limit + add(0.toByte()) + startVector(1, length, 1) + space -= length + buffer.writePosition = space + buffer.put(s) + return endString() + } + + /** + * Create a byte array in the buffer. + * + * @param arr A source array with data + * @return The offset in the buffer where the encoded array starts. + */ + public fun createByteVector(arr: ByteArray): VectorOffset { + val length = arr.size + startVector(1, length, 1) + space -= length + buffer.writePosition = space + buffer.put(arr) + return VectorOffset(endVector()) + } + + /** + * Create a byte array in the buffer. + * + * @param arr a source array with data. + * @param offset the offset in the source array to start copying from. + * @param length the number of bytes to copy from the source array. + * @return The offset in the buffer where the encoded array starts. + */ + public fun createByteVector(arr: ByteArray, offset: Int, length: Int): VectorOffset { + startVector(1, length, 1) + space -= length + buffer.writePosition = space + buffer.put(arr, offset, length) + return VectorOffset(endVector()) + } + + /** + * Create a byte array in the buffer. + * + * The source [ReadBuffer] position is advanced until [ReadBuffer.limit] + * after this call. + * + * @param data A source [ReadBuffer] with data. + * @return The offset in the buffer where the encoded array starts. + */ + public fun createByteVector(data: ReadBuffer, from: Int = 0, until: Int = data.limit): VectorOffset { + val length: Int = until - from + startVector(1, length, 1) + space -= length + buffer.writePosition = space + buffer.put(data, from, until) + return VectorOffset(endVector()) + } + + /** + * Should not be accessing the final buffer before it is finished. + */ + public fun finished() { + if (!finished) throw AssertionError( + "FlatBuffers: you can only access the serialized buffer after it has been" + + " finished by FlatBufferBuilder.finish()." + ) + } + + /** + * Should not be creating any other object, string or vector + * while an object is being constructed. + */ + public fun notNested() { + if (nested) throw AssertionError("FlatBuffers: object serialization must not be nested.") + } + + /** + * Structures are always stored inline, they need to be created right + * where they're used. You'll get this assertion failure if you + * created it elsewhere. + * + * @param obj The offset of the created object. + */ + public fun nested(obj: Int) { + if (obj != offset()) throw AssertionError("FlatBuffers: struct must be serialized inline.") + } + + /** + * Start encoding a new object in the buffer. Users will not usually need to + * call this directly. The `FlatBuffers` compiler will generate helper methods + * that call this method internally. + * + * + * For example, using the "Monster" code found on the "landing page". An + * object of type `Monster` can be created using the following code: + * + *
`int testArrayOfString = Monster.createTestarrayofstringVector(fbb, new int[] {
+   * fbb.createString("test1"),
+   * fbb.createString("test2")
+   * });
+   *
+   * Monster.startMonster(fbb);
+   * Monster.addPos(fbb, Vec3.createVec3(fbb, 1.0f, 2.0f, 3.0f, 3.0,
+   * Color.Green, (short)5, (byte)6));
+   * Monster.addHp(fbb, (short)80);
+   * Monster.addName(fbb, str);
+   * Monster.addInventory(fbb, inv);
+   * Monster.addTestType(fbb, (byte)Any.Monster);
+   * Monster.addTest(fbb, mon2);
+   * Monster.addTest4(fbb, test4);
+   * Monster.addTestarrayofstring(fbb, testArrayOfString);
+   * int mon = Monster.endMonster(fbb);
+   `
* + * + * + * Here: + * + * * The call to `Monster#startMonster(FlatBufferBuilder)` will call this + * method with the right number of fields set. + * * `Monster#endMonster(FlatBufferBuilder)` will ensure [.endObject] is called. + * + * + * + * It's not recommended to call this method directly. If it's called manually, you must ensure + * to audit all calls to it whenever fields are added or removed from your schema. This is + * automatically done by the code generated by the `FlatBuffers` compiler. + * + * @param numFields The number of fields found in this object. + */ + public fun startTable(numFields: Int) { + notNested() + if (vtable.size < numFields) { + vtable = IntArray(numFields) + } + vtableInUse = numFields + for (i in 0 until vtableInUse) + vtable[i] = 0 + nested = true + objectStart = offset() + } + + /** + * Add a [Boolean] to a table at `o` into its vtable, with value `x` and default `d`. + * If `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + */ + public fun add(o: Int, x: Boolean, d: Boolean?) { + if (forceDefaults || x != d) { + add(x) + slot(o) + } + } + // unboxed specialization + public fun add(o: Int, x: Boolean, d: Boolean) { + if (forceDefaults || x != d) { + add(x) + slot(o) + } + } + + /** + * Add a [UByte] to a table at `o` into its vtable, with value `x` and default `d`. + * If `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + */ + public fun add(o: Int, x: UByte, d: UByte?): Unit = add(o, x.toByte(), d?.toByte()) + // unboxed specialization + public fun add(o: Int, x: UByte, d: UByte): Unit = add(o, x.toByte(), d.toByte()) + + /** + * Add a [Byte] to a table at `o` into its vtable, with value `x` and default `d`. + * If `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + */ + public fun add(o: Int, x: Byte, d: Byte?) { + if (forceDefaults || x != d) { + add(x) + slot(o) + } + } + // unboxed specialization + public fun add(o: Int, x: Byte, d: Byte) { + if (forceDefaults || x != d) { + add(x) + slot(o) + } + } + /** + * Add a [UShort] to a table at `o` into its vtable, with value `x` and default `d`. + * If `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + */ + public fun add(o: Int, x: UShort, d: UShort?): Unit = add(o, x.toShort(), d?.toShort()) + // unboxed specialization + public fun add(o: Int, x: UShort, d: UShort): Unit = add(o, x.toShort(), d.toShort()) + + + /** + * Add a [Short] to a table at `o` into its vtable, with value `x` and default `d`. + * If `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + */ + public fun add(o: Int, x: Short, d: Short?) { + if (forceDefaults || x != d) { + add(x) + slot(o) + } + } + // unboxed specialization + public fun add(o: Int, x: Short, d: Short) { + if (forceDefaults || x != d) { + add(x) + slot(o) + } + } + + /** + * Add a [UInt] to a table at `o` into its vtable, with value `x` and default `d`. + * If `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + */ + public fun add(o: Int, x: UInt, d: UInt?): Unit = add(o, x.toInt(), d?.toInt()) + // unboxed specialization + public fun add(o: Int, x: UInt, d: UInt): Unit = add(o, x.toInt(), d.toInt()) + + /** + * Add a [Int] to a table at `o` into its vtable, with value `x` and default `d`. + * If `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + */ + public fun add(o: Int, x: Int, d: Int?) { + if (forceDefaults || x != d) { + add(x) + slot(o) + } + } + // unboxed specialization + public fun add(o: Int, x: Int, d: Int) { + if (forceDefaults || x != d) { + add(x) + slot(o) + } + } + /** + * Add a [ULong] to a table at `o` into its vtable, with value `x` and default `d`. + * If `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + */ + public fun add(o: Int, x: ULong, d: ULong?): Unit = add(o, x.toLong(), d?.toLong()) + // unboxed specialization + public fun add(o: Int, x: ULong, d: ULong): Unit = add(o, x.toLong(), d.toLong()) + /** + * Add a [Long] to a table at `o` into its vtable, with value `x` and default `d`. + * If `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + */ + public fun add(o: Int, x: Long, d: Long?) { + if (forceDefaults || x != d) { + add(x) + slot(o) + } + } + // unboxed specialization + public fun add(o: Int, x: Long, d: Long) { + if (forceDefaults || x != d) { + add(x) + slot(o) + } + } + + /** + * Add a [Float] to a table at `o` into its vtable, with value `x` and default `d`. + * If `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + */ + public fun add(o: Int, x: Float, d: Float?) { + if (forceDefaults || x != d) { + add(x) + slot(o) + } + } + // unboxed specialization + public fun add(o: Int, x: Float, d: Float) { + if (forceDefaults || x != d) { + add(x) + slot(o) + } + } + + /** + * Add a [Double] to a table at `o` into its vtable, with value `x` and default `d`. + * If `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + */ + public fun add(o: Int, x: Double, d: Double?) { + if (forceDefaults || x != d) { + add(x) + slot(o) + } + } + // unboxed specialization + public fun add(o: Int, x: Double, d: Double) { + if (forceDefaults || x != d) { + add(x) + slot(o) + } + } + + /** + * Add an `offset` to a table at `o` into its vtable, with value `x` and default `d`. + * + * @param o The index into the vtable. + * @param x An `offset` to put into the buffer, depending on how defaults are handled. If + * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the + * default value, it can be skipped. + * @param d An `offset` default value to compare against when `force_defaults` is `false`. + */ + public fun add(o: Int, x: Offset<*>, d: Int) { + if (forceDefaults || x.value != d) { + add(x) + slot(o) + } + } + public fun add(o: Int, x: VectorOffset<*>, d: Int) { + if (forceDefaults || x.value != d) { + add(x) + slot(o) + } + } + + /** + * Add a struct to the table. Structs are stored inline, so nothing additional is being added. + * + * @param vOffset The index into the vtable. + * @param x The offset of the created struct. + * @param d The default value is always `0`. + */ + public fun addStruct(vOffset: Int, x: Offset<*>, d: Offset<*>?): Unit = addStruct(vOffset, x.value, d?.value) + // unboxed specialization + public fun addStruct(vOffset: Int, x: Offset<*>, d: Offset<*>): Unit = addStruct(vOffset, x.value, d.value) + public fun addStruct(vOffset: Int, x: Int, d: Int?) { + if (x != d) { + nested(x) + slot(vOffset) + } + } + // unboxed specialization + public fun addStruct(vOffset: Int, x: Int, d: Int) { + if (x != d) { + nested(x) + slot(vOffset) + } + } + + /** + * Set the current vtable at `voffset` to the current location in the buffer. + * + * @param vOffset The index into the vtable to store the offset relative to the end of the + * buffer. + */ + public fun slot(vOffset: Int) { + vtable[vOffset] = offset() + } + + /** + * Finish off writing the object that is under construction. + * + * @return The offset to the object inside [.dataBuffer]. + * @see .startTable + */ + public fun endTable(): Offset { + if (!nested) throw AssertionError("FlatBuffers: endTable called without startTable") + + val vtable = this.vtable + + add(0) + val vtableloc = offset() + // Write out the current vtable. + var i: Int = vtableInUse - 1 + // Trim trailing zeroes. + while (i >= 0 && vtable[i] == 0) { + i-- + } + val trimmedSize = i + 1 + while (i >= 0) { + // Offset relative to the start of the table. + add((if (vtable[i] != 0) vtableloc - vtable[i] else 0).toShort()) + i-- + } + + add((vtableloc - objectStart).toShort()) + add(((trimmedSize + 2) * Short.SIZE_BYTES).toShort()) + + // Search for an existing vtable that matches the current one. + var existingVtable = 0 + i = 0 + outer_loop@ while (i < numVtables) { + val vt1: Int = buffer.capacity - vtables[i] + val vt2 = space + val len: Short = buffer.getShort(vt1) + if (len == buffer.getShort(vt2)) { + var j: Int = Short.SIZE_BYTES + while (j < len) { + if (buffer.getShort(vt1 + j) != buffer.getShort(vt2 + j)) { + i++ + continue@outer_loop + } + j += Short.SIZE_BYTES + } + existingVtable = vtables[i] + break@outer_loop + } + i++ + } + if (existingVtable != 0) { + // Found a match: + // Remove the current vtable. + space = buffer.capacity - vtableloc + // Point table to existing vtable. + buffer.set(space, existingVtable - vtableloc) + } else { + // No match: + // Add the location of the current vtable to the list of vtables. + if (numVtables == vtables.size) vtables = vtables.copyOf(numVtables * 2) + vtables[numVtables++] = offset() + // Point table to current vtable. + buffer.set(buffer.capacity - vtableloc, offset() - vtableloc) + } + nested = false + return Offset(vtableloc) + } + + /** + * Checks that a required field has been set in a given table that has + * just been constructed. + * + * @param table The offset to the start of the table from the `ByteBuffer` capacity. + * @param field The offset to the field in the vtable. + */ + public fun required(table: Offset<*>, field: Int, fileName: String? = null) { + val tableStart: Int = buffer.capacity - table + val vtableStart: Int = tableStart - buffer.getInt(tableStart) + val ok = buffer.getShort(vtableStart + field).toInt() != 0 + // If this fails, the caller will show what field needs to be set. + if (!ok) throw AssertionError("FlatBuffers: field ${fileName ?: field} must be set") + } + + /** + * Finalize a buffer, pointing to the given `root_table`. + * + * @param rootTable An offset to be added to the buffer. + * @param sizePrefix Whether to prefix the size to the buffer. + */ + protected fun finish(rootTable: Offset<*>, sizePrefix: Boolean) { + prep(minalign, Int.SIZE_BYTES + if (sizePrefix) Int.SIZE_BYTES else 0) + add(rootTable) + if (sizePrefix) { + add(buffer.capacity - space) + } + buffer.writePosition = space + finished = true + } + + /** + * Finalize a buffer, pointing to the given `root_table`. + * + * @param rootTable An offset to be added to the buffer. + */ + public fun finish(rootTable: Offset<*>) { + finish(rootTable, false) + } + + /** + * Finalize a buffer, pointing to the given `root_table`, with the size prefixed. + * + * @param rootTable An offset to be added to the buffer. + */ + public fun finishSizePrefixed(rootTable: Offset<*>) { + finish(rootTable, true) + } + + /** + * Finalize a buffer, pointing to the given `root_table`. + * + * @param rootTable An offset to be added to the buffer. + * @param fileIdentifier A FlatBuffer file identifier to be added to the buffer before + * `root_table`. + * @param sizePrefix Whether to prefix the size to the buffer. + */ + protected fun finish(rootTable: Offset<*>, fileIdentifier: String, sizePrefix: Boolean) { + val identifierSize = 4 + prep(minalign, Int.SIZE_BYTES + identifierSize + if (sizePrefix) Int.SIZE_BYTES else 0) + if (fileIdentifier.length != identifierSize) throw AssertionError( + "FlatBuffers: file identifier must be length " + + identifierSize + ) + for (i in identifierSize - 1 downTo 0) { + add(fileIdentifier[i].code.toByte()) + } + finish(rootTable, sizePrefix) + } + + /** + * Finalize a buffer, pointing to the given `root_table`. + * + * @param rootTable An offset to be added to the buffer. + * @param fileIdentifier A FlatBuffer file identifier to be added to the buffer before + * `root_table`. + */ + public fun finish(rootTable: Offset<*>, fileIdentifier: String) { + finish(rootTable, fileIdentifier, false) + } + + /** + * Finalize a buffer, pointing to the given `root_table`, with the size prefixed. + * + * @param rootTable An offset to be added to the buffer. + * @param fileIdentifier A FlatBuffer file identifier to be added to the buffer before + * `root_table`. + */ + public fun finishSizePrefixed(rootTable: Offset<*>, fileIdentifier: String) { + finish(rootTable, fileIdentifier, true) + } + + /** + * In order to save space, fields that are set to their default value + * don't get serialized into the buffer. Forcing defaults provides a + * way to manually disable this optimization. + * + * @param forceDefaults When set to `true`, always serializes default values. + * @return Returns `this`. + */ + public fun forceDefaults(forceDefaults: Boolean): FlatBufferBuilder { + this.forceDefaults = forceDefaults + return this + } + + /** + * Get the ByteBuffer representing the FlatBuffer. Only call this after you've + * called `finish()`. The actual data starts at the ByteBuffer's current position, + * not necessarily at `0`. + * + * @return The [ReadBuffer] representing the FlatBuffer + */ + public fun dataBuffer(): ReadWriteBuffer { + finished() + return buffer + } + + /** + * A utility function to copy and return the ByteBuffer data as a `byte[]`. + * + * @return A full copy of the [data buffer][.dataBuffer]. + */ + public fun sizedByteArray(start: Int = space, length: Int = buffer.capacity - space): ByteArray { + finished() + val array = ByteArray(length) + buffer.getBytes(array, start) + return array + } + + /** + * Helper function to test if a field is present in the table + * + * @param offset virtual table offset + * @return true if the filed is present + */ + public fun Table.isFieldPresent(offset: Int): Boolean = this.offset(offset) != 0 +} + +public fun Double.sign(): Double = when { + this.isNaN() -> Double.NaN + this > 0 -> 1.0 + this < 0 -> -1.0 + else -> this +} + +public fun Float.sign(): Float = when { + this.isNaN() -> Float.NaN + this > 0 -> 1.0f + this < 0 -> -1.0f + else -> this +} + +public fun Int.sign(): Int = when { + this > 0 -> 1 + this < 0 -> -1 + else -> this +} diff --git a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Flatbuffers.kt b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Flatbuffers.kt new file mode 100644 index 000000000..bbebd29de --- /dev/null +++ b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Flatbuffers.kt @@ -0,0 +1,367 @@ +/* + * Copyright 2021 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.flatbuffers.kotlin + +import kotlin.jvm.JvmInline +import kotlin.math.min + +// For now a typealias to guarantee type safety. +public typealias UnionOffset = Offset +public typealias UnionOffsetArray = OffsetArray +public typealias StringOffsetArray = OffsetArray + +public inline fun UnionOffsetArray(size: Int, crossinline call: (Int) -> Offset): UnionOffsetArray = + UnionOffsetArray(IntArray(size) { call(it).value }) +public inline fun StringOffsetArray(size: Int, crossinline call: (Int) -> Offset): StringOffsetArray = + StringOffsetArray(IntArray(size) { call(it).value }) +/** + * Represents a "pointer" to a pointer types (table, string, struct) within the buffer + */ +@JvmInline +public value class Offset(public val value: Int) { + public fun toUnion(): UnionOffset = UnionOffset(value) +} + +/** + * Represents an array of offsets. Used to avoid boxing + * offset types. + */ +@JvmInline +public value class OffsetArray(public val value: IntArray) { + public inline val size: Int + get() = value.size + public inline operator fun get(index: Int): Offset = Offset(value[index]) +} + +public inline fun OffsetArray(size: Int, crossinline call: (Int) -> Offset): OffsetArray { + return OffsetArray(IntArray(size) { call(it).value }) +} + + +/** + * Represents a "pointer" to a vector type with elements T + */ +@JvmInline +public value class VectorOffset(public val value: Int) + +public fun Int.toOffset(): Offset = Offset(this) + +public operator fun Offset.minus(other: Int): Offset = Offset(this.value - other) + +public operator fun Int.minus(other: Offset): Int { + return this - other.value +} +/** + * All tables in the generated code derive from this class, and add their own accessors. + */ +public open class Table { + + /** Used to hold the position of the `bb` buffer. */ + public var bufferPos: Int = 0 + + /** The underlying ReadWriteBuffer to hold the data of the Table. */ + public var bb: ReadWriteBuffer = emptyBuffer + + /** Used to hold the vtable position. */ + public var vtableStart: Int = 0 + + /** Used to hold the vtable size. */ + public var vtableSize: Int = 0 + + protected inline fun Int.invalid(default: T, valid: (Int) -> T) : T = + if (this != 0) valid(this) else default + + protected inline fun lookupField(i: Int, default: T, found: (Int) -> T) : T = + offset(i).invalid(default) { found(it) } + + /** + * Look up a field in the vtable. + * + * @param vtableOffset An `int` offset to the vtable in the Table's ReadWriteBuffer. + * @return Returns an offset into the object, or `0` if the field is not present. + */ + public fun offset(vtableOffset: Int): Int = + if (vtableOffset < vtableSize) bb.getShort(vtableStart + vtableOffset).toInt() else 0 + + /** + * Retrieve a relative offset. + * + * @param offset An `int` index into the Table's ReadWriteBuffer containing the relative offset. + * @return Returns the relative offset stored at `offset`. + */ + public fun indirect(offset: Int): Int = offset + bb.getInt(offset) + + /** + * Create a Java `String` from UTF-8 data stored inside the FlatBuffer. + * + * This allocates a new string and converts to wide chars upon each access, + * which is not very efficient. Instead, each FlatBuffer string also comes with an + * accessor based on __vector_as_ReadWriteBuffer below, which is much more efficient, + * assuming your Java program can handle UTF-8 data directly. + * + * @param offset An `int` index into the Table's ReadWriteBuffer. + * @return Returns a `String` from the data stored inside the FlatBuffer at `offset`. + */ + public fun string(offset: Int): String = string(offset, bb) + + /** + * Get the length of a vector. + * + * @param offset An `int` index into the Table's ReadWriteBuffer. + * @return Returns the length of the vector whose offset is stored at `offset`. + */ + public fun vectorLength(offset: Int): Int { + var newOffset = offset + newOffset += bufferPos + newOffset += bb.getInt(newOffset) + return bb.getInt(newOffset) + } + + /** + * Get the start data of a vector. + * + * @param offset An `int` index into the Table's ReadWriteBuffer. + * @return Returns the start of the vector data whose offset is stored at `offset`. + */ + public fun vector(offset: Int): Int { + var newOffset = offset + newOffset += bufferPos + return newOffset + bb.getInt(newOffset) + Int.SIZE_BYTES // data starts after the length + } + /** + * Initialize vector as a ReadWriteBuffer. + * + * This is more efficient than using duplicate, since it doesn't copy the data + * nor allocates a new [ReadBuffer], creating no garbage to be collected. + * + * @param buffer The [ReadBuffer] for the array + * @param vectorOffset The position of the vector in the byte buffer + * @param elemSize The size of each element in the array + * @return The [ReadBuffer] for the array + */ + public fun vectorAsBuffer(buffer: ReadWriteBuffer, vectorOffset: Int, elemSize: Int): ReadBuffer { + val o = offset(vectorOffset) + if (o == 0) return emptyBuffer + val vectorStart = vector(o) + return buffer.slice(vectorStart, vectorLength(o) * elemSize) + } + + /** + * Initialize any Table-derived type to point to the union at the given `offset`. + * + * @param t A `Table`-derived type that should point to the union at `offset`. + * @param offset An `int` index into the Table's ReadWriteBuffer. + * @return Returns the Table that points to the union at `offset`. + */ + public fun union(t: Table, offset: Int): Table = union(t, offset, bb) + + /** + * Sort tables by the key. + * + * @param offsets An 'int' indexes of the tables into the bb. + * @param bb A `ReadWriteBuffer` to get the tables. + */ + public fun sortTables(offsets: Array>, bb: ReadWriteBuffer) { + val off = offsets.sortedWith { o1, o2 -> keysCompare(o1, o2, bb) } + for (i in offsets.indices) offsets[i] = off[i] + } + + /** + * Compare two tables by the key. + * + * @param o1 An 'Integer' index of the first key into the bb. + * @param o2 An 'Integer' index of the second key into the bb. + * @param buffer A `ReadWriteBuffer` to get the keys. + */ + public open fun keysCompare(o1: Offset<*>, o2: Offset<*>, buffer: ReadWriteBuffer): Int = 0 + + /** + * Re-init the internal state with an external buffer `ReadWriteBuffer` and an offset within. + * + * This method exists primarily to allow recycling Table instances without risking memory leaks + * due to `ReadWriteBuffer` references. + */ + public inline fun reset(i: Int, reuseBuffer: ReadWriteBuffer): T { + bb = reuseBuffer + if (bb != emptyBuffer) { + bufferPos = i + vtableStart = bufferPos - bb.getInt(bufferPos) + vtableSize = bb.getShort(vtableStart).toInt() + } else { + bufferPos = 0 + vtableStart = 0 + vtableSize = 0 + } + return this as T + } + + /** + * Resets the internal state with a null `ReadWriteBuffer` and a zero position. + * + * This method exists primarily to allow recycling Table instances without risking memory leaks + * due to `ReadWriteBuffer` references. The instance will be unusable until it is assigned + * again to a `ReadWriteBuffer`. + */ + public inline fun reset(): T = reset(0, emptyBuffer) + + public companion object { + + public fun offset(vtableOffset: Int, offset: Offset<*>, bb: ReadWriteBuffer): Int { + val vtable: Int = bb.capacity - offset.value + return bb.getShort(vtable + vtableOffset - bb.getInt(vtable)) + vtable + } + + /** + * Retrieve a relative offset. + * + * @param offset An `int` index into a ReadWriteBuffer containing the relative offset. + * @param bb from which the relative offset will be retrieved. + * @return Returns the relative offset stored at `offset`. + */ + public fun indirect(offset: Int, bb: ReadWriteBuffer): Int { + return offset + bb.getInt(offset) + } + + /** + * Create a Java `String` from UTF-8 data stored inside the FlatBuffer. + * + * This allocates a new string and converts to wide chars upon each access, + * which is not very efficient. Instead, each FlatBuffer string also comes with an + * accessor based on __vector_as_ReadWriteBuffer below, which is much more efficient, + * assuming your Java program can handle UTF-8 data directly. + * + * @param offset An `int` index into the Table's ReadWriteBuffer. + * @param bb Table ReadWriteBuffer used to read a string at given offset. + * @return Returns a `String` from the data stored inside the FlatBuffer at `offset`. + */ + public fun string(offset: Int, bb: ReadWriteBuffer): String { + var newOffset = offset + newOffset += bb.getInt(newOffset) + val length: Int = bb.getInt(newOffset) + return bb.getString(newOffset + Int.SIZE_BYTES, length) + } + + /** + * Initialize any Table-derived type to point to the union at the given `offset`. + * + * @param t A `Table`-derived type that should point to the union at `offset`. + * @param offset An `int` index into the Table's ReadWriteBuffer. + * @param bb Table ReadWriteBuffer used to initialize the object Table-derived type. + * @return Returns the Table that points to the union at `offset`. + */ + public fun union(t: Table, offset: Int, bb: ReadWriteBuffer): Table = + t.reset(indirect(offset, bb), bb) + + /** + * Check if a [ReadWriteBuffer] contains a file identifier. + * + * @param bb A `ReadWriteBuffer` to check if it contains the identifier + * `ident`. + * @param ident A `String` identifier of the FlatBuffer file. + * @return True if the buffer contains the file identifier + */ + public fun hasIdentifier(bb: ReadWriteBuffer?, ident: String): Boolean { + val identifierLength = 4 + if (ident.length != identifierLength) + throw AssertionError("FlatBuffers: file identifier must be length $identifierLength") + for (i in 0 until identifierLength) { + if (ident[i].code.toByte() != bb!![bb.limit + Int.SIZE_BYTES + i]) return false + } + return true + } + + /** + * Compare two strings in the buffer. + * + * @param offsetA An 'int' index of the first string into the bb. + * @param offsetB An 'int' index of the second string into the bb. + * @param bb A `ReadWriteBuffer` to get the strings. + */ + public fun compareStrings(offsetA: Int, offsetB: Int, bb: ReadWriteBuffer): Int { + var offset1 = offsetA + var offset2 = offsetB + offset1 += bb.getInt(offset1) + offset2 += bb.getInt(offset2) + val len1: Int = bb.getInt(offset1) + val len2: Int = bb.getInt(offset2) + val startPos1: Int = offset1 + Int.SIZE_BYTES + val startPos2: Int = offset2 + Int.SIZE_BYTES + val len: Int = min(len1, len2) + for (i in 0 until len) { + if (bb[i + startPos1] != bb[i + startPos2]) { + return bb[i + startPos1] - bb[i + startPos2] + } + } + return len1 - len2 + } + + /** + * Compare string from the buffer with the 'String' object. + * + * @param offset An 'int' index of the first string into the bb. + * @param key Second string as a byte array. + * @param bb A `ReadWriteBuffer` to get the first string. + */ + public fun compareStrings(offset: Int, key: ByteArray, bb: ReadWriteBuffer): Int { + var offset1 = offset + offset1 += bb.getInt(offset1) + val len1: Int = bb.getInt(offset1) + val len2 = key.size + val startPos: Int = offset1 + Int.SIZE_BYTES + val len: Int = min(len1, len2) + for (i in 0 until len) { + if (bb[i + startPos] != key[i]) return bb[i + startPos] - key[i] + } + return len1 - len2 + } + } +} + +/** + * All structs in the generated code derive from this class, and add their own accessors. + */ +public open class Struct { + /** Used to hold the position of the `bb` buffer. */ + protected var bufferPos: Int = 0 + + /** The underlying ByteBuffer to hold the data of the Struct. */ + protected var bb: ReadWriteBuffer = emptyBuffer + + /** + * Re-init the internal state with an external buffer `ByteBuffer` and an offset within. + * + * This method exists primarily to allow recycling Table instances without risking memory leaks + * due to `ByteBuffer` references. + */ + protected inline fun reset(i: Int, reuseBuffer: ReadWriteBuffer): T { + bb = reuseBuffer + bufferPos = if (bb != emptyBuffer) i else 0 + return this as T + } + + /** + * Resets internal state with a null `ByteBuffer` and a zero position. + * + * This method exists primarily to allow recycling Struct instances without risking memory leaks + * due to `ByteBuffer` references. The instance will be unusable until it is assigned + * again to a `ByteBuffer`. + */ + private inline fun reset(): T = reset(0, emptyBuffer) +} + +public inline val T.value: T get() = this + +public const val VERSION_2_0_8: Int = 1 diff --git a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/FlexBuffers.kt b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/FlexBuffers.kt index a22dd13dc..0f9cd7d45 100644 --- a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/FlexBuffers.kt +++ b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/FlexBuffers.kt @@ -43,7 +43,7 @@ public class Reference internal constructor( internal val end: Int, internal val parentWidth: ByteWidth, internal val byteWidth: ByteWidth, - internal val type: FlexBufferType + public val type: FlexBufferType ) { internal constructor(bb: ReadBuffer, end: Int, parentWidth: ByteWidth, packedType: Int) : @@ -294,7 +294,8 @@ public class Reference internal constructor( T_KEY -> buffer.getKeyString(buffer.indirect(end, parentWidth)) T_MAP -> "{ ${toMap().entries.joinToString(", ") { "${it.key}: ${it.value}"}} }" T_VECTOR, T_VECTOR_BOOL, T_VECTOR_FLOAT, T_VECTOR_INT, - T_VECTOR_UINT, T_VECTOR_KEY, T_VECTOR_STRING_DEPRECATED -> "[ ${toVector().joinToString(", ") { it.toString() }} ]" + T_VECTOR_UINT, T_VECTOR_KEY, T_VECTOR_STRING_DEPRECATED -> + "[ ${toVector().joinToString(", ") { it.toString() }} ]" T_INT -> toLong().toString() T_UINT -> toULong().toString() T_FLOAT -> toDouble().toString() @@ -629,10 +630,18 @@ public open class TypedVector( return block(childPos, byteWidth) } - internal fun getBoolean(index: Int): Boolean = resolveAt(index) { pos: Int, _: ByteWidth -> buffer.getBoolean(pos) } - internal fun getInt(index: Int): Long = resolveAt(index) { pos: Int, width: ByteWidth -> buffer.readLong(pos, width) } - internal fun getUInt(index: Int): ULong = resolveAt(index) { pos: Int, width: ByteWidth -> buffer.readULong(pos, width) } - internal fun getFloat(index: Int): Double = resolveAt(index) { pos: Int, width: ByteWidth -> buffer.readFloat(pos, width) } + internal fun getBoolean(index: Int): Boolean = resolveAt(index) { + pos: Int, _: ByteWidth -> buffer.getBoolean(pos) + } + internal fun getInt(index: Int): Long = resolveAt(index) { + pos: Int, width: ByteWidth -> buffer.readLong(pos, width) + } + internal fun getUInt(index: Int): ULong = resolveAt(index) { + pos: Int, width: ByteWidth -> buffer.readULong(pos, width) + } + internal fun getFloat(index: Int): Double = resolveAt(index) { + pos: Int, width: ByteWidth -> buffer.readFloat(pos, width) + } } /** @@ -706,7 +715,8 @@ public data class Key( /** * A Map class that provide support to access Key-Value data from Flexbuffers. */ -public class Map internal constructor(buffer: ReadBuffer, end: Int, byteWidth: ByteWidth) : +public class Map + internal constructor(buffer: ReadBuffer, end: Int, byteWidth: ByteWidth): Sized(buffer, end, byteWidth), kotlin.collections.Map { @@ -869,14 +879,14 @@ public class Map internal constructor(buffer: ReadBuffer, end: Int, byteWidth: B while (otherPos < otherLimit) { val c2 = other[otherPos] // not a single byte codepoint - if (c2.toInt() >= 0x80) { + if (c2.code >= 0x80) { break } val b: Byte = buffer[bufferPos] when { - b == ZeroByte -> return -c2.toInt() + b == ZeroByte -> return -c2.code b < 0 -> break - b != c2.toByte() -> return b - c2.toByte() + b != c2.code.toByte() -> return b - c2.code.toByte() } ++bufferPos ++otherPos diff --git a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/FlexBuffersBuilder.kt b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/FlexBuffersBuilder.kt index a4cd9d34c..52e16af9b 100644 --- a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/FlexBuffersBuilder.kt +++ b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/FlexBuffersBuilder.kt @@ -17,6 +17,7 @@ package com.google.flatbuffers.kotlin +@ExperimentalUnsignedTypes public class FlexBuffersBuilder( public val buffer: ReadWriteBuffer, private val shareFlag: Int = SHARE_KEYS @@ -49,13 +50,14 @@ public class FlexBuffersBuilder( * @return [ReadBuffer] containing the FlexBuffer message */ public fun finish(): ReadBuffer { - // If you hit this assert, you likely have objects that were never included + // If you hit this, you likely have objects that were never included // in a parent. You need to have exactly one root to finish a buffer. // Check your Start/End calls are matched, and all objects are inside // some other object. if (stack.size != 1) error("There is must be only on object as root. Current ${stack.size}.") // Write root value. val byteWidth = align(stack[0].elemWidth(buffer.writePosition, 0)) + buffer.requestAdditionalCapacity(byteWidth.value + 2) writeAny(stack[0], byteWidth) // Write root type. buffer.put(stack[0].storedPackedType()) @@ -198,7 +200,9 @@ public class FlexBuffersBuilder( public operator fun set(key: String? = null, value: String): Int { val iKey = putKey(key) val holder = if (shareFlag and SHARE_STRINGS != 0) { - stringValuePool.getOrPut(value) { writeString(iKey, value).also { stringValuePool[value] = it } }.copy(key = iKey) + stringValuePool.getOrPut(value) { + writeString(iKey, value).also { stringValuePool[value] = it } + }.copy(key = iKey) } else { writeString(iKey, value) } @@ -491,13 +495,17 @@ public class FlexBuffersBuilder( return if ((shareFlag and SHARE_KEYS) != 0) { stringKeyPool.getOrPut(key) { val pos: Int = buffer.writePosition - buffer.put(key) + val encodedKeySize = Utf8.encodedLength(key) + buffer.requestAdditionalCapacity(encodedKeySize + 1) + buffer.put(key, encodedKeySize) buffer.put(ZeroByte) pos } } else { val pos: Int = buffer.writePosition - buffer.put(key) + val encodedKeySize = Utf8.encodedLength(key) + buffer.requestAdditionalCapacity(encodedKeySize + 1) + buffer.put(key, encodedKeySize) buffer.put(ZeroByte) pos } @@ -510,26 +518,31 @@ public class FlexBuffersBuilder( } private fun writeString(key: Int, s: String): Value { - val size = Utf8.encodedLength(s) - val bitWidth = size.toULong().widthInUBits() + val encodedSize = Utf8.encodedLength(s) + val bitWidth = encodedSize.toULong().widthInUBits() val byteWidth = align(bitWidth) - writeInt(size, byteWidth) + writeInt(encodedSize, byteWidth) + buffer.requestAdditionalCapacity(encodedSize + 1) val sloc: Int = buffer.writePosition - if (size > 0) - buffer.put(s, size) + if (encodedSize > 0) + buffer.put(s, encodedSize) buffer.put(ZeroByte) return Value(T_STRING, key, bitWidth, sloc.toULong()) } - private fun writeDouble(toWrite: Double, byteWidth: ByteWidth): Unit = when (byteWidth.value) { - 4 -> buffer.put(toWrite.toFloat()) - 8 -> buffer.put(toWrite) - else -> Unit + private fun writeDouble(toWrite: Double, byteWidth: ByteWidth) { + buffer.requestAdditionalCapacity(byteWidth.value) + when (byteWidth.value) { + 4 -> buffer.put(toWrite.toFloat()) + 8 -> buffer.put(toWrite) + else -> Unit + } } private fun writeOffset(toWrite: Int, byteWidth: ByteWidth) { + buffer.requestAdditionalCapacity(byteWidth.value) val relativeOffset = (buffer.writePosition - toWrite) if (byteWidth.value != 8 && relativeOffset >= 1L shl byteWidth.value * 8) error("invalid offset $relativeOffset, writer pos ${buffer.writePosition}") writeInt(relativeOffset, byteWidth) @@ -542,6 +555,7 @@ public class FlexBuffersBuilder( writeInt(blob.size, byteWidth) val sloc: Int = buffer.writePosition + buffer.requestAdditionalCapacity(blob.size + trailing.compareTo(false)) buffer.put(blob, 0, blob.size) if (trailing) { buffer.put(ZeroByte) @@ -559,18 +573,12 @@ public class FlexBuffersBuilder( writeIntegerArray(0, value.size, byteWidth) { value[it].toULong() } private fun writeFloatArray(value: FloatArray) { - val byteWidth = Float.SIZE_BYTES - // since we know we are writing an array, we can avoid multiple copy/growth of the buffer by requesting - // the right size on the spot - buffer.requestCapacity(buffer.writePosition + (value.size * byteWidth)) + buffer.requestAdditionalCapacity(Float.SIZE_BYTES * value.size) value.forEach { buffer.put(it) } } private fun writeFloatArray(value: DoubleArray) { - val byteWidth = Double.SIZE_BYTES - // since we know we are writing an array, we can avoid multiple copy/growth of the buffer by requesting - // the right size on the spot - buffer.requestCapacity(buffer.writePosition + (value.size * byteWidth)) + buffer.requestAdditionalCapacity(Double.SIZE_BYTES * value.size) value.forEach { buffer.put(it) } } @@ -580,9 +588,7 @@ public class FlexBuffersBuilder( byteWidth: ByteWidth, crossinline valueBlock: (Int) -> ULong ) { - // since we know we are writing an array, we can avoid multiple copy/growth of the buffer by requesting - // the right size on the spot - buffer.requestCapacity(buffer.writePosition + (size * byteWidth)) + buffer.requestAdditionalCapacity(size * byteWidth.value) return when (byteWidth.value) { 1 -> for (i in start until start + size) { buffer.put(valueBlock(i).toUByte()) @@ -600,20 +606,26 @@ public class FlexBuffersBuilder( } } - private fun writeInt(value: Int, byteWidth: ByteWidth) = when (byteWidth.value) { - 1 -> buffer.put(value.toUByte()) - 2 -> buffer.put(value.toUShort()) - 4 -> buffer.put(value.toUInt()) - 8 -> buffer.put(value.toULong()) - else -> Unit + private fun writeInt(value: Int, byteWidth: ByteWidth) { + buffer.requestAdditionalCapacity(byteWidth.value) + when (byteWidth.value) { + 1 -> buffer.put(value.toUByte()) + 2 -> buffer.put(value.toUShort()) + 4 -> buffer.put(value.toUInt()) + 8 -> buffer.put(value.toULong()) + else -> Unit + } } - private fun writeInt(value: ULong, byteWidth: ByteWidth) = when (byteWidth.value) { - 1 -> buffer.put(value.toUByte()) - 2 -> buffer.put(value.toUShort()) - 4 -> buffer.put(value.toUInt()) - 8 -> buffer.put(value) - else -> Unit + private fun writeInt(value: ULong, byteWidth: ByteWidth) { + buffer.requestAdditionalCapacity(byteWidth.value) + when(byteWidth.value) { + 1 -> buffer.put(value.toUByte()) + 2 -> buffer.put(value.toUShort()) + 4 -> buffer.put(value.toUInt()) + 8 -> buffer.put(value) + else -> Unit + } } // Align to prepare for writing a scalar with a certain size. @@ -621,6 +633,7 @@ public class FlexBuffersBuilder( private fun align(alignment: BitWidth): ByteWidth { val byteWidth = 1 shl alignment.value var padBytes = paddingBytes(buffer.writePosition, byteWidth) + buffer.requestCapacity(buffer.capacity + padBytes) while (padBytes-- != 0) { buffer.put(ZeroByte) } @@ -659,6 +672,7 @@ public class FlexBuffersBuilder( private inline fun createVector(key: Int, start: Int, length: Int, keys: Value? = null): Value { return createAnyVector(key, start, length, T_VECTOR, keys) { // add types since we are not creating a typed vector. + buffer.requestAdditionalCapacity(stack.size) for (i in start until stack.size) { buffer.put(stack[i].storedPackedType(it)) } @@ -668,6 +682,7 @@ public class FlexBuffersBuilder( private fun putMap(key: Int, start: Int, length: Int, keys: Value? = null): Value { return createAnyVector(key, start, length, T_MAP, keys) { // add types since we are not creating a typed vector. + buffer.requestAdditionalCapacity(stack.size) for (i in start until stack.size) { buffer.put(stack[i].storedPackedType(it)) } @@ -692,7 +707,7 @@ public class FlexBuffersBuilder( keys: Value? = null, crossinline typeBlock: (BitWidth) -> Unit = {} ): Value { - // Figure out smallest bit width we can store this vector with. + // Figure out the smallest bit width we can store this vector with. var bitWidth = W_8.max(length.toULong().widthInUBits()) var prefixElems = 1 if (keys != null) { diff --git a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/FlexBuffersInternals.kt b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/FlexBuffersInternals.kt index 15d002725..9a1e24355 100644 --- a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/FlexBuffersInternals.kt +++ b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/FlexBuffersInternals.kt @@ -17,13 +17,18 @@ package com.google.flatbuffers.kotlin -public inline class BitWidth(public val value: Int) { +import kotlin.jvm.JvmInline + +@JvmInline +public value class BitWidth(public val value: Int) { public inline fun max(other: BitWidth): BitWidth = if (this.value >= other.value) this else other } -public inline class ByteWidth(public val value: Int) +@JvmInline +public value class ByteWidth(public val value: Int) -public inline class FlexBufferType(public val value: Int) { +@JvmInline +public value class FlexBufferType(public val value: Int) { public operator fun minus(other: FlexBufferType): FlexBufferType = FlexBufferType(this.value - other.value) public operator fun plus(other: FlexBufferType): FlexBufferType = FlexBufferType(this.value + other.value) public operator fun compareTo(other: FlexBufferType): Int = this.value - other.value @@ -108,11 +113,12 @@ internal fun FlexBufferType.isIndirectScalar(): Boolean = when (this) { internal fun FlexBufferType.isTypedVector(): Boolean = this >= T_VECTOR_INT && this <= T_VECTOR_STRING_DEPRECATED || this == T_VECTOR_BOOL -internal fun FlexBufferType.isTypedVectorElementType(): Boolean = (this.value in T_INT.value..T_KEY.value) || this == T_BOOL +internal fun FlexBufferType.isTypedVectorElementType(): Boolean = + (this.value in T_INT.value..T_KEY.value) || this == T_BOOL // returns the typed vector of a given scalar type. internal fun FlexBufferType.toTypedVector(): FlexBufferType = (this - T_INT) + T_VECTOR_INT -// returns the element type of a given typed vector. +// returns the element type of given typed vector. internal fun FlexBufferType.toElementTypedVector(): FlexBufferType = this - T_VECTOR_INT + T_INT // Holds information about the elements inserted on the buffer. @@ -126,7 +132,8 @@ internal data class Value( inline fun storedPackedType(parentBitWidth: BitWidth = W_8): Byte = packedType(storedWidth(parentBitWidth), type) - private inline fun packedType(bitWidth: BitWidth, type: FlexBufferType): Byte = (bitWidth.value or (type.value shl 2)).toByte() + private inline fun packedType(bitWidth: BitWidth, type: FlexBufferType): Byte = + (bitWidth.value or (type.value shl 2)).toByte() private inline fun storedWidth(parentBitWidth: BitWidth): BitWidth = if (type.isInline()) minBitWidth.max(parentBitWidth) else minBitWidth @@ -199,7 +206,6 @@ internal fun FlexBufferType.typeToString(): String = when (this) { } // Few repeated values used in hot path is cached here -internal val emptyBuffer = ArrayReadWriteBuffer(1) internal fun emptyBlob() = Blob(emptyBuffer, 1, ByteWidth(1)) internal fun emptyVector() = Vector(emptyBuffer, 1, ByteWidth(1)) internal fun emptyMap() = Map(ArrayReadWriteBuffer(3), 3, ByteWidth(1)) diff --git a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Utf8.kt b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Utf8.kt index 4b02cc5c8..bccc1518f 100644 --- a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Utf8.kt +++ b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Utf8.kt @@ -14,6 +14,7 @@ * limitations under the License. */ @file:Suppress("NOTHING_TO_INLINE") + package com.google.flatbuffers.kotlin public object Utf8 { @@ -32,15 +33,15 @@ public object Utf8 { var i = 0 // This loop optimizes for pure ASCII. - while (i < utf16Length && sequence[i].toInt() < 0x80) { + while (i < utf16Length && sequence[i].code < 0x80) { i++ } // This loop optimizes for chars less than 0x800. while (i < utf16Length) { val c = sequence[i] - if (c.toInt() < 0x800) { - utf8Length += 0x7f - c.toInt() ushr 31 // branch free! + if (c.code < 0x800) { + utf8Length += 0x7f - c.code ushr 31 // branch free! } else { utf8Length += encodedLengthGeneral(sequence, i) break @@ -60,8 +61,8 @@ public object Utf8 { var i = start while (i < utf16Length) { val c = sequence[i] - if (c.toInt() < 0x800) { - utf8Length += 0x7f - c.toInt() ushr 31 // branch free! + if (c.code < 0x800) { + utf8Length += 0x7f - c.code ushr 31 // branch free! } else { utf8Length += 2 if (c.isSurrogate()) { @@ -109,7 +110,7 @@ public object Utf8 { public inline fun isFourByte(b: Byte): Boolean = b < 0xF8.toByte() public fun handleOneByte(byte1: Byte, resultArr: CharArray, resultPos: Int) { - resultArr[resultPos] = byte1.toChar() + resultArr[resultPos] = byte1.toInt().toChar() } public fun handleTwoBytes( @@ -209,21 +210,21 @@ public object Utf8 { return 0 } val c = input[start] - return if (c.toInt() < 0x80) { + return if (c.code < 0x80) { // One byte (0xxx xxxx) - out[0] = c.toByte() + out[0] = c.code.toByte() 1 - } else if (c.toInt() < 0x800) { + } else if (c.code < 0x800) { // Two bytes (110x xxxx 10xx xxxx) - out[0] = (0xC0 or (c.toInt() ushr 6)).toByte() - out[1] = (0x80 or (0x3F and c.toInt())).toByte() + out[0] = (0xC0 or (c.code ushr 6)).toByte() + out[1] = (0x80 or (0x3F and c.code)).toByte() 2 } else if (c < Char.MIN_SURROGATE || Char.MAX_SURROGATE < c) { // Three bytes (1110 xxxx 10xx xxxx 10xx xxxx) // Maximum single-char code point is 0xFFFF, 16 bits. - out[0] = (0xE0 or (c.toInt() ushr 12)).toByte() - out[1] = (0x80 or (0x3F and (c.toInt() ushr 6))).toByte() - out[2] = (0x80 or (0x3F and c.toInt())).toByte() + out[0] = (0xE0 or (c.code ushr 12)).toByte() + out[1] = (0x80 or (0x3F and (c.code ushr 6))).toByte() + out[2] = (0x80 or (0x3F and c.code)).toByte() 3 } else { // Four bytes (1111 xxxx 10xx xxxx 10xx xxxx 10xx xxxx) @@ -330,7 +331,10 @@ public object Utf8 { return resultArr.concatToString(0, resultPos) } - public fun encodeUtf8Array(input: CharSequence, out: ByteArray, offset: Int = 0, length: Int = out.size - offset): Int { + public fun encodeUtf8Array(input: CharSequence, + out: ByteArray, + offset: Int = 0, + length: Int = out.size - offset): Int { val utf16Length = input.length var j = offset var i = 0 @@ -341,8 +345,8 @@ public object Utf8 { if (utf16Length == 0) return 0 var cc: Char = input[i] - while (i < utf16Length && i + j < limit && input[i].also { cc = it }.toInt() < 0x80) { - out[j + i] = cc.toByte() + while (i < utf16Length && i + j < limit && input[i].also { cc = it }.code < 0x80) { + out[j + i] = cc.code.toByte() i++ } if (i == utf16Length) { @@ -352,16 +356,16 @@ public object Utf8 { var c: Char while (i < utf16Length) { c = input[i] - if (c.toInt() < 0x80 && j < limit) { - out[j++] = c.toByte() - } else if (c.toInt() < 0x800 && j <= limit - 2) { // 11 bits, two UTF-8 bytes - out[j++] = (0xF shl 6 or (c.toInt() ushr 6)).toByte() - out[j++] = (0x80 or (0x3F and c.toInt())).toByte() + if (c.code < 0x80 && j < limit) { + out[j++] = c.code.toByte() + } else if (c.code < 0x800 && j <= limit - 2) { // 11 bits, two UTF-8 bytes + out[j++] = (0xF shl 6 or (c.code ushr 6)).toByte() + out[j++] = (0x80 or (0x3F and c.code)).toByte() } else if ((c < Char.MIN_SURROGATE || Char.MAX_SURROGATE < c) && j <= limit - 3) { // Maximum single-char code point is 0xFFFF, 16 bits, three UTF-8 bytes - out[j++] = (0xF shl 5 or (c.toInt() ushr 12)).toByte() - out[j++] = (0x80 or (0x3F and (c.toInt() ushr 6))).toByte() - out[j++] = (0x80 or (0x3F and c.toInt())).toByte() + out[j++] = (0xF shl 5 or (c.code ushr 12)).toByte() + out[j++] = (0x80 or (0x3F and (c.code ushr 6))).toByte() + out[j++] = (0x80 or (0x3F and c.code)).toByte() } else if (j <= limit - 4) { // Minimum code point represented by a surrogate pair is 0x10000, 17 bits, // four UTF-8 bytes @@ -384,7 +388,7 @@ public object Utf8 { ) { errorSurrogate(i, utf16Length) } - error("Failed writing character ${c.toShort().toString(radix = 16)} at index $j") + error("Failed writing character ${c.code.toShort().toString(radix = 16)} at index $j") } i++ } @@ -400,13 +404,13 @@ public object Utf8 { return toCodePoint(c1, c2) } } - return c1.toInt() + return c1.code } private fun isSurrogatePair(high: Char, low: Char) = high.isHighSurrogate() and low.isLowSurrogate() - private fun toCodePoint(high: Char, low: Char): Int = (high.toInt() shl 10) + low.toInt() + - (MIN_SUPPLEMENTARY_CODE_POINT - (Char.MIN_HIGH_SURROGATE.toInt() shl 10) - Char.MIN_LOW_SURROGATE.toInt()) + private fun toCodePoint(high: Char, low: Char): Int = (high.code shl 10) + low.code + + (MIN_SUPPLEMENTARY_CODE_POINT - (Char.MIN_HIGH_SURROGATE.code shl 10) - Char.MIN_LOW_SURROGATE.code) private fun errorSurrogate(i: Int, utf16Length: Int): Unit = error("Unpaired surrogate at index $i of $utf16Length length") diff --git a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/JSON.kt b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/json.kt similarity index 90% rename from kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/JSON.kt rename to kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/json.kt index ee20138b3..33867974b 100644 --- a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/JSON.kt +++ b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/json.kt @@ -19,6 +19,7 @@ package com.google.flatbuffers.kotlin import com.google.flatbuffers.kotlin.FlexBuffersBuilder.Companion.SHARE_KEYS_AND_STRINGS import kotlin.experimental.and +import kotlin.jvm.JvmInline import kotlin.math.pow /** @@ -72,19 +73,19 @@ public fun Map.toJson(): String = ArrayReadWriteBuffer(1024).let { toJson(it); i * @param out [ReadWriteBuffer] the JSON will be written. */ public fun Map.toJson(out: ReadWriteBuffer) { - out.put('{'.toByte()) + out.put('{'.code.toByte()) // key values pairs for (i in 0 until size) { val key = keyAt(i) out.jsonEscape(buffer, key.start, key.sizeInBytes) - out.put(':'.toByte()) + out.put(':'.code.toByte()) get(i).toJson(out) if (i != size - 1) { - out.put(','.toByte()) + out.put(','.code.toByte()) } } // close bracket - out.put('}'.toByte()) + out.put('}'.code.toByte()) } /** @@ -97,20 +98,21 @@ public fun Vector.toJson(): String = ArrayReadWriteBuffer(1024).let { toJson(it) * @param out that the JSON is being concatenated. */ public fun Vector.toJson(out: ReadWriteBuffer) { - out.put('['.toByte()) - for (i in 0 until size) { + out.put('['.code.toByte()) + for (i in indices) { get(i).toJson(out) if (i != size - 1) { - out.put(','.toByte()) + out.put(','.code.toByte()) } } - out.put(']'.toByte()) + out.put(']'.code.toByte()) } /** * JSONParser class is used to parse a JSON as FlexBuffers. Calling [JSONParser.parse] fiils [output] * and returns a [Reference] ready to be used. */ +@ExperimentalUnsignedTypes public class JSONParser(public var output: FlexBuffersBuilder = FlexBuffersBuilder(1024, SHARE_KEYS_AND_STRINGS)) { private var readPos = 0 private var scopes = ScopeStack() @@ -150,7 +152,7 @@ public class JSONParser(public var output: FlexBuffersBuilder = FlexBuffersBuild TOK_NULL -> T_NULL.also { output.putNull(key) } TOK_BEGIN_QUOTE -> parseString(data, key) TOK_NUMBER -> parseNumber(data, data.data(), key) - else -> makeError(data, "Unexpected Character while parsing", 'x'.toByte()) + else -> makeError(data, "Unexpected Character while parsing", 'x'.code.toByte()) } } @@ -590,7 +592,7 @@ public class JSONParser(public var output: FlexBuffersBuilder = FlexBuffersBuild val end = i + 4 while (i < end) { val part: Byte = data[i] - result = (result.toInt() shl 4).toChar() + result = (result.code shl 4).toChar() result += when (part) { in CHAR_0..CHAR_9 -> part - CHAR_0 in CHAR_a..CHAR_f -> part - CHAR_a + 10 @@ -606,13 +608,13 @@ public class JSONParser(public var output: FlexBuffersBuilder = FlexBuffersBuild CHAR_r -> '\r' CHAR_n -> '\n' CHAR_f -> 12.toChar() // '\f' - CHAR_DOUBLE_QUOTE, CHAR_BACKSLASH, CHAR_FORWARDSLASH -> byte1.toChar() + CHAR_DOUBLE_QUOTE, CHAR_BACKSLASH, CHAR_FORWARDSLASH -> byte1.toInt().toChar() else -> makeError(data, "Invalid escape sequence.", byte1) } } private fun Byte.print(): String = when (this) { - in 0x21..0x7E -> "'${this.toChar()}'" // visible ascii chars + in 0x21..0x7E -> "'${this.toInt().toChar()}'" // visible ascii chars CHAR_EOF -> "EOF" else -> "'0x${this.toString(16)}'" } @@ -685,20 +687,21 @@ private inline fun ReadWriteBuffer.jsonEscape(data: ReadBuffer, start: Int, size // Following escape strategy defined in RFC7159. private val JSON_ESCAPE_CHARS: Array = arrayOfNulls(128).apply { - this['\n'.toInt()] = "\\n".encodeToByteArray() - this['\t'.toInt()] = "\\t".encodeToByteArray() - this['\r'.toInt()] = "\\r".encodeToByteArray() - this['\b'.toInt()] = "\\b".encodeToByteArray() + this['\n'.code] = "\\n".encodeToByteArray() + this['\t'.code] = "\\t".encodeToByteArray() + this['\r'.code] = "\\r".encodeToByteArray() + this['\b'.code] = "\\b".encodeToByteArray() this[0x0c] = "\\f".encodeToByteArray() - this['"'.toInt()] = "\\\"".encodeToByteArray() - this['\\'.toInt()] = "\\\\".encodeToByteArray() + this['"'.code] = "\\\"".encodeToByteArray() + this['\\'.code] = "\\\\".encodeToByteArray() for (i in 0..0x1f) { this[i] = "\\u${i.toPaddedHex()}".encodeToByteArray() } } // Scope is used to the define current space that the scanner is operating. -private inline class Scope(val id: Int) +@JvmInline +private value class Scope(val id: Int) private val SCOPE_DOC_EMPTY = Scope(0) private val SCOPE_DOC_FILLED = Scope(1) private val SCOPE_OBJ_EMPTY = Scope(2) @@ -738,7 +741,8 @@ private class ScopeStack( } } -private inline class Token(val id: Int) { +@JvmInline +private value class Token(val id: Int) { fun print(): String = when (this) { TOK_EOF -> "TOK_EOF" TOK_NONE -> "TOK_NONE" @@ -767,41 +771,41 @@ private val TOK_FALSE = Token(7) private val TOK_NULL = Token(8) private val TOK_BEGIN_QUOTE = Token(9) -private const val CHAR_NEWLINE = '\n'.toByte() -private const val CHAR_OPEN_OBJECT = '{'.toByte() -private const val CHAR_COLON = ':'.toByte() -private const val CHAR_CLOSE_OBJECT = '}'.toByte() -private const val CHAR_OPEN_ARRAY = '['.toByte() -private const val CHAR_CLOSE_ARRAY = ']'.toByte() -private const val CHAR_DOUBLE_QUOTE = '"'.toByte() -private const val CHAR_BACKSLASH = '\\'.toByte() -private const val CHAR_FORWARDSLASH = '/'.toByte() -private const val CHAR_f = 'f'.toByte() -private const val CHAR_a = 'a'.toByte() -private const val CHAR_r = 'r'.toByte() -private const val CHAR_t = 't'.toByte() -private const val CHAR_n = 'n'.toByte() -private const val CHAR_b = 'b'.toByte() -private const val CHAR_e = 'e'.toByte() -private const val CHAR_E = 'E'.toByte() -private const val CHAR_u = 'u'.toByte() -private const val CHAR_A = 'A'.toByte() -private const val CHAR_F = 'F'.toByte() +private const val CHAR_NEWLINE = '\n'.code.toByte() +private const val CHAR_OPEN_OBJECT = '{'.code.toByte() +private const val CHAR_COLON = ':'.code.toByte() +private const val CHAR_CLOSE_OBJECT = '}'.code.toByte() +private const val CHAR_OPEN_ARRAY = '['.code.toByte() +private const val CHAR_CLOSE_ARRAY = ']'.code.toByte() +private const val CHAR_DOUBLE_QUOTE = '"'.code.toByte() +private const val CHAR_BACKSLASH = '\\'.code.toByte() +private const val CHAR_FORWARDSLASH = '/'.code.toByte() +private const val CHAR_f = 'f'.code.toByte() +private const val CHAR_a = 'a'.code.toByte() +private const val CHAR_r = 'r'.code.toByte() +private const val CHAR_t = 't'.code.toByte() +private const val CHAR_n = 'n'.code.toByte() +private const val CHAR_b = 'b'.code.toByte() +private const val CHAR_e = 'e'.code.toByte() +private const val CHAR_E = 'E'.code.toByte() +private const val CHAR_u = 'u'.code.toByte() +private const val CHAR_A = 'A'.code.toByte() +private const val CHAR_F = 'F'.code.toByte() private const val CHAR_EOF = (-1).toByte() -private const val CHAR_COMMA = ','.toByte() -private const val CHAR_0 = '0'.toByte() -private const val CHAR_1 = '1'.toByte() -private const val CHAR_2 = '2'.toByte() -private const val CHAR_3 = '3'.toByte() -private const val CHAR_4 = '4'.toByte() -private const val CHAR_5 = '5'.toByte() -private const val CHAR_6 = '6'.toByte() -private const val CHAR_7 = '7'.toByte() -private const val CHAR_8 = '8'.toByte() -private const val CHAR_9 = '9'.toByte() -private const val CHAR_MINUS = '-'.toByte() -private const val CHAR_PLUS = '+'.toByte() -private const val CHAR_DOT = '.'.toByte() +private const val CHAR_COMMA = ','.code.toByte() +private const val CHAR_0 = '0'.code.toByte() +private const val CHAR_1 = '1'.code.toByte() +private const val CHAR_2 = '2'.code.toByte() +private const val CHAR_3 = '3'.code.toByte() +private const val CHAR_4 = '4'.code.toByte() +private const val CHAR_5 = '5'.code.toByte() +private const val CHAR_6 = '6'.code.toByte() +private const val CHAR_7 = '7'.code.toByte() +private const val CHAR_8 = '8'.code.toByte() +private const val CHAR_9 = '9'.code.toByte() +private const val CHAR_MINUS = '-'.code.toByte() +private const val CHAR_PLUS = '+'.code.toByte() +private const val CHAR_DOT = '.'.code.toByte() // This template utilizes the One Definition Rule to create global arrays in a // header. As seen in: diff --git a/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/Asserts.kt b/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/Asserts.kt new file mode 100644 index 000000000..563a5a6bc --- /dev/null +++ b/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/Asserts.kt @@ -0,0 +1,55 @@ +package com.google.flatbuffers.kotlin + +import kotlin.test.assertTrue + +fun assertArrayEquals(expected: Array, actual: Array) = + assertTrue(expected contentEquals actual, arrayFailMessage(expected, actual)) + +fun assertArrayEquals(expected: IntArray, actual: IntArray) = + assertTrue(expected contentEquals actual, arrayFailMessage(expected, actual)) + +fun assertArrayEquals(expected: ShortArray, actual: ShortArray) = + assertTrue(expected contentEquals actual, arrayFailMessage(expected, actual)) + +fun assertArrayEquals(expected: LongArray, actual: LongArray) = + assertTrue(expected contentEquals actual, arrayFailMessage(expected, actual)) + +fun assertArrayEquals(expected: ByteArray, actual: ByteArray) = + assertTrue(expected contentEquals actual, arrayFailMessage(expected, actual)) + +fun assertArrayEquals(expected: DoubleArray, actual: DoubleArray) = + assertTrue(expected contentEquals actual, arrayFailMessage(expected, actual)) + +fun assertArrayEquals(expected: FloatArray, actual: FloatArray) = + assertTrue(expected contentEquals actual, arrayFailMessage(expected, actual)) + +fun arrayFailMessage(expected: Array, actual: Array): String = + failMessage(expected.contentToString(), actual.contentToString()) + +fun arrayFailMessage(expected: IntArray, actual: IntArray): String = + failMessage(expected.contentToString(), actual.contentToString()) + +fun arrayFailMessage(expected: ShortArray, actual: ShortArray): String = + failMessage(expected.contentToString(), actual.contentToString()) + +fun arrayFailMessage(expected: LongArray, actual: LongArray): String = + failMessage(expected.contentToString(), actual.contentToString()) + +fun failMessage(expected: String, actual: String): String = + "Expected: $expected\nActual: $actual" + +fun arrayFailMessage(expected: FloatArray, actual: FloatArray): String { + return "Expected: ${expected.contentToString()}\nActual: ${actual.contentToString()}" +} + +fun arrayFailMessage(expected: DoubleArray, actual: DoubleArray): String { + return "Expected: ${expected.contentToString()}\nActual: ${actual.contentToString()}" +} + +fun arrayFailMessage(expected: BooleanArray, actual: BooleanArray): String { + return "Expected: ${expected.contentToString()}\nActual: ${actual.contentToString()}" +} + +fun arrayFailMessage(expected: ByteArray, actual: ByteArray): String { + return "Expected: ${expected.contentToString()}\nActual: ${actual.contentToString()}" +} diff --git a/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/BuffersTest.kt b/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/BuffersTest.kt new file mode 100644 index 000000000..acae4dd38 --- /dev/null +++ b/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/BuffersTest.kt @@ -0,0 +1,78 @@ +package com.google.flatbuffers.kotlin + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class BuffersTest { + + @Test + fun readBufferStringTest() { + val text = "Hello world!" + val bytes = text.encodeToByteArray() + val fullRead = ArrayReadBuffer(bytes) + val helloRead = ArrayReadBuffer(bytes, limit = 5) + val worldRead = fullRead.slice(6, 6) + + assertEquals(bytes.size, fullRead.limit) + assertEquals(text, fullRead.getString(0, fullRead.limit)) + assertEquals("Hello" , helloRead.getString(0, helloRead.limit)) + assertEquals("world!" , worldRead.getString()) + assertEquals(fullRead.getString(0, 5) , helloRead.getString(0, helloRead.limit)) + assertEquals(fullRead.getString(6, 6) , worldRead.getString(0, worldRead.limit)) + + for (i in 0 until helloRead.limit) { + assertEquals(fullRead[i], helloRead[i]) + } + for (i in 0 until worldRead.limit) { + assertEquals(fullRead[6 + i], worldRead[i]) + } + } + + @Test + fun readWriteBufferPrimitivesTest() { + val text = "Hello world!" + val bytes = text.encodeToByteArray() + val wt = ArrayReadWriteBuffer(bytes) + wt.requestCapacity(4096) + wt.put("Tests") + val str1 = wt.writePosition + assertEquals("Tests world!", wt.getString(0, bytes.size)) + assertEquals("Tests", wt.getString(0, str1)) + wt.put(Int.MAX_VALUE) + assertEquals(Int.MAX_VALUE, wt.getInt(str1)) + + val pos = wt.writePosition + wt.put(Double.NEGATIVE_INFINITY) + assertEquals(Double.NEGATIVE_INFINITY, wt.getDouble(pos)) + + val jap = " are really すごい!".encodeToByteArray() + wt.writePosition = str1 + wt.put(jap) + assertEquals("Tests are really すごい!", wt.getString()) + } + + @Test + fun readWriteBufferGrowthTest() { + val a = ArrayReadWriteBuffer(1) + assertEquals(1, a.capacity) + a.put(0.toByte()) + assertEquals(1, a.capacity) + assertFailsWith(IndexOutOfBoundsException::class) { a.put(0xFF.toShort()) } + a.requestCapacity(8) + a.writePosition = 0 + a.put(0xFF.toShort()) + assertEquals(8, a.capacity) + assertEquals(0xFF, a.getShort(0)) + + a.requestCapacity(8 + 12) + a.put(ByteArray(12) { it.toByte() }) + + // we grow as power or two, so 20 jumps to 32 + assertEquals(32, a.capacity) + a.requestCapacity(513, false) + assertEquals(1024, a.capacity) + a.requestCapacity(234, false) + assertEquals(1024, a.capacity) + } +} diff --git a/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/ByteArrayTest.kt b/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/ByteArrayTest.kt index 560b0f359..776ab9574 100644 --- a/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/ByteArrayTest.kt +++ b/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/ByteArrayTest.kt @@ -139,60 +139,9 @@ class ByteArrayTest { val testSet = "∮ E⋅da = Q" val encoded = testSet.encodeToByteArray() val data = ByteArray(encoded.size) - data.setString(0, testSet) + data.setCharSequence(0, testSet) assertArrayEquals(encoded, data) assertEquals(testSet, data.getString(0, encoded.size)) } } -fun assertArrayEquals(expected: Array, actual: Array) = - assertTrue(expected contentEquals actual, arrayFailMessage(expected, actual)) - -fun assertArrayEquals(expected: IntArray, actual: IntArray) = - assertTrue(expected contentEquals actual, arrayFailMessage(expected, actual)) - -fun assertArrayEquals(expected: ShortArray, actual: ShortArray) = - assertTrue(expected contentEquals actual, arrayFailMessage(expected, actual)) - -fun assertArrayEquals(expected: LongArray, actual: LongArray) = - assertTrue(expected contentEquals actual, arrayFailMessage(expected, actual)) - -fun assertArrayEquals(expected: ByteArray, actual: ByteArray) = - assertTrue(expected contentEquals actual, arrayFailMessage(expected, actual)) - -fun assertArrayEquals(expected: DoubleArray, actual: DoubleArray) = - assertTrue(expected contentEquals actual, arrayFailMessage(expected, actual)) - -fun assertArrayEquals(expected: FloatArray, actual: FloatArray) = - assertTrue(expected contentEquals actual, arrayFailMessage(expected, actual)) - -fun arrayFailMessage(expected: Array, actual: Array): String = - failMessage(expected.contentToString(), actual.contentToString()) - -fun arrayFailMessage(expected: IntArray, actual: IntArray): String = - failMessage(expected.contentToString(), actual.contentToString()) - -fun arrayFailMessage(expected: ShortArray, actual: ShortArray): String = - failMessage(expected.contentToString(), actual.contentToString()) - -fun arrayFailMessage(expected: LongArray, actual: LongArray): String = - failMessage(expected.contentToString(), actual.contentToString()) - -fun failMessage(expected: String, actual: String): String = - "Expected: $expected\nActual: $actual" - -fun arrayFailMessage(expected: FloatArray, actual: FloatArray): String { - return "Expected: ${expected.contentToString()}\nActual: ${actual.contentToString()}" -} - -fun arrayFailMessage(expected: DoubleArray, actual: DoubleArray): String { - return "Expected: ${expected.contentToString()}\nActual: ${actual.contentToString()}" -} - -fun arrayFailMessage(expected: BooleanArray, actual: BooleanArray): String { - return "Expected: ${expected.contentToString()}\nActual: ${actual.contentToString()}" -} - -fun arrayFailMessage(expected: ByteArray, actual: ByteArray): String { - return "Expected: ${expected.contentToString()}\nActual: ${actual.contentToString()}" -} diff --git a/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/FlatBufferBuilderTest.kt b/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/FlatBufferBuilderTest.kt new file mode 100644 index 000000000..90c10215b --- /dev/null +++ b/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/FlatBufferBuilderTest.kt @@ -0,0 +1,575 @@ +/* + * Copyright 2021 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:Suppress("UNCHECKED_CAST") + +package com.google.flatbuffers.kotlin + +import Attacker +import AttackerOffsetArray +import CharacterEArray +import dictionaryLookup.LongFloatEntry +import dictionaryLookup.LongFloatMap +import Movie +import dictionaryLookup.LongFloatEntryOffsetArray +import myGame.example.* +import myGame.example.Test.Companion.createTest +import optionalScalars.OptionalByte +import optionalScalars.ScalarStuff +import kotlin.test.Test +import kotlin.test.assertEquals + + +@ExperimentalUnsignedTypes +class FlatBufferBuilderTest { + + @Test + fun testSingleTable() { + val fbb = FlatBufferBuilder() + val name = fbb.createString("Frodo") + val invValues = ubyteArrayOf(10u, 11u, 12u, 13u, 14u) + val inv = Monster.createInventoryVector(fbb, invValues) + Monster.startMonster(fbb) + Monster.addPos( + fbb, Vec3.createVec3( + fbb, 1.0f, 2.0f, 3.0f, 3.0, + Color.Green, 5.toShort(), 6.toByte() + ) + ) + Monster.addHp(fbb, 80.toShort()) + Monster.addName(fbb, name) + Monster.addMana(fbb, 150) + Monster.addInventory(fbb, inv) + Monster.addTestType(fbb, AnyE.Monster) + Monster.addTestbool(fbb, true) + Monster.addTesthashu32Fnv1(fbb, (Int.MAX_VALUE + 1L).toUInt()) + val root = Monster.endMonster(fbb) + fbb.finish(root) + + val monster = Monster.asRoot(fbb.dataBuffer()) + assertEquals(monster.name, "Frodo") + assertEquals(monster.mana, 150.toShort()) + assertEquals(monster.hp, 80) + + val pos = monster.pos!! + assertEquals(monster.inventory(0), invValues[0]) + assertEquals(monster.inventory(1), invValues[1]) + assertEquals(monster.inventory(2), invValues[2]) + assertEquals(monster.inventory(3), invValues[3]) + assertEquals(pos.x, 1.0f) + assertEquals(pos.y, 2.0f) + assertEquals(pos.z, 3.0f) + assertEquals(pos.test1, 3.0) + assertEquals(pos.test2, Color.Green) + assertEquals(pos.test3!!.a, 5.toShort()) + assertEquals(pos.test3!!.b, 6.toByte()) + + val inventoryBuffer = monster.inventoryAsBuffer() + assertEquals(invValues.size, inventoryBuffer.limit) + for (i in invValues.indices) { + assertEquals(invValues[i], inventoryBuffer.getUByte(i)) + } + } + + @Test + fun testSortedVector() { + val fbb = FlatBufferBuilder() + val names = arrayOf(fbb.createString("Frodo"), fbb.createString("Barney"), fbb.createString("Wilma")) + val monsters = MonsterOffsetArray(3) { + Monster.startMonster(fbb) + Monster.addName(fbb, names[it]) + Monster.endMonster(fbb) + } + val ary = Monster.createTestarrayoftablesVector(fbb, monsters) + Monster.startMonster(fbb) + Monster.addName(fbb, names[0]) + Monster.addTestarrayoftables(fbb, ary) + val root = Monster.endMonster(fbb) + fbb.finish(root) + val a = Monster.asRoot(fbb.dataBuffer()) + assertEquals(a.name, "Frodo") + assertEquals(a.testarrayoftablesLength, 3) + val monster0 = a.testarrayoftables(0)!! + val monster1 = a.testarrayoftables(1)!! + val monster2 = a.testarrayoftables(2)!! + assertEquals(monster0.name, "Frodo") + assertEquals(monster1.name, "Barney") + assertEquals(monster2.name, "Wilma") + + // test AsBuffer feature + + } + + @Test + fun testCreateBufferVector() { + val fbb = FlatBufferBuilder(16) + val str = fbb.createString("MyMonster") + val inventory = ubyteArrayOf(0u, 1u, 2u, 3u, 4u, 5u, 6u, 88u, 99u, 122u, 1u) + val vec = Monster.createInventoryVector(fbb, inventory) + Monster.startMonster(fbb) + Monster.addInventory(fbb, vec) + Monster.addName(fbb, str) + val monster1 = Monster.endMonster(fbb) + Monster.finishMonsterBuffer(fbb, monster1) + val monsterObject: Monster = Monster.asRoot(fbb.dataBuffer()) + val iBuffer = monsterObject.inventoryAsBuffer() + + assertEquals(monsterObject.inventoryLength, inventory.size) + assertEquals(iBuffer.limit, inventory.size) + + for (i in inventory.indices) { + assertEquals(inventory[i], monsterObject.inventory(i)) + assertEquals(inventory[i], iBuffer.getUByte(i)) + } + } + + @Test + fun testCreateUninitializedVector() { + val fbb = FlatBufferBuilder(16) + val str = fbb.createString("MyMonster") + val inventory = byteArrayOf(10, 11, 12, 13, 14) + val uninitializedBuffer = fbb.createUnintializedVector(1, inventory.size, 1) + for (i in inventory) { + uninitializedBuffer.put(i) + } + val vec = fbb.endVector() + Monster.startMonster(fbb) + Monster.addInventory(fbb, vec) + Monster.addName(fbb, str) + val monster1 = Monster.endMonster(fbb) + Monster.finishMonsterBuffer(fbb, monster1) + val monsterObject: Monster = Monster.asRoot(fbb.dataBuffer()) + assertEquals(inventory[1].toUByte(), monsterObject.inventory(1)) + assertEquals(inventory.size, monsterObject.inventoryLength) + val inventoryBuffer = monsterObject.inventoryAsBuffer() + assertEquals(inventory[1].toInt().toUByte(), inventoryBuffer.getUByte(1)) + assertEquals(inventory.size, inventoryBuffer.limit) + } + + @Test + fun testBuilderBasics() { + val fbb = FlatBufferBuilder() + val names = arrayOf(fbb.createString("Frodo"), fbb.createString("Barney"), fbb.createString("Wilma")) + val off = Array>(3) { Offset(0) } + Monster.startMonster(fbb) + Monster.addName(fbb, names[0]) + off[0] = Monster.endMonster(fbb) + Monster.startMonster(fbb) + Monster.addName(fbb, names[1]) + off[1] = Monster.endMonster(fbb) + Monster.startMonster(fbb) + Monster.addName(fbb, names[2]) + off[2] = Monster.endMonster(fbb) + val sortMons = fbb.createSortedVectorOfTables(Monster(), off) + + // We set up the same values as monsterdata.json: + + val inv = Monster.createInventoryVector(fbb, byteArrayOf(0,1,2,3,4).toUByteArray()) + + val fred = fbb.createString("Fred") + Monster.startMonster(fbb) + Monster.addName(fbb, fred) + val mon2 = Monster.endMonster(fbb) + + Monster.startTest4Vector(fbb, 2) + createTest(fbb, 10.toShort(), 20.toByte()) + createTest(fbb, 30.toShort(), 40.toByte()) + val test4 = fbb.endVector() + + val strings = StringOffsetArray(2) { fbb.createString("test$it") } + val testArrayOfString = + Monster.createTestarrayofstringVector(fbb, strings) + + Monster.startMonster(fbb) + Monster.addName(fbb, names[0]) + Monster.addPos(fbb, Vec3.createVec3( + fbb, 1.0f, 2.0f, 3.0f, 3.0, + Color.Green, 5.toShort(), 6.toByte() + )) + Monster.addHp(fbb, 80) + Monster.addMana(fbb, 150) + Monster.addInventory(fbb, inv) + Monster.addTestType(fbb, AnyE.Monster) + Monster.addTest(fbb, mon2.toUnion()) + Monster.addTest4(fbb, test4) + Monster.addTestarrayofstring(fbb, testArrayOfString) + Monster.addTestbool(fbb, true) + Monster.addTesthashu32Fnv1(fbb, (Int.MAX_VALUE + 1L).toUInt()) + Monster.addTestarrayoftables(fbb, sortMons) + val mon = Monster.endMonster(fbb) + Monster.finishMonsterBuffer(fbb, mon) + //Attempt to mutate Monster fields and check whether the buffer has been mutated properly + // revert to original values after testing + val monster = Monster.asRoot(fbb.dataBuffer()) + + // mana is optional and does not exist in the buffer so the mutation should fail + // the mana field should retain its default value + assertEquals(monster.mana, 150.toShort()) + assertEquals(monster.hp, 80) + + // Accessing a vector of sorted by the key tables + assertEquals(monster.testarrayoftables(0)!!.name, "Barney") + assertEquals(monster.testarrayoftables(1)!!.name, "Frodo") + assertEquals(monster.testarrayoftables(2)!!.name, "Wilma") + + // Example of searching for a table by the key + assertEquals(monster.testarrayoftablesByKey("Frodo")!!.name, "Frodo") + assertEquals(monster.testarrayoftablesByKey("Barney")!!.name, "Barney") + assertEquals(monster.testarrayoftablesByKey("Wilma")!!.name, "Wilma") + + for (i in 0 until monster.inventoryLength) { + assertEquals(monster.inventory(i), (i).toUByte()) + } + + // get a struct field and edit one of its fields + val pos2 = monster.pos!! + assertEquals(pos2.x, 1.0f) + assertEquals(pos2.test2, Color.Green) + } + + @Test + fun testVectorOfUnions() { + val fbb = FlatBufferBuilder() + val swordAttackDamage = 1 + val attacker = Attacker.createAttacker(fbb, swordAttackDamage).toUnion() + val attackers = UnionOffsetArray(1) { attacker } + val characters = CharacterEArray(1) + characters[0] = CharacterE.MuLan.value + + Movie.finishMovieBuffer( + fbb, + Movie.createMovie( + fbb, + CharacterE.MuLan, + attacker, + Movie.createCharactersTypeVector(fbb, characters), + Movie.createCharactersVector(fbb, attackers) + ) + ) + + val movie: Movie = Movie.asRoot(fbb.dataBuffer()) + + + + assertEquals(movie.charactersTypeLength, 1) + assertEquals(movie.charactersLength, 1) + + assertEquals(movie.charactersType(0), CharacterE.MuLan) + assertEquals((movie.characters(Attacker(), 0) as Attacker).swordAttackDamage, swordAttackDamage) + } + + @Test + fun TestVectorOfBytes() { + val fbb = FlatBufferBuilder(16) + var str = fbb.createString("ByteMonster") + val data = ubyteArrayOf(0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u) + var offset = Monster.createInventoryVector(fbb, data) + Monster.startMonster(fbb) + Monster.addName(fbb, str) + Monster.addInventory(fbb, offset) + var monster1 = Monster.endMonster(fbb) + Monster.finishMonsterBuffer(fbb, monster1) + + val monsterObject = Monster.asRoot(fbb.dataBuffer()) + assertEquals("ByteMonster", monsterObject.name) + assertEquals(data.size, monsterObject.inventoryLength) + assertEquals(monsterObject.inventory(4), data[4]) + offset = fbb.createByteVector(data.toByteArray()) as VectorOffset // TODO: fix me + str = fbb.createString("ByteMonster") + Monster.startMonster(fbb) + Monster.addName(fbb, str) + Monster.addInventory(fbb, offset) + monster1 = Monster.endMonster(fbb) + Monster.finishMonsterBuffer(fbb, monster1) + + val monsterObject2 = Monster.asRoot(fbb.dataBuffer()) + assertEquals(monsterObject2.inventoryLength, data.size) + for (i in data.indices) { + assertEquals(monsterObject2.inventory(i), data[i]) + } + fbb.clear() + offset = fbb.createByteVector(data.toByteArray(), 3, 4) as VectorOffset + str = fbb.createString("ByteMonster") + Monster.startMonster(fbb) + Monster.addName(fbb, str) + Monster.addInventory(fbb, offset) + monster1 = Monster.endMonster(fbb) + Monster.finishMonsterBuffer(fbb, monster1) + + val monsterObject3 = Monster.asRoot(fbb.dataBuffer()) + assertEquals(monsterObject3.inventoryLength, 4) + assertEquals(monsterObject3.inventory(0), data[3]) + fbb.clear() + offset = Monster.createInventoryVector(fbb, data) + str = fbb.createString("ByteMonster") + Monster.startMonster(fbb) + Monster.addName(fbb, str) + Monster.addInventory(fbb, offset) + monster1 = Monster.endMonster(fbb) + Monster.finishMonsterBuffer(fbb, monster1) + + val monsterObject4 = Monster.asRoot(fbb.dataBuffer()) + assertEquals(monsterObject4.inventoryLength, data.size) + assertEquals(monsterObject4.inventory(8), 8u) + fbb.clear() + + val largeData = ByteArray(1024) + offset = fbb.createByteVector(largeData) as VectorOffset //TODO: fix me + str = fbb.createString("ByteMonster") + Monster.startMonster(fbb) + Monster.addName(fbb, str) + Monster.addInventory(fbb, offset) + monster1 = Monster.endMonster(fbb) + Monster.finishMonsterBuffer(fbb, monster1) + + val monsterObject5 = Monster.asRoot(fbb.dataBuffer()) + assertEquals(monsterObject5.inventoryLength, largeData.size) + assertEquals(monsterObject5.inventory(25), largeData[25].toUByte()) + fbb.clear() + + var bb = ArrayReadBuffer(largeData, 512) + offset = fbb.createByteVector(bb) as VectorOffset //TODO: fix me + str = fbb.createString("ByteMonster") + Monster.startMonster(fbb) + Monster.addName(fbb, str) + Monster.addInventory(fbb, offset) + monster1 = Monster.endMonster(fbb) + Monster.finishMonsterBuffer(fbb, monster1) + val monsterObject6 = Monster.asRoot(fbb.dataBuffer()) + assertEquals(monsterObject6.inventoryLength, 512) + assertEquals(monsterObject6.inventory(0), largeData[0].toUByte()) + fbb.clear() + + bb = ArrayReadBuffer(largeData, largeData.size - 216) + val stringBuffer = ArrayReadBuffer("AlreadyBufferedString".encodeToByteArray()) + offset = fbb.createByteVector(bb) as VectorOffset //TODO: fix me + str = fbb.createString(stringBuffer) + Monster.startMonster(fbb) + Monster.addName(fbb, str) + Monster.addInventory(fbb, offset) + monster1 = Monster.endMonster(fbb) + Monster.finishMonsterBuffer(fbb, monster1) + + val monsterObject7 = Monster.asRoot(fbb.dataBuffer()) + assertEquals(monsterObject7.inventoryLength, 216) + assertEquals("AlreadyBufferedString", monsterObject7.name) + } + + @Test + fun testEnums() { + assertEquals(Color.name(Color.Red), "Red") + assertEquals(Color.name(Color.Blue), "Blue") + assertEquals(AnyE.name(AnyE.None), "NONE") + assertEquals(AnyE.name(AnyE.Monster), "Monster") + } + + @Test + fun testSharedStringPool() { + val fb = FlatBufferBuilder(1) + val testString = "My string" + val offset = fb.createSharedString(testString) + for (i in 0..9) { + assertEquals(offset, fb.createSharedString(testString)) + } + } + + @Test + fun testScalarOptional() { + val fbb = FlatBufferBuilder(1) + ScalarStuff.startScalarStuff(fbb) + var pos = ScalarStuff.endScalarStuff(fbb) + fbb.finish(pos) + var scalarStuff: ScalarStuff = ScalarStuff.asRoot(fbb.dataBuffer()) + assertEquals(scalarStuff.justI8, 0.toByte()) + assertEquals(scalarStuff.maybeI8, null) + assertEquals(scalarStuff.defaultI8, 42.toByte()) + assertEquals(scalarStuff.justU8, 0u) + assertEquals(scalarStuff.maybeU8, null) + assertEquals(scalarStuff.defaultU8, 42u) + assertEquals(scalarStuff.justI16, 0.toShort()) + assertEquals(scalarStuff.maybeI16, null) + assertEquals(scalarStuff.defaultI16, 42.toShort()) + assertEquals(scalarStuff.justU16, 0u) + assertEquals(scalarStuff.maybeU16, null) + assertEquals(scalarStuff.defaultU16, 42u) + assertEquals(scalarStuff.justI32, 0) + assertEquals(scalarStuff.maybeI32, null) + assertEquals(scalarStuff.defaultI32, 42) + assertEquals(scalarStuff.justU32, 0u) + assertEquals(scalarStuff.maybeU32, null) + assertEquals(scalarStuff.defaultU32, 42u) + assertEquals(scalarStuff.justI64, 0L) + assertEquals(scalarStuff.maybeI64, null) + assertEquals(scalarStuff.defaultI64, 42L) + assertEquals(scalarStuff.justU64, 0UL) + assertEquals(scalarStuff.maybeU64, null) + assertEquals(scalarStuff.defaultU64, 42UL) + assertEquals(scalarStuff.justF32, 0.0f) + assertEquals(scalarStuff.maybeF32, null) + assertEquals(scalarStuff.defaultF32, 42.0f) + assertEquals(scalarStuff.justF64, 0.0) + assertEquals(scalarStuff.maybeF64, null) + assertEquals(scalarStuff.defaultF64, 42.0) + assertEquals(scalarStuff.justBool, false) + assertEquals(scalarStuff.maybeBool, null) + assertEquals(scalarStuff.defaultBool, true) + assertEquals(scalarStuff.justEnum, OptionalByte.None) + assertEquals(scalarStuff.maybeEnum, null) + assertEquals(scalarStuff.defaultEnum, OptionalByte.One) + fbb.clear() + ScalarStuff.startScalarStuff(fbb) + ScalarStuff.addJustI8(fbb, 5.toByte()) + ScalarStuff.addMaybeI8(fbb, 5.toByte()) + ScalarStuff.addDefaultI8(fbb, 5.toByte()) + ScalarStuff.addJustU8(fbb, 6u) + ScalarStuff.addMaybeU8(fbb, 6u) + ScalarStuff.addDefaultU8(fbb, 6u) + ScalarStuff.addJustI16(fbb, 7.toShort()) + ScalarStuff.addMaybeI16(fbb, 7.toShort()) + ScalarStuff.addDefaultI16(fbb, 7.toShort()) + ScalarStuff.addJustU16(fbb, 8u) + ScalarStuff.addMaybeU16(fbb, 8u) + ScalarStuff.addDefaultU16(fbb, 8u) + ScalarStuff.addJustI32(fbb, 9) + ScalarStuff.addMaybeI32(fbb, 9) + ScalarStuff.addDefaultI32(fbb, 9) + ScalarStuff.addJustU32(fbb, 10u) + ScalarStuff.addMaybeU32(fbb, 10u) + ScalarStuff.addDefaultU32(fbb, 10u) + ScalarStuff.addJustI64(fbb, 11L) + ScalarStuff.addMaybeI64(fbb, 11L) + ScalarStuff.addDefaultI64(fbb, 11L) + ScalarStuff.addJustU64(fbb, 12UL) + ScalarStuff.addMaybeU64(fbb, 12UL) + ScalarStuff.addDefaultU64(fbb, 12UL) + ScalarStuff.addJustF32(fbb, 13.0f) + ScalarStuff.addMaybeF32(fbb, 13.0f) + ScalarStuff.addDefaultF32(fbb, 13.0f) + ScalarStuff.addJustF64(fbb, 14.0) + ScalarStuff.addMaybeF64(fbb, 14.0) + ScalarStuff.addDefaultF64(fbb, 14.0) + ScalarStuff.addJustBool(fbb, true) + ScalarStuff.addMaybeBool(fbb, true) + ScalarStuff.addDefaultBool(fbb, true) + ScalarStuff.addJustEnum(fbb, OptionalByte.Two) + ScalarStuff.addMaybeEnum(fbb, OptionalByte.Two) + ScalarStuff.addDefaultEnum(fbb, OptionalByte.Two) + pos = ScalarStuff.endScalarStuff(fbb) + fbb.finish(pos) + scalarStuff = ScalarStuff.asRoot(fbb.dataBuffer()) + assertEquals(scalarStuff.justI8, 5.toByte()) + assertEquals(scalarStuff.maybeI8, 5.toByte()) + assertEquals(scalarStuff.defaultI8, 5.toByte()) + assertEquals(scalarStuff.justU8, 6u) + assertEquals(scalarStuff.maybeU8, 6u) + assertEquals(scalarStuff.defaultU8, 6u) + assertEquals(scalarStuff.justI16, 7.toShort()) + assertEquals(scalarStuff.maybeI16, 7.toShort()) + assertEquals(scalarStuff.defaultI16, 7.toShort()) + assertEquals(scalarStuff.justU16, 8u) + assertEquals(scalarStuff.maybeU16, 8u) + assertEquals(scalarStuff.defaultU16, 8u) + assertEquals(scalarStuff.justI32, 9) + assertEquals(scalarStuff.maybeI32, 9) + assertEquals(scalarStuff.defaultI32, 9) + assertEquals(scalarStuff.justU32, 10u) + assertEquals(scalarStuff.maybeU32, 10u) + assertEquals(scalarStuff.defaultU32, 10u) + assertEquals(scalarStuff.justI64, 11L) + assertEquals(scalarStuff.maybeI64, 11L) + assertEquals(scalarStuff.defaultI64, 11L) + assertEquals(scalarStuff.justU64, 12UL) + assertEquals(scalarStuff.maybeU64, 12UL) + assertEquals(scalarStuff.defaultU64, 12UL) + assertEquals(scalarStuff.justF32, 13.0f) + assertEquals(scalarStuff.maybeF32, 13.0f) + assertEquals(scalarStuff.defaultF32, 13.0f) + assertEquals(scalarStuff.justF64, 14.0) + assertEquals(scalarStuff.maybeF64, 14.0) + assertEquals(scalarStuff.defaultF64, 14.0) + assertEquals(scalarStuff.justBool, true) + assertEquals(scalarStuff.maybeBool, true) + assertEquals(scalarStuff.defaultBool, true) + assertEquals(scalarStuff.justEnum, OptionalByte.Two) + assertEquals(scalarStuff.maybeEnum, OptionalByte.Two) + assertEquals(scalarStuff.defaultEnum, OptionalByte.Two) + } + +// @todo Seems like nesting code generation is broken for all generators. +// disabling test for now. +// @Test +// fun testNamespaceNesting() { +// // reference / manipulate these to verify compilation +// val fbb = FlatBufferBuilder(1) +// TableInNestedNS.startTableInNestedNS(fbb) +// TableInNestedNS.addFoo(fbb, 1234) +// val nestedTableOff = TableInNestedNS.endTableInNestedNs(fbb) +// TableInFirstNS.startTableInFirstNS(fbb) +// TableInFirstNS.addFooTable(fbb, nestedTableOff) +// TableInFirstNS.endTableInFirstNs(fbb) +// } + + @Test + fun testNestedFlatBuffer() { + val nestedMonsterName = "NestedMonsterName" + val nestedMonsterHp: Short = 600 + val nestedMonsterMana: Short = 1024 + val fbb1 = FlatBufferBuilder(16) + val str1 = fbb1.createString(nestedMonsterName) + Monster.startMonster(fbb1) + Monster.addName(fbb1, str1) + Monster.addHp(fbb1, nestedMonsterHp) + Monster.addMana(fbb1, nestedMonsterMana) + val monster1 = Monster.endMonster(fbb1) + Monster.finishMonsterBuffer(fbb1, monster1) + val fbb1Bytes: ByteArray = fbb1.sizedByteArray() + val fbb2 = FlatBufferBuilder(16) + val str2 = fbb2.createString("My Monster") + val nestedBuffer = Monster.createTestnestedflatbufferVector(fbb2, fbb1Bytes.toUByteArray()) + Monster.startMonster(fbb2) + Monster.addName(fbb2, str2) + Monster.addHp(fbb2, 50.toShort()) + Monster.addMana(fbb2, 32.toShort()) + Monster.addTestnestedflatbuffer(fbb2, nestedBuffer) + val monster = Monster.endMonster(fbb2) + Monster.finishMonsterBuffer(fbb2, monster) + + // Now test the data extracted from the nested buffer + val mons = Monster.asRoot(fbb2.dataBuffer()) + val nestedMonster = mons.testnestedflatbufferAsMonster + assertEquals(nestedMonsterMana, nestedMonster!!.mana) + assertEquals(nestedMonsterHp, nestedMonster.hp) + assertEquals(nestedMonsterName, nestedMonster.name) + } + + @Test + fun testDictionaryLookup() { + val fbb = FlatBufferBuilder(16) + val lfIndex = LongFloatEntry.createLongFloatEntry(fbb, 0, 99.0f) + val vectorEntriesIdx = LongFloatMap.createEntriesVector(fbb, LongFloatEntryOffsetArray(1) { lfIndex }) + val rootIdx = LongFloatMap.createLongFloatMap(fbb, vectorEntriesIdx) + LongFloatMap.finishLongFloatMapBuffer(fbb, rootIdx) + val map: LongFloatMap = LongFloatMap.asRoot(fbb.dataBuffer()) + + assertEquals(1, map.entriesLength) + + val e: LongFloatEntry = map.entries(0)!! + assertEquals(0L, e.key) + assertEquals(99.0f, e.value) + val e2: LongFloatEntry = map.entriesByKey(0)!! + assertEquals(0L, e2.key) + assertEquals(99.0f, e2.value) + } +} diff --git a/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/FlexBuffersTest.kt b/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/FlexBuffersTest.kt index 71820b638..524dd0e0d 100644 --- a/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/FlexBuffersTest.kt +++ b/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/FlexBuffersTest.kt @@ -21,6 +21,7 @@ import kotlin.test.Test import kotlin.test.assertEquals class FlexBuffersTest { + @Test fun testWriteInt() { val values = listOf( diff --git a/kotlin/flatbuffers-kotlin/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/ByteArray.kt b/kotlin/flatbuffers-kotlin/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/ByteArray.kt index 7e5d37629..94da7d3f0 100644 --- a/kotlin/flatbuffers-kotlin/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/ByteArray.kt +++ b/kotlin/flatbuffers-kotlin/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/ByteArray.kt @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + @file:JvmName("JVMByteArray") @file:Suppress("NOTHING_TO_INLINE") diff --git a/kotlin/flatbuffers-kotlin/src/jvmTest/kotlin/com/google/flatbuffers/kotlin/Utf8Test.kt b/kotlin/flatbuffers-kotlin/src/jvmTest/kotlin/com/google/flatbuffers/kotlin/Utf8Test.kt index 96b9c0a47..9b2174166 100644 --- a/kotlin/flatbuffers-kotlin/src/jvmTest/kotlin/com/google/flatbuffers/kotlin/Utf8Test.kt +++ b/kotlin/flatbuffers-kotlin/src/jvmTest/kotlin/com/google/flatbuffers/kotlin/Utf8Test.kt @@ -22,11 +22,16 @@ class Utf8Test { @Test fun testUtf8EncodingDecoding() { - val utf8Lines = String(this.javaClass.classLoader.getResourceAsStream("utf8_sample.txt")!!.readBytes()) + val classLoader = this.javaClass.classLoader + val utf8Lines = String(classLoader.getResourceAsStream("utf8_sample.txt")!!.readBytes()) .split("\n") .filter { it.trim().isNotEmpty() } - val utf8Bytes = utf8Lines.map { s -> ByteArray(Utf8.encodedLength(s)).also { Utf8.encodeUtf8Array(s, it) } } + val utf8Bytes = utf8Lines.map { + s -> ByteArray(Utf8.encodedLength(s)).also { + Utf8.encodeUtf8Array(s, it) + } + } utf8Bytes.indices.forEach { assertArrayEquals(utf8Lines[it].encodeToByteArray(), utf8Bytes[it]) assertEquals(utf8Lines[it], Utf8.decodeUtf8Array(utf8Bytes[it])) diff --git a/kotlin/flatbuffers-kotlin/src/nativeMain/kotlin/com/google/flatbuffers/kotlin/ByteArray.kt b/kotlin/flatbuffers-kotlin/src/nativeMain/kotlin/com/google/flatbuffers/kotlin/ByteArray.kt index e9dc08752..7c6095224 100644 --- a/kotlin/flatbuffers-kotlin/src/nativeMain/kotlin/com/google/flatbuffers/kotlin/ByteArray.kt +++ b/kotlin/flatbuffers-kotlin/src/nativeMain/kotlin/com/google/flatbuffers/kotlin/ByteArray.kt @@ -14,6 +14,7 @@ * limitations under the License. */ @file:Suppress("NOTHING_TO_INLINE") + package com.google.flatbuffers.kotlin /** diff --git a/kotlin/gradle.properties b/kotlin/gradle.properties index 5cb42e3b8..d16170eaf 100644 --- a/kotlin/gradle.properties +++ b/kotlin/gradle.properties @@ -1,4 +1,6 @@ #Gradle +group = "com.google.flatbuffers" +version = "2.0.0-SNAPSHOT" org.gradle.parallel=true org.gradle.caching=true @@ -7,8 +9,12 @@ org.gradle.caching=true kotlin.code.style=official #MPP -kotlin.mpp.enableGranularSourceSetsMetadata=true -kotlin.native.enableDependencyPropagation=false -kotlin.mpp.enableCompatibilityMetadataVariant=true +kotlin.js.compiler=ir +kotlin.native.ignoreDisabledTargets=true kotlin.mpp.stability.nowarn=true kotlin.incremental.multiplatform=true +kotlin.native.binary.memoryModel=experimental + +kotlin.native.distribution.type=prebuilt + +org.gradle.jvmargs="-XX:+HeapDumpOnOutOfMemoryError" diff --git a/kotlin/gradle/libs.versions.toml b/kotlin/gradle/libs.versions.toml index e3230b7da..5e7923649 100644 --- a/kotlin/gradle/libs.versions.toml +++ b/kotlin/gradle/libs.versions.toml @@ -1,7 +1,8 @@ [versions] -kotlin = "1.7.21" +kotlin = "1.8.21" +plugin-kotlin = "1.6.10" plugin-gver = "0.42.0" -kotlinx-benchmark = "0.4.6" +kotlinx-benchmark = "0.4.8" junit = "4.12" gson = "2.8.5" moshi-kotlin = "1.11.0" @@ -11,9 +12,14 @@ kotlin-compiler = { module = "org.jetbrains.kotlin:kotlin-compiler", version.ref moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi-kotlin" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } kotlinx-benchmark-runtime = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "kotlinx-benchmark" } -plugin-gver = { module = "com.github.ben-manes:gradle-versions-plugin", version.ref = "plugin-gver" } + +junit = { module="junit:junit", version.ref="junit"} +kotlin-allopen = { module = "org.jetbrains.kotlin:kotlin-allopen", version.ref = "kotlin"} + plugin-kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } plugin-kotlinx-benchmark = { module="org.jetbrains.kotlinx:kotlinx-benchmark-plugin", version.ref="kotlinx-benchmark"} plugin-jmhreport = { module = "gradle.plugin.io.morethan.jmhreport:gradle-jmh-report", version="0.9.0" } plugin-download = { module = "de.undercouch:gradle-download-task", version = "5.3.0"} -junit = { module="junit:junit", version.ref="junit"} + + + diff --git a/src/BUILD.bazel b/src/BUILD.bazel index 1084e76b0..679b10f78 100644 --- a/src/BUILD.bazel +++ b/src/BUILD.bazel @@ -123,6 +123,7 @@ cc_library( "idl_gen_json_schema.h", "idl_gen_kotlin.cpp", "idl_gen_kotlin.h", + "idl_gen_kotlin_kmp.cpp", "idl_gen_lobster.cpp", "idl_gen_lobster.h", "idl_gen_php.cpp", diff --git a/src/flatc_main.cpp b/src/flatc_main.cpp index e4ddb00f3..45f95a08f 100644 --- a/src/flatc_main.cpp +++ b/src/flatc_main.cpp @@ -125,6 +125,11 @@ int main(int argc, const char *argv[]) { "Generate Kotlin classes for tables/structs" }, flatbuffers::NewKotlinCodeGenerator()); + flatc.RegisterCodeGenerator( + flatbuffers::FlatCOption{ "", "kotlin-kmp", "", + "Generate Kotlin multiplatform classes for tables/structs" }, + flatbuffers::NewKotlinKMPCodeGenerator()); + flatc.RegisterCodeGenerator( flatbuffers::FlatCOption{ "", "lobster", "", "Generate Lobster files for tables/structs" }, diff --git a/src/idl_gen_kotlin.h b/src/idl_gen_kotlin.h index 22d8ff6ca..c3861a2c1 100644 --- a/src/idl_gen_kotlin.h +++ b/src/idl_gen_kotlin.h @@ -24,6 +24,8 @@ namespace flatbuffers { // Constructs a new Kotlin code generator. std::unique_ptr NewKotlinCodeGenerator(); +// Constructs a new Kotlin code generator. +std::unique_ptr NewKotlinKMPCodeGenerator(); } // namespace flatbuffers #endif // FLATBUFFERS_IDL_GEN_KOTLIN_H_ diff --git a/src/idl_gen_kotlin_kmp.cpp b/src/idl_gen_kotlin_kmp.cpp new file mode 100644 index 000000000..cda77d94d --- /dev/null +++ b/src/idl_gen_kotlin_kmp.cpp @@ -0,0 +1,1623 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// independent from idl_parser, since this code is not needed for most clients + +#include +#include + +#include "flatbuffers/code_generators.h" +#include "flatbuffers/idl.h" +#include "flatbuffers/util.h" +#include "idl_gen_kotlin.h" +#include "idl_namer.h" + +namespace flatbuffers { + +namespace kotlin { + +namespace { + +typedef std::map > FbbParamMap; +static TypedFloatConstantGenerator KotlinFloatGen("Double.", "Float.", "NaN", + "POSITIVE_INFINITY", + "NEGATIVE_INFINITY"); + +static const CommentConfig comment_config = { "/**", " *", " */" }; +static const std::string ident_pad = " "; +static std::set KotlinKeywords() { + return { "package", "as", "typealias", "class", "this", "super", + "val", "var", "fun", "for", "null", "true", + "false", "is", "in", "throw", "return", "break", + "continue", "object", "if", "try", "else", "while", + "do", "when", "interface", "typeof", "Any", "Character" }; +} + +static Namer::Config KotlinDefaultConfig() { + return { /*types=*/Case::kKeep, + /*constants=*/Case::kUpperCamel, + /*methods=*/Case::kLowerCamel, + /*functions=*/Case::kKeep, + /*fields=*/Case::kLowerCamel, + /*variables=*/Case::kLowerCamel, + /*variants=*/Case::kUpperCamel, + /*enum_variant_seperator=*/"", // I.e. Concatenate. + /*escape_keywords=*/Namer::Config::Escape::AfterConvertingCase, + /*namespaces=*/Case::kLowerCamel, + /*namespace_seperator=*/".", + /*object_prefix=*/"", + /*object_suffix=*/"T", + /*keyword_prefix=*/"", + /*keyword_suffix=*/"E", + /*filenames=*/Case::kUpperCamel, + /*directories=*/Case::kLowerCamel, + /*output_path=*/"", + /*filename_suffix=*/"", + /*filename_extension=*/".kt" }; +} +} // namespace + +class KotlinKMPGenerator : public BaseGenerator { + public: + KotlinKMPGenerator(const Parser &parser, const std::string &path, + const std::string &file_name) + : BaseGenerator(parser, path, file_name, "", ".", "kt"), + namer_(WithFlagOptions(KotlinDefaultConfig(), parser.opts, path), + KotlinKeywords()) {} + + KotlinKMPGenerator &operator=(const KotlinKMPGenerator &); + bool generate() FLATBUFFERS_OVERRIDE { + std::string one_file_code; + + for (auto it = parser_.enums_.vec.begin(); it != parser_.enums_.vec.end(); + ++it) { + CodeWriter enumWriter(ident_pad); + auto &enum_def = **it; + + GenEnum(enum_def, enumWriter); + enumWriter += ""; + GenEnumOffsetAlias(enum_def, enumWriter); + + if (parser_.opts.one_file) { + one_file_code += enumWriter.ToString(); + } else { + if (!SaveType(namer_.EscapeKeyword(enum_def.name), + *enum_def.defined_namespace, enumWriter.ToString(), true)) + return false; + } + } + + for (auto it = parser_.structs_.vec.begin(); + it != parser_.structs_.vec.end(); ++it) { + CodeWriter structWriter(ident_pad); + auto &struct_def = **it; + + GenStruct(struct_def, structWriter, parser_.opts); + structWriter += ""; + GenStructOffsetAlias(struct_def, structWriter); + + if (parser_.opts.one_file) { + one_file_code += structWriter.ToString(); + } else { + if (!SaveType(namer_.EscapeKeyword(struct_def.name), + *struct_def.defined_namespace, structWriter.ToString(), + true)) + return false; + } + } + + if (parser_.opts.one_file) { + return SaveType(file_name_, *parser_.current_namespace_, one_file_code, + true); + } + return true; + } + + std::string TypeInNameSpace(const Namespace *ns, + const std::string &name = "") const { + auto qualified = namer_.Namespace(*ns); + return qualified.empty() ? name : qualified + qualifying_separator_ + name; + } + + std::string TypeInNameSpace(const Definition &def, + const std::string &suffix = "") const { + return TypeInNameSpace(def.defined_namespace, def.name + suffix); + } + + // Save out the generated code for a single class while adding + // declaration boilerplate. + bool SaveType(const std::string &defname, const Namespace &ns, + const std::string &classcode, bool needs_includes) const { + if (!classcode.length()) return true; + + std::string code = + "// " + std::string(FlatBuffersGeneratedWarning()) + "\n\n"; + auto qualified = ns.GetFullyQualifiedName(""); + std::string namespace_name = namer_.Namespace(ns); + if (!namespace_name.empty()) { + code += "package " + namespace_name; + code += "\n\n"; + } + if (needs_includes) { code += "import com.google.flatbuffers.kotlin.*\n"; } + code += "import kotlin.jvm.JvmInline\n"; + code += classcode; + const std::string dirs = + namer_.Directories(ns, SkipDir::None, Case::kUnknown); + EnsureDirExists(dirs); + const std::string filename = + dirs + namer_.File(defname, /*skips=*/SkipFile::Suffix); + return SaveFile(filename.c_str(), code, false); + } + + static bool IsEnum(const Type &type) { + return type.enum_def != nullptr && IsInteger(type.base_type); + } + + std::string GenerateKotlinPrimiteArray(const Type &type) const { + if (IsScalar(type.base_type) && !IsEnum(type)) { return GenType(type); } + + if (IsEnum(type) || type.base_type == BASE_TYPE_UTYPE) { + return TypeInNameSpace(type.enum_def->defined_namespace, + namer_.Type(*type.enum_def)); + } + switch (type.base_type) { + case BASE_TYPE_STRUCT: + return "Offset<" + TypeInNameSpace(*type.struct_def) + ">"; + case BASE_TYPE_UNION: return "UnionOffset"; + case BASE_TYPE_STRING: return "Offset"; + case BASE_TYPE_UTYPE: return "Offset"; + default: return "Offset<" + GenTypeBasic(type.element) + ">"; + } + } + + std::string GenerateKotlinOffsetArray(const Type &type) const { + if (IsScalar(type.base_type) && !IsEnum(type)) { + return GenType(type) + "Array"; + } + + if (IsEnum(type) || type.base_type == BASE_TYPE_UTYPE) { + return TypeInNameSpace(type.enum_def->defined_namespace, + namer_.Type(*type.enum_def) + "Array"); + } + switch (type.base_type) { + case BASE_TYPE_STRUCT: + return TypeInNameSpace(*type.struct_def) + "OffsetArray"; + case BASE_TYPE_UNION: return "UnionOffsetArray"; + case BASE_TYPE_STRING: return "StringOffsetArray"; + case BASE_TYPE_UTYPE: return "UByteArray"; + default: return GenTypeBasic(type.element) + "OffsetArray"; + } + } + + std::string GenTypeBasic(const BaseType &type) const { + switch (type) { + case BASE_TYPE_NONE: + case BASE_TYPE_UTYPE: return "UByte"; + case BASE_TYPE_BOOL: return "Boolean"; + case BASE_TYPE_CHAR: return "Byte"; + case BASE_TYPE_UCHAR: return "UByte"; + case BASE_TYPE_SHORT: return "Short"; + case BASE_TYPE_USHORT: return "UShort"; + case BASE_TYPE_INT: return "Int"; + case BASE_TYPE_UINT: return "UInt"; + case BASE_TYPE_LONG: return "Long"; + case BASE_TYPE_ULONG: return "ULong"; + case BASE_TYPE_FLOAT: return "Float"; + case BASE_TYPE_DOUBLE: return "Double"; + case BASE_TYPE_STRING: + case BASE_TYPE_STRUCT: return "Offset"; + case BASE_TYPE_UNION: return "UnionOffset"; + case BASE_TYPE_VECTOR: + case BASE_TYPE_ARRAY: return "VectorOffset"; + // VECTOR64 not supported + case BASE_TYPE_VECTOR64: FLATBUFFERS_ASSERT(0); + } + return "Int"; + } + + std::string GenType(const Type &type) const { + auto base_type = GenTypeBasic(type.base_type); + + if (IsEnum(type) || type.base_type == BASE_TYPE_UTYPE) { + return TypeInNameSpace(type.enum_def->defined_namespace, + namer_.Type(*type.enum_def)); + } + switch (type.base_type) { + case BASE_TYPE_ARRAY: + case BASE_TYPE_VECTOR: { + switch (type.element) { + case BASE_TYPE_STRUCT: + return base_type + "<" + TypeInNameSpace(*type.struct_def) + ">"; + case BASE_TYPE_UNION: + return base_type + "<" + GenTypeBasic(type.element) + ">"; + case BASE_TYPE_STRING: return base_type + ""; + case BASE_TYPE_UTYPE: return base_type + ""; + default: return base_type + "<" + GenTypeBasic(type.element) + ">"; + } + } + case BASE_TYPE_STRUCT: + return base_type + "<" + TypeInNameSpace(*type.struct_def) + ">"; + case BASE_TYPE_STRING: return base_type + ""; + case BASE_TYPE_UNION: return base_type; + default: return base_type; + } + // clang-format on + } + + std::string GenTypePointer(const Type &type) const { + switch (type.base_type) { + case BASE_TYPE_STRING: return "String"; + case BASE_TYPE_VECTOR: return GenTypeGet(type.VectorType()); + case BASE_TYPE_STRUCT: return TypeInNameSpace(*type.struct_def); + default: return "Table"; + } + } + + // with the addition of optional scalar types, + // we are adding the nullable '?' operator to return type of a field. + std::string GetterReturnType(const FieldDef &field) const { + auto base_type = field.value.type.base_type; + + auto r_type = GenTypeGet(field.value.type); + if (field.IsScalarOptional() || + // string, structs and unions + (base_type == BASE_TYPE_STRING || base_type == BASE_TYPE_STRUCT || + base_type == BASE_TYPE_UNION) || + // vector of anything not scalar + (base_type == BASE_TYPE_VECTOR && + !IsScalar(field.value.type.VectorType().base_type))) { + r_type += "?"; + } + return r_type; + } + + std::string GenTypeGet(const Type &type) const { + return IsScalar(type.base_type) ? GenType(type) : GenTypePointer(type); + } + + std::string GenEnumDefaultValue(const FieldDef &field) const { + auto &value = field.value; + FLATBUFFERS_ASSERT(value.type.enum_def); + auto &enum_def = *value.type.enum_def; + auto enum_val = enum_def.FindByValue(value.constant); + return enum_val ? (TypeInNameSpace(enum_def) + "." + enum_val->name) + : value.constant; + } + + // differently from GenDefaultValue, the default values are meant + // to be inserted in the buffer as the object is building. + std::string GenDefaultBufferValue(const FieldDef &field) const { + auto &value = field.value; + auto base_type = value.type.base_type; + auto field_name = field.name; + std::string suffix = IsScalar(base_type) ? LiteralSuffix(value.type) : ""; + if (field.IsScalarOptional()) { return "null"; } + if (IsFloat(base_type)) { + auto val = KotlinFloatGen.GenFloatConstant(field); + if (base_type == BASE_TYPE_DOUBLE && val.back() == 'f') { + val.pop_back(); + } + return val; + } + + if (base_type == BASE_TYPE_BOOL) { + return value.constant == "0" ? "false" : "true"; + } + + if (IsEnum(field.value.type)) { + return value.constant + suffix; + } else if ((IsVector(field.value.type) && + field.value.type.element == BASE_TYPE_UTYPE) || + (IsVector(field.value.type) && + field.value.type.VectorType().base_type == BASE_TYPE_UNION)) { + return value.constant; + } else { + return value.constant + suffix; + } + } + + std::string GenDefaultValue(const FieldDef &field) const { + auto &value = field.value; + auto base_type = value.type.base_type; + auto field_name = field.name; + std::string suffix = LiteralSuffix(value.type); + if (field.IsScalarOptional()) { return "null"; } + if (IsFloat(base_type)) { + auto val = KotlinFloatGen.GenFloatConstant(field); + if (base_type == BASE_TYPE_DOUBLE && val.back() == 'f') { + val.pop_back(); + } + return val; + } + + if (base_type == BASE_TYPE_BOOL) { + return value.constant == "0" ? "false" : "true"; + } + + if (IsEnum(field.value.type) || + (IsVector(field.value.type) && IsEnum(field.value.type.VectorType()))) { + return WrapEnumValue(field.value.type, value.constant + suffix); + } + + if (IsVector(field.value.type) && + (field.value.type.VectorType().base_type == BASE_TYPE_UNION || + field.value.type.VectorType().base_type == BASE_TYPE_STRUCT || + field.value.type.VectorType().base_type == BASE_TYPE_STRING)) { + return "null"; + } + if (IsVector(field.value.type)) { + switch (field.value.type.element) { + case BASE_TYPE_UTYPE: + return namer_.Type(*field.value.type.enum_def) + "(" + + value.constant + suffix + ")"; + case BASE_TYPE_UNION: + case BASE_TYPE_STRUCT: + case BASE_TYPE_STRING: return "null"; + case BASE_TYPE_BOOL: return value.constant == "0" ? "false" : "true"; + case BASE_TYPE_FLOAT: return value.constant + "f"; + case BASE_TYPE_DOUBLE: { + return value.constant + ".toDouble()"; + } + default: return value.constant + suffix; + } + } + return value.constant + suffix; + } + + void GenEnum(EnumDef &enum_def, CodeWriter &writer) const { + if (enum_def.generated) return; + + GenerateComment(enum_def.doc_comment, writer, &comment_config); + auto enum_type = namer_.Type(enum_def); + auto field_type = GenTypeBasic(enum_def.underlying_type.base_type); + writer += "@Suppress(\"unused\")"; + writer += "@JvmInline"; + writer += "value class " + enum_type + " (val value: " + field_type + ") {"; + writer.IncrementIdentLevel(); + + GenerateCompanionObject(writer, [&]() { + // Write all properties + auto vals = enum_def.Vals(); + + for (auto it = vals.begin(); it != vals.end(); ++it) { + auto &ev = **it; + auto val = enum_def.ToString(ev); + auto suffix = LiteralSuffix(enum_def.underlying_type); + writer.SetValue("name", namer_.Variant(ev)); + writer.SetValue("type", enum_type); + writer.SetValue("val", val + suffix); + GenerateComment(ev.doc_comment, writer, &comment_config); + writer += "val {{name}} = {{type}}({{val}})"; + } + + // Generate a generate string table for enum values. + // Problem is, if values are very sparse that could generate really + // big tables. Ideally in that case we generate a map lookup + // instead, but for the moment we simply don't output a table at all. + auto range = enum_def.Distance(); + // Average distance between values above which we consider a table + // "too sparse". Change at will. + static const uint64_t kMaxSparseness = 5; + if (range / static_cast(enum_def.size()) < kMaxSparseness) { + GeneratePropertyOneLine(writer, "names", "Array", [&]() { + writer += "arrayOf(\\"; + auto val = enum_def.Vals().front(); + for (auto it = vals.begin(); it != vals.end(); ++it) { + auto ev = *it; + for (auto k = enum_def.Distance(val, ev); k > 1; --k) + writer += "\"\", \\"; + val = ev; + writer += "\"" + (*it)->name + "\"\\"; + if (it + 1 != vals.end()) { writer += ", \\"; } + } + writer += ")"; + }); + std::string e_param = "e: " + enum_type; + GenerateFunOneLine( + writer, "name", e_param, "String", + [&]() { + writer += "names[e.value.toInt()\\"; + if (enum_def.MinValue()->IsNonZero()) + writer += " - " + namer_.Variant(*enum_def.MinValue()) + + ".value.toInt()\\"; + writer += "]"; + }, + parser_.opts.gen_jvmstatic); + } + }); + writer.DecrementIdentLevel(); + writer += "}"; + } + + // Returns the function name that is able to read a value of the given type. + std::string ByteBufferGetter(const Type &type, + std::string bb_var_name) const { + switch (type.base_type) { + case BASE_TYPE_STRING: return "string"; + case BASE_TYPE_STRUCT: return "__struct"; + case BASE_TYPE_UNION: return "union"; + case BASE_TYPE_VECTOR: + return ByteBufferGetter(type.VectorType(), bb_var_name); + case BASE_TYPE_INT: return bb_var_name + ".getInt"; + case BASE_TYPE_UINT: return bb_var_name + ".getUInt"; + case BASE_TYPE_SHORT: return bb_var_name + ".getShort"; + case BASE_TYPE_USHORT: return bb_var_name + ".getUShort"; + case BASE_TYPE_ULONG: return bb_var_name + ".getULong"; + case BASE_TYPE_LONG: return bb_var_name + ".getLong"; + case BASE_TYPE_FLOAT: return bb_var_name + ".getFloat"; + case BASE_TYPE_DOUBLE: return bb_var_name + ".getDouble"; + case BASE_TYPE_UTYPE: + case BASE_TYPE_UCHAR: return bb_var_name + ".getUByte"; + case BASE_TYPE_CHAR: + case BASE_TYPE_NONE: return bb_var_name + ".get"; + case BASE_TYPE_BOOL: return "0.toByte() != " + bb_var_name + ".get"; + default: return bb_var_name + "." + namer_.Method("get", GenType(type)); + } + } + + // Returns the function name that is able to read a value of the given type. + std::string GenLookupByKey(flatbuffers::FieldDef *key_field, + const std::string &bb_var_name, + const char *num = nullptr) const { + auto type = key_field->value.type; + return ByteBufferGetter(type, bb_var_name) + "(" + + GenOffsetGetter(key_field, num) + ")"; + } + + // Returns the method name for use with add/put calls. + static std::string GenMethod(const Type &type) { + return IsStruct(type) ? "Struct" : ""; + } + + // Recursively generate arguments for a constructor, to deal with nested + // structs. + void GenStructArgs(const StructDef &struct_def, CodeWriter &writer, + const char *nameprefix) const { + for (auto it = struct_def.fields.vec.begin(); + it != struct_def.fields.vec.end(); ++it) { + auto &field = **it; + if (IsStruct(field.value.type)) { + // Generate arguments for a struct inside a struct. To ensure + // names don't clash, and to make it obvious these arguments are + // constructing a nested struct, prefix the name with the field + // name. + GenStructArgs(*field.value.type.struct_def, writer, + (nameprefix + (field.name + "_")).c_str()); + } else { + writer += std::string(", ") + nameprefix + "\\"; + writer += namer_.Field(field) + ": \\"; + writer += GenType(field.value.type) + "\\"; + } + } + } + + // Recusively generate struct construction statements of the form: + // builder.putType(name); + // and insert manual padding. + void GenStructBody(const StructDef &struct_def, CodeWriter &writer, + const char *nameprefix) const { + writer.SetValue("align", NumToString(struct_def.minalign)); + writer.SetValue("size", NumToString(struct_def.bytesize)); + writer += "builder.prep({{align}}, {{size}})"; + auto fields_vec = struct_def.fields.vec; + for (auto it = fields_vec.rbegin(); it != fields_vec.rend(); ++it) { + auto &field = **it; + + if (field.padding) { + writer.SetValue("pad", NumToString(field.padding)); + writer += "builder.pad({{pad}})"; + } + if (IsStruct(field.value.type)) { + GenStructBody(*field.value.type.struct_def, writer, + (nameprefix + (field.name + "_")).c_str()); + } else { + auto suffix = IsEnum(field.value.type) ? ".value" : ""; + writer.SetValue("type", GenMethod(field.value.type)); + writer.SetValue("argname", + nameprefix + namer_.Variable(field) + suffix); + writer += "builder.put{{type}}({{argname}})"; + } + } + } + + std::string GenOffsetGetter(flatbuffers::FieldDef *key_field, + const char *num = nullptr) const { + std::string key_offset = + "offset(" + NumToString(key_field->value.offset) + ", "; + if (num) { + key_offset += num; + key_offset += ", buffer)"; + } else { + key_offset += "(bb.capacity - tableOffset).toOffset(), bb)"; + } + return key_offset; + } + + bool StructHasUnsignedField(StructDef &struct_def) { + auto vec = struct_def.fields.vec; + for (auto it = vec.begin(); it != vec.end(); ++it) { + auto &field = **it; + if (IsUnsigned(field.value.type.base_type)) { return true; } + } + return false; + } + + // This method generate alias types for offset arrays. We need it + // to avoid unboxing/boxing of offsets when put into an array. + // e.g: + // Array> generates boxing. + // So we creates a new type to avoid it: + // typealias MonterOffsetArray = IntArray + void GenStructOffsetAlias(StructDef &struct_def, CodeWriter &writer) const { + if (struct_def.generated) return; + auto name = namer_.Type(struct_def); + // This assumes offset as Ints always. + writer += "typealias " + name + "OffsetArray = OffsetArray<" + name + ">"; + + // public inline fun MonsterOffsetArray(size: Int, crossinline call: + // (Int) -> Offset): OffsetArray { + // return OffsetArray(IntArray(size) { call(it).value }) + // } + writer += ""; + writer += "inline fun " + name + + "OffsetArray(size: Int, crossinline call: (Int) -> Offset<" + + name + ">): " + name + "OffsetArray ="; + writer.IncrementIdentLevel(); + writer += name + "OffsetArray(IntArray(size) { call(it).value })"; + } + + // This method generate alias types for offset arrays. We need it + // to avoid unboxing/boxing of offsets when put into an array. + // e.g: + // Array> generates boxing. + // So we creates a new type to avoid it: + // typealias MonterOffsetArray = IntArray + void GenEnumOffsetAlias(EnumDef &enum_def, CodeWriter &writer) const { + if (enum_def.generated) return; + // This assumes offset as Ints always. + writer += "typealias " + namer_.Type(enum_def) + + "Array = " + GenTypeBasic(enum_def.underlying_type.base_type) + + "Array"; + } + + void GenStruct(StructDef &struct_def, CodeWriter &writer, + IDLOptions options) const { + if (struct_def.generated) return; + + GenerateComment(struct_def.doc_comment, writer, &comment_config); + auto fixed = struct_def.fixed; + + writer.SetValue("struct_name", namer_.Type(struct_def)); + writer.SetValue("superclass", fixed ? "Struct" : "Table"); + + writer += "@Suppress(\"unused\")"; + writer += "class {{struct_name}} : {{superclass}}() {\n"; + + writer.IncrementIdentLevel(); + + { + auto esc_type = namer_.EscapeKeyword(struct_def.name); + // Generate the init() method that sets the field in a pre-existing + // accessor object. This is to allow object reuse. + GenerateFunOneLine(writer, "init", "i: Int, buffer: ReadWriteBuffer", + esc_type, [&]() { writer += "reset(i, buffer)"; }); + + // Generate assign method + GenerateFunOneLine(writer, "assign", "i: Int, buffer: ReadWriteBuffer", + esc_type, [&]() { writer += "init(i, buffer)"; }); + writer += ""; // line break + + // Generate all getters + GenerateStructGetters(struct_def, writer); + + // Generate Static Fields + GenerateCompanionObject(writer, [&]() { + if (!struct_def.fixed) { + FieldDef *key_field = nullptr; + + // Generate version check method. + // Force compile time error if not using the same version + // runtime. + GenerateFunOneLine( + writer, "validateVersion", "", "", + [&]() { writer += "VERSION_2_0_8"; }, options.gen_jvmstatic); + + writer += ""; + GenerateGetRootAsAccessors(namer_.Type(struct_def), writer, options); + + writer += ""; + GenerateBufferHasIdentifier(struct_def, writer, options); + + writer += ""; + GenerateTableCreator(struct_def, writer, options); + + GenerateStartStructMethod(struct_def, writer, options); + + // Static Add for fields + auto fields = struct_def.fields.vec; + int field_pos = -1; + for (auto it = fields.begin(); it != fields.end(); ++it) { + auto &field = **it; + field_pos++; + if (field.deprecated) continue; + if (field.key) key_field = &field; + writer += ""; + GenerateAddField(NumToString(field_pos), field, writer, options); + if (IsVector(field.value.type)) { + auto vector_type = field.value.type.VectorType(); + if (!IsStruct(vector_type)) { + writer += ""; + GenerateCreateVectorField(field, writer, options); + } + writer += ""; + GenerateStartVectorField(field, writer, options); + } + } + + writer += ""; + GenerateEndStructMethod(struct_def, writer, options); + auto file_identifier = parser_.file_identifier_; + if (parser_.root_struct_def_ == &struct_def) { + writer += ""; + GenerateFinishStructBuffer(struct_def, file_identifier, writer, + options); + writer += ""; + GenerateFinishSizePrefixed(struct_def, file_identifier, writer, + options); + } + + if (struct_def.has_key) { + writer += ""; + GenerateLookupByKey(key_field, struct_def, writer, options); + } + } else { + writer += ""; + GenerateStaticConstructor(struct_def, writer, options); + } + }); + } + + // class closing + writer.DecrementIdentLevel(); + writer += "}"; + } + + // TODO: move key_field to reference instead of pointer + void GenerateLookupByKey(FieldDef *key_field, StructDef &struct_def, + CodeWriter &writer, const IDLOptions options) const { + std::stringstream params; + params << "obj: " << namer_.Type(struct_def) << "?" + << ", "; + params << "vectorLocation: Int, "; + params << "key: " << GenTypeGet(key_field->value.type) << ", "; + params << "bb: ReadWriteBuffer"; + + auto statements = [&]() { + auto base_type = key_field->value.type.base_type; + writer.SetValue("struct_name", namer_.Type(struct_def)); + if (base_type == BASE_TYPE_STRING) { + writer += "val byteKey = key.encodeToByteArray()"; + } + writer += "var span = bb.getInt(vectorLocation - 4)"; + writer += "var start = 0"; + writer += "while (span != 0) {"; + writer.IncrementIdentLevel(); + writer += "var middle = span / 2"; + writer += + "val tableOffset = indirect(vector" + "Location + 4 * (start + middle), bb)"; + if (IsString(key_field->value.type)) { + writer += "val comp = compareStrings(\\"; + writer += GenOffsetGetter(key_field) + "\\"; + writer += ", byteKey, bb)"; + } else { + auto get_val = GenLookupByKey(key_field, "bb"); + writer += "val value = " + get_val; + writer += "val comp = value.compareTo(key)"; + } + writer += "when {"; + writer.IncrementIdentLevel(); + writer += "comp > 0 -> span = middle"; + writer += "comp < 0 -> {"; + writer.IncrementIdentLevel(); + writer += "middle++"; + writer += "start += middle"; + writer += "span -= middle"; + writer.DecrementIdentLevel(); + writer += "}"; // end comp < 0 + writer += "else -> {"; + writer.IncrementIdentLevel(); + writer += "return (obj ?: {{struct_name}}()).assign(tableOffset, bb)"; + writer.DecrementIdentLevel(); + writer += "}"; // end else + writer.DecrementIdentLevel(); + writer += "}"; // end when + writer.DecrementIdentLevel(); + writer += "}"; // end while + writer += "return null"; + }; + GenerateFun(writer, "lookupByKey", params.str(), + namer_.Type(struct_def) + "?", statements, + options.gen_jvmstatic); + } + + void GenerateFinishSizePrefixed(StructDef &struct_def, + const std::string &identifier, + CodeWriter &writer, + const IDLOptions options) const { + auto id = identifier.length() > 0 ? ", \"" + identifier + "\"" : ""; + auto gen_type = "Offset<" + namer_.Type(struct_def.name) + ">"; + auto params = "builder: FlatBufferBuilder, offset: " + gen_type; + auto method_name = + namer_.LegacyJavaMethod2("finishSizePrefixed", struct_def, "Buffer"); + GenerateFunOneLine( + writer, method_name, params, "", + [&]() { writer += "builder.finishSizePrefixed(offset" + id + ")"; }, + options.gen_jvmstatic); + } + void GenerateFinishStructBuffer(StructDef &struct_def, + const std::string &identifier, + CodeWriter &writer, + const IDLOptions options) const { + auto id = identifier.length() > 0 ? ", \"" + identifier + "\"" : ""; + auto gen_type = "Offset<" + namer_.Type(struct_def.name) + ">"; + auto params = "builder: FlatBufferBuilder, offset: " + gen_type; + auto method_name = + namer_.LegacyKotlinMethod("finish", struct_def, "Buffer"); + GenerateFunOneLine( + writer, method_name, params, "", + [&]() { writer += "builder.finish(offset" + id + ")"; }, + options.gen_jvmstatic); + } + + void GenerateEndStructMethod(StructDef &struct_def, CodeWriter &writer, + const IDLOptions options) const { + // Generate end{{TableName}}(builder: FlatBufferBuilder) method + auto name = namer_.Method("end", struct_def.name); + auto params = "builder: FlatBufferBuilder"; + auto returns = "Offset<" + namer_.Type(struct_def) + '>'; + auto field_vec = struct_def.fields.vec; + + GenerateFun( + writer, name, params, returns, + [&]() { + writer += "val o: " + returns + " = builder.endTable()"; + writer.IncrementIdentLevel(); + for (auto it = field_vec.begin(); it != field_vec.end(); ++it) { + auto &field = **it; + if (field.deprecated || !field.IsRequired()) { continue; } + writer.SetValue("offset", NumToString(field.value.offset)); + writer.SetValue("field_name", field.name); + writer += "builder.required(o, {{offset}}, \"{{field_name}}\")"; + } + writer.DecrementIdentLevel(); + writer += "return o"; + }, + options.gen_jvmstatic); + } + + // Generate a method to create a vector from a Kotlin array. + void GenerateCreateVectorField(FieldDef &field, CodeWriter &writer, + const IDLOptions options) const { + auto vector_type = field.value.type.VectorType(); + auto method_name = namer_.Method("create", field, "vector"); + auto array_param = GenerateKotlinOffsetArray(vector_type); + auto params = "builder: FlatBufferBuilder, vector:" + array_param; + auto return_type = GenType(field.value.type); + writer.SetValue("size", NumToString(InlineSize(vector_type))); + writer.SetValue("align", NumToString(InlineAlignment(vector_type))); + writer.SetValue("root", GenMethod(vector_type)); + + GenerateFun( + writer, method_name, params, return_type, + [&]() { + writer += "builder.startVector({{size}}, vector.size, {{align}})"; + writer += "for (i in vector.size - 1 downTo 0) {"; + writer.IncrementIdentLevel(); + writer += "builder.add{{root}}(vector[i])"; + writer.DecrementIdentLevel(); + writer += "}"; + writer += "return builder.endVector()"; + }, + options.gen_jvmstatic); + } + + void GenerateStartVectorField(FieldDef &field, CodeWriter &writer, + const IDLOptions options) const { + // Generate a method to start a vector, data to be added manually + // after. + auto vector_type = field.value.type.VectorType(); + auto params = "builder: FlatBufferBuilder, numElems: Int"; + writer.SetValue("size", NumToString(InlineSize(vector_type))); + writer.SetValue("align", NumToString(InlineAlignment(vector_type))); + + GenerateFunOneLine( + writer, namer_.Method("start", field, "Vector"), params, "", + [&]() { + writer += "builder.startVector({{size}}, numElems, {{align}})"; + }, + options.gen_jvmstatic); + } + + void GenerateAddField(std::string field_pos, FieldDef &field, + CodeWriter &writer, const IDLOptions options) const { + auto field_type = GenType(field.value.type); + auto secondArg = namer_.Variable(field.name) + ": " + field_type; + + auto content = [&]() { + auto method = GenMethod(field.value.type); + auto default_value = GenDefaultBufferValue(field); + auto field_param = namer_.Field(field); + if (IsEnum(field.value.type) || IsStruct(field.value.type)) { + field_param += ".value"; + } + + writer.SetValue("field_name", namer_.Field(field)); + writer.SetValue("field_param", field_param); + writer.SetValue("method_name", method); + writer.SetValue("pos", field_pos); + writer.SetValue("default", default_value); + + if (field.key) { + // field has key attribute, so always need to exist + // even if its value is equal to default. + // Generated code will bypass default checking + // resulting in { builder.addShort(name); slot(id); } + writer += "builder.add{{method_name}}({{field_name}})"; + writer += "builder.slot({{pos}})"; + } else { + writer += "builder.add{{method_name}}({{pos}}, \\"; + writer += "{{field_param}}, {{default}})"; + } + }; + auto signature = namer_.LegacyKotlinMethod("add", field, ""); + auto params = "builder: FlatBufferBuilder, " + secondArg; + if (field.key) { + GenerateFun(writer, signature, params, "", content, + options.gen_jvmstatic); + } else { + GenerateFunOneLine(writer, signature, params, "", content, + options.gen_jvmstatic); + } + } + + // fun startMonster(builder: FlatBufferBuilder) = builder.startTable(11) + void GenerateStartStructMethod(StructDef &struct_def, CodeWriter &code, + const IDLOptions options) const { + GenerateFunOneLine( + code, namer_.LegacyJavaMethod2("start", struct_def, ""), + "builder: FlatBufferBuilder", "", + [&]() { + code += "builder.startTable(" + + NumToString(struct_def.fields.vec.size()) + ")"; + }, + options.gen_jvmstatic); + } + + void GenerateTableCreator(StructDef &struct_def, CodeWriter &writer, + const IDLOptions options) const { + // Generate a method that creates a table in one go. This is only possible + // when the table has no struct fields, since those have to be created + // inline, and there's no way to do so in Java. + bool has_no_struct_fields = true; + int num_fields = 0; + auto fields_vec = struct_def.fields.vec; + + for (auto it = fields_vec.begin(); it != fields_vec.end(); ++it) { + auto &field = **it; + if (field.deprecated) continue; + if (IsStruct(field.value.type)) { + has_no_struct_fields = false; + } else { + num_fields++; + } + } + // JVM specifications restrict default constructor params to be < 255. + // Longs and doubles take up 2 units, so we set the limit to be < 127. + if (has_no_struct_fields && num_fields && num_fields < 127) { + // Generate a table constructor of the form: + // public static int createName(FlatBufferBuilder builder, args...) + + auto name = namer_.LegacyJavaMethod2("create", struct_def, ""); + std::stringstream params; + params << "builder: FlatBufferBuilder"; + for (auto it = fields_vec.begin(); it != fields_vec.end(); ++it) { + auto &field = **it; + if (field.deprecated) continue; + params << ", " << namer_.Variable(field); + if (!IsScalar(field.value.type.base_type)) { + params << "Offset: "; + } else { + params << ": "; + } + auto optional = field.IsScalarOptional() ? "?" : ""; + params << GenType(field.value.type) << optional; + } + + GenerateFun( + writer, name, params.str(), "Offset<" + namer_.Type(struct_def) + '>', + [&]() { + writer.SetValue("vec_size", NumToString(fields_vec.size())); + writer.SetValue("end_method", + namer_.Method("end", struct_def.name)); + writer += "builder.startTable({{vec_size}})"; + + auto sortbysize = struct_def.sortbysize; + auto largest = sortbysize ? sizeof(largest_scalar_t) : 1; + for (size_t size = largest; size; size /= 2) { + for (auto it = fields_vec.rbegin(); it != fields_vec.rend(); + ++it) { + auto &field = **it; + auto base_type_size = SizeOf(field.value.type.base_type); + if (!field.deprecated && + (!sortbysize || size == base_type_size)) { + writer.SetValue("field_name", namer_.Field(field)); + + // we wrap on null check for scalar optionals + writer += field.IsScalarOptional() + ? "{{field_name}}?.run { \\" + : "\\"; + + writer += namer_.LegacyKotlinMethod("add", field, "") + + "(builder, {{field_name}}\\"; + if (!IsScalar(field.value.type.base_type)) { + writer += "Offset\\"; + } + // we wrap on null check for scalar optionals + writer += field.IsScalarOptional() ? ") }" : ")"; + } + } + } + writer += "return {{end_method}}(builder)"; + }, + options.gen_jvmstatic); + } + } + void GenerateBufferHasIdentifier(StructDef &struct_def, CodeWriter &writer, + IDLOptions options) const { + auto file_identifier = parser_.file_identifier_; + // Check if a buffer has the identifier. + if (parser_.root_struct_def_ != &struct_def || !file_identifier.length()) + return; + auto name = namer_.Function(struct_def); + GenerateFunOneLine( + writer, name + "BufferHasIdentifier", "buffer: ReadWriteBuffer", + "Boolean", + [&]() { + writer += "hasIdentifier(buffer, \"" + file_identifier + "\")"; + }, + options.gen_jvmstatic); + } + + void GenerateStructGetters(StructDef &struct_def, CodeWriter &writer) const { + auto fields_vec = struct_def.fields.vec; + FieldDef *key_field = nullptr; + for (auto it = fields_vec.begin(); it != fields_vec.end(); ++it) { + auto &field = **it; + if (field.deprecated) continue; + if (field.key) key_field = &field; + + GenerateComment(field.doc_comment, writer, &comment_config); + + auto field_name = namer_.Field(field); + auto field_type = GenTypeGet(field.value.type); + auto field_default_value = GenDefaultValue(field); + auto return_type = GetterReturnType(field); + auto bbgetter = ByteBufferGetter(field.value.type, "bb"); + auto offset_val = NumToString(field.value.offset); + auto offset_prefix = + "val o = offset(" + offset_val + "); return o != 0 ? "; + auto value_base_type = field.value.type.base_type; + // Most field accessors need to retrieve and test the field offset + // first, this is the offset value for that: + writer.SetValue("offset", NumToString(field.value.offset)); + writer.SetValue("return_type", return_type); + writer.SetValue("field_type", field_type); + writer.SetValue("field_name", field_name); + writer.SetValue("field_default", field_default_value); + writer.SetValue("bbgetter", bbgetter); + // Generate the accessors that don't do object reuse. + if (value_base_type == BASE_TYPE_STRUCT) { + // Calls the accessor that takes an accessor object with a + // new object. + // val pos + // get() = pos(Vec3()) + GenerateGetterOneLine(writer, field_name, return_type, [&]() { + writer += "{{field_name}}({{field_type}}())"; + }); + } else if (value_base_type == BASE_TYPE_VECTOR && + field.value.type.element == BASE_TYPE_STRUCT) { + // Accessors for vectors of structs also take accessor objects, + // this generates a variant without that argument. + // ex: fun weapons(j: Int) = weapons(Weapon(), j) + GenerateFunOneLine(writer, field_name, "j: Int", return_type, [&]() { + writer += "{{field_name}}({{field_type}}(), j)"; + }); + } + + if (IsScalar(value_base_type)) { + if (struct_def.fixed) { + GenerateGetterOneLine(writer, field_name, return_type, [&]() { + std::string found = "{{bbgetter}}(bufferPos + {{offset}})"; + writer += WrapEnumValue(field.value.type, found); + }); + } else { + GenerateGetterOneLine(writer, field_name, return_type, [&]() { + std::string found = "{{bbgetter}}(it + bufferPos)"; + writer += LookupFieldOneLine(offset_val, + WrapEnumValue(field.value.type, found), + "{{field_default}}"); + }); + } + } else { + switch (value_base_type) { + case BASE_TYPE_STRUCT: + if (struct_def.fixed) { + // create getter with object reuse + // ex: + // fun pos(obj: Vec3) : Vec3? = obj.assign(bufferPos + 4, bb) + // ? adds nullability annotation + GenerateFunOneLine( + writer, field_name, "obj: " + field_type, return_type, [&]() { + writer += "obj.assign(bufferPos + {{offset}}, bb)"; + }); + } else { + // create getter with object reuse + // ex: + // fun pos(obj: Vec3) : Vec3? { + // val o = offset(4) + // return if(o != 0) { + // obj.assign(o + bufferPos, bb) + // else { + // null + // } + // } + // ? adds nullability annotation + GenerateFunOneLine( + writer, field_name, "obj: " + field_type, return_type, [&]() { + auto fixed = field.value.type.struct_def->fixed; + + writer.SetValue("seek", Indirect("it + bufferPos", fixed)); + writer += LookupFieldOneLine( + offset_val, "obj.assign({{seek}}, bb)", "null"); + }); + } + break; + case BASE_TYPE_STRING: + // create string getter + // e.g. + // val Name : String? + // get() = { + // val o = offset(10) + // return if (o != 0) string(o + bufferPos) else null + // } + // ? adds nullability annotation + GenerateGetterOneLine(writer, field_name, return_type, [&]() { + writer += LookupFieldOneLine(offset_val, "string(it + bufferPos)", + "null"); + }); + break; + case BASE_TYPE_VECTOR: { + // e.g. + // fun inventory(j: Int) : UByte { + // val o = offset(14) + // return if (o != 0) { + // bb.get(vector(it) + j * 1).toUByte() + // } else { + // 0 + // } + // } + + auto vectortype = field.value.type.VectorType(); + std::string params = "j: Int"; + + if (vectortype.base_type == BASE_TYPE_STRUCT || + vectortype.base_type == BASE_TYPE_UNION) { + params = "obj: " + field_type + ", j: Int"; + } + + GenerateFunOneLine(writer, field_name, params, return_type, [&]() { + auto inline_size = NumToString(InlineSize(vectortype)); + auto index = "vector(it) + j * " + inline_size; + std::string found = ""; + writer.SetValue("index", index); + + if (IsEnum(vectortype)) { + found = "{{field_type}}({{bbgetter}}({{index}}))"; + } else { + switch (vectortype.base_type) { + case BASE_TYPE_STRUCT: { + bool fixed = vectortype.struct_def->fixed; + writer.SetValue("index", Indirect(index, fixed)); + found = "obj.assign({{index}}, bb)"; + break; + } + case BASE_TYPE_UNION: + found = "{{bbgetter}}(obj, {{index}})"; + break; + case BASE_TYPE_UTYPE: + found = "{{field_type}}({{bbgetter}}({{index}}))"; + break; + default: found = "{{bbgetter}}({{index}})"; + } + } + writer += + LookupFieldOneLine(offset_val, found, "{{field_default}}"); + }); + break; + } + case BASE_TYPE_UNION: + GenerateFunOneLine( + writer, field_name, "obj: " + field_type, return_type, [&]() { + writer += LookupFieldOneLine( + offset_val, bbgetter + "(obj, it + bufferPos)", "null"); + }); + break; + default: FLATBUFFERS_ASSERT(0); + } + } + + if (value_base_type == BASE_TYPE_VECTOR) { + // Generate Lenght functions for vectors + GenerateGetterOneLine(writer, field_name + "Length", "Int", [&]() { + writer += LookupFieldOneLine(offset_val, "vectorLength(it)", "0"); + }); + + // See if we should generate a by-key accessor. + if (field.value.type.element == BASE_TYPE_STRUCT && + !field.value.type.struct_def->fixed) { + auto &sd = *field.value.type.struct_def; + auto &fields = sd.fields.vec; + for (auto kit = fields.begin(); kit != fields.end(); ++kit) { + auto &kfield = **kit; + if (kfield.key) { + auto qualified_name = TypeInNameSpace(sd); + auto name = namer_.Method(field, "ByKey"); + auto params = "key: " + GenTypeGet(kfield.value.type); + auto rtype = qualified_name + "?"; + GenerateFunOneLine(writer, name, params, rtype, [&]() { + writer += LookupFieldOneLine( + offset_val, + qualified_name + ".lookupByKey(null, vector(it), key, bb)", + "null"); + }); + + auto param2 = "obj: " + qualified_name + + ", key: " + GenTypeGet(kfield.value.type); + GenerateFunOneLine(writer, name, param2, rtype, [&]() { + writer += LookupFieldOneLine( + offset_val, + qualified_name + ".lookupByKey(obj, vector(it), key, bb)", + "null"); + }); + + break; + } + } + } + } + + if ((value_base_type == BASE_TYPE_VECTOR && + IsScalar(field.value.type.VectorType().base_type)) || + value_base_type == BASE_TYPE_STRING) { + auto end_idx = + NumToString(value_base_type == BASE_TYPE_STRING + ? 1 + : InlineSize(field.value.type.VectorType())); + + // Generate a ByteBuffer accessor for strings & vectors of scalars. + // e.g. + // fun inventoryInByteBuffer(buffer: Bytebuffer): + // ByteBuffer = vectorAsBuffer(buffer, 14, 1) + GenerateFunOneLine( + writer, field_name + "AsBuffer", "", "ReadBuffer", [&]() { + writer.SetValue("end", end_idx); + writer += "vectorAsBuffer(bb, {{offset}}, {{end}})"; + }); + } + + // generate object accessors if is nested_flatbuffer + // fun testnestedflatbufferAsMonster() : Monster? + //{ return testnestedflatbufferAsMonster(new Monster()); } + + if (field.nested_flatbuffer) { + auto nested_type_name = TypeInNameSpace(*field.nested_flatbuffer); + auto nested_method_name = + field_name + "As" + field.nested_flatbuffer->name; + + GenerateGetterOneLine( + writer, nested_method_name, nested_type_name + "?", [&]() { + writer += nested_method_name + "(" + nested_type_name + "())"; + }); + + GenerateFunOneLine( + writer, nested_method_name, "obj: " + nested_type_name, + nested_type_name + "?", [&]() { + writer += LookupFieldOneLine( + offset_val, "obj.assign(indirect(vector(it)), bb)", "null"); + }); + } + + writer += ""; // Initial line break between fields + } + if (struct_def.has_key && !struct_def.fixed) { + // Key Comparison method + GenerateOverrideFun( + writer, "keysCompare", + "o1: Offset<*>, o2: Offset<*>, buffer: ReadWriteBuffer", "Int", + [&]() { + if (IsString(key_field->value.type)) { + writer.SetValue("offset", NumToString(key_field->value.offset)); + writer += + " return compareStrings(offset({{offset}}, o1, " + "buffer), offset({{offset}}, o2, buffer), buffer)"; + + } else { + auto getter1 = GenLookupByKey(key_field, "buffer", "o1"); + auto getter2 = GenLookupByKey(key_field, "buffer", "o2"); + writer += "val a = " + getter1; + writer += "val b = " + getter2; + writer += "return (a - b).toInt().sign()"; + } + }); + } + } + + static std::string LiteralSuffix(const Type &type) { + auto base = IsVector(type) ? type.element : type.base_type; + switch (base) { + case BASE_TYPE_UINT: + case BASE_TYPE_UCHAR: + case BASE_TYPE_UTYPE: + case BASE_TYPE_USHORT: return "u"; + case BASE_TYPE_ULONG: return "UL"; + case BASE_TYPE_LONG: return "L"; + default: return ""; + } + } + + std::string WrapEnumValue(const Type &type, const std::string value) const { + if (IsEnum(type)) { return GenType(type) + "(" + value + ")"; } + if (IsVector(type) && IsEnum(type.VectorType())) { + return GenType(type.VectorType()) + "(" + value + ")"; + } + return value; + } + + void GenerateCompanionObject(CodeWriter &code, + const std::function &callback) const { + code += "companion object {"; + code.IncrementIdentLevel(); + callback(); + code.DecrementIdentLevel(); + code += "}"; + } + + // Generate a documentation comment, if available. + void GenerateComment(const std::vector &dc, CodeWriter &writer, + const CommentConfig *config) const { + if (dc.begin() == dc.end()) { + // Don't output empty comment blocks with 0 lines of comment content. + return; + } + + if (config != nullptr && config->first_line != nullptr) { + writer += std::string(config->first_line); + } + std::string line_prefix = + ((config != nullptr && config->content_line_prefix != nullptr) + ? config->content_line_prefix + : "///"); + for (auto it = dc.begin(); it != dc.end(); ++it) { + writer += line_prefix + *it; + } + if (config != nullptr && config->last_line != nullptr) { + writer += std::string(config->last_line); + } + } + + void GenerateGetRootAsAccessors(const std::string &struct_name, + CodeWriter &writer, + IDLOptions options) const { + // Generate a special accessor for the table that when used as the root + // ex: fun getRootAsMonster(buffer: ByteBuffer): Monster {...} + writer.SetValue("gr_name", struct_name); + + // create convenience method that doesn't require an existing object + GenerateJvmStaticAnnotation(writer, options.gen_jvmstatic); + GenerateFunOneLine(writer, "asRoot", "buffer: ReadWriteBuffer", struct_name, + [&]() { writer += "asRoot(buffer, {{gr_name}}())"; }); + + // create method that allows object reuse + // ex: fun Monster getRootAsMonster(buffer: ByteBuffer, obj: Monster) {...} + GenerateJvmStaticAnnotation(writer, options.gen_jvmstatic); + GenerateFunOneLine( + writer, "asRoot", "buffer: ReadWriteBuffer, obj: {{gr_name}}", + struct_name, [&]() { + writer += + "obj.assign(buffer.getInt(buffer.limit) + buffer.limit, buffer)"; + }); + } + + void GenerateStaticConstructor(const StructDef &struct_def, CodeWriter &code, + const IDLOptions options) const { + // create a struct constructor function + auto params = StructConstructorParams(struct_def); + GenerateFun( + code, namer_.LegacyJavaMethod2("create", struct_def, ""), params, + "Offset<" + namer_.Type(struct_def) + '>', + [&]() { + GenStructBody(struct_def, code, ""); + code += "return Offset(builder.offset())"; + }, + options.gen_jvmstatic); + } + + std::string StructConstructorParams(const StructDef &struct_def, + const std::string &prefix = "") const { + // builder: FlatBufferBuilder + std::stringstream out; + auto field_vec = struct_def.fields.vec; + if (prefix.empty()) { out << "builder: FlatBufferBuilder"; } + for (auto it = field_vec.begin(); it != field_vec.end(); ++it) { + auto &field = **it; + if (IsStruct(field.value.type)) { + // Generate arguments for a struct inside a struct. To ensure + // names don't clash, and to make it obvious these arguments are + // constructing a nested struct, prefix the name with the field + // name. + out << StructConstructorParams(*field.value.type.struct_def, + prefix + (namer_.Variable(field) + "_")); + } else { + out << ", " << prefix << namer_.Variable(field) << ": " + << GenType(field.value.type); + } + } + return out.str(); + } + + static void GenerateVarGetterSetterOneLine(CodeWriter &writer, + const std::string &name, + const std::string &type, + const std::string &getter, + const std::string &setter) { + // Generates Kotlin getter for properties + // e.g.: + // val prop: Mytype + // get() = { + // return x + // } + writer.SetValue("name", name); + writer.SetValue("type", type); + writer += "var {{name}} : {{type}}"; + writer.IncrementIdentLevel(); + writer += "get() = " + getter; + writer += "set(value) = " + setter; + writer.DecrementIdentLevel(); + } + + static void GeneratePropertyOneLine(CodeWriter &writer, + const std::string &name, + const std::string &type, + const std::function &body) { + // Generates Kotlin getter for properties + // e.g.: + // val prop: Mytype = x + writer.SetValue("_name", name); + writer.SetValue("_type", type); + writer += "val {{_name}} : {{_type}} = \\"; + body(); + } + static void GenerateGetterOneLine(CodeWriter &writer, const std::string &name, + const std::string &type, + const std::function &body) { + // Generates Kotlin getter for properties + // e.g.: + // val prop: Mytype get() = x + writer.SetValue("_name", name); + writer.SetValue("_type", type); + writer += "val {{_name}} : {{_type}} get() = \\"; + body(); + } + + static void GenerateGetter(CodeWriter &writer, const std::string &name, + const std::string &type, + const std::function &body) { + // Generates Kotlin getter for properties + // e.g.: + // val prop: Mytype + // get() = { + // return x + // } + writer.SetValue("name", name); + writer.SetValue("type", type); + writer += "val {{name}} : {{type}}"; + writer.IncrementIdentLevel(); + writer += "get() {"; + writer.IncrementIdentLevel(); + body(); + writer.DecrementIdentLevel(); + writer += "}"; + writer.DecrementIdentLevel(); + } + + static void GenerateFun(CodeWriter &writer, const std::string &name, + const std::string ¶ms, + const std::string &returnType, + const std::function &body, + bool gen_jvmstatic = false) { + // Generates Kotlin function + // e.g.: + // fun path(j: Int): Vec3 { + // return path(Vec3(), j) + // } + auto noreturn = returnType.empty(); + writer.SetValue("name", name); + writer.SetValue("params", params); + writer.SetValue("return_type", noreturn ? "" : ": " + returnType); + GenerateJvmStaticAnnotation(writer, gen_jvmstatic); + writer += "fun {{name}}({{params}}) {{return_type}} {"; + writer.IncrementIdentLevel(); + body(); + writer.DecrementIdentLevel(); + writer += "}"; + } + + static void GenerateFunOneLine(CodeWriter &writer, const std::string &name, + const std::string ¶ms, + const std::string &returnType, + const std::function &body, + bool gen_jvmstatic = false) { + // Generates Kotlin function + // e.g.: + // fun path(j: Int): Vec3 = return path(Vec3(), j) + auto ret = returnType.empty() ? "" : " : " + returnType; + GenerateJvmStaticAnnotation(writer, gen_jvmstatic); + writer += "fun " + name + "(" + params + ")" + ret + " = \\"; + body(); + } + + static void GenerateOverrideFun(CodeWriter &writer, const std::string &name, + const std::string ¶ms, + const std::string &returnType, + const std::function &body) { + // Generates Kotlin function + // e.g.: + // override fun path(j: Int): Vec3 = return path(Vec3(), j) + writer += "override \\"; + GenerateFun(writer, name, params, returnType, body); + } + + static void GenerateOverrideFunOneLine(CodeWriter &writer, + const std::string &name, + const std::string ¶ms, + const std::string &returnType, + const std::string &statement) { + // Generates Kotlin function + // e.g.: + // override fun path(j: Int): Vec3 = return path(Vec3(), j) + writer.SetValue("name", name); + writer.SetValue("params", params); + writer.SetValue("return_type", + returnType.empty() ? "" : " : " + returnType); + writer += "override fun {{name}}({{params}}){{return_type}} = \\"; + writer += statement; + } + + static std::string LookupFieldOneLine(const std::string &offset, + const std::string &found, + const std::string ¬_found) { + return "lookupField(" + offset + ", " + not_found + " ) { " + found + " }"; + } + + static std::string Indirect(const std::string &index, bool fixed) { + // We apply indirect() and struct is not fixed. + if (!fixed) return "indirect(" + index + ")"; + return index; + } + + static std::string NotFoundReturn(BaseType el) { + switch (el) { + case BASE_TYPE_FLOAT: return "0.0f"; + case BASE_TYPE_DOUBLE: return "0.0"; + case BASE_TYPE_BOOL: return "false"; + case BASE_TYPE_LONG: + case BASE_TYPE_INT: + case BASE_TYPE_CHAR: + case BASE_TYPE_SHORT: return "0"; + case BASE_TYPE_UINT: + case BASE_TYPE_UCHAR: + case BASE_TYPE_USHORT: + case BASE_TYPE_UTYPE: return "0u"; + case BASE_TYPE_ULONG: return "0uL"; + default: return "null"; + } + } + + // Prepend @JvmStatic to methods in companion object. + static void GenerateJvmStaticAnnotation(CodeWriter &code, + bool gen_jvmstatic) { + if (gen_jvmstatic) { code += "@JvmStatic"; } + } + + const IdlNamer namer_; +}; +} // namespace kotlin + +static bool GenerateKotlinKMP(const Parser &parser, const std::string &path, + const std::string &file_name) { + kotlin::KotlinKMPGenerator generator(parser, path, file_name); + return generator.generate(); +} + +namespace { + +class KotlinKMPCodeGenerator : public CodeGenerator { + public: + Status GenerateCode(const Parser &parser, const std::string &path, + const std::string &filename) override { + if (!GenerateKotlinKMP(parser, path, filename)) { return Status::ERROR; } + return Status::OK; + } + + Status GenerateCode(const uint8_t *, int64_t, + const CodeGenOptions &) override { + return Status::NOT_IMPLEMENTED; + } + + Status GenerateMakeRule(const Parser &parser, const std::string &path, + const std::string &filename, + std::string &output) override { + (void)parser; + (void)path; + (void)filename; + (void)output; + return Status::NOT_IMPLEMENTED; + } + + Status GenerateGrpcCode(const Parser &parser, const std::string &path, + const std::string &filename) override { + (void)parser; + (void)path; + (void)filename; + return Status::NOT_IMPLEMENTED; + } + + Status GenerateRootFile(const Parser &parser, + const std::string &path) override { + (void)parser; + (void)path; + return Status::NOT_IMPLEMENTED; + } + bool IsSchemaOnly() const override { return true; } + + bool SupportsBfbsGeneration() const override { return false; } + + bool SupportsRootFileGeneration() const override { return false; } + + IDLOptions::Language Language() const override { + return IDLOptions::kKotlinKmp; + } + + std::string LanguageName() const override { return "Kotlin"; } +}; +} // namespace + +std::unique_ptr NewKotlinKMPCodeGenerator() { + return std::unique_ptr(new KotlinKMPCodeGenerator()); +} + +} // namespace flatbuffers diff --git a/src/idl_namer.h b/src/idl_namer.h index 337ac920b..9a7fdb8e3 100644 --- a/src/idl_namer.h +++ b/src/idl_namer.h @@ -88,8 +88,9 @@ class IdlNamer : public Namer { } std::string Directories(const struct Namespace &ns, - SkipDir skips = SkipDir::None) const { - return Directories(ns.components, skips); + SkipDir skips = SkipDir::None, + Case input_case = Case::kUpperCamel) const { + return Directories(ns.components, skips, input_case); } // Legacy fields do not really follow the usual config and should be diff --git a/src/idl_parser.cpp b/src/idl_parser.cpp index 2b401cf0c..67d63857e 100644 --- a/src/idl_parser.cpp +++ b/src/idl_parser.cpp @@ -2679,9 +2679,10 @@ std::vector Parser::GetIncludedFiles() const { bool Parser::SupportsOptionalScalars(const flatbuffers::IDLOptions &opts) { static FLATBUFFERS_CONSTEXPR unsigned long supported_langs = IDLOptions::kRust | IDLOptions::kSwift | IDLOptions::kLobster | - IDLOptions::kKotlin | IDLOptions::kCpp | IDLOptions::kJava | - IDLOptions::kCSharp | IDLOptions::kTs | IDLOptions::kBinary | - IDLOptions::kGo | IDLOptions::kPython | IDLOptions::kJson | + IDLOptions::kKotlin | IDLOptions::kKotlinKmp | IDLOptions::kCpp | + IDLOptions::kJava | IDLOptions::kCSharp | IDLOptions::kTs | + IDLOptions::kBinary | IDLOptions::kGo | IDLOptions::kPython | + IDLOptions::kJson | IDLOptions::kNim; unsigned long langs = opts.lang_to_generate; return (langs > 0 && langs < IDLOptions::kMAX) && !(langs & ~supported_langs); @@ -2702,7 +2703,7 @@ bool Parser::SupportsAdvancedUnionFeatures() const { ~(IDLOptions::kCpp | IDLOptions::kTs | IDLOptions::kPhp | IDLOptions::kJava | IDLOptions::kCSharp | IDLOptions::kKotlin | IDLOptions::kBinary | IDLOptions::kSwift | IDLOptions::kNim | - IDLOptions::kJson)) == 0; + IDLOptions::kJson | IDLOptions::kKotlinKmp)) == 0; } bool Parser::SupportsAdvancedArrayFeatures() const { diff --git a/src/namer.h b/src/namer.h index 6a7fadcd1..097d4490b 100644 --- a/src/namer.h +++ b/src/namer.h @@ -185,15 +185,19 @@ class Namer { // right seperator. Output path prefixing and the trailing separator may be // skiped using `skips`. // Callers may want to use `EnsureDirExists` with the result. + // input_case is used to tell how to modify namespace. e.g. kUpperCamel will + // add a underscode between case changes, so MyGame turns into My_Game + // (depending also on the output_case). virtual std::string Directories(const std::vector &directories, - SkipDir skips = SkipDir::None) const { + SkipDir skips = SkipDir::None, + Case input_case = Case::kUpperCamel) const { const bool skip_output_path = (skips & SkipDir::OutputPath) != SkipDir::None; const bool skip_trailing_seperator = (skips & SkipDir::TrailingPathSeperator) != SkipDir::None; std::string result = skip_output_path ? "" : config_.output_path; for (auto d = directories.begin(); d != directories.end(); d++) { - result += ConvertCase(*d, config_.directories, Case::kUpperCamel); + result += ConvertCase(*d, config_.directories, input_case); result.push_back(kPathSeparator); } if (skip_trailing_seperator && !result.empty()) result.pop_back(); diff --git a/tests/KotlinTest.sh b/tests/KotlinTest.sh index e41ce3a37..2b41d5cb1 100755 --- a/tests/KotlinTest.sh +++ b/tests/KotlinTest.sh @@ -34,7 +34,7 @@ fi all_kt_files=`find . -name "*.kt" -print` # Compile java FlatBuffer library -javac ${testdir}/../java/com/google/flatbuffers/*.java -d $targetdir +javac ${testdir}/../java/src/main/java/com/google/flatbuffers/*.java -d $targetdir # Compile Kotlin files kotlinc $all_kt_files -classpath $targetdir -include-runtime -d $targetdir # Make jar