Skip to content

Commit

Permalink
WIP QDBM multiple implementations
Browse files Browse the repository at this point in the history
  • Loading branch information
russhwolf committed Oct 14, 2023
1 parent 11e544b commit d442e91
Show file tree
Hide file tree
Showing 11 changed files with 780 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ jobs:
java-version: 17
distribution: corretto

- name: QDBM install
run: |
sudo apt-get install libqdbm-dev
- name: Linux build
run: |
./gradlew build publishToMavenLocal --no-daemon --stacktrace
Expand Down
7 changes: 7 additions & 0 deletions multiplatform-settings/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget

plugins {
id("com.android.library")
kotlin("multiplatform")
Expand All @@ -25,6 +27,11 @@ plugins {
standardConfiguration()

kotlin {
targets.getByName<KotlinNativeTarget>("linuxX64") {
compilations["main"].cinterops.create("qdbm-depot")
compilations["main"].cinterops.create("qdbm-relic")
compilations["main"].cinterops.create("qdbm-villa")
}
sourceSets {
commonMain {
dependencies {
Expand Down
122 changes: 122 additions & 0 deletions multiplatform-settings/src/linuxX64Main/kotlin/QdbmDepotSettings.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* 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.qdbm.depot.DEPOT
import com.russhwolf.settings.cinterop.qdbm.depot.DP_DOVER
import com.russhwolf.settings.cinterop.qdbm.depot.DP_OCREAT
import com.russhwolf.settings.cinterop.qdbm.depot.DP_OREADER
import com.russhwolf.settings.cinterop.qdbm.depot.DP_OWRITER
import com.russhwolf.settings.cinterop.qdbm.depot.dpclose
import com.russhwolf.settings.cinterop.qdbm.depot.dpecode
import com.russhwolf.settings.cinterop.qdbm.depot.dperrmsg
import com.russhwolf.settings.cinterop.qdbm.depot.dpget
import com.russhwolf.settings.cinterop.qdbm.depot.dpiterinit
import com.russhwolf.settings.cinterop.qdbm.depot.dpiternext
import com.russhwolf.settings.cinterop.qdbm.depot.dpopen
import com.russhwolf.settings.cinterop.qdbm.depot.dpout
import com.russhwolf.settings.cinterop.qdbm.depot.dpput
import kotlinx.cinterop.CPointer
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.MemScope
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.toKString

// TODO clean up error checking?
// TODO allow specifying directory
@OptIn(ExperimentalForeignApi::class)
@ExperimentalSettingsImplementation
public class QdbmDepotSettings(private val path: String) : Settings {

override val keys: Set<String>
get() = depotOperation { depot ->
depot.foldKeys(mutableListOf<String>()) { list, key -> list.apply { add(key) } }.toSet()
}

override val size: Int get() = depotOperation { depot -> depot.foldKeys(0) { size, _ -> size + 1 } }

public override fun clear(): Unit = depotOperation { depot -> depot.forEachKey { dpout(depot, it, -1) } }
public override fun remove(key: String): Unit = depotOperation { depot -> dpout(depot, key, -1) }
public override fun hasKey(key: String): Boolean = depotOperation { depot ->
depot.forEachKey { if (key == it) return true }
return false
}

public override fun putInt(key: String, value: Int): Unit = saveString(key, value.toString())
public override fun getInt(key: String, defaultValue: Int): Int = getIntOrNull(key) ?: defaultValue
public override fun getIntOrNull(key: String): Int? = loadString(key)?.toInt()

public override fun putLong(key: String, value: Long): Unit = saveString(key, value.toString())
public override fun getLong(key: String, defaultValue: Long): Long = getLongOrNull(key) ?: defaultValue
public override fun getLongOrNull(key: String): Long? = loadString(key)?.toLong()

public override fun putString(key: String, value: String): Unit = saveString(key, value)
public override fun getString(key: String, defaultValue: String): String = getStringOrNull(key) ?: defaultValue
public override fun getStringOrNull(key: String): String? = loadString(key)

public override fun putFloat(key: String, value: Float): Unit = saveString(key, value.toString())
public override fun getFloat(key: String, defaultValue: Float): Float = getFloatOrNull(key) ?: defaultValue
public override fun getFloatOrNull(key: String): Float? = loadString(key)?.toFloat()

public override fun putDouble(key: String, value: Double): Unit = saveString(key, value.toString())
public override fun getDouble(key: String, defaultValue: Double): Double = getDoubleOrNull(key) ?: defaultValue
public override fun getDoubleOrNull(key: String): Double? = loadString(key)?.toDouble()

public override fun putBoolean(key: String, value: Boolean): Unit = saveString(key, value.toString())
public override fun getBoolean(key: String, defaultValue: Boolean): Boolean = getBooleanOrNull(key) ?: defaultValue
public override fun getBooleanOrNull(key: String): Boolean? = loadString(key)?.toBoolean()

private inline fun saveString(key: String, value: String): Unit = depotOperation { depot ->
dpput(depot, key, -1, value, -1, DP_DOVER.toInt())
}

private inline fun loadString(key: String): String? = depotOperation { depot ->
val output = dpget(depot, key, -1, 0, -1, null)
output?.toKString()
}

private inline fun CPointer<DEPOT>.forEachKey(block: (key: String) -> Unit) {
val depot = this
if (dpiterinit(depot) != 0) {
while (true) {
val key = dpiternext(depot, null)?.toKString()
if (key != null) {
block(key)
} else {
break
}
}
}
}

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

private inline fun <T> depotOperation(action: MemScope.(depot: CPointer<DEPOT>) -> T): T = memScoped {
val depot = dpopen(path, (DP_OWRITER or DP_OREADER or DP_OCREAT).toInt(), 0)
if (depot == null) {
val message = dperrmsg(dpecode)?.toKString()
error("error on depot open: $message")
}
val out = action(depot)
dpclose(depot)
out
}
}
170 changes: 170 additions & 0 deletions multiplatform-settings/src/linuxX64Main/kotlin/QdbmRelicSettings.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* 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.qdbm.relic.DBM
import com.russhwolf.settings.cinterop.qdbm.relic.DBM_REPLACE
import com.russhwolf.settings.cinterop.qdbm.relic.datum
import com.russhwolf.settings.cinterop.qdbm.relic.dbm_clearerr
import com.russhwolf.settings.cinterop.qdbm.relic.dbm_close
import com.russhwolf.settings.cinterop.qdbm.relic.dbm_delete
import com.russhwolf.settings.cinterop.qdbm.relic.dbm_error
import com.russhwolf.settings.cinterop.qdbm.relic.dbm_fetch
import com.russhwolf.settings.cinterop.qdbm.relic.dbm_firstkey
import com.russhwolf.settings.cinterop.qdbm.relic.dbm_nextkey
import com.russhwolf.settings.cinterop.qdbm.relic.dbm_open
import com.russhwolf.settings.cinterop.qdbm.relic.dbm_store
import kotlinx.cinterop.ByteVar
import kotlinx.cinterop.CPointer
import kotlinx.cinterop.CValue
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.MemScope
import kotlinx.cinterop.cValue
import kotlinx.cinterop.cstr
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.plus
import kotlinx.cinterop.pointed
import kotlinx.cinterop.reinterpret
import kotlinx.cinterop.toCValues
import kotlinx.cinterop.useContents
import kotlinx.cinterop.value
import platform.posix.O_CREAT
import platform.posix.O_RDWR
import platform.posix.S_IRGRP
import platform.posix.S_IROTH
import platform.posix.S_IRUSR
import platform.posix.S_IWUSR
import platform.posix.errno

// TODO clean up error checking?
// TODO allow specifying directory
@OptIn(ExperimentalForeignApi::class)
@ExperimentalSettingsImplementation
public class QdbmRelicSettings(private val path: String) : Settings {

override val keys: Set<String>
get() = dbmOperation { dbm ->
dbm.foldKeys(mutableListOf<String>()) { list, key -> list.apply { add(key.toKString()!!) } }.toSet()
}

override val size: Int get() = dbmOperation { dbm -> dbm.foldKeys(0) { size, _ -> size + 1 } }

public override fun clear(): Unit = dbmOperation { dbm -> dbm.forEachKey { dbm_delete(dbm, it) } }
public override fun remove(key: String): Unit = dbmOperation { dbm -> dbm_delete(dbm, datumOf(key)) }
public override fun hasKey(key: String): Boolean = dbmOperation { dbm ->
dbm.forEachKey { if (key == it.toKString()) return true }
return false
}

public override fun putInt(key: String, value: Int): Unit = saveBytes(key, value.toByteArray())
public override fun getInt(key: String, defaultValue: Int): Int = getIntOrNull(key) ?: defaultValue
public override fun getIntOrNull(key: String): Int? = loadBytes(key)?.toInt()

public override fun putLong(key: String, value: Long): Unit = saveBytes(key, value.toByteArray())
public override fun getLong(key: String, defaultValue: Long): Long = getLongOrNull(key) ?: defaultValue
public override fun getLongOrNull(key: String): Long? = loadBytes(key)?.toLong()

public override fun putString(key: String, value: String): Unit = saveBytes(key, value.encodeToByteArray())
public override fun getString(key: String, defaultValue: String): String = getStringOrNull(key) ?: defaultValue
public override fun getStringOrNull(key: String): String? = loadBytes(key)?.decodeToString()

public override fun putFloat(key: String, value: Float): Unit = saveBytes(key, value.toRawBits().toByteArray())
public override fun getFloat(key: String, defaultValue: Float): Float = getFloatOrNull(key) ?: defaultValue
public override fun getFloatOrNull(key: String): Float? = loadBytes(key)?.toInt()?.let { Float.fromBits(it) }

public override fun putDouble(key: String, value: Double): Unit = saveBytes(key, value.toRawBits().toByteArray())
public override fun getDouble(key: String, defaultValue: Double): Double = getDoubleOrNull(key) ?: defaultValue
public override fun getDoubleOrNull(key: String): Double? = loadBytes(key)?.toLong()?.let { Double.fromBits(it) }

public override fun putBoolean(key: String, value: Boolean): Unit = saveBytes(key, byteArrayOf(if (value) 1 else 0))
public override fun getBoolean(key: String, defaultValue: Boolean): Boolean = getBooleanOrNull(key) ?: defaultValue
public override fun getBooleanOrNull(key: String): Boolean? = loadBytes(key)?.get(0)?.equals(0.toByte())?.not()

private inline fun saveBytes(key: String, bytes: ByteArray): Unit = dbmOperation { dbm ->
dbm_store(dbm, datumOf(key), datumOf(bytes), DBM_REPLACE.toInt())
}

private inline fun loadBytes(key: String): ByteArray? = dbmOperation { dbm ->
val datum = dbm_fetch(dbm, datumOf(key))
datum.toByteArray()
}

private inline fun CPointer<DBM>.forEachKey(block: (key: CValue<datum>) -> Unit) {
val dbm = this
var key = dbm_firstkey(dbm)
while (key.useContents { dptr != null }) {
block(key)
key = dbm_nextkey(dbm)
}
}

private inline fun <A> CPointer<DBM>.foldKeys(initial: A, block: (accumulator: A, key: CValue<datum>) -> A): A {
var accumulator = initial
forEachKey { accumulator = block(accumulator, it) }
return accumulator
}

private inline fun <T> dbmOperation(action: MemScope.(dbm: CPointer<DBM>) -> T): T = memScoped {
val dbm = dbm_open(path.cstr, O_RDWR or O_CREAT, S_IRUSR or S_IWUSR or S_IRGRP or S_IROTH)
?: error("Error on dbm_open: $errno")
val out = action(dbm)
val error = dbm_error(dbm)
if (error != 0) {
try {
error("error: $error")
} finally {
dbm_clearerr(dbm)
}
}
dbm_close(dbm)
out
}

private inline fun ByteArray.toLong(): Long = foldIndexed(0) { index, total: Long, byte: Byte ->
((0xff.toLong() and byte.toLong()) shl index * Byte.SIZE_BITS) or total
}

private inline fun ByteArray.toInt(): Int = foldIndexed(0) { index, total: Int, byte: Byte ->
((0xff and byte.toInt()) shl index * Byte.SIZE_BITS) or total
}

private inline fun Long.toByteArray(): ByteArray = ByteArray(Long.SIZE_BYTES) { index ->
((this shr (Byte.SIZE_BITS * index)) and 0xff).toByte()
}

private inline fun Int.toByteArray(): ByteArray = ByteArray(Int.SIZE_BYTES) { index ->
((this shr (Byte.SIZE_BITS * index)) and 0xff).toByte()
}

private inline fun CValue<datum>.toKString(): String? = toByteArray()?.decodeToString()
private inline fun CValue<datum>.toByteArray(): ByteArray? = useContents {
val size = dsize.toInt()
val firstPtr: CPointer<ByteVar> = dptr?.reinterpret()
?: return null
return ByteArray(size) {
val pointedValue = firstPtr.plus(it)?.pointed?.value
pointedValue ?: 0
}
}

private inline fun MemScope.datumOf(string: String): CValue<datum> = datumOf(string.encodeToByteArray())
private inline fun MemScope.datumOf(bytes: ByteArray): CValue<datum> = cValue {
val cValues = bytes.toCValues()
dptr = cValues.ptr
dsize = cValues.size.toULong()
}
}
Loading

0 comments on commit d442e91

Please sign in to comment.