在现代软件开发中,跨平台兼容性是一个重要的考量。本文将介绍如何使用Kotlin Native创建一个能够在Windows和Linux平台上工作的项目。项目的核心功能是检测当前运行的平台,并显示相应的消息。此外,还将探讨如何在这两个平台上创建原生窗口,并添加切换到全屏模式的功能。需要注意的是,全屏模式的实现在Windows平台上更为简单,而在Linux平台上,将展示如何最大化窗口并去除装饰,后续会添加真正的全屏切换功能。
首先,从Windows平台开始,因为它的实现相对简单。将添加两个不同的线程:第一个用于系统消息循环,第二个用于Vulkan渲染。为了在线程之间传递数据,首先需要定义共享数据。在之前的部分中,已经在原生互操作中添加了global.def文件。现在,将定义所需的数据:
internal class CommonData(
val semaphore: sem_tVar,
var hInstance: HINSTANCE? = null,
var hwnd: HWND? = null,
var showWindowCentered: Boolean = true,
var showWindowFullscreen: Boolean = false,
var onTheRun: Boolean = true,
var windowsSurface: WindowsSurface? = null
)
在这里,添加了所有需要传递给系统消息循环的数据,最重要的是:用于线程同步的信号量、用于停止所有处理的"onTheRun"变量,以及对新窗口类的引用。还添加了获取指向共享数据的指针的类。
接下来,需要对公共代码进行一些更改:
internal expect class Platform {
fun Initialize()
companion object {
val type: PlatformEnum
val VK_KHR_PLATFORM_SURFACE_EXTENSION_NAME: String
}
}
期望每个原生平台都有一个"Initialize"方法,它将创建窗口并开始渲染,并添加了一个常量来定义表面扩展的名称。现在,主函数将如下所示:
@ExperimentalUnsignedTypes
fun main(args: Array) {
val platform = Platform()
platform.Initialize()
}
现在,是时候创建窗口了。这与在C++中的做法大致相同——使用标准的WinAPI方法,并将类引用传递给WndProc以调用类方法。首先,让在类初始化中获取共享数据:
init {
val kotlinObject = DetachedObjectGraph(sharedData.kotlinObject).attach()
val sharedData = kotlinObject.userdata!!.asStableRef().get()
sharedData.windowsSurface = this
}
然后,让创建原生窗口本身并运行它:
fun initialize() {
memScoped {
val hInstance = GetModuleHandleW(null)
val hBrush = CreateSolidBrush(0x00000000)
val wc: WNDCLASSEXW = alloc()
wc.cbSize = sizeOf().convert()
wc.style = (CS_HREDRAW or CS_VREDRAW or CS_OWNDC).convert()
wc.lpfnWndProc = staticCFunction { hwnd, msg, wParam, lParam ->
when (msg.toInt()) {
WM_CLOSE -> {
val kotlinObject = DetachedObjectGraph(sharedData.kotlinObject).attach()
val sharedData = kotlinObject.userdata!!.asStableRef().get()
sharedData.onTheRun = false
DestroyWindow(hwnd)
}
WM_DESTROY -> {
PostQuitMessage(0)
}
// ...
WM_KEYDOWN -> {
if (GetAsyncKeyState(VK_ESCAPE) != 0.toShort()) {
val kotlinObject = DetachedObjectGraph(sharedData.kotlinObject).attach()
val sharedData = kotlinObject.userdata!!.asStableRef().get()
if (sharedData.showWindowFullscreen) {
sharedData.windowsSurface?.changeFullscreen(false)
}
PostMessageW(hwnd, WM_CLOSE, 0, 0)
}
}
WM_SYSKEYDOWN -> {
if (wParam == VK_F4.toULong()) {
return@staticCFunction 1
}
}
else -> {
return@staticCFunction DefWindowProcW(hwnd, msg, wParam, lParam)
}
}
}
wc.hInstance = hInstance
// ...
val failure: ATOM = 0u
if (RegisterClassExW(wc.ptr) == failure) {
throw RuntimeException("Failed to create native window!")
}
hwnd = CreateWindowExW(
WS_EX_CLIENTEDGE,
"kvarc",
"kvarc",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
_width,
_height,
null,
null,
hInstance,
null
)
if (hwnd == null) {
MessageBoxW(
null,
"Failed to create native window!",
"kvarc",
(MB_OK).convert()
)
throw RuntimeException("Failed to create native window!")
}
// ...
ShowWindow(hwnd, SW_SHOWNORMAL)
UpdateWindow(hwnd)
}
}
窗口消息循环:
fun messageLoop() {
memScoped {
val msg: MSG = alloc()
while (GetMessageW(msg.ptr, null, 0u, 0u) > 0) {
TranslateMessage(msg.ptr)
DispatchMessageW(msg.ptr)
}
}
}
由于已经创建了窗口,是时候为窗口和渲染添加线程了:
@ExperimentalUnsignedTypes
internal actual class Platform {
actual fun Initialize() {
try {
val arena = Arena()
val semaphore = arena.alloc()
sem_init(semaphore.ptr, 0, 0)
// ...
memScoped {
val winThread = alloc()
pthread_create(winThread.ptr, null, staticCFunction { _: COpaquePointer? ->
initRuntimeIfNeeded()
val kotlinObject = DetachedObjectGraph(sharedData.kotlinObject).attach()
val data = kotlinObject.userdata!!.asStableRef().get()
val win = WindowsSurface(data.showWindowFullscreen)
win.initialize()
// ...
@Suppress("UNCHECKED_CAST")
data.hwnd = win.hwnd!!
sem_post(data.semaphore.ptr)
win.messageLoop()
win.dispose()
null as COpaquePointer?
// keep it lies not needed
}, null).ensureUnixCallResult("pthread_create")
val vkThread = alloc()
pthread_create(vkThread.ptr, null, staticCFunction { _: COpaquePointer? ->
initRuntimeIfNeeded()
val kotlinObject = DetachedObjectGraph(sharedData.kotlinObject).attach()
val data = kotlinObject.userdata!!.asStableRef().get()
sem_wait(data.semaphore.ptr)
// TODO Vulkan loop
null as COpaquePointer?
// keep it lies not needed
}, null).ensureUnixCallResult("pthread_create")
pthread_join(vkThread.value, null).ensureUnixCallResult("pthread_join")
pthread_join(winThread.value, null).ensureUnixCallResult("pthread_join")
sem_destroy(semaphore.ptr)
commonDataStableRef.dispose()
arena.clear()
}
} catch (ex: Exception) {
logError("Failed to start with exception: ${ex.message}")
}
}
}