Skip to content
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

DConf proof-of-concept implementation #227

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ jobs:
restore-keys: |
${{ runner.os }}-konan-

- name: dconf install
run: |
sudo apt-get install libdconf-dev

- name: Linux build
run: |
./gradlew build publishToMavenLocal --no-daemon --stacktrace
Expand Down
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/