Skip to content

Commit 7acf30c

Browse files
committed
grpc: Implement native side
1 parent dc2a76a commit 7acf30c

File tree

7 files changed

+227
-37
lines changed

7 files changed

+227
-37
lines changed

grpc/grpc-client/src/commonMain/kotlin/kotlinx/rpc/grpc/client/GrpcCallCredentials.kt

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,7 @@ public interface GrpcCallCredentials {
7878
* Implementations should return a [GrpcMetadata] object containing the necessary authentication
7979
* information for the request.
8080
*
81-
* The method is suspending to allow asynchronous operations such as:
82-
* - Token retrieval from secure storage
83-
* - OAuth token refresh or exchange
84-
* - Dynamic token generation or signing
85-
* - Network calls to authentication services
81+
* The method is suspending to allow asynchronous operations such as token retrieval from secure storage.
8682
*
8783
* ## Context Information
8884
*
@@ -145,9 +141,10 @@ public interface GrpcCallCredentials {
145141
* @property authority The authority (host:port) for this call.
146142
*/
147143
// TODO: check whether we should add GrpcCallOptions in the context (KRPC-232)
144+
// TODO: determine if possible and necessary to add the MethodDescriptor
148145
public data class Context(
149-
val method: MethodDescriptor<*, *>,
150146
val authority: String,
147+
val methodName: String,
151148
)
152149
}
153150

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
@file:OptIn(ExperimentalForeignApi::class)
6+
7+
package kotlinx.rpc.grpc.client
8+
9+
import cnames.structs.grpc_call_credentials
10+
import kotlinx.cinterop.*
11+
import kotlinx.coroutines.CoroutineScope
12+
import kotlinx.coroutines.launch
13+
import kotlinx.rpc.grpc.GrpcMetadata
14+
import kotlinx.rpc.grpc.StatusException
15+
import kotlinx.rpc.grpc.internal.destroyEntries
16+
import kotlinx.rpc.grpc.internal.toRaw
17+
import kotlinx.rpc.grpc.statusCode
18+
import libkgrpc.*
19+
import platform.posix.size_tVar
20+
21+
// Stable reference holder for Kotlin objects
22+
private class CredentialsPluginState(
23+
val kotlinCreds: GrpcCallCredentials,
24+
val scope: CoroutineScope
25+
)
26+
27+
private fun getMetadataCallback(
28+
state: COpaquePointer?,
29+
context: CValue<grpc_auth_metadata_context>,
30+
cb: grpc_credentials_plugin_metadata_cb?,
31+
user_data: COpaquePointer?,
32+
creds_md: CValuesRef<grpc_metadata>?,
33+
num_creds_md: CPointer<size_tVar>?,
34+
status: CPointer<grpc_status_code.Var>?,
35+
error_details: CPointer<CPointerVar<ByteVar>>?
36+
): Int {
37+
val pluginState = state!!.asStableRef<CredentialsPluginState>().get()
38+
39+
// Launch coroutine to call suspend function asynchronously
40+
pluginState.scope.launch {
41+
// Extract context information
42+
val serviceUrl = context.useContents { service_url?.toKString() ?: "" }
43+
val methodName = context.useContents { method_name?.toKString() ?: "" }
44+
val authority = extractAuthority(serviceUrl)
45+
46+
// Create Kotlin context
47+
val kotlinContext = GrpcCallCredentials.Context(
48+
authority = authority,
49+
methodName = methodName,
50+
)
51+
52+
var metadata = GrpcMetadata()
53+
var status = grpc_status_code.GRPC_STATUS_OK
54+
var errorDetails: String? = null
55+
try {
56+
// Call the Kotlin suspend function
57+
metadata = with(pluginState.kotlinCreds) {
58+
kotlinContext.getRequestMetadata()
59+
}
60+
} catch (e: StatusException) {
61+
status = e.getStatus().statusCode.toRaw()
62+
errorDetails = e.message
63+
} catch (e: Exception) {
64+
status = grpc_status_code.GRPC_STATUS_UNAUTHENTICATED
65+
errorDetails = e.message
66+
}
67+
68+
// Convert GrpcMetadata to grpc_metadata array
69+
memScoped {
70+
val metadataArray = with(metadata) {
71+
this@memScoped.allocRawGrpcMetadata()
72+
}
73+
74+
try {
75+
// Invoke the callback with success
76+
cb?.invoke(
77+
user_data,
78+
metadataArray.metadata,
79+
metadataArray.count,
80+
status,
81+
errorDetails?.cstr?.ptr
82+
)
83+
} finally {
84+
metadataArray.destroyEntries()
85+
}
86+
}
87+
}
88+
89+
// Return 0 to indicate asynchronous processing
90+
return 0
91+
}
92+
93+
private fun debugStringCallback(state: COpaquePointer?): CPointer<ByteVar>? {
94+
return gpr_strdup("KotlinCallCredentials")
95+
}
96+
97+
private fun extractAuthority(serviceUrl: String): String {
98+
// service_url format: "https://example.com:443/service"
99+
return serviceUrl
100+
.removePrefix("http://")
101+
.removePrefix("https://")
102+
.substringBefore("/")
103+
}
104+
105+
private fun destroyCallback(state: COpaquePointer?) {
106+
state?.asStableRef<CredentialsPluginState>()?.dispose()
107+
}
108+
109+
internal fun GrpcCallCredentials.createRaw(
110+
scope: CoroutineScope
111+
): CPointer<grpc_call_credentials>? = memScoped {
112+
// Create stable reference to keep Kotlin object alive
113+
val pluginState = CredentialsPluginState(this@createRaw, scope)
114+
val stableRef = StableRef.create(pluginState)
115+
116+
// Create plugin structure
117+
val plugin = alloc<grpc_metadata_credentials_plugin> {
118+
get_metadata = staticCFunction(::getMetadataCallback)
119+
debug_string = staticCFunction(::debugStringCallback)
120+
destroy = staticCFunction(::destroyCallback)
121+
state = stableRef.asCPointer()
122+
type = "kgrpc_call_credentials".cstr.ptr
123+
}
124+
125+
// Determine security level
126+
val minSecurityLevel = if (this@createRaw.requiresTransportSecurity) {
127+
GRPC_PRIVACY_AND_INTEGRITY
128+
} else {
129+
GRPC_SECURITY_NONE
130+
}
131+
132+
// Create and return credentials
133+
grpc_metadata_credentials_create_from_plugin(
134+
plugin.readValue(),
135+
minSecurityLevel,
136+
null
137+
)
138+
}

grpc/grpc-client/src/nativeMain/kotlin/kotlinx/rpc/grpc/client/credentials.native.kt

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,50 @@ import libkgrpc.grpc_tls_credentials_options_destroy
1818
import kotlin.experimental.ExperimentalNativeApi
1919
import kotlin.native.ref.createCleaner
2020

21-
public actual abstract class ClientCredentials internal constructor(
22-
internal val raw: CPointer<grpc_channel_credentials>,
23-
) {
24-
@Suppress("unused")
25-
internal val rawCleaner = createCleaner(raw) {
26-
grpc_channel_credentials_release(it)
21+
public actual abstract class ClientCredentials {
22+
internal abstract val clientCredentials: ClientCredentials
23+
internal abstract val callCredentials: GrpcCallCredentials?
24+
25+
internal abstract fun takeRaw(): CPointer<grpc_channel_credentials>
26+
}
27+
28+
public actual class InsecureClientCredentials() : ClientCredentials() {
29+
override val clientCredentials: ClientCredentials
30+
get() = this
31+
override val callCredentials: GrpcCallCredentials?
32+
get() = null
33+
34+
override fun takeRaw(): CPointer<grpc_channel_credentials> {
35+
return grpc_insecure_credentials_create() ?: error("grpc_insecure_credentials_create() returned null")
2736
}
2837
}
2938

30-
public actual class InsecureClientCredentials internal constructor(
31-
raw: CPointer<grpc_channel_credentials>,
32-
) : ClientCredentials(raw)
39+
public actual class TlsClientCredentials(
40+
private var credentials: CPointer<grpc_channel_credentials>?
41+
) : ClientCredentials() {
42+
43+
@Suppress("unused")
44+
private val rawCleaner = createCleaner(credentials) {
45+
if (it != null) {
46+
grpc_channel_credentials_release(it)
47+
}
48+
}
49+
50+
override val clientCredentials: ClientCredentials
51+
get() = this
52+
override val callCredentials: GrpcCallCredentials?
53+
get() = null
3354

34-
public actual class TlsClientCredentials internal constructor(
35-
raw: CPointer<grpc_channel_credentials>,
36-
) : ClientCredentials(raw)
55+
override fun takeRaw(): CPointer<grpc_channel_credentials> {
56+
val credentials = this.credentials
57+
this.credentials = null
58+
return credentials ?: error("Credentials are already taken")
59+
}
60+
}
3761

3862
@InternalRpcApi
3963
public actual fun createInsecureClientCredentials(): ClientCredentials {
40-
return InsecureClientCredentials(
41-
grpc_insecure_credentials_create() ?: error("grpc_insecure_credentials_create() returned null")
42-
)
64+
return InsecureClientCredentials()
4365
}
4466

4567
internal actual fun TlsClientCredentialsBuilder(): TlsClientCredentialsBuilder = NativeTlsClientCredentialsBuilder()
@@ -74,6 +96,16 @@ private class NativeTlsClientCredentialsBuilder : TlsClientCredentialsBuilder {
7496
}
7597
}
7698

99+
internal class CombinedClientCredentials(
100+
override val clientCredentials: ClientCredentials,
101+
override val callCredentials: GrpcCallCredentials,
102+
): ClientCredentials() {
103+
override fun takeRaw(): CPointer<grpc_channel_credentials> {
104+
// doesn't return a composite key but just the client credentials key.
105+
return clientCredentials.takeRaw()
106+
}
107+
}
108+
77109
public actual operator fun ClientCredentials.plus(other: GrpcCallCredentials): ClientCredentials {
78-
TODO("Not yet implemented")
110+
return CombinedClientCredentials(clientCredentials, callCredentials?.combine(other) ?: other)
79111
}

grpc/grpc-client/src/nativeMain/kotlin/kotlinx/rpc/grpc/client/internal/ManagedChannel.native.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
*/
44

55
@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
6+
@file:OptIn(ExperimentalForeignApi::class)
67

78
package kotlinx.rpc.grpc.client.internal
89

10+
import kotlinx.cinterop.ExperimentalForeignApi
11+
import kotlinx.coroutines.GlobalScope
912
import kotlinx.rpc.grpc.client.ClientCredentials
1013
import kotlinx.rpc.grpc.client.GrpcClientConfiguration
1114
import kotlinx.rpc.grpc.client.TlsClientCredentials
15+
import kotlinx.rpc.grpc.client.createRaw
1216
import kotlinx.rpc.grpc.internal.internalError
1317
import kotlinx.rpc.internal.utils.InternalRpcApi
1418

@@ -40,7 +44,11 @@ internal class NativeManagedChannelBuilder(
4044
target,
4145
authority = config?.overrideAuthority,
4246
keepAlive = config?.keepAlive,
43-
credentials = credentials.value,
47+
rawChannelCredentials = credentials.value.clientCredentials.takeRaw(),
48+
rawCallCredentials = credentials.value.clientCredentials.callCredentials?.createRaw(
49+
// TODO: Replace GlobalScope with something more appropriate.
50+
GlobalScope,
51+
)
4452
)
4553
}
4654

grpc/grpc-client/src/nativeMain/kotlin/kotlinx/rpc/grpc/client/internal/NativeManagedChannel.kt

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
package kotlinx.rpc.grpc.client.internal
88

9+
import cnames.structs.grpc_call_credentials
910
import cnames.structs.grpc_channel
11+
import cnames.structs.grpc_channel_credentials
1012
import kotlinx.atomicfu.atomic
1113
import kotlinx.cinterop.CPointer
1214
import kotlinx.cinterop.ExperimentalForeignApi
@@ -22,7 +24,6 @@ import kotlinx.coroutines.Job
2224
import kotlinx.coroutines.SupervisorJob
2325
import kotlinx.coroutines.cancelChildren
2426
import kotlinx.coroutines.withTimeoutOrNull
25-
import kotlinx.rpc.grpc.client.ClientCredentials
2627
import kotlinx.rpc.grpc.client.GrpcCallOptions
2728
import kotlinx.rpc.grpc.client.GrpcClientConfiguration
2829
import kotlinx.rpc.grpc.client.rawDeadline
@@ -37,7 +38,9 @@ import libkgrpc.grpc_arg_type
3738
import libkgrpc.grpc_channel_args
3839
import libkgrpc.grpc_channel_create
3940
import libkgrpc.grpc_channel_create_call
41+
import libkgrpc.grpc_channel_credentials_release
4042
import libkgrpc.grpc_channel_destroy
43+
import libkgrpc.grpc_composite_channel_credentials_create
4144
import libkgrpc.grpc_slice_unref
4245
import kotlin.coroutines.cancellation.CancellationException
4346
import kotlin.experimental.ExperimentalNativeApi
@@ -49,14 +52,15 @@ import kotlin.time.Duration
4952
* Native implementation of [ManagedChannel].
5053
*
5154
* @param target The target address to connect to.
52-
* @param credentials The credentials to use for the connection.
55+
* @param rawChannelCredentials The credentials to use for the connection.
5356
*/
5457
internal class NativeManagedChannel(
5558
target: String,
5659
val authority: String?,
5760
val keepAlive: GrpcClientConfiguration.KeepAlive?,
58-
// we must store them, otherwise the credentials are getting released
59-
credentials: ClientCredentials,
61+
// this is not a composite channel credentials
62+
val rawChannelCredentials: CPointer<grpc_channel_credentials>,
63+
val rawCallCredentials: CPointer<grpc_call_credentials>?,
6064
) : ManagedChannel, ManagedChannelPlatform() {
6165

6266
// a reference to make sure the grpc_init() was called. (it is released after shutdown)
@@ -70,6 +74,10 @@ internal class NativeManagedChannel(
7074
// the channel's completion queue, handling all request operations
7175
private val cq = CompletionQueue()
7276

77+
private val rawCompositeCredentials = rawCallCredentials?.let {
78+
grpc_composite_channel_credentials_create(rawChannelCredentials, it, null)
79+
}
80+
7381
internal val raw: CPointer<grpc_channel> = memScoped {
7482
val args = mutableListOf<GrpcArg>()
7583

@@ -100,14 +108,28 @@ internal class NativeManagedChannel(
100108

101109
var rawArgs = if (args.isNotEmpty()) args.toRaw(this) else null
102110

103-
grpc_channel_create(target, credentials.raw, rawArgs?.ptr)
111+
// if we have composite credentials, which bundles call credentials and channel credentials,
112+
// we use it. Otherwise, we use the channel credentials alone.
113+
var credentials = rawCompositeCredentials ?: rawChannelCredentials
114+
grpc_channel_create(target, credentials, rawArgs?.ptr)
104115
?: error("Failed to create channel")
105116
}
106117

107118
@Suppress("unused")
108119
private val rawCleaner = createCleaner(raw) {
109120
grpc_channel_destroy(it)
110121
}
122+
@Suppress("unused")
123+
internal val rawCredentialsCleaner = createCleaner(rawChannelCredentials) {
124+
grpc_channel_credentials_release(it)
125+
}
126+
127+
@Suppress("unused")
128+
internal val rawCompositeCredentialsCleaner = createCleaner(rawCompositeCredentials) {
129+
if (it != null) {
130+
grpc_channel_credentials_release(it)
131+
}
132+
}
111133

112134
override val platformApi: ManagedChannelPlatform = this
113135

grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/proto/GrpcCallCredentialsTest.kt

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,12 @@ import kotlinx.rpc.grpc.StatusCode
1111
import kotlinx.rpc.grpc.StatusException
1212
import kotlinx.rpc.grpc.append
1313
import kotlinx.rpc.grpc.buildGrpcMetadata
14-
import kotlinx.rpc.grpc.client.BearerTokenCredentials
1514
import kotlinx.rpc.grpc.client.GrpcCallCredentials
1615
import kotlinx.rpc.grpc.client.GrpcCallCredentials.Context
1716
import kotlinx.rpc.grpc.client.GrpcClient
18-
import kotlinx.rpc.grpc.client.JwtCredentials
1917
import kotlinx.rpc.grpc.client.TlsClientCredentials
2018
import kotlinx.rpc.grpc.client.plus
2119
import kotlinx.rpc.grpc.getAll
22-
import kotlin.io.encoding.Base64
23-
import kotlin.io.encoding.ExperimentalEncodingApi
24-
import kotlin.time.Duration.Companion.milliseconds
25-
import kotlin.time.Duration.Companion.seconds
26-
import kotlin.time.TimeSource
2720
import kotlinx.rpc.grpc.server.TlsServerCredentials
2821
import kotlinx.rpc.grpc.test.EchoRequest
2922
import kotlinx.rpc.grpc.test.EchoService

grpc/grpc-core/src/nativeInterop/cinterop/libkgrpc.def

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
headers = kgrpc.h grpc/grpc.h grpc/credentials.h grpc/byte_buffer_reader.h \
2-
grpc/support/alloc.h grpc/impl/propagation_bits.h
2+
grpc/support/alloc.h grpc/impl/propagation_bits.h grpc/support/string_util.h
33

44
headerFilter= kgrpc.h grpc/slice.h grpc/byte_buffer.h grpc/grpc.h \
55
grpc/impl/grpc_types.h grpc/credentials.h grpc/support/time.h grpc/byte_buffer_reader.h \
6-
grpc/support/alloc.h grpc/impl/propagation_bits.h
6+
grpc/support/alloc.h grpc/impl/propagation_bits.h grpc/support/string_util.h
77

88
noStringConversion = grpc_slice_from_copied_buffer my_grpc_slice_from_copied_buffer
99
strictEnums = grpc_status_code grpc_connectivity_state grpc_call_error

0 commit comments

Comments
 (0)