Skip to content

Commit

Permalink
WIP DConf implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
russhwolf committed Jan 29, 2025
1 parent eca5f8c commit 2985f73
Show file tree
Hide file tree
Showing 5 changed files with 326 additions and 2 deletions.
38 changes: 38 additions & 0 deletions multiplatform-settings/api/multiplatform-settings.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,44 @@ final class com.russhwolf.settings/StorageSettings : com.russhwolf.settings/Sett
final fun remove(kotlin/String) // com.russhwolf.settings/StorageSettings.remove|remove(kotlin.String){}[0]
}

// Targets: [linuxX64]
final class com.russhwolf.settings/DConfSettings : com.russhwolf.settings/Settings { // com.russhwolf.settings/DConfSettings|null[0]
constructor <init>(kotlin/String) // com.russhwolf.settings/DConfSettings.<init>|<init>(kotlin.String){}[0]

final val keys // com.russhwolf.settings/DConfSettings.keys|{}keys[0]
final fun <get-keys>(): kotlin.collections/Set<kotlin/String> // com.russhwolf.settings/DConfSettings.keys.<get-keys>|<get-keys>(){}[0]
final val size // com.russhwolf.settings/DConfSettings.size|{}size[0]
final fun <get-size>(): kotlin/Int // com.russhwolf.settings/DConfSettings.size.<get-size>|<get-size>(){}[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 class Factory : com.russhwolf.settings/Settings.Factory { // com.russhwolf.settings/DConfSettings.Factory|null[0]
constructor <init>(kotlin/String) // com.russhwolf.settings/DConfSettings.Factory.<init>|<init>(kotlin.String){}[0]

final fun create(kotlin/String?): com.russhwolf.settings/Settings // com.russhwolf.settings/DConfSettings.Factory.create|create(kotlin.String?){}[0]
}
}

// Targets: [mingwX64]
final class com.russhwolf.settings/RegistrySettings : com.russhwolf.settings/Settings { // com.russhwolf.settings/RegistrySettings|null[0]
constructor <init>(kotlin/String) // com.russhwolf.settings/RegistrySettings.<init>|<init>(kotlin.String){}[0]
Expand Down
7 changes: 5 additions & 2 deletions multiplatform-settings/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi

/*
* Copyright 2019 Russell Wolf
*
Expand All @@ -15,6 +13,8 @@ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
* 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

plugins {
id("standard-configuration")
Expand All @@ -26,6 +26,9 @@ standardConfig {
}

kotlin {
targets.getByName<KotlinNativeTarget>("linuxX64") {
compilations["main"].cinterops.create("dconf")
}
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* Copyright 2024 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 com.russhwolf.settings.cinterop.dconf.gintVar
import kotlinx.cinterop.CPointer
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.MemScope
import kotlinx.cinterop.alloc
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.removePrefix("/")}")
}
}

override val keys: Set<String>
get() = dConfOperation { dConfClient ->
buildSet { forEachKey(dConfClient) { add(it) } }
}

override val size: Int
get() = dConfOperation { dConfClient ->
foldKeys(dConfClient, 0) { size, _ -> size + 1 } ?: 0
}

override fun clear(): Unit = dConfOperation { dConfClient ->
// TODO can we do this without repeating remove internals? (nested dConfOperation causes problems)
forEachKey(dConfClient) { key ->
val error = allocPointerTo<GError>()
val out = dconf_client_write_sync(dConfClient, "$dir$key", 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 ->
forEachKey(dConfClient) { if (it == key) return@dConfOperation true }
false
}

override fun putInt(key: String, value: Int): Unit = 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): Unit = 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): Unit = 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): Unit = 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): Unit = 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): Unit = 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 MemScope.forEachKey(dConfClient: CPointer<DConfClient>?, block: MemScope.(key: String) -> Unit) {
val lengthVar = alloc<gintVar>()
val list = dconf_client_list(dConfClient, dir, lengthVar.ptr) ?: return

var index = 0
var item = list[index]
while (item != null) {
val key = item.toKString()
if (dconf_is_rel_key(key, null) != FALSE) {
block(key)
}

item = list[++index]
}
}

private inline fun <A> MemScope.foldKeys(
dConfClient: CPointer<DConfClient>?,
initial: A,
block: MemScope.(accumulator: A, key: String) -> A
): A {
var accumulator = initial
forEachKey(dConfClient) { accumulator = block(accumulator, it) }
return accumulator
}

internal fun readGVariant(key: String): CPointer<GVariant>? = dConfOperation { dConfClient ->
dconf_client_read(dConfClient, "$dir$key")
}

internal fun <T> writeGVariant(key: String, value: T) = dConfOperation { dConfClient ->
val gVariant = gVariantOf(value)
val error = allocPointerTo<GError>()
val out = dconf_client_write_sync(dConfClient, "$dir$key", gVariant, null, null, error.ptr)
if (out == FALSE) {
checkError(error.pointed)
}
}

internal fun removeGVariant(key: String) = writeGVariant(key, null)

internal fun <T> gVariantOf(value: T): CPointer<GVariant>? {
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")
}
}

internal inline fun <T> dConfOperation(action: MemScope.(dConfClient: CPointer<DConfClient>?) -> 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?) {
if (error != null) {
error("dconf error: ${error.message?.toKString()}")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright 2024 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.
*/

@file:OptIn(ExperimentalForeignApi::class)

package com.russhwolf.settings

import com.russhwolf.settings.cinterop.dconf.GError
import com.russhwolf.settings.cinterop.dconf.TRUE
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.g_object_ref
import com.russhwolf.settings.cinterop.dconf.g_object_unref
import com.russhwolf.settings.cinterop.dconf.g_variant_get_int32
import com.russhwolf.settings.cinterop.dconf.g_variant_new_int32
import com.russhwolf.settings.cinterop.dconf.gintVar
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.alloc
import kotlinx.cinterop.allocPointerTo
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.ptr
import kotlinx.cinterop.reinterpret
import kotlinx.cinterop.value
import platform.posix.NULL
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

@OptIn(ExperimentalSettingsImplementation::class)
public class DConfSettingsTest : BaseSettingsTest(
platformFactory = DConfSettings.Factory("com/russhwolf/settings/test"),
hasListeners = false
) {
@Test
fun foo(): Unit = memScoped {
val dConfClient = dconf_client_new()
g_object_ref(dConfClient)

try {
val error = allocPointerTo<GError>()
val writeStatus = dconf_client_write_sync(
client = dConfClient,
key = "/com/russhwolf/settings/test/foo",
value = g_variant_new_int32(42),
tag = NULL?.reinterpret(),
cancellable = NULL?.reinterpret(),
error = error.ptr
)

assertEquals(TRUE, writeStatus)

dconf_client_sync(dConfClient)

val keysSizeVar = alloc<gintVar>()
val keys = dconf_client_list(dConfClient, "/com/russhwolf/settings/test/", keysSizeVar.ptr)
val keysSize = keysSizeVar.value
assertTrue(keysSize > 0)
val read = dconf_client_read(dConfClient, "/com/russhwolf/settings/test/foo")
assertNotNull(read)
assertEquals(42, g_variant_get_int32(read))
} finally {
dconf_client_sync(dConfClient)
g_object_unref(dConfClient)
}

}
// 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
}
4 changes: 4 additions & 0 deletions multiplatform-settings/src/nativeInterop/cinterop/dconf.def
Original file line number Diff line number Diff line change
@@ -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/

0 comments on commit 2985f73

Please sign in to comment.