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 10, 2023
1 parent 11e544b commit 8f8fa8d
Show file tree
Hide file tree
Showing 11 changed files with 767 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.MemScope
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.toKString

// TODO clean up error checking?
// TODO allow specifying directory
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 ->
println("saving $key=$value")
dpput(depot, key, -1, value, -1, DP_DOVER.toInt())
}

private inline fun loadString(key: String): String? = depotOperation { depot ->
println("loading $key")
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) {
println("interating through key $key")
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
}
}
167 changes: 167 additions & 0 deletions multiplatform-settings/src/linuxX64Main/kotlin/QdbmRelicSettings.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* 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.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
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)?.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 8f8fa8d

Please sign in to comment.