-
Notifications
You must be signed in to change notification settings - Fork 19
WIP: JVM support #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: JVM support #38
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ kotlin { | |
| publishLibraryVariants("release", "debug") | ||
| } | ||
|
|
||
| jvm() | ||
| iosX64() | ||
| iosArm64() | ||
| iosSimulatorArm64() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.powersync.compose | ||
|
|
||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.remember | ||
| import com.powersync.DatabaseDriverFactory | ||
|
|
||
| @Composable | ||
| public actual fun rememberDatabaseDriverFactory(): DatabaseDriverFactory { | ||
| return remember { | ||
| DatabaseDriverFactory() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ kotlin { | |
| } | ||
| } | ||
|
|
||
| jvm() | ||
| iosX64() | ||
| iosArm64() | ||
| iosSimulatorArm64() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import com.powersync.plugins.sonatype.setupGithubRepository | ||
| import de.undercouch.gradle.tasks.download.Download | ||
| import org.apache.tools.ant.taskdefs.condition.Os | ||
| import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget | ||
| import com.powersync.plugins.sonatype.setupGithubRepository | ||
|
|
||
| plugins { | ||
| alias(libs.plugins.kotlinMultiplatform) | ||
|
|
@@ -14,8 +15,15 @@ plugins { | |
| val sqliteVersion = "3450000" | ||
| val sqliteReleaseYear = "2024" | ||
|
|
||
| val sqliteSrcFolder = | ||
| project.layout.buildDirectory.dir("interop/sqlite").get() | ||
| val osName = when { | ||
| Os.isFamily(Os.FAMILY_WINDOWS) -> "Windows" | ||
| Os.isFamily(Os.FAMILY_MAC) -> "macOS" | ||
| Os.isFamily(Os.FAMILY_UNIX) -> "Linux" | ||
| else -> "Unknown" | ||
| } | ||
|
|
||
| val sqliteSrcFolder = project.layout.buildDirectory.dir("interop/sqlite").get() | ||
| val jvmNativeBuildFolder = "${layout.buildDirectory.get().asFile}/native/powersync-sqlite" | ||
|
|
||
| val downloadSQLiteSources by tasks.registering(Download::class) { | ||
| val zipFileName = "sqlite-amalgamation-$sqliteVersion.zip" | ||
|
|
@@ -59,6 +67,19 @@ val buildCInteropDef by tasks.registering { | |
| outputs.files(defFile) | ||
| } | ||
|
|
||
| tasks.create<Exec>("buildNative") { | ||
| group = "build" | ||
|
|
||
| outputs.dir(jvmNativeBuildFolder) | ||
| workingDir = file(jvmNativeBuildFolder) | ||
|
|
||
| environment("TARGET", "$osName/cmake") | ||
| environment("SOURCE_PATH", "$projectDir/src/jvmMain/cpp") | ||
| environment("INTEROP_PATH", "$sqliteSrcFolder") | ||
|
|
||
| commandLine("$projectDir/src/jvmMain/cpp/build.sh") | ||
| } | ||
|
|
||
| kotlin { | ||
| androidTarget { | ||
| publishLibraryVariants("release", "debug") | ||
|
|
@@ -67,6 +88,13 @@ kotlin { | |
| iosX64() | ||
| iosArm64() | ||
| iosSimulatorArm64() | ||
| jvm { | ||
| val processResources = compilations["main"].processResourcesTaskName | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bundle the binary into the jar |
||
| (tasks[processResources] as ProcessResources).apply { | ||
| dependsOn("buildNative") | ||
| from("$jvmNativeBuildFolder/${osName}/output") | ||
| } | ||
| } | ||
|
|
||
| targets.withType<KotlinNativeTarget> { | ||
| compilations.getByName("main") { | ||
|
|
@@ -113,6 +141,11 @@ kotlin { | |
| implementation(libs.ktor.client.ios) | ||
| } | ||
|
|
||
| jvmMain.dependencies { | ||
| implementation(libs.sqldelight.driver.desktop) | ||
| implementation(libs.ktor.client.okhttp) | ||
| } | ||
|
|
||
| commonTest.dependencies { | ||
| implementation(libs.kotlin.test) | ||
| } | ||
|
|
@@ -180,4 +213,3 @@ afterEvaluate { | |
| } | ||
|
|
||
| setupGithubRepository() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| cmake_minimum_required(VERSION 3.18.1) | ||
|
|
||
| project(powersync-sqlite) | ||
|
|
||
| set(PACKAGE_NAME "powersync-sqlite") | ||
| set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/../output) | ||
|
|
||
| find_package(JNI REQUIRED) | ||
|
|
||
| include_directories(${JNI_INCLUDE_DIRS}) | ||
| include_directories(${SQLITE3_INTEROP_DIR}) | ||
|
|
||
| add_library(sqlite3_static STATIC | ||
| "${SQLITE3_INTEROP_DIR}/sqlite3.c" | ||
| ) | ||
|
|
||
| add_library( | ||
| ${PACKAGE_NAME} | ||
| SHARED | ||
| sqlite_bindings.cpp | ||
| ) | ||
|
|
||
| target_link_libraries( | ||
| ${PACKAGE_NAME} | ||
| PRIVATE | ||
| sqlite3_static | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| #!/bin/bash | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure whats the convention for the location of build files 🤔 |
||
| set -e | ||
|
|
||
| [[ -z "$TARGET" ]] && echo "Please set the PLATFORM variable" && exit 1 | ||
| [[ -z "$SOURCE_PATH" ]] && echo "Please set the SOURCE_PATH variable" && exit 1 | ||
| [[ -z "$INTEROP_PATH" ]] && echo "Please set the INTEROP_PATH variable" && exit 1 | ||
|
|
||
| mkdir -p "$TARGET" | ||
| cd "$TARGET" | ||
|
|
||
| cmake -DSQLITE3_INTEROP_DIR="$INTEROP_PATH" "$SOURCE_PATH" | ||
| cmake --build . | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| #include <jni.h> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mentioned in the PR note, this is copied from |
||
| #include <inttypes.h> | ||
| #include <sqlite3.h> | ||
| #include <string> | ||
|
|
||
| typedef struct context { | ||
| JavaVM *javaVM; | ||
| jobject bindingsObj; | ||
| jclass bindingsClz; | ||
| } Context; | ||
| Context g_ctx; | ||
|
|
||
| extern "C" { | ||
|
|
||
| JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { | ||
| JNIEnv *env; | ||
| memset(&g_ctx, 0, sizeof(g_ctx)); | ||
| g_ctx.javaVM = vm; | ||
|
|
||
| if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { | ||
| return JNI_ERR; // JNI version not supported. | ||
| } | ||
|
|
||
| return JNI_VERSION_1_6; | ||
| } | ||
|
|
||
| static void | ||
| update_hook_callback(void *pData, int opCode, char const *pDbName, char const *pTableName, | ||
| sqlite3_int64 iRow) { | ||
| // Get JNIEnv for the current thread | ||
| JNIEnv *env; | ||
| JavaVM *javaVM = g_ctx.javaVM; | ||
| javaVM->GetEnv((void **) &env, JNI_VERSION_1_6); | ||
|
|
||
| if (g_ctx.bindingsClz) { | ||
| jmethodID updateId = env->GetMethodID( | ||
| g_ctx.bindingsClz, "onTableUpdate", "(Ljava/lang/String;)V"); | ||
|
|
||
| jstring tableString = env->NewStringUTF(std::string(pTableName).c_str()); | ||
| env->CallVoidMethod(g_ctx.bindingsObj, updateId, tableString); | ||
| } | ||
| } | ||
|
|
||
| static jint | ||
| commit_hook(void *pool) { | ||
| // Get JNIEnv for the current thread | ||
| JNIEnv *env; | ||
| JavaVM *javaVM = g_ctx.javaVM; | ||
| javaVM->GetEnv((void **) &env, JNI_VERSION_1_6); | ||
|
|
||
| if (g_ctx.bindingsClz) { | ||
| jmethodID methodId = env->GetMethodID( | ||
| g_ctx.bindingsClz, "onTransactionCommit", "(Z)V"); | ||
|
|
||
| env->CallVoidMethod(g_ctx.bindingsObj, methodId, JNI_TRUE); | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| static void rollback_hook(void *pool) { | ||
| // Get JNIEnv for the current thread | ||
| JNIEnv *env; | ||
| JavaVM *javaVM = g_ctx.javaVM; | ||
| javaVM->GetEnv((void **) &env, JNI_VERSION_1_6); | ||
|
|
||
| if (g_ctx.bindingsClz) { | ||
| jmethodID methodId = env->GetMethodID( | ||
| g_ctx.bindingsClz, "onTransactionCommit", "(Z)V"); | ||
|
|
||
| env->CallVoidMethod(g_ctx.bindingsObj, methodId, JNI_FALSE); | ||
| } | ||
| } | ||
|
|
||
| jint powersync_init(sqlite3 *db, char **pzErrMsg, | ||
| const sqlite3_api_routines *pApi) { | ||
|
|
||
| sqlite3_update_hook(db, update_hook_callback, NULL); | ||
| sqlite3_commit_hook(db, commit_hook, NULL); | ||
| sqlite3_rollback_hook(db, rollback_hook, NULL); | ||
|
|
||
| return SQLITE_OK; | ||
| } | ||
|
|
||
| JNIEXPORT void JNICALL | ||
| Java_com_powersync_DatabaseDriverFactory_setupSqliteBinding(JNIEnv *env, jobject thiz) { | ||
| jclass clz = env->GetObjectClass(thiz); | ||
| g_ctx.bindingsClz = (jclass) env->NewGlobalRef(clz); | ||
| g_ctx.bindingsObj = env->NewGlobalRef(thiz); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| public actual object BuildConfig { | ||
| public actual val isDebug: Boolean | ||
| // TODO: need to determine a good way to set this on JVM presuming we don't want to bundle BuildKonfig in the | ||
| // library. | ||
| get() = true | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Didn't want to add the dependency on |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package com.powersync | ||
|
|
||
| import app.cash.sqldelight.async.coroutines.synchronous | ||
| import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver | ||
| import com.powersync.db.internal.InternalSchema | ||
| import java.util.Properties | ||
| import kotlinx.coroutines.CoroutineScope | ||
|
|
||
| @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") | ||
| public actual class DatabaseDriverFactory { | ||
| private var driver: PsSqlDriver? = null | ||
| private external fun setupSqliteBinding() | ||
|
|
||
| @Suppress("unused") | ||
| private fun onTableUpdate(tableName: String) { | ||
| driver?.updateTable(tableName) | ||
| } | ||
|
|
||
| @Suppress("unused") | ||
| private fun onTransactionCommit(success: Boolean) { | ||
| driver?.also { driver -> | ||
| if (success) { | ||
| driver.fireTableUpdates() | ||
| } else { | ||
| driver.clearTableUpdates() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public actual fun createDriver( | ||
| scope: CoroutineScope, | ||
| dbFilename: String | ||
| ): PsSqlDriver { | ||
| val schema = InternalSchema.synchronous() | ||
| this.driver = PsSqlDriver( | ||
| scope = scope, | ||
| driver = JdbcSqliteDriver("jdbc:sqlite:$dbFilename", Properties(), schema) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is the cause of the crash, not loading sqlite driver with extension support so the |
||
| ) | ||
| setupSqliteBinding() | ||
| return this.driver as PsSqlDriver | ||
| } | ||
|
|
||
| public companion object { | ||
| init { | ||
| // There is presumably a better way to load the library from the jar. | ||
| @Suppress("UnsafeDynamicallyLoadedCode") | ||
| System.load(OSXLibraryLoader().loadLibraryFromResources()) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package com.powersync | ||
|
|
||
| import com.powersync.LibraryLoader.Companion.SQLITE_BINARY_FILENAME | ||
| import java.io.File | ||
| import java.io.InputStream | ||
|
|
||
| private interface LibraryLoader { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AI recommended this approach so I'm sure there is a better way 😂 |
||
| companion object { | ||
| const val SQLITE_BINARY_FILENAME: String = "libpowersync-sqlite" | ||
| } | ||
|
|
||
| fun loadLibraryFromResources(): String | ||
|
|
||
| fun createTempFile(prefix: String, suffix: String): File { | ||
| val dir = System.getProperty("java.io.tmpdir") | ||
| return File.createTempFile(prefix, suffix, File(dir)) | ||
| } | ||
| } | ||
|
|
||
| // TODO: Need to create and test implementations of this for windows/linux, or even better implement a cleaner | ||
| // of extracting the shared library. | ||
| public class OSXLibraryLoader : LibraryLoader { | ||
| override fun loadLibraryFromResources(): String { | ||
| val path = "/$SQLITE_BINARY_FILENAME.dylib" | ||
| val tempFile = createTempFile(SQLITE_BINARY_FILENAME, ".dylib") | ||
| tempFile.deleteOnExit() | ||
|
|
||
| val inputStream: InputStream = LibraryLoader::class.java.getResourceAsStream(path) | ||
| ?: throw IllegalArgumentException("File $path not found in resources") | ||
| inputStream.use { input -> | ||
| tempFile.outputStream().use { output -> | ||
| input.copyTo(output) | ||
| } | ||
| } | ||
|
|
||
| return tempFile.absolutePath | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import org.jetbrains.compose.desktop.application.dsl.TargetFormat | ||
|
|
||
| plugins { | ||
| // Gradle complains when I attempt to re-use the plugin definition from the project toml. Something to do | ||
| // with the way the sonatype module is set up I presume. Need to figure this out before merge | ||
| id("org.jetbrains.kotlin.jvm") | ||
| alias(projectLibs.plugins.jetbrainsCompose) | ||
| } | ||
|
|
||
| dependencies { | ||
| implementation(project(":composeApp")) | ||
| implementation(compose.desktop.currentOs) | ||
| } | ||
|
|
||
| group = "com.powersync" | ||
| version = "1.0.0" | ||
|
|
||
| compose.desktop { | ||
| application { | ||
| mainClass = "com.powersync.demos.MainKt" | ||
|
|
||
| nativeDistributions { | ||
| targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) | ||
| packageName = "PowerSync Demo" | ||
| packageVersion = project.version as String | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package com.powersync.demos | ||
|
|
||
| import androidx.compose.ui.unit.DpSize | ||
| import androidx.compose.ui.unit.dp | ||
| import androidx.compose.ui.window.Window | ||
| import androidx.compose.ui.window.application | ||
| import androidx.compose.ui.window.rememberWindowState | ||
|
|
||
| fun main() = application { | ||
| val windowState = rememberWindowState(size = DpSize(1200.dp, 900.dp)) | ||
|
|
||
| Window(state = windowState, onCloseRequest = ::exitApplication, title = "PowerSync Demo") { | ||
| App() | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Run
build.shto build the binary and save it tobuild/native/powersync-sqlite/${OS}/output