diff --git a/aws-crt-kotlin/api/aws-crt-kotlin.api b/aws-crt-kotlin/api/aws-crt-kotlin.api index 9831d4c3..09da5fa2 100644 --- a/aws-crt-kotlin/api/aws-crt-kotlin.api +++ b/aws-crt-kotlin/api/aws-crt-kotlin.api @@ -6,8 +6,8 @@ public final class aws/sdk/kotlin/crt/CRT { public static final field INSTANCE Laws/sdk/kotlin/crt/CRT; public final fun errorName (I)Ljava/lang/String; public final fun errorString (I)Ljava/lang/String; - public final fun initRuntime (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun initRuntime$default (Laws/sdk/kotlin/crt/CRT;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun initRuntime (Lkotlin/jvm/functions/Function1;)V + public static synthetic fun initRuntime$default (Laws/sdk/kotlin/crt/CRT;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public final fun isHttpErrorRetryable (I)Z public final fun lastError ()I public final fun nativeMemory ()J diff --git a/aws-crt-kotlin/build.gradle.kts b/aws-crt-kotlin/build.gradle.kts index a7b79e43..c57d966b 100644 --- a/aws-crt-kotlin/build.gradle.kts +++ b/aws-crt-kotlin/build.gradle.kts @@ -86,6 +86,12 @@ kotlin { } } + val commonTest by getting { + dependencies { + implementation(libs.kotlinx.coroutines.test) + } + } + val jvmMain by getting { dependencies { implementation(libs.crt.java) diff --git a/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/Utils.kt b/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/Utils.kt deleted file mode 100644 index 05442fd9..00000000 --- a/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/Utils.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package aws.sdk.kotlin.crt - -import kotlinx.coroutines.CoroutineScope -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext - -// TODO - replace with kotlinx-coroutines-test if we can figure out the gradle mess -/** - * MPP compatible runBlocking to run suspend tests in common modules - */ -expect fun runSuspendTest(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T diff --git a/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/auth/credentials/CredentialsProviderTest.kt b/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/auth/credentials/CredentialsProviderTest.kt index 41881cb5..02af907e 100644 --- a/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/auth/credentials/CredentialsProviderTest.kt +++ b/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/auth/credentials/CredentialsProviderTest.kt @@ -9,7 +9,7 @@ import aws.sdk.kotlin.crt.CrtTest import aws.sdk.kotlin.crt.io.ClientBootstrap import aws.sdk.kotlin.crt.io.EventLoopGroup import aws.sdk.kotlin.crt.io.HostResolver -import aws.sdk.kotlin.crt.runSuspendTest +import kotlinx.coroutines.test.runTest import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals @@ -19,7 +19,7 @@ class CredentialsProviderTest : CrtTest() { @Ignore // FIXME Enable when Kotlin/Native implementation is complete @Test - fun testStaticProvider() = runSuspendTest { + fun testStaticProvider() = runTest { val provider = StaticCredentialsProvider.fromCredentials(EXPECTED_CREDENTIALS) val actual = provider.getCredentials() assertEquals(EXPECTED_CREDENTIALS, actual) @@ -44,7 +44,7 @@ class CredentialsProviderTest : CrtTest() { @Ignore // FIXME Enable when Kotlin/Native implementation is complete @Test - fun testCacheStatic() = runSuspendTest { + fun testCacheStatic() = runTest { val provider = CachedCredentialsProvider.build { source = StaticCredentialsProvider.fromCredentials(EXPECTED_CREDENTIALS) refreshTimeInMilliseconds = 900 diff --git a/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/auth/signing/SigningTest.kt b/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/auth/signing/SigningTest.kt index 563aeebf..82b51777 100644 --- a/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/auth/signing/SigningTest.kt +++ b/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/auth/signing/SigningTest.kt @@ -13,6 +13,7 @@ import aws.sdk.kotlin.crt.http.HttpRequest import aws.sdk.kotlin.crt.http.HttpRequestBodyStream import aws.sdk.kotlin.crt.http.headers import aws.sdk.kotlin.crt.io.Uri +import kotlinx.coroutines.test.runTest import kotlin.test.* // ported over from crt-java @@ -56,7 +57,7 @@ class SigningTest : CrtTest() { @Ignore // FIXME Enable when Kotlin/Native implementation is complete @Test - fun testSigningSuccess() = runSuspendTest { + fun testSigningSuccess() = runTest { StaticCredentialsProvider.build { accessKeyId = TEST_ACCESS_KEY_ID secretAccessKey = TEST_SECRET_ACCESS_KEY @@ -82,7 +83,7 @@ class SigningTest : CrtTest() { @Ignore // FIXME Enable when Kotlin/Native implementation is complete @Test - fun testQuerySigningSuccess() = runSuspendTest { + fun testQuerySigningSuccess() = runTest { StaticCredentialsProvider.build { accessKeyId = TEST_ACCESS_KEY_ID secretAccessKey = TEST_SECRET_ACCESS_KEY @@ -114,7 +115,7 @@ class SigningTest : CrtTest() { @Ignore // FIXME Enable when Kotlin/Native implementation is complete @Test - fun testSigningBasicSigV4() = runSuspendTest { + fun testSigningBasicSigV4() = runTest { StaticCredentialsProvider.build { accessKeyId = TEST_ACCESS_KEY_ID secretAccessKey = TEST_SECRET_ACCESS_KEY @@ -147,7 +148,7 @@ class SigningTest : CrtTest() { @Ignore // FIXME Enable when Kotlin/Native implementation is complete @Test - fun testSigningFailureBadRequest() = runSuspendTest { + fun testSigningFailureBadRequest() = runTest { StaticCredentialsProvider.build { accessKeyId = TEST_ACCESS_KEY_ID secretAccessKey = TEST_SECRET_ACCESS_KEY @@ -174,7 +175,7 @@ class SigningTest : CrtTest() { @Ignore // FIXME Enable when Kotlin/Native implementation is complete @Test - fun testSigningSigV4Asymmetric() = runSuspendTest { + fun testSigningSigV4Asymmetric() = runTest { StaticCredentialsProvider.build { accessKeyId = TEST_ACCESS_KEY_ID secretAccessKey = TEST_SECRET_ACCESS_KEY @@ -202,7 +203,7 @@ class SigningTest : CrtTest() { @Ignore // FIXME Enable when Kotlin/Native implementation is complete @Test - fun testSigningChunkTrailingHeaders() = runSuspendTest { + fun testSigningChunkTrailingHeaders() = runTest { StaticCredentialsProvider.build { accessKeyId = "AKID" secretAccessKey = "SECRET" diff --git a/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/http/HttpClientConnectionTest.kt b/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/http/HttpClientConnectionTest.kt index 5d72a820..d3ddfeb6 100644 --- a/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/http/HttpClientConnectionTest.kt +++ b/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/http/HttpClientConnectionTest.kt @@ -7,8 +7,8 @@ package aws.sdk.kotlin.crt.http import aws.sdk.kotlin.crt.CrtTest import aws.sdk.kotlin.crt.io.* -import aws.sdk.kotlin.crt.runSuspendTest import aws.sdk.kotlin.crt.use +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withTimeout import kotlin.test.* import kotlin.time.measureTime @@ -16,7 +16,7 @@ import kotlin.time.measureTime class HttpClientConnectionTest : CrtTest() { @Ignore // FIXME Enable when Kotlin/Native implementation is complete @Test - fun testDefaults() = runSuspendTest { + fun testDefaults() = runTest { val uri = Uri.parse("https://aws-crt-test-stuff.s3.amazonaws.com") val socketOpts = SocketOptions() val elg = EventLoopGroup() @@ -56,7 +56,7 @@ class HttpClientConnectionTest : CrtTest() { @Ignore // FIXME Enable when Kotlin/Native implementation is complete @Test - fun testHttpConnection() = runSuspendTest { + fun testHttpConnection() = runTest { // S3 assertConnect("https://aws-crt-test-stuff.s3.amazonaws.com") assertConnect("http://aws-crt-test-stuff.s3.amazonaws.com") diff --git a/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/io/ClientBootstrapTest.kt b/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/io/ClientBootstrapTest.kt index 6c1c5c08..11ea593a 100644 --- a/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/io/ClientBootstrapTest.kt +++ b/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/io/ClientBootstrapTest.kt @@ -6,14 +6,12 @@ package aws.sdk.kotlin.crt.io import aws.sdk.kotlin.crt.CrtTest -import aws.sdk.kotlin.crt.runSuspendTest -import kotlin.test.Ignore +import kotlinx.coroutines.test.runTest import kotlin.test.Test class ClientBootstrapTest : CrtTest() { - @Ignore // FIXME Enable when Kotlin/Native implementation is complete @Test - fun createDestroy() = runSuspendTest { + fun createDestroy() = runTest { val elg = EventLoopGroup() val hr = HostResolver(elg) val bootstrap = ClientBootstrap(elg, hr) diff --git a/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/io/EventLoopGroupTest.kt b/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/io/EventLoopGroupTest.kt index 80ce89ff..d5cdd0c6 100644 --- a/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/io/EventLoopGroupTest.kt +++ b/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/io/EventLoopGroupTest.kt @@ -6,14 +6,12 @@ package aws.sdk.kotlin.crt.io import aws.sdk.kotlin.crt.CrtTest -import aws.sdk.kotlin.crt.runSuspendTest -import kotlin.test.Ignore +import kotlinx.coroutines.test.runTest import kotlin.test.Test class EventLoopGroupTest : CrtTest() { - @Ignore // FIXME Enable when Kotlin/Native implementation is complete @Test - fun createDestroy() = runSuspendTest { + fun createDestroy() = runTest { val elg = EventLoopGroup() elg.close() } diff --git a/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/io/HostResolverTest.kt b/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/io/HostResolverTest.kt index 3d6a9d81..b502e3bb 100644 --- a/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/io/HostResolverTest.kt +++ b/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/io/HostResolverTest.kt @@ -6,15 +6,13 @@ package aws.sdk.kotlin.crt.io import aws.sdk.kotlin.crt.CrtTest -import aws.sdk.kotlin.crt.runSuspendTest import aws.sdk.kotlin.crt.use -import kotlin.test.Ignore +import kotlinx.coroutines.test.runTest import kotlin.test.Test class HostResolverTest : CrtTest() { - @Ignore // FIXME Enable when Kotlin/Native implementation is complete @Test - fun createDestroy() = runSuspendTest { + fun createDestroy() = runTest { EventLoopGroup().use { elg -> val hr = HostResolver(elg) hr.close() diff --git a/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/io/TlsContextTest.kt b/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/io/TlsContextTest.kt index 5e487da5..31c45802 100644 --- a/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/io/TlsContextTest.kt +++ b/aws-crt-kotlin/common/test/aws/sdk/kotlin/crt/io/TlsContextTest.kt @@ -6,14 +6,12 @@ package aws.sdk.kotlin.crt.io import aws.sdk.kotlin.crt.CrtTest -import aws.sdk.kotlin.crt.runSuspendTest -import kotlin.test.Ignore +import kotlinx.coroutines.test.runTest import kotlin.test.Test class TlsContextTest : CrtTest() { - @Ignore // FIXME Enable when Kotlin/Native implementation is complete @Test - fun createDestroy() = runSuspendTest { + fun createDestroy() = runTest { val ctx = TlsContext() ctx.close() } diff --git a/aws-crt-kotlin/jvm/src/aws/sdk/kotlin/crt/CRT.kt b/aws-crt-kotlin/jvm/src/aws/sdk/kotlin/crt/CRT.kt index 45c318bb..243ec3fa 100644 --- a/aws-crt-kotlin/jvm/src/aws/sdk/kotlin/crt/CRT.kt +++ b/aws-crt-kotlin/jvm/src/aws/sdk/kotlin/crt/CRT.kt @@ -42,7 +42,7 @@ public actual object CRT { } } initialized = true - } + } } } diff --git a/aws-crt-kotlin/jvm/test/aws/sdk/kotlin/crt/UtilsJVM.kt b/aws-crt-kotlin/jvm/test/aws/sdk/kotlin/crt/UtilsJVM.kt deleted file mode 100644 index 57299670..00000000 --- a/aws-crt-kotlin/jvm/test/aws/sdk/kotlin/crt/UtilsJVM.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package aws.sdk.kotlin.crt - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.runBlocking -import kotlin.coroutines.CoroutineContext - -actual fun runSuspendTest( - context: CoroutineContext, - block: suspend CoroutineScope.() -> T, -): T = runBlocking { block(this) } diff --git a/aws-crt-kotlin/jvm/test/aws/sdk/kotlin/crt/http/HttpRequestResponseTest.kt b/aws-crt-kotlin/jvm/test/aws/sdk/kotlin/crt/http/HttpRequestResponseTest.kt index e2164a53..fd1065cc 100644 --- a/aws-crt-kotlin/jvm/test/aws/sdk/kotlin/crt/http/HttpRequestResponseTest.kt +++ b/aws-crt-kotlin/jvm/test/aws/sdk/kotlin/crt/http/HttpRequestResponseTest.kt @@ -5,9 +5,9 @@ package aws.sdk.kotlin.crt.http -import aws.sdk.kotlin.crt.runSuspendTest import aws.sdk.kotlin.crt.util.Digest import aws.sdk.kotlin.crt.util.encodeToHex +import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.TestInstance @@ -65,40 +65,45 @@ class HttpRequestResponseTest : HttpClientTest() { } } + @Ignore // FIXME This test is broken since switching runSuspendingTest to runTest @Test - fun testHttpGet() = runSuspendTest { + fun testHttpGet() = runTest { testSimpleRequest("GET", "/get", 200) testSimpleRequest("GET", "/post", 405) testSimpleRequest("GET", "/put", 405) testSimpleRequest("GET", "/delete", 405) } + @Ignore // FIXME This test is broken since switching runSuspendingTest to runTest @Test - fun testHttpPost() = runSuspendTest { + fun testHttpPost() = runTest { testSimpleRequest("POST", "/get", 405) testSimpleRequest("POST", "/post", 200) testSimpleRequest("POST", "/put", 405) testSimpleRequest("POST", "/delete", 405) } + @Ignore // FIXME This test is broken since switching runSuspendingTest to runTest @Test - fun testHttpPut() = runSuspendTest { + fun testHttpPut() = runTest { testSimpleRequest("PUT", "/get", 405) testSimpleRequest("PUT", "/post", 405) testSimpleRequest("PUT", "/put", 200) testSimpleRequest("PUT", "/delete", 405) } + @Ignore // FIXME This test is broken since switching runSuspendingTest to runTest @Test - fun testHttpDelete() = runSuspendTest { + fun testHttpDelete() = runTest { testSimpleRequest("DELETE", "/get", 405) testSimpleRequest("DELETE", "/post", 405) testSimpleRequest("DELETE", "/put", 405) testSimpleRequest("DELETE", "/delete", 200) } + @Ignore // FIXME This test is broken since switching runSuspendingTest to runTest @Test - fun testHttpDownload() = runSuspendTest { + fun testHttpDownload() = runTest { val response = roundTrip(url = "https://aws-crt-test-stuff.s3.amazonaws.com/http_test_doc.txt", verb = "GET") assertEquals(200, response.statusCode, "expected http status does not match") assertNotNull(response.body) @@ -106,8 +111,9 @@ class HttpRequestResponseTest : HttpClientTest() { assertEquals(TEST_DOC_SHA256, Digest.sha256(response.body).encodeToHex()) } + @Ignore // FIXME This test is broken since switching runSuspendingTest to runTest @Test - fun testHttpUpload() = runSuspendTest { + fun testHttpUpload() = runTest { val bodyToSend = TEST_DOC_LINE // Set up mock server diff --git a/aws-crt-kotlin/native/interop/crt.def b/aws-crt-kotlin/native/interop/crt.def index 1ae6eab2..660576c4 100644 --- a/aws-crt-kotlin/native/interop/crt.def +++ b/aws-crt-kotlin/native/interop/crt.def @@ -10,6 +10,7 @@ headerFilter = aws/common/* aws/io/* aws/http/* aws/compression/* linkerOpts = -laws-c-common -laws-c-cal -laws-c-io -laws-c-http -laws-c-compression linkerOpts.osx = -framework Security +linkerOpts.ios = -framework Security linkerOpts.linux = -ls2n -lcrypto --- diff --git a/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/CRTNative.kt b/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/CRTNative.kt index 9f9153f8..9404f962 100644 --- a/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/CRTNative.kt +++ b/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/CRTNative.kt @@ -42,7 +42,7 @@ public actual object CRT { atexit(staticCFunction(::cleanup)) initialized = true - } + } } /** diff --git a/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/Logging.kt b/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/Logging.kt index 22c1760b..449d6296 100644 --- a/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/Logging.kt +++ b/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/Logging.kt @@ -64,7 +64,7 @@ internal object Logging { aws_logger_set(s_crt_kotlin_logger.ptr) initialized = true - } + } } } } diff --git a/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/ClientBootstrapNative.kt b/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/ClientBootstrapNative.kt index c9e20f76..43c10480 100644 --- a/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/ClientBootstrapNative.kt +++ b/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/ClientBootstrapNative.kt @@ -5,15 +5,55 @@ package aws.sdk.kotlin.crt.io -import aws.sdk.kotlin.crt.AsyncShutdown -import aws.sdk.kotlin.crt.Closeable +import aws.sdk.kotlin.crt.* +import aws.sdk.kotlin.crt.Allocator +import kotlinx.cinterop.* +import kotlinx.coroutines.channels.Channel +import libcrt.aws_client_bootstrap +import libcrt.aws_client_bootstrap_new +import libcrt.aws_client_bootstrap_options +import libcrt.aws_client_bootstrap_release + +@OptIn(ExperimentalForeignApi::class) +public actual class ClientBootstrap actual constructor( + elg: EventLoopGroup, + hr: HostResolver, +) : CrtResource(), Closeable, AsyncShutdown { + private val bootstrap: CPointer + private val shutdownCompleteChannel = Channel(Channel.RENDEZVOUS) + private val channelStableRef = StableRef.create(shutdownCompleteChannel) + + override val ptr: CPointer + get() = bootstrap + + init { + val opts = cValue { + event_loop_group = elg.ptr + host_resolver = hr.ptr + on_shutdown_complete = staticCFunction(::onShutdownComplete) + user_data = channelStableRef.asCPointer() + } + + bootstrap = checkNotNull(aws_client_bootstrap_new(Allocator.Default, opts)) { + "aws_client_bootstrap_new()" + } + } -public actual class ClientBootstrap actual constructor(elg: EventLoopGroup, hr: HostResolver) : Closeable, AsyncShutdown { override suspend fun waitForShutdown() { - TODO("Not yet implemented") + shutdownCompleteChannel.receive() + channelStableRef.dispose() } override fun close() { - TODO("Not yet implemented") + aws_client_bootstrap_release(bootstrap) + } +} + +@OptIn(ExperimentalForeignApi::class) +private fun onShutdownComplete(userData: COpaquePointer?) { + if (userData != null) { + val shutdownCompleteChannel = userData.asStableRef>().get() + shutdownCompleteChannel.trySend(Unit) + shutdownCompleteChannel.close() } } diff --git a/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/EventLoopGroupNative.kt b/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/EventLoopGroupNative.kt index bf12fe84..2420cf20 100644 --- a/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/EventLoopGroupNative.kt +++ b/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/EventLoopGroupNative.kt @@ -5,8 +5,11 @@ package aws.sdk.kotlin.crt.io -import aws.sdk.kotlin.crt.AsyncShutdown -import aws.sdk.kotlin.crt.Closeable +import aws.sdk.kotlin.crt.* +import aws.sdk.kotlin.crt.Allocator +import kotlinx.cinterop.* +import kotlinx.coroutines.channels.Channel +import libcrt.* /** * Creates a new event loop group for the I/O subsystem to use to run blocking I/O requests @@ -16,12 +19,42 @@ import aws.sdk.kotlin.crt.Closeable * Otherwise, maxThreads will be the number of event loops in the group. * @throws [aws.sdk.kotlin.crt.CrtRuntimeException] If the system is unable to allocate space for a native event loop group */ -public actual class EventLoopGroup actual constructor(maxThreads: Int) : Closeable, AsyncShutdown { +@OptIn(ExperimentalForeignApi::class) +public actual class EventLoopGroup actual constructor(maxThreads: Int) : CrtResource(), Closeable, AsyncShutdown { + private val elg: CPointer + + override val ptr: CPointer + get() = elg + + private val shutdownCompleteChannel = Channel(Channel.RENDEZVOUS) + private val channelStableRef = StableRef.create(shutdownCompleteChannel) + + init { + val shutdownOpts = cValue { + shutdown_callback_fn = staticCFunction(::onShutdownComplete) + shutdown_callback_user_data = channelStableRef.asCPointer() + } + + elg = checkNotNull(aws_event_loop_group_new_default(Allocator.Default, maxThreads.toUShort(), shutdownOpts)) { + "aws_event_loop_group_new_default()" + } + } + override suspend fun waitForShutdown() { - TODO("Not yet implemented") + shutdownCompleteChannel.receive() + channelStableRef.dispose() } override fun close() { - TODO("Not yet implemented") + aws_event_loop_group_release(elg) + } +} + +@OptIn(ExperimentalForeignApi::class) +private fun onShutdownComplete(userData: COpaquePointer?) { + if (userData != null) { + val shutdownCompleteChannel = userData.asStableRef>().get() + shutdownCompleteChannel.trySend(Unit) + shutdownCompleteChannel.close() } } diff --git a/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/HostResolverNative.kt b/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/HostResolverNative.kt index bf7d4432..42141ca4 100644 --- a/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/HostResolverNative.kt +++ b/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/HostResolverNative.kt @@ -5,17 +5,57 @@ package aws.sdk.kotlin.crt.io -import aws.sdk.kotlin.crt.AsyncShutdown -import aws.sdk.kotlin.crt.Closeable +import aws.sdk.kotlin.crt.* +import aws.sdk.kotlin.crt.Allocator +import kotlinx.cinterop.* +import kotlinx.coroutines.channels.Channel +import libcrt.* -public actual class HostResolver actual constructor(elg: EventLoopGroup, maxEntries: Int) : Closeable, AsyncShutdown { +@OptIn(ExperimentalForeignApi::class) +public actual class HostResolver actual constructor(elg: EventLoopGroup, maxEntries: Int) : CrtResource(), Closeable, AsyncShutdown { public actual constructor(elg: EventLoopGroup) : this(elg, DEFAULT_MAX_ENTRIES) + private val resolver: CPointer + override val ptr: CPointer + get() = resolver + + private val shutdownCompleteChannel = Channel(Channel.RENDEZVOUS) + private val channelStableRef = StableRef.create(shutdownCompleteChannel) + + init { + resolver = memScoped { + val shutdownOpts = cValue { + shutdown_callback_fn = staticCFunction(::onShutdownComplete) + shutdown_callback_user_data = channelStableRef.asCPointer() + } + + val resolverOpts = cValue { + el_group = elg.ptr + shutdown_options = shutdownOpts.ptr + max_entries = maxEntries.convert() + } + + checkNotNull(aws_host_resolver_new_default(Allocator.Default, resolverOpts)) { + "aws_host_resolver_new_default()" + } + } + } + override suspend fun waitForShutdown() { - TODO("Not yet implemented") + shutdownCompleteChannel.receive() + channelStableRef.dispose() } override fun close() { - TODO("Not yet implemented") + aws_host_resolver_release(resolver) + } +} + +@OptIn(ExperimentalForeignApi::class) +private fun onShutdownComplete(userData: COpaquePointer?) { + if (userData != null) { + val shutdownCompleteChannel = userData.asStableRef>().get() + shutdownCompleteChannel.trySend(Unit) + shutdownCompleteChannel.close() } } diff --git a/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/MutableBufferNative.kt b/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/MutableBufferNative.kt index d285a7ab..c69688cb 100644 --- a/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/MutableBufferNative.kt +++ b/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/MutableBufferNative.kt @@ -4,33 +4,66 @@ */ package aws.sdk.kotlin.crt.io +import aws.sdk.kotlin.crt.Allocator +import aws.sdk.kotlin.crt.Closeable +import kotlinx.cinterop.* +import libcrt.* + /** * Represents a mutable linear range of bytes that can be written to. * Instance of this class has no additional state except the bytes themselves. * * NOTE: Platform implementations should provide direct access to the underlying bytes */ -public actual class MutableBuffer { +@OptIn(ExperimentalForeignApi::class) +public actual class MutableBuffer(private val buffer: aws_byte_buf? = null, private val capacity: Int) : Closeable { // TODO implement CrtResource? + private val buf = buffer ?: Allocator.Default.alloc() + + public val bytes: ByteArray + get() = buf.buffer!!.readBytes(buf.capacity.toInt()) + + init { + if (buffer == null) { + aws_byte_buf_init(buf = buf.ptr, allocator = Allocator.Default.allocator, capacity = capacity.toULong()) + } + } + /** * The amount of remaining write capacity before the buffer is full */ public actual val writeRemaining: Int - get() = TODO("Not yet implemented") + get() = buf.capacity.toInt() - buf.len.toInt() /** * Write as much of [length] bytes from [src] as possible starting at [offset]. * The number of bytes written is returned which may be less than [length] */ public actual fun write(src: ByteArray, offset: Int, length: Int): Int { - TODO("Not yet implemented") + src.usePinned { pinnedSrc -> + val offsetPinnedSrc = pinnedSrc.addressOf(offset).reinterpret() + val numBytesToWrite = minOf(length, writeRemaining) + return if (aws_byte_buf_write(buf.ptr, offsetPinnedSrc, numBytesToWrite.toULong())) numBytesToWrite else 0 + } + } + + public override fun close() { + if (buffer == null) { + aws_byte_buf_clean_up(buf.ptr) + } } public actual companion object { /** * Create a buffer instance backed by [src] */ - public actual fun of(src: ByteArray): MutableBuffer { - TODO("Not yet implemented") + public actual fun of(src: ByteArray): MutableBuffer = src.usePinned { pinnedSrc -> + val tempBuf: CValue = aws_byte_buf_from_array(pinnedSrc.addressOf(0), src.size.toULong()) + + val buf = Allocator.Default.alloc() + // initialize the buf->buffer + aws_byte_buf_init_copy(dest = buf.ptr, allocator = Allocator.Default.allocator, src = tempBuf) + + MutableBuffer(buf, buf.capacity.toInt()) } } } diff --git a/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/TlsContextNative.kt b/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/TlsContextNative.kt index 13a7d2a6..5126756e 100644 --- a/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/TlsContextNative.kt +++ b/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/TlsContextNative.kt @@ -5,15 +5,119 @@ package aws.sdk.kotlin.crt.io -import aws.sdk.kotlin.crt.Closeable +import aws.sdk.kotlin.crt.* +import aws.sdk.kotlin.crt.Allocator +import aws.sdk.kotlin.crt.awsAssertOpSuccess +import aws.sdk.kotlin.crt.util.asAwsByteCursor +import aws.sdk.kotlin.crt.util.free +import aws.sdk.kotlin.crt.util.toAwsString +import kotlinx.cinterop.* +import libcrt.* + +@OptIn(ExperimentalForeignApi::class) +public actual class TlsContext actual constructor(options: TlsContextOptions?) : CrtResource(), Closeable { + private val ctx: CPointer + private val tlsCtxOpts: aws_tls_ctx_options = Allocator.Default.alloc() -public actual class TlsContext actual constructor(options: TlsContextOptions?) : Closeable { public actual companion object {} + @OptIn(ExperimentalForeignApi::class) + override val ptr: CPointer + get() = ctx + + init { + aws_tls_ctx_options_init_default_client(tlsCtxOpts.ptr, Allocator.Default) + val kopts = options ?: TlsContextOptions.defaultClient() + + try { + // Certificate file or path will cause an init which overrides other fields, so parse those first + if (kopts.certificate != null && kopts.privateKey != null) { + initClientMtls(kopts.certificate, kopts.privateKey) + } else if (kopts.certificatePath != null && kopts.privateKeyPath != null) { + initClientMtlsFromPath(kopts.certificatePath, kopts.privateKeyPath) + } + + if (kopts.caRoot != null) { + overrideDefaultTrustStore(kopts.caRoot) + } else if (kopts.caFile != null && kopts.caDir != null) { + overrideDefaultTrustStoreFromPath(kopts.caFile, kopts.caDir) + } + + tlsCtxOpts.minimum_tls_version = kopts.minTlsVersion.value.convert() + tlsCtxOpts.cipher_pref = kopts.tlsCipherPreference.value.convert() + tlsCtxOpts.verify_peer = kopts.verifyPeer + + if (kopts.alpn.isNotBlank()) { + awsAssertOpSuccess(aws_tls_ctx_options_set_alpn_list(tlsCtxOpts.ptr, kopts.alpn)) { + "aws_tls_ctx_options_set_alpn_list()" + } + } + } catch (ex: CrtRuntimeException) { + Allocator.Default.free(tlsCtxOpts.rawPtr) + throw ex + } + + ctx = aws_tls_client_ctx_new(Allocator.Default, tlsCtxOpts.ptr) ?: run { + aws_tls_ctx_options_clean_up(tlsCtxOpts.ptr) + Allocator.Default.free(tlsCtxOpts.rawPtr) + throw CrtRuntimeException("aws_tls_client_ctx_new()") + } + } + + // aws_tls_ctx_options_init_client_mtls() + private fun initClientMtls(certificate: String, privateKey: String) { + val cert = certificate.toAwsString() + val pkey = privateKey.toAwsString() + + try { + val certCursor = cert.asAwsByteCursor() + val pkeyCursor = pkey.asAwsByteCursor() + + awsAssertOpSuccess(aws_tls_ctx_options_init_client_mtls(tlsCtxOpts.ptr, Allocator.Default, certCursor, pkeyCursor)) { + "aws_tls_ctx_options_init_client_mtls()" + } + } finally { + cert.free() + pkey.free() + } + } + + // aws_tls_ctx_options_init_client_mtls_from_path() + private fun initClientMtlsFromPath(certificatePath: String, privateKeyPath: String) { + awsAssertOpSuccess( + aws_tls_ctx_options_init_client_mtls_from_path(tlsCtxOpts.ptr, Allocator.Default, certificatePath, privateKeyPath), + ) { "aws_tls_ctx_options_init_client_mtls_from_path(): certificatePath: `$certificatePath`; privateKeyPath: $privateKeyPath" } + } + + // aws_tls_ctx_options_override_default_trust_store() + private fun overrideDefaultTrustStore(caRoot: String) { + val ca = caRoot.toAwsString() + try { + val caCursor = ca.asAwsByteCursor() + awsAssertOpSuccess(aws_tls_ctx_options_override_default_trust_store(tlsCtxOpts.ptr, caCursor)) { + "aws_tls_ctx_options_override_default_trust_store()" + } + } finally { + ca.free() + } + } + + // aws_tls_ctx_options_override_default_trust_store_from_path() + private fun overrideDefaultTrustStoreFromPath(caFile: String?, caPath: String?) { + awsAssertOpSuccess(aws_tls_ctx_options_override_default_trust_store_from_path(tlsCtxOpts.ptr, caFile, caPath)) { + "aws_tls_ctx_options_override_default_trust_store_from_path()" + } + } + override fun close() { - TODO("Not yet implemented") + aws_tls_ctx_release(ctx) + aws_tls_ctx_options_clean_up(tlsCtxOpts.ptr) + Allocator.Default.free(tlsCtxOpts.rawPtr) } } -internal actual fun isCipherSupported(cipher: TlsCipherPreference): Boolean = TODO("Not yet implemented") -internal actual fun isAlpnSupported(): Boolean = TODO("Not yet implemented") +@OptIn(ExperimentalForeignApi::class) +internal actual fun isCipherSupported(cipher: TlsCipherPreference): Boolean = aws_tls_is_cipher_pref_supported(cipher.value.convert()) + +@OptIn(ExperimentalForeignApi::class) +internal actual fun isAlpnSupported(): Boolean = aws_tls_is_alpn_available() diff --git a/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/UriNative.kt b/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/UriNative.kt index b471f52b..4cea5bf1 100644 --- a/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/UriNative.kt +++ b/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/io/UriNative.kt @@ -5,4 +5,36 @@ package aws.sdk.kotlin.crt.io -internal actual fun parseUri(uri: String): Uri = TODO("Not yet implemented") +import aws.sdk.kotlin.crt.Allocator +import aws.sdk.kotlin.crt.awsAssertOpSuccess +import aws.sdk.kotlin.crt.util.asAwsByteCursor +import aws.sdk.kotlin.crt.util.toKString +import kotlinx.cinterop.* +import libcrt.aws_uri +import libcrt.aws_uri_clean_up +import libcrt.aws_uri_init_parse + +@OptIn(ExperimentalForeignApi::class) +internal actual fun parseUri(uri: String): Uri = memScoped { + uri.encodeToByteArray().usePinned { pinned -> + val uriCursor = pinned.asAwsByteCursor() + val awsUri = alloc() + + awsAssertOpSuccess(aws_uri_init_parse(awsUri.ptr, Allocator.Default, uriCursor)) { + "aws_uri_init_parse()" + } + + Uri.build { + scheme = Protocol.createOrDefault(awsUri.scheme.toKString()) + host = awsUri.host_name.toKString() + port = awsUri.port.toInt().takeIf { it > 0 } + path = awsUri.path.toKString() + parameters = awsUri.query_string.takeIf { it.len.toInt() > 0 }?.toKString() + userInfo = awsUri.takeIf { it.user.len.toInt() > 0 }?.let { + UserInfo(it.user.toKString(), it.password.toKString()) + } + }.also { + aws_uri_clean_up(awsUri.ptr) + } + } +} diff --git a/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/util/StringUtils.kt b/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/util/StringUtils.kt new file mode 100644 index 00000000..c4af6ee3 --- /dev/null +++ b/aws-crt-kotlin/native/src/aws/sdk/kotlin/crt/util/StringUtils.kt @@ -0,0 +1,61 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.sdk.kotlin.crt.util + +import aws.sdk.kotlin.crt.Allocator +import kotlinx.cinterop.* +import libcrt.* + +/** + * Decode an aws_string as to a kotlin [String] assuming UTF-8 bytes + * This does NOT free the aws_string instance + */ +@OptIn(ExperimentalForeignApi::class) +public fun CPointer.toKString(): String? { + val bytes = aws_string_c_str(this) + return bytes?.toKString() +} + +/** + * Get a byte cursor from the current aws_string instance + */ +@OptIn(ExperimentalForeignApi::class) +public fun CPointer.asAwsByteCursor(): CValue = aws_byte_cursor_from_string(this) + +/** + * Free the aws_string instance + */ +@OptIn(ExperimentalForeignApi::class) +public fun CPointer.free(): Unit = aws_string_destroy(this) + +/** + * Interpret a byte cursor as a Kotlin string + */ +@OptIn(ExperimentalForeignApi::class) +public inline fun aws_byte_cursor.toKString(): String = ptr?.readBytes(len.convert())?.decodeToString() ?: "" + +/** + * Initialize an aws_byte_cursor from a (pinned) Kotlin [ByteArray]. + * NOTE: the cursor is only valid while the array is pinned + */ +@OptIn(ExperimentalForeignApi::class) +public fun Pinned.asAwsByteCursor(): CValue { + val arr = get() + val addr = addressOf(0) + return cValue { + len = arr.size.convert() + ptr = addr.reinterpret() + } +} + +/** + * Decode the Kotlin [String] as an aws_string instance + * Caller is responsible for freeing. + */ +@OptIn(ExperimentalForeignApi::class) +public fun String.toAwsString(): CPointer = checkNotNull(aws_string_new_from_c_str(Allocator.Default, this)) { + "aws_string_new_from_c_string()" +} diff --git a/aws-crt-kotlin/native/test/aws/sdk/kotlin/crt/UtilsNative.kt b/aws-crt-kotlin/native/test/aws/sdk/kotlin/crt/UtilsNative.kt deleted file mode 100644 index 474ddff1..00000000 --- a/aws-crt-kotlin/native/test/aws/sdk/kotlin/crt/UtilsNative.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package aws.sdk.kotlin.crt - -import kotlinx.coroutines.CoroutineScope -import kotlin.coroutines.CoroutineContext - -actual fun runSuspendTest(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T { - TODO("Not yet implemented") -} diff --git a/aws-crt-kotlin/native/test/aws/sdk/kotlin/crt/io/MutableBufferTest.kt b/aws-crt-kotlin/native/test/aws/sdk/kotlin/crt/io/MutableBufferTest.kt new file mode 100644 index 00000000..eb40ccbf --- /dev/null +++ b/aws-crt-kotlin/native/test/aws/sdk/kotlin/crt/io/MutableBufferTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.crt.io + +import aws.sdk.kotlin.crt.CrtTest +import kotlinx.cinterop.ExperimentalForeignApi +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +@OptIn(ExperimentalForeignApi::class) +class MutableBufferTest : CrtTest() { + @Test + fun testSimpleWrite() { + val capacity = 10 + val buffer = MutableBuffer(capacity = capacity) + assertEquals(capacity, buffer.writeRemaining) + + val data = "Hello!" + buffer.write(data.encodeToByteArray()) + assertEquals(capacity - data.length, buffer.writeRemaining) + + assertContentEquals(data.encodeToByteArray(), buffer.bytes.copyOfRange(0, data.length)) + + buffer.close() + } + + @Test + fun testWriteFillingBuffer() { + val capacity = 5 + val buffer = MutableBuffer(capacity = capacity) + assertEquals(capacity, buffer.writeRemaining) + + val data = "Hello, this data won't fit!" + assertEquals(5, buffer.write(data.encodeToByteArray())) + buffer.close() + } + + @Test + fun testWriteToFullBuffer() { + val str = "Hello!" + val bytes = str.encodeToByteArray() + val buffer = MutableBuffer.of(bytes) // creates a full buffer + + assertEquals(0, buffer.writeRemaining) + + // since it's full, should write 0 bytes + assertEquals(0, buffer.write(bytes)) + buffer.close() + } +}