From 8b40481c87b646df2b7b8c2780b46167f74c63f8 Mon Sep 17 00:00:00 2001 From: Russell Wolf Date: Thu, 17 Mar 2022 21:45:01 -0400 Subject: [PATCH] WIP DConf implementation --- .../api/multiplatform-settings.klib.api | 33 +++ multiplatform-settings/build.gradle.kts | 9 +- .../com/russhwolf/settings/DConfSettings.kt | 201 ++++++++++++++++++ .../russhwolf/settings/DConfSettingsTest.kt | 28 +++ .../src/nativeInterop/cinterop/dconf.def | 4 + 5 files changed, 272 insertions(+), 3 deletions(-) create mode 100644 multiplatform-settings/src/linuxX64Main/kotlin/com/russhwolf/settings/DConfSettings.kt create mode 100644 multiplatform-settings/src/linuxX64Test/kotlin/com/russhwolf/settings/DConfSettingsTest.kt create mode 100644 multiplatform-settings/src/nativeInterop/cinterop/dconf.def diff --git a/multiplatform-settings/api/multiplatform-settings.klib.api b/multiplatform-settings/api/multiplatform-settings.klib.api index e16a837f..99f88944 100644 --- a/multiplatform-settings/api/multiplatform-settings.klib.api +++ b/multiplatform-settings/api/multiplatform-settings.klib.api @@ -202,6 +202,39 @@ final class com.russhwolf.settings/StorageSettings : com.russhwolf.settings/Sett final val size // com.russhwolf.settings/StorageSettings.size|{}size[0] final fun (): kotlin/Int // com.russhwolf.settings/StorageSettings.size.|(){}[0] } +// Targets: [linuxX64] +final class com.russhwolf.settings/DConfSettings : com.russhwolf.settings/Settings { // com.russhwolf.settings/DConfSettings|null[0] + constructor (kotlin/String) // com.russhwolf.settings/DConfSettings.|(kotlin.String){}[0] + final class Factory : com.russhwolf.settings/Settings.Factory { // com.russhwolf.settings/DConfSettings.Factory|null[0] + constructor (kotlin/String) // com.russhwolf.settings/DConfSettings.Factory.|(kotlin.String){}[0] + final fun create(kotlin/String?): com.russhwolf.settings/Settings // com.russhwolf.settings/DConfSettings.Factory.create|create(kotlin.String?){}[0] + } + final fun clear() // com.russhwolf.settings/DConfSettings.clear|clear(){}[0] + final fun getBoolean(kotlin/String, kotlin/Boolean): kotlin/Boolean // com.russhwolf.settings/DConfSettings.getBoolean|getBoolean(kotlin.String;kotlin.Boolean){}[0] + final fun getBooleanOrNull(kotlin/String): kotlin/Boolean? // com.russhwolf.settings/DConfSettings.getBooleanOrNull|getBooleanOrNull(kotlin.String){}[0] + final fun getDouble(kotlin/String, kotlin/Double): kotlin/Double // com.russhwolf.settings/DConfSettings.getDouble|getDouble(kotlin.String;kotlin.Double){}[0] + final fun getDoubleOrNull(kotlin/String): kotlin/Double? // com.russhwolf.settings/DConfSettings.getDoubleOrNull|getDoubleOrNull(kotlin.String){}[0] + final fun getFloat(kotlin/String, kotlin/Float): kotlin/Float // com.russhwolf.settings/DConfSettings.getFloat|getFloat(kotlin.String;kotlin.Float){}[0] + final fun getFloatOrNull(kotlin/String): kotlin/Float? // com.russhwolf.settings/DConfSettings.getFloatOrNull|getFloatOrNull(kotlin.String){}[0] + final fun getInt(kotlin/String, kotlin/Int): kotlin/Int // com.russhwolf.settings/DConfSettings.getInt|getInt(kotlin.String;kotlin.Int){}[0] + final fun getIntOrNull(kotlin/String): kotlin/Int? // com.russhwolf.settings/DConfSettings.getIntOrNull|getIntOrNull(kotlin.String){}[0] + final fun getLong(kotlin/String, kotlin/Long): kotlin/Long // com.russhwolf.settings/DConfSettings.getLong|getLong(kotlin.String;kotlin.Long){}[0] + final fun getLongOrNull(kotlin/String): kotlin/Long? // com.russhwolf.settings/DConfSettings.getLongOrNull|getLongOrNull(kotlin.String){}[0] + final fun getString(kotlin/String, kotlin/String): kotlin/String // com.russhwolf.settings/DConfSettings.getString|getString(kotlin.String;kotlin.String){}[0] + final fun getStringOrNull(kotlin/String): kotlin/String? // com.russhwolf.settings/DConfSettings.getStringOrNull|getStringOrNull(kotlin.String){}[0] + final fun hasKey(kotlin/String): kotlin/Boolean // com.russhwolf.settings/DConfSettings.hasKey|hasKey(kotlin.String){}[0] + final fun putBoolean(kotlin/String, kotlin/Boolean) // com.russhwolf.settings/DConfSettings.putBoolean|putBoolean(kotlin.String;kotlin.Boolean){}[0] + final fun putDouble(kotlin/String, kotlin/Double) // com.russhwolf.settings/DConfSettings.putDouble|putDouble(kotlin.String;kotlin.Double){}[0] + final fun putFloat(kotlin/String, kotlin/Float) // com.russhwolf.settings/DConfSettings.putFloat|putFloat(kotlin.String;kotlin.Float){}[0] + final fun putInt(kotlin/String, kotlin/Int) // com.russhwolf.settings/DConfSettings.putInt|putInt(kotlin.String;kotlin.Int){}[0] + final fun putLong(kotlin/String, kotlin/Long) // com.russhwolf.settings/DConfSettings.putLong|putLong(kotlin.String;kotlin.Long){}[0] + final fun putString(kotlin/String, kotlin/String) // com.russhwolf.settings/DConfSettings.putString|putString(kotlin.String;kotlin.String){}[0] + final fun remove(kotlin/String) // com.russhwolf.settings/DConfSettings.remove|remove(kotlin.String){}[0] + final val keys // com.russhwolf.settings/DConfSettings.keys|{}keys[0] + final fun (): kotlin.collections/Set // com.russhwolf.settings/DConfSettings.keys.|(){}[0] + final val size // com.russhwolf.settings/DConfSettings.size|{}size[0] + final fun (): kotlin/Int // com.russhwolf.settings/DConfSettings.size.|(){}[0] +} // Targets: [mingwX64] final class com.russhwolf.settings/RegistrySettings : com.russhwolf.settings/Settings { // com.russhwolf.settings/RegistrySettings|null[0] constructor (kotlin/String) // com.russhwolf.settings/RegistrySettings.|(kotlin.String){}[0] diff --git a/multiplatform-settings/build.gradle.kts b/multiplatform-settings/build.gradle.kts index 8d1fc281..3a80648a 100644 --- a/multiplatform-settings/build.gradle.kts +++ b/multiplatform-settings/build.gradle.kts @@ -1,6 +1,3 @@ -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi -import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl - /* * Copyright 2019 Russell Wolf * @@ -16,6 +13,9 @@ import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl * See the License for the specific language governing permissions and * limitations under the License. */ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl plugins { id("standard-configuration") @@ -27,6 +27,9 @@ standardConfig { } kotlin { + targets.getByName("linuxX64") { + compilations["main"].cinterops.create("dconf") + } @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { freeCompilerArgs.add("-Xexpect-actual-classes") diff --git a/multiplatform-settings/src/linuxX64Main/kotlin/com/russhwolf/settings/DConfSettings.kt b/multiplatform-settings/src/linuxX64Main/kotlin/com/russhwolf/settings/DConfSettings.kt new file mode 100644 index 00000000..2d68e4b7 --- /dev/null +++ b/multiplatform-settings/src/linuxX64Main/kotlin/com/russhwolf/settings/DConfSettings.kt @@ -0,0 +1,201 @@ +/* + * Copyright 2022 Russell Wolf + * + * 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.russhwolf.settings + +import com.russhwolf.settings.cinterop.dconf.DConfClient +import com.russhwolf.settings.cinterop.dconf.FALSE +import com.russhwolf.settings.cinterop.dconf.GError +import com.russhwolf.settings.cinterop.dconf.GVariant +import com.russhwolf.settings.cinterop.dconf.dconf_client_list +import com.russhwolf.settings.cinterop.dconf.dconf_client_new +import com.russhwolf.settings.cinterop.dconf.dconf_client_read +import com.russhwolf.settings.cinterop.dconf.dconf_client_sync +import com.russhwolf.settings.cinterop.dconf.dconf_client_write_sync +import com.russhwolf.settings.cinterop.dconf.dconf_is_rel_key +import com.russhwolf.settings.cinterop.dconf.g_object_ref +import com.russhwolf.settings.cinterop.dconf.g_object_unref +import com.russhwolf.settings.cinterop.dconf.g_variant_get_boolean +import com.russhwolf.settings.cinterop.dconf.g_variant_get_double +import com.russhwolf.settings.cinterop.dconf.g_variant_get_int32 +import com.russhwolf.settings.cinterop.dconf.g_variant_get_int64 +import com.russhwolf.settings.cinterop.dconf.g_variant_get_string +import com.russhwolf.settings.cinterop.dconf.g_variant_new_boolean +import com.russhwolf.settings.cinterop.dconf.g_variant_new_double +import com.russhwolf.settings.cinterop.dconf.g_variant_new_int32 +import com.russhwolf.settings.cinterop.dconf.g_variant_new_int64 +import com.russhwolf.settings.cinterop.dconf.g_variant_new_string +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.MemScope +import kotlinx.cinterop.allocPointerTo +import kotlinx.cinterop.get +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.pointed +import kotlinx.cinterop.ptr +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.toKString +import platform.posix.NULL + + +@OptIn(ExperimentalForeignApi::class) +@ExperimentalSettingsImplementation +public class DConfSettings(private val dir: String) : Settings { + // TODO sanitize slashes on `dir` + public class Factory(private val rootDir: String) : Settings.Factory { + // TODO sanitize slashes on `rootDir` and `name` + override fun create(name: String?): Settings { + val dir = if (name != null) "/$rootDir/$name/" else "/$rootDir/" + return DConfSettings(dir) + } + } + + override val keys: Set + get() = dConfOperation { dConfClient -> + buildSet { dConfClient?.forEachKey { add(it) } } + } + + override val size: Int + get() = dConfOperation { dConfClient -> + dConfClient?.foldKeys(0) { size, _ -> size + 1 } ?: 0 + } + + override fun clear(): Unit = dConfOperation { dConfClient -> + // TODO can we do this without repeating remove internals? (nested dConfOperation causes problems) + dConfClient?.forEachKey { + val error = allocPointerTo() + val out = dconf_client_write_sync(dConfClient, "$dir$it", NULL?.reinterpret(), null, null, error.ptr) + if (out == FALSE) { + checkError(error.pointed) + } + } + } + + override fun remove(key: String) { + removeGVariant(key) + } + + override fun hasKey(key: String): Boolean = dConfOperation { dConfClient -> + dConfClient?.forEachKey { if (it == key) return@dConfOperation true } + false + } + + override fun putInt(key: String, value: Int) { + writeGVariant(key, value) + } + + override fun getInt(key: String, defaultValue: Int): Int = getIntOrNull(key) ?: defaultValue + override fun getIntOrNull(key: String): Int? = readGVariant(key)?.let { g_variant_get_int32(it) } + + + override fun putLong(key: String, value: Long) { + writeGVariant(key, value) + } + + override fun getLong(key: String, defaultValue: Long): Long = getLongOrNull(key) ?: defaultValue + override fun getLongOrNull(key: String): Long? = readGVariant(key)?.let { g_variant_get_int64(it) } + + override fun putString(key: String, value: String) { + writeGVariant(key, value) + } + + override fun getString(key: String, defaultValue: String): String = getStringOrNull(key) ?: defaultValue + override fun getStringOrNull(key: String): String? = + readGVariant(key)?.let { g_variant_get_string(it, null)?.toKString() } + + override fun putFloat(key: String, value: Float) { + writeGVariant(key, value) + } + + override fun getFloat(key: String, defaultValue: Float): Float = getFloatOrNull(key) ?: defaultValue + override fun getFloatOrNull(key: String): Float? = readGVariant(key)?.let { g_variant_get_double(it).toFloat() } + + override fun putDouble(key: String, value: Double) { + writeGVariant(key, value) + } + + override fun getDouble(key: String, defaultValue: Double): Double = getDoubleOrNull(key) ?: defaultValue + override fun getDoubleOrNull(key: String): Double? = readGVariant(key)?.let { g_variant_get_double(it) } + + override fun putBoolean(key: String, value: Boolean) { + writeGVariant(key, value) + } + + override fun getBoolean(key: String, defaultValue: Boolean): Boolean = getBooleanOrNull(key) ?: defaultValue + override fun getBooleanOrNull(key: String): Boolean? = readGVariant(key)?.let { g_variant_get_boolean(it) == 1 } + + private inline fun CPointer.forEachKey(block: (key: String) -> Unit) { + val list = dconf_client_list(this, dir, null) ?: return + + var index = 0 + var item = list[index] + while (item != null) { + val key = item.toKString() + if (dconf_is_rel_key(key, null) != 0) { + block(key) + } + + item = list[++index] + } + } + + private inline fun CPointer.foldKeys(initial: A, block: (accumulator: A, key: String) -> A): A { + var accumulator = initial + forEachKey { accumulator = block(accumulator, it) } + return accumulator + } + + private inline fun readGVariant(key: String): CPointer? = dConfOperation { dConfClient -> + dconf_client_read(dConfClient, "$dir$key") + } + + private inline fun writeGVariant(key: String, value: T) = dConfOperation { dConfClient -> + val gVariant = gVariantOf(value) + val error = allocPointerTo() + val out = dconf_client_write_sync(dConfClient, "$dir$key", gVariant, null, null, error.ptr) + if (out == FALSE) { + checkError(error.pointed) + } + } + + private inline fun removeGVariant(key: String) = writeGVariant(key, null) + + private inline fun gVariantOf(value: T): CPointer? { + return when (value) { + null -> NULL?.reinterpret() + is Int -> g_variant_new_int32(value) + is Long -> g_variant_new_int64(value) + is String -> g_variant_new_string(value) + is Float -> g_variant_new_double(value.toDouble()) + is Double -> g_variant_new_double(value) + is Boolean -> g_variant_new_boolean(if (value) 1 else 0) + else -> error("Invalid value type for gVariant! value=$value") + } + } + + private inline fun dConfOperation(action: MemScope.(dConfClient: CPointer?) -> T): T = memScoped { + val dConfClient = dconf_client_new() + g_object_ref(dConfClient) + val out = action(dConfClient) + dconf_client_sync(dConfClient) + g_object_unref(dConfClient) + out + } + + private fun checkError(error: GError?) { + // TODO process error val + } +} diff --git a/multiplatform-settings/src/linuxX64Test/kotlin/com/russhwolf/settings/DConfSettingsTest.kt b/multiplatform-settings/src/linuxX64Test/kotlin/com/russhwolf/settings/DConfSettingsTest.kt new file mode 100644 index 00000000..e458f749 --- /dev/null +++ b/multiplatform-settings/src/linuxX64Test/kotlin/com/russhwolf/settings/DConfSettingsTest.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2022 Russell Wolf + * + * 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.russhwolf.settings + +@OptIn(ExperimentalSettingsImplementation::class) +public class DConfSettingsTest : BaseSettingsTest( + platformFactory = DConfSettings.Factory("com/russhwolf/settings/test"), + hasListeners = false +) { + + // TODO add test cases to verify that we write to the files we think we do + + // TODO add cleanup methods so we don't leave test DBs lying around +} diff --git a/multiplatform-settings/src/nativeInterop/cinterop/dconf.def b/multiplatform-settings/src/nativeInterop/cinterop/dconf.def new file mode 100644 index 00000000..2854a194 --- /dev/null +++ b/multiplatform-settings/src/nativeInterop/cinterop/dconf.def @@ -0,0 +1,4 @@ +headers = dconf.h +package = com.russhwolf.settings.cinterop.dconf +compilerOpts = -I/usr/include/dconf -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include +linkerOpts = -ldconf -lglib-2.0 -lgobject-2.0 -L/usr/lib -L/usr/lib/x86_64-linux-gnu/