Skip to content

Commit

Permalink
feat: 简单的 Python 通信框架
Browse files Browse the repository at this point in the history
  • Loading branch information
xfqwdsj committed Apr 13, 2024
1 parent 3d2d497 commit f0a774b
Show file tree
Hide file tree
Showing 13 changed files with 537 additions and 1 deletion.
92 changes: 92 additions & 0 deletions client-py/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* This file is part of Fhraise.
* Copyright (c) 2024 HSAS Foodies. All Rights Reserved.
*
* Fhraise is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* Fhraise is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with Fhraise. If not, see <https://www.gnu.org/licenses/>.
*/

import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.SharedLibrary
import xyz.xfqlittlefan.fhraise.buildsrc.projectVersion

plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
}

group = "xyz.xfqlittlefan.fhraise"
project.version = projectVersion

kotlin {
val linuxTargets = mutableListOf<KotlinNativeTarget>()
val mingwTargets = mutableListOf<KotlinNativeTarget>()

linuxTargets += linuxArm64()
linuxTargets += linuxX64()

mingwTargets += mingwX64()

val defaultSharedLibConfigure: SharedLibrary.() -> Unit = {
export(project(":py"))
}

configure(linuxTargets) {
binaries {
sharedLib {
baseName = "fhraisepy"
defaultSharedLibConfigure()
}
}
}

configure(mingwTargets) {
binaries {
sharedLib {
baseName = "libfhraisepy"
defaultSharedLibConfigure()
}
}
}

applyDefaultHierarchyTemplate()

sourceSets {
val nativeMain by getting {
dependencies {
api(project(":py"))
implementation(libs.kotlinx.datetime)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.websockets)
implementation(libs.ktor.serialization.kotlinx.cbor)
}
}

val nonMingwMain by creating {
dependsOn(nativeMain)
dependencies {
implementation(libs.ktor.client.cio)
}
}

val mingwMain by getting {
dependencies {
implementation(libs.ktor.client.winhttp)
}
}

val linuxMain by getting {
dependsOn(nonMingwMain)
}
}
}
110 changes: 110 additions & 0 deletions client-py/src/nativeMain/kotlin/Client.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* This file is part of Fhraise.
* Copyright (c) 2024 HSAS Foodies. All Rights Reserved.
*
* Fhraise is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* Fhraise is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with Fhraise. If not, see <https://www.gnu.org/licenses/>.
*/

package xyz.xfqlittlefan.fhraise.py

import io.ktor.client.*
import io.ktor.client.plugins.websocket.*
import io.ktor.serialization.kotlinx.*
import kotlinx.cinterop.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.cbor.Cbor
import kotlin.coroutines.resume
import kotlin.experimental.ExperimentalNativeApi

class Client(private val host: String, private val port: UShort) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val messageChannel = Channel<Message?>(3)

@OptIn(ExperimentalForeignApi::class)
private val messageErrorChannel = Channel<CPointer<ThrowableVar>>(3)
private val resultChannel = Channel<Message>(3)

@OptIn(ExperimentalSerializationApi::class)
private val client = HttpClient {
install(WebSockets) {
contentConverter = KotlinxWebsocketSerializationConverter(Cbor)
}
}

@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class)
fun connect(throwable: CPointer<CPointerVar<ThrowableVar>>?) = runBlocking {
logger.debug("Connecting to $host:$port.")
suspendCancellableCoroutine { continuation ->
logger.debug("Launching coroutine.")
scope.launch(Dispatchers.IO) {
runCatching(throwable) {
logger.debug("Starting connection.")
client.webSocket(host = host, port = port.toInt(), path = pyWsPath) {
continuation.resume(true)
logger.debug("Connected.")
while (true) {
logger.debug("Waiting for message...")
val receiveResult =
runCatching(null) { messageChannel.send(receiveDeserialized<Message>()) }.onFailure {
logger.error("Failed to receive message.")
it as ThrowableWrapper
messageErrorChannel.send(it.throwable)
messageChannel.send(null)
}
if (receiveResult.isFailure) continue
logger.debug("Waiting for result...")
sendSerialized<Message>(resultChannel.receive())
}
}
}.onFailure {
if (continuation.isActive) {
logger.error("Failed to connect.")
continuation.resume(false)
} else {
logger.error("Uncaught error in $this.")
throw it
}
}
}
}
}

@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class)
fun receive(
type: CPointer<CPointerVar<ByteVar>>,
ref: CPointer<COpaquePointerVar>,
throwable: CPointer<CPointerVar<ThrowableVar>>?,
getResult: CPointer<CFunction<() -> CPointer<*>>>
): Boolean {
val message = runBlocking { messageChannel.receive() }

if (message == null) {
return runBlocking {
throwable?.pointed?.value = messageErrorChannel.receive()
false
}
}

type.pointed.value = message::class.qualifiedName!!.cstrPtr
ref.pointed.value = StableRef.create(message).asCPointer()

return runBlocking {
runCatching(throwable) {
resultChannel.send(getResult().asStableRef<Message>().get())
}.isSuccess
}
}
}
50 changes: 50 additions & 0 deletions client-py/src/nativeMain/kotlin/Logger.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* This file is part of Fhraise.
* Copyright (c) 2024 HSAS Foodies. All Rights Reserved.
*
* Fhraise is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* Fhraise is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with Fhraise. If not, see <https://www.gnu.org/licenses/>.
*/

package xyz.xfqlittlefan.fhraise.py

import kotlinx.datetime.Clock

class Logger internal constructor(private val tag: Any) {
@Deprecated("This constructor is for calling from C code only.", level = DeprecationLevel.HIDDEN)
constructor(tag: String) : this("<From C> $tag" as Any)

fun debug(message: String) {
println("Debug", message)
}

fun info(message: String) {
println("Info", message)
}

fun warn(message: String) {
println("Warn", message)
}

fun error(message: String) {
println("Error", message)
}

private fun println(level: String, message: String) {
message.split("\n").forEach {
println("${Clock.System.now()} [$tag] $level: $it")
}
}
}

internal val Any.logger: Logger get() = Logger(this::class.let { it.qualifiedName ?: it })
95 changes: 95 additions & 0 deletions client-py/src/nativeMain/kotlin/Throwable.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* This file is part of Fhraise.
* Copyright (c) 2024 HSAS Foodies. All Rights Reserved.
*
* Fhraise is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* Fhraise is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with Fhraise. If not, see <https://www.gnu.org/licenses/>.
*/

package xyz.xfqlittlefan.fhraise.py

import kotlinx.cinterop.*
import kotlin.experimental.ExperimentalNativeApi

@OptIn(ExperimentalForeignApi::class)
class ThrowableVar(rawPtr: NativePtr) : CStructVar(rawPtr) {
@Suppress("DEPRECATION")
companion object : Type(40, 8)

var type: CArrayPointer<ByteVar>?
get() = memberAt<CArrayPointerVar<ByteVar>>(0).value
set(value) {
memberAt<CArrayPointerVar<ByteVar>>(0).value = value
}

var ref: COpaquePointer?
get() = memberAt<COpaquePointerVar>(8).value
set(value) {
memberAt<COpaquePointerVar>(8).value = value
}

var message: CArrayPointer<ByteVar>?
get() = memberAt<CArrayPointerVar<ByteVar>>(16).value
set(value) {
memberAt<CArrayPointerVar<ByteVar>>(16).value = value
}

var stacktrace: CArrayPointer<CArrayPointerVar<ByteVar>>?
get() = memberAt<CArrayPointerVar<CArrayPointerVar<ByteVar>>>(24).value
set(value) {
memberAt<CArrayPointerVar<CArrayPointerVar<ByteVar>>>(24).value = value
}

var stacktraceSize: Int?
get() = memberAt<IntVar>(32).value
set(value) {
memberAt<IntVar>(32).value = value ?: 0
}
}

@ExperimentalForeignApi
internal class ThrowableWrapper(val throwable: CPointer<ThrowableVar>) : Throwable()

@ExperimentalNativeApi
@ExperimentalForeignApi
internal fun Throwable.wrapToC(): CPointer<ThrowableVar> {
logger.debug("Wrapping throwable.")
logger.warn(getStackTrace().joinToString("\n"))

val throwable = nativeHeap.alloc<ThrowableVar>()
throwable.type = this::class.qualifiedName?.cstrPtr
throwable.ref = StableRef.create(this).asCPointer()
throwable.message = this.message?.cstrPtr
val stacktraceList = mutableListOf<ByteVar>()
this.getStackTrace().forEach {
stacktraceList.add(it.cstrPtr.pointed)
}
val stacktraceArray = nativeHeap.allocArrayOfPointersTo(stacktraceList)
throwable.stacktrace = stacktraceArray
throwable.stacktraceSize = stacktraceList.size
return throwable.ptr
}

@ExperimentalNativeApi
@ExperimentalForeignApi
internal inline fun <R> runCatching(
throwablePtr: CPointer<CPointerVar<ThrowableVar>>?, block: () -> R
) = runCatching(block).fold(onSuccess = {
Result.success(it)
}, onFailure = {
val throwableVarPtr = it.wrapToC()
if (throwablePtr != null && throwablePtr.rawValue != nativeNullPtr) {
throwablePtr.pointed.value = throwableVarPtr
}
Result.failure(ThrowableWrapper(throwableVarPtr))
})
30 changes: 30 additions & 0 deletions client-py/src/nativeMain/kotlin/Types.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* This file is part of Fhraise.
* Copyright (c) 2024 HSAS Foodies. All Rights Reserved.
*
* Fhraise is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* Fhraise is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with Fhraise. If not, see <https://www.gnu.org/licenses/>.
*/

package xyz.xfqlittlefan.fhraise.py

import kotlinx.cinterop.*

@ExperimentalForeignApi
internal val String.cstrPtr: CPointer<ByteVar>
get() {
val cstr = cstr
val ptr = nativeHeap.allocArray<ByteVar>(cstr.size)
cstr.place(ptr)
return ptr
}
Loading

0 comments on commit f0a774b

Please sign in to comment.