Native: move samples to backend.native/tests/
This commit is contained in:
committed by
Space
parent
b7337d2e64
commit
7bf6d64cfb
@@ -0,0 +1,13 @@
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
macosX64("objc") {
|
||||
binaries {
|
||||
executable {
|
||||
entryPoint = "sample.objc.main"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
kotlin.code.style=official
|
||||
kotlin.import.noCommonSourceSets=true
|
||||
@@ -0,0 +1,103 @@
|
||||
package sample.objc
|
||||
|
||||
import kotlinx.cinterop.*
|
||||
import platform.Foundation.NSOperationQueue
|
||||
import platform.Foundation.NSThread
|
||||
import platform.darwin.dispatch_async_f
|
||||
import platform.darwin.dispatch_get_main_queue
|
||||
import platform.darwin.dispatch_sync_f
|
||||
import kotlin.native.concurrent.*
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
inline fun <reified T> executeAsync(queue: NSOperationQueue, crossinline producerConsumer: () -> Pair<T, (T) -> Unit>) {
|
||||
dispatch_async_f(queue.underlyingQueue, StableRef.create(
|
||||
producerConsumer()
|
||||
).asCPointer(), staticCFunction { it ->
|
||||
val result = it!!.asStableRef<Pair<T, (T) -> Unit>>()
|
||||
result.get().second(result.get().first)
|
||||
result.dispose()
|
||||
})
|
||||
}
|
||||
|
||||
inline fun mainContinuation(singleShot: Boolean = true, noinline block: () -> Unit) = Continuation0(
|
||||
block, staticCFunction { invokerArg ->
|
||||
if (NSThread.isMainThread()) {
|
||||
invokerArg!!.callContinuation0()
|
||||
} else {
|
||||
dispatch_sync_f(dispatch_get_main_queue(), invokerArg, staticCFunction { args ->
|
||||
args!!.callContinuation0()
|
||||
})
|
||||
}
|
||||
}, singleShot)
|
||||
|
||||
inline fun <T1> mainContinuation(singleShot: Boolean = true, noinline block: (T1) -> Unit) = Continuation1(
|
||||
block, staticCFunction { invokerArg ->
|
||||
if (NSThread.isMainThread()) {
|
||||
invokerArg!!.callContinuation1<T1>()
|
||||
} else {
|
||||
dispatch_sync_f(dispatch_get_main_queue(), invokerArg, staticCFunction { args ->
|
||||
args!!.callContinuation1<T1>()
|
||||
})
|
||||
}
|
||||
}, singleShot)
|
||||
|
||||
inline fun <T1, T2> mainContinuation(singleShot: Boolean = true, noinline block: (T1, T2) -> Unit) = Continuation2(
|
||||
block, staticCFunction { invokerArg ->
|
||||
if (NSThread.isMainThread()) {
|
||||
invokerArg!!.callContinuation2<T1, T2>()
|
||||
} else {
|
||||
dispatch_sync_f(dispatch_get_main_queue(), invokerArg, staticCFunction { args ->
|
||||
args!!.callContinuation2<T1, T2>()
|
||||
})
|
||||
}
|
||||
}, singleShot)
|
||||
|
||||
// This object allows to create frozen Kotlin continuations suitable for execution on other threads/queues.
|
||||
// It takes frozen operation and any after call, and creates lambda which could be used to run operation
|
||||
// anywhere and provide result to `after` callback.
|
||||
@ThreadLocal
|
||||
object Continuator {
|
||||
val map = mutableMapOf<Any, Pair<Int, *>>()
|
||||
|
||||
fun wrap(operation: () -> Unit, after: () -> Unit): () -> Unit {
|
||||
assert(NSThread.isMainThread())
|
||||
val id = Any()
|
||||
map[id] = Pair(0, after)
|
||||
return {
|
||||
initRuntimeIfNeeded()
|
||||
operation()
|
||||
executeAsync(NSOperationQueue.mainQueue) {
|
||||
Pair(id, { id: Any -> Continuator.execute(id) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <P> wrap(operation: () -> P, block: (P) -> Unit): () -> Unit {
|
||||
assert(NSThread.isMainThread())
|
||||
val id = Any()
|
||||
map[id] = Pair(1, block)
|
||||
return {
|
||||
initRuntimeIfNeeded()
|
||||
// Note, that operation here must return detachable value (for example, frozen).
|
||||
executeAsync(NSOperationQueue.mainQueue) {
|
||||
Pair(Pair(id, operation()), { it: Pair<Any, P> ->
|
||||
Continuator.execute(it.first, it.second)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun execute(id: Any) {
|
||||
val countAndBlock = map.remove(id)
|
||||
assertNotNull(countAndBlock)
|
||||
assert(countAndBlock.first == 0)
|
||||
(countAndBlock.second as Function0<Unit>)()
|
||||
}
|
||||
|
||||
fun <P> execute(id: Any, parameter: P) {
|
||||
val countAndBlock = map.remove(id)
|
||||
assertNotNull(countAndBlock)
|
||||
assert(countAndBlock.first == 1)
|
||||
(countAndBlock.second as Function1<P, Unit>)(parameter)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
package sample.objc
|
||||
|
||||
import kotlinx.cinterop.*
|
||||
import platform.AppKit.*
|
||||
import platform.Contacts.CNContactStore
|
||||
import platform.Contacts.CNEntityType
|
||||
import platform.Foundation.*
|
||||
import platform.darwin.*
|
||||
import platform.posix.QOS_CLASS_BACKGROUND
|
||||
import platform.posix.memcpy
|
||||
import kotlin.native.concurrent.*
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
data class QueryResult(val json: Map<String, *>?, val error: String?)
|
||||
|
||||
private val NSData.json: Map<String, *>?
|
||||
get() = NSJSONSerialization.JSONObjectWithData(this, 0, null) as? Map<String, *>
|
||||
|
||||
fun main() {
|
||||
autoreleasepool {
|
||||
runApp()
|
||||
}
|
||||
}
|
||||
|
||||
val appDelegate = MyAppDelegate()
|
||||
|
||||
private fun runApp() {
|
||||
val app = NSApplication.sharedApplication()
|
||||
|
||||
app.delegate = appDelegate
|
||||
app.setActivationPolicy(NSApplicationActivationPolicy.NSApplicationActivationPolicyRegular)
|
||||
app.activateIgnoringOtherApps(true)
|
||||
|
||||
app.run()
|
||||
}
|
||||
|
||||
|
||||
class Controller : NSObject() {
|
||||
private var index = 1
|
||||
private val httpDelegate = HttpDelegate()
|
||||
|
||||
@ObjCAction
|
||||
fun onClick() {
|
||||
if (!appDelegate.canClick) {
|
||||
appDelegate.contentText.string = "Another load in progress..."
|
||||
return
|
||||
}
|
||||
|
||||
// Here we call continuator service to ensure we can access mutable state from continuation.
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND.convert(), 0),
|
||||
Continuator.wrap({ println("In queue ${dispatch_get_current_queue()}")}) {
|
||||
println("After in queue ${dispatch_get_current_queue()}: $index")
|
||||
})
|
||||
|
||||
appDelegate.canClick = false
|
||||
// Fetch URL in the background on the button click.
|
||||
httpDelegate.fetchUrl("https://jsonplaceholder.typicode.com/todos/${index++}")
|
||||
}
|
||||
|
||||
@ObjCAction
|
||||
fun onQuit() {
|
||||
NSApplication.sharedApplication().stop(this)
|
||||
}
|
||||
|
||||
@ObjCAction
|
||||
fun onRequest() {
|
||||
val addressBookRef = CNContactStore()
|
||||
addressBookRef.requestAccessForEntityType(CNEntityType.CNEntityTypeContacts, mainContinuation {
|
||||
granted, error ->
|
||||
appDelegate.contentText.string = if (granted)
|
||||
"Access granted!"
|
||||
else
|
||||
"Access denied: $error"
|
||||
})
|
||||
}
|
||||
|
||||
class HttpDelegate: NSObject(), NSURLSessionDataDelegateProtocol {
|
||||
private val asyncQueue = NSOperationQueue()
|
||||
private var receivedData: NSData? = null
|
||||
|
||||
fun fetchUrl(url: String) {
|
||||
receivedData = null
|
||||
val session = NSURLSession.sessionWithConfiguration(
|
||||
NSURLSessionConfiguration.defaultSessionConfiguration(),
|
||||
this,
|
||||
delegateQueue = asyncQueue
|
||||
)
|
||||
session.dataTaskWithURL(NSURL(string = url)).resume()
|
||||
}
|
||||
|
||||
override fun URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData: NSData) {
|
||||
initRuntimeIfNeeded()
|
||||
receivedData = didReceiveData
|
||||
}
|
||||
|
||||
override fun URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError: NSError?) {
|
||||
initRuntimeIfNeeded()
|
||||
|
||||
executeAsync(NSOperationQueue.mainQueue) {
|
||||
val response = task.response as? NSHTTPURLResponse
|
||||
Pair(when {
|
||||
response == null -> QueryResult(null, didCompleteWithError?.localizedDescription)
|
||||
response.statusCode.toInt() != 200 -> QueryResult(null, "${response.statusCode.toInt()})")
|
||||
else -> QueryResult(receivedData?.json, null)
|
||||
}, { result: QueryResult ->
|
||||
appDelegate.contentText.string = result.json?.toString() ?: "Error: ${result.error}"
|
||||
appDelegate.canClick = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MyAppDelegate() : NSObject(), NSApplicationDelegateProtocol {
|
||||
|
||||
private val window: NSWindow
|
||||
private val controller = Controller()
|
||||
val contentText: NSText
|
||||
var canClick = true
|
||||
|
||||
init {
|
||||
val mainDisplayRect = NSScreen.mainScreen()!!.frame
|
||||
val windowRect = mainDisplayRect.useContents {
|
||||
NSMakeRect(
|
||||
origin.x + size.width * 0.25,
|
||||
origin.y + size.height * 0.25,
|
||||
size.width * 0.5,
|
||||
size.height * 0.5
|
||||
)
|
||||
}
|
||||
|
||||
val windowStyle = NSWindowStyleMaskTitled or NSWindowStyleMaskMiniaturizable or
|
||||
NSWindowStyleMaskClosable or NSWindowStyleMaskResizable
|
||||
|
||||
window = NSWindow(windowRect, windowStyle, NSBackingStoreBuffered, false).apply {
|
||||
title = "URL async fetcher"
|
||||
opaque = true
|
||||
hasShadow = true
|
||||
preferredBackingLocation = NSWindowBackingLocationVideoMemory
|
||||
hidesOnDeactivate = false
|
||||
backgroundColor = NSColor.grayColor()
|
||||
releasedWhenClosed = false
|
||||
|
||||
val delegateImpl = object : NSObject(), NSWindowDelegateProtocol {
|
||||
override fun windowShouldClose(sender: NSWindow): Boolean {
|
||||
NSApplication.sharedApplication().stop(this)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapping to autoreleasepool is a workaround for false-positive memory leak detected:
|
||||
// NSWindow.delegate setter appears to put the delegate to autorelease pool.
|
||||
// Since this code runs during top-level val initializer, it misses the autoreleasepool in [main],
|
||||
// so the object gets released too late.
|
||||
autoreleasepool {
|
||||
delegate = delegateImpl
|
||||
}
|
||||
}
|
||||
|
||||
val buttonPress = NSButton(NSMakeRect(10.0, 10.0, 100.0, 40.0)).apply {
|
||||
title = "Click"
|
||||
target = controller
|
||||
action = NSSelectorFromString("onClick")
|
||||
}
|
||||
window.contentView!!.addSubview(buttonPress)
|
||||
val buttonQuit = NSButton(NSMakeRect(120.0, 10.0, 100.0, 40.0)).apply {
|
||||
title = "Quit"
|
||||
target = controller
|
||||
action = NSSelectorFromString("onQuit")
|
||||
}
|
||||
window.contentView!!.addSubview(buttonQuit)
|
||||
|
||||
val buttonRequest = NSButton(NSMakeRect(230.0, 10.0, 100.0, 40.0)).apply {
|
||||
title = "Request"
|
||||
target = controller
|
||||
action = NSSelectorFromString("onRequest")
|
||||
}
|
||||
window.contentView!!.addSubview(buttonRequest)
|
||||
|
||||
contentText = NSText(NSMakeRect(10.0, 80.0, 600.0, 350.0)).apply {
|
||||
string = "Press 'Click' to start fetching"
|
||||
verticallyResizable = false
|
||||
horizontallyResizable = false
|
||||
|
||||
}
|
||||
window.contentView!!.addSubview(contentText)
|
||||
}
|
||||
|
||||
override fun applicationWillFinishLaunching(notification: NSNotification) {
|
||||
window.makeKeyAndOrderFront(this)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user