EyeAuras.CLink 0.6.2
CLink Usage From C And C#
CLink has one caller model in both languages: pass an absolute endpoint URL, open an endpoint, move opaque byte messages, and close handles when done.
The URL scheme selects the backend. CLink core does not care whether bytes are RPC, setup messages, telemetry, or payloads.
Each RID has one full-backend native SDK. The API never silently falls back to a different scheme; unsupported operations return their precise status at the selected backend boundary.
Address Examples
file:///C:/temp/ea-clink/service.ctl?maxPacketSize=65536
mem://pid/12345/0x000001ABCD000000?blockSize=256&maxPacketSize=65536
pipe://localhost/EA_CLink/session/12?protocol=clink-packet-v1&maxPacketSize=65536
tcp://127.0.0.1:43123?protocol=clink-packet-v1&maxPacketSize=65536
shared-mem://Local/EA_CLink_abc123?mapSize=1048576&session=12&epoch=4&channel=3&role=a&sync=Local/EA_CLink_abc123_sync&maxPacketSize=65536
Do not pass friendly names such as pipe://agent or mem://service-name. A URL must contain the backend-specific details needed to open the exact endpoint.
C Snippet Helpers
The C ABI is intentionally explicit. The examples below use small helpers so the real operation sequence stays visible.
#include "clink.h"
#include <stdint.h>
#include <string.h>
static clink_utf8_span clink_text(const char* text)
{
clink_utf8_span span = { (const uint8_t*) text, (uint64_t) strlen(text) };
return span;
}
static clink_listener_options clink_listener_url(const char* address)
{
clink_listener_options options = { sizeof(options), CLINK_ABI_VERSION };
options.address = clink_text(address);
return options;
}
static clink_session_options clink_session_url(const char* address)
{
clink_session_options options = { sizeof(options), CLINK_ABI_VERSION };
options.address = clink_text(address);
return options;
}
static clink_connection_options clink_connection_wait(uint32_t timeout_ms)
{
clink_connection_options options = { sizeof(options), CLINK_ABI_VERSION };
options.timeout_ms = timeout_ms;
return options;
}
Core Transport
Use this when a listener address can create a session and channel. file:// and same-process mem:// use this shape today.
C
typedef struct clink_demo_pair
{
clink_runtime_handle runtime;
clink_listener_handle listener;
clink_session_handle session;
clink_connection_handle client;
clink_connection_handle server;
} clink_demo_pair;
static void clink_demo_pair_close(clink_demo_pair* pair)
{
if (pair->server) (void) clink_connection_close(pair->server);
if (pair->client) (void) clink_connection_close(pair->client);
if (pair->session) (void) clink_session_close(pair->session);
if (pair->listener) (void) clink_listener_close(pair->listener);
if (pair->runtime) (void) clink_runtime_destroy(pair->runtime);
}
clink_status core_transport_example(void)
{
const char* address = "file:///C:/temp/ea-clink/demo.ctl?maxPacketSize=65536";
clink_demo_pair pair = { 0 };
clink_status status = CLINK_STATUS_OK;
/* 1. Create the process-local CLink owner. */
status = clink_runtime_create(&pair.runtime);
if (status != CLINK_STATUS_OK) goto cleanup;
/* 2. Publish a listener at the backend URL. */
clink_listener_options listener_options = clink_listener_url(address);
status = clink_listener_open_ex(pair.runtime, &listener_options, &pair.listener);
if (status != CLINK_STATUS_OK) goto cleanup;
/* 3. Open a client session to that same URL. */
clink_session_info session_info = { sizeof(session_info), CLINK_ABI_VERSION };
clink_session_options session_options = clink_session_url(address);
status = clink_session_open(pair.runtime, &session_options, &pair.session, &session_info);
if (status != CLINK_STATUS_OK) goto cleanup;
/* 4. Client requests a channel inside the session. */
clink_channel_info channel_info = { sizeof(channel_info), CLINK_ABI_VERSION };
clink_connection_options connection_options = clink_connection_wait(5000);
status = clink_connection_open_in_session(pair.session, &connection_options, &pair.client, &channel_info);
if (status != CLINK_STATUS_OK) goto cleanup;
/* 5. Listener accepts the peer endpoint for that channel. */
clink_accept_result accept_result = { sizeof(accept_result), CLINK_ABI_VERSION };
status = clink_listener_accept_wait(pair.listener, 5000, &pair.server, &accept_result);
if (status != CLINK_STATUS_OK) goto cleanup;
/* 6. Endpoints move opaque byte messages. */
const uint8_t request[] = { 1, 2, 3 };
status = clink_connection_send_wait(pair.client, request, sizeof(request), 5000);
if (status != CLINK_STATUS_OK) goto cleanup;
uint8_t buffer[16];
uint64_t received = 0;
status = clink_connection_recv_wait(pair.server, buffer, sizeof(buffer), &received, 5000);
cleanup:
clink_demo_pair_close(&pair);
return status;
}
C#
using EyeAuras.CLink;
var address = "file:///C:/temp/ea-clink/demo.ctl?maxPacketSize=65536";
using var serverRuntime = CLinkRuntime.Create();
using var listener = serverRuntime.OpenListener(new CLinkListenerOptions
{
Address = address,
});
using var clientRuntime = CLinkRuntime.Create();
using var session = clientRuntime.OpenSession(new CLinkSessionOptions
{
Address = address,
});
using var client = session.OpenConnection(new CLinkConnectionOptions
{
TimeoutMs = 5000,
});
var acceptStatus = listener.AcceptWait(TimeSpan.FromSeconds(5), out var server, out var accept);
if (acceptStatus != CLinkStatus.Ok || server is null)
{
throw new InvalidOperationException($"Accept failed: {acceptStatus}");
}
using (server)
{
var sendStatus = client.SendWait(new byte[] { 1, 2, 3 }, TimeSpan.FromSeconds(5));
if (sendStatus != CLinkStatus.Ok)
{
throw new InvalidOperationException($"Send failed: {sendStatus}");
}
var buffer = new byte[16];
var receiveStatus = server.ReceiveWait(buffer, out var received, TimeSpan.FromSeconds(5));
if (receiveStatus != CLinkStatus.Ok)
{
throw new InvalidOperationException($"Receive failed: {receiveStatus}");
}
}
For same-process memory setup in C#, replace the file:// address with a managed descriptor block:
using var block = CLinkMemoryDescriptorBlock.Allocate();
var address = $"{block.Address}&maxPacketSize=65536";
Keep the block alive until after listener close.
One-Peer Convenience API
Use this when a caller needs one connection and does not need to own an admission session or open more channels within it. The native core selects the address backend, performs direct connect or setup/admission as appropriate, and attaches any hidden session lifetime to the returned connection. It is the transport boundary used by the managed and Rust RPC runtimes.
For remote file:// admission, choose either this peer API on both sides or
the explicit AcceptSession*/channel APIs above for one listener. Do not mix
those remote-admission styles: the native listener returns Busy rather than
guessing which caller owns admission state. Same-process direct session/channel
use remains compatible with its existing queue-based accept path.
C
clink_status one_peer_example(void)
{
const char* address = "file:///C:/temp/ea-clink/demo.ctl?maxPacketSize=65536";
clink_runtime_handle server_runtime = NULL;
clink_runtime_handle client_runtime = NULL;
clink_listener_handle listener = NULL;
clink_connection_handle client = NULL;
clink_connection_handle server = NULL;
clink_session_options session_options = clink_session_url(address);
clink_connection_options channel_options = clink_connection_wait(5000);
clink_channel_info client_info = { sizeof(client_info), CLINK_ABI_VERSION };
clink_accept_result accept = { sizeof(accept), CLINK_ABI_VERSION };
clink_status status;
status = clink_runtime_create(&server_runtime);
if (status != CLINK_STATUS_OK) goto cleanup;
clink_listener_options listener_options = clink_listener_url(address);
status = clink_listener_open_peer(
server_runtime, &listener_options, 5000, &channel_options, &listener);
if (status != CLINK_STATUS_OK) goto cleanup;
status = clink_runtime_create(&client_runtime);
if (status != CLINK_STATUS_OK) goto cleanup;
status = clink_connection_open_wait(
client_runtime, &session_options, 5000, &channel_options, &client, &client_info);
if (status != CLINK_STATUS_OK) goto cleanup;
status = clink_listener_accept_connection_wait(listener, 5000, &server, &accept);
cleanup:
if (server) (void) clink_connection_close(server);
if (client) (void) clink_connection_close(client);
if (listener) (void) clink_listener_close(listener);
if (client_runtime) (void) clink_runtime_destroy(client_runtime);
if (server_runtime) (void) clink_runtime_destroy(server_runtime);
return status;
}
setup_timeout_ms is the direct connection or file-admission deadline. Numeric
pipe/TCP attempts receive its exact remaining budget. Hostname resolution is
synchronous and resolver latency occurs before the connect budget can be
enforced; use a numeric address when a strict wall-clock setup bound is required.
connection_options.timeout_ms separately bounds channel open once admission
has succeeded.
C#
var address = "file:///C:/temp/ea-clink/demo.ctl?maxPacketSize=65536";
using var serverRuntime = CLinkRuntime.Create();
using var listener = serverRuntime.OpenPeerListener(
new CLinkListenerOptions { Address = address },
TimeSpan.FromSeconds(5),
new CLinkConnectionOptions { TimeoutMs = 5000 });
using var clientRuntime = CLinkRuntime.Create();
using var client = clientRuntime.OpenConnectionWait(
new CLinkSessionOptions { Address = address },
TimeSpan.FromSeconds(5),
new CLinkConnectionOptions { TimeoutMs = 5000 });
var status = listener.AcceptConnectionWait(
TimeSpan.FromSeconds(5), out var server, out var accept);
if (status != CLinkStatus.Ok || server is null)
{
throw new InvalidOperationException($"Peer accept failed: {status}");
}
using (server)
{
// client and server now exchange opaque bytes through normal CLink calls.
}
The returned CLinkConnection is the sole public owner of any hidden session;
disposing it releases that session. TryOpenConnectionWait exposes the same
operation when a status result is preferable to an exception.
clink_listener_open_peer/OpenPeerListener is the matching server boundary.
For file://, mem://, pipe://, and tcp:// listener URLs it can accept
successive peers. For a concrete shared-mem:// endpoint, its first peer accept
transfers the pre-attached connection and later accepts wait for timeout or
cancel. The caller does not branch on that distinction.
Direct Endpoint Attach
Use this when the URL already names a concrete endpoint. Current examples are
direct pipe://localhost/...?...protocol=clink-packet-v1, Preview
Windows/Linux/macOS tcp://host:port?...protocol=clink-packet-v1, and returned
shared-mem://... channel endpoints. TCP is plaintext and has no reconnect or
TLS in this phase.
C
clink_status direct_endpoint_attach_example(clink_runtime_handle runtime)
{
const char* endpoint_url =
"pipe://localhost/EA_CLink/session/12?protocol=clink-packet-v1&maxPacketSize=65536";
clink_utf8_span endpoint = clink_text(endpoint_url);
clink_connection_handle connection = 0;
const uint8_t payload[] = { 7, 8, 9 };
clink_status status;
/* Open the concrete endpoint URL directly. */
status = clink_connection_open(runtime, &endpoint, &connection);
if (status != CLINK_STATUS_OK) return status;
/* Use it like any other CLink connection. */
status = clink_connection_send_wait(connection, payload, sizeof(payload), 5000);
(void) clink_connection_close(connection);
return status;
}
C#
var endpointAddress = "pipe://localhost/EA_CLink/session/12?protocol=clink-packet-v1&maxPacketSize=65536";
var status = runtime.TryOpenConnection(endpointAddress, out var connection);
if (status != CLinkStatus.Ok || connection is null)
{
throw new InvalidOperationException($"Open failed: {status}");
}
using (connection)
{
status = connection.SendWait(new byte[] { 7, 8, 9 }, TimeSpan.FromSeconds(5));
if (status != CLinkStatus.Ok)
{
throw new InvalidOperationException($"Send failed: {status}");
}
}
shared-mem:// endpoint addresses are usually returned by an accepted channel. Treat an attached handle as an alias for one existing channel role; do not concurrently send or receive through two handles for the same role.
Optional Bootstrap
CLinkControl is one optional byte protocol that can run over any CLink endpoint. A product can use it to ask for one or more endpoint URLs, then open those URLs normally.
CLink does not require CLinkControl. Products may use command-line arguments, environment variables, registry values, files, named pipes, sockets, or their own byte protocol to exchange endpoint URLs.
C
clink_status optional_bootstrap_example(clink_runtime_handle runtime)
{
const char* bootstrap_url =
"pipe://localhost/EA_CLink/bootstrap?protocol=clink-packet-v1&maxPacketSize=65536";
clink_utf8_span bootstrap_address = clink_text(bootstrap_url);
clink_connection_handle bootstrap = 0;
clink_connection_handle data = 0;
clink_status status;
/* 1. Open a known bootstrap endpoint. */
status = clink_connection_open(runtime, &bootstrap_address, &bootstrap);
if (status != CLINK_STATUS_OK) return status;
/* 2. Product code exchanges bytes over bootstrap and receives a concrete URL. */
const char* returned_endpoint_text = product_open_endpoint_over_bootstrap(bootstrap);
clink_utf8_span returned_endpoint = clink_text(returned_endpoint_text);
/* 3. Open the returned URL normally. */
status = clink_connection_open(runtime, &returned_endpoint, &data);
if (data) (void) clink_connection_close(data);
if (bootstrap) (void) clink_connection_close(bootstrap);
return status;
}
C#
var bootstrapAddress = "pipe://localhost/EA_CLink/bootstrap?protocol=clink-packet-v1&maxPacketSize=65536";
var status = runtime.TryOpenConnection(bootstrapAddress, out var bootstrap);
if (status != CLinkStatus.Ok || bootstrap is null)
{
throw new InvalidOperationException($"Bootstrap open failed: {status}");
}
using (bootstrap)
{
// Product code runs CLinkControl or another bootstrap protocol over bootstrap.
IReadOnlyList<string> endpointUrls = ProductOpenEndpointOverBootstrap(bootstrap);
status = runtime.TryOpenConnection(endpointUrls[0], out var data);
if (status != CLinkStatus.Ok || data is null)
{
throw new InvalidOperationException($"Endpoint open failed: {status}");
}
using (data)
{
// Move profile-specific bytes here.
}
}
The important rule is that the bootstrap result is still just a concrete URL. Once the URL is known, C and C# callers use the same normal open/send/receive APIs.
Preview Native C gRPC Over h2c Or Verified HTTPS
This is an RPC adapter, not a CLink backend. Include clink_grpc.h, generate C
code with protoc-gen-clink-c, and use an RPC-only authority such as
http://127.0.0.1:50051. Raw clink_connection_open* must reject that URL.
The client sequence is:
clink_grpc_client_options_init
-> clink_grpc_client_open(http://authority, ..., &session)
-> generated native gRPC registration helper(s)
-> generated typed start/write/complete/read operations or raw RPC calls
-> one host thread owns clink_rpc_session_pump_try/pump_wait
-> clink_rpc_call_wait + clink_grpc_call_get_protocol_status
-> clink_rpc_call_close
-> clink_rpc_session_close
For HTTPS, initialize clink_grpc_tls_client_options, retain the default
CLINK_GRPC_TLS_TRUST_SYSTEM or select CLINK_GRPC_TLS_TRUST_CUSTOM_ONLY /
CLINK_GRPC_TLS_TRUST_SYSTEM_PLUS_CUSTOM, provide bounded CA bytes when the
selected mode needs them, and call
clink_grpc_client_open_tls with an https:// authority. Servers initialize
clink_grpc_tls_server_options, provide the certificate chain and unencrypted
private key bytes, and call clink_grpc_server_open_tls. Both opens copy their
TLS inputs. Name verification and h2 ALPN are mandatory; there is no insecure
flag or server-name override. macOS system roots are target-native verified;
Windows CryptoAPI and Linux bundle/directory implementations remain Preview
until their target-native trust matrices run.
To replace a live listener's server identity, initialize another
clink_grpc_tls_server_options and call
clink_grpc_server_rotate_tls_identity. The adapter validates and copies the
complete certificate/key pair before atomically publishing it to future
handshakes. A rejected replacement leaves the prior identity untouched, and
accepted sessions continue with the TLS state they already negotiated. The
caller may release the supplied bytes when the operation returns.
For mutual TLS, append the client certificate chain and unencrypted private
key to clink_grpc_tls_client_options. On the server, provide client_ca and
select CLINK_GRPC_TLS_CLIENT_AUTH_OPTIONAL or
CLINK_GRPC_TLS_CLIENT_AUTH_REQUIRED; authentication is disabled by default.
The opens copy all material and zeroize owned private-key bytes on release.
clink_grpc_session_get_tls_peer_identity returns only present/verified flags
and the peer leaf certificate's SHA-256 fingerprint. It does not return a
borrowed certificate or place identity in application metadata.
To use an explicit HTTP CONNECT proxy for an HTTPS target, initialize ordinary
client options and call clink_grpc_client_options_configure_proxy with a
strict http://host[:port] proxy address. The optional authorization argument
is the complete bounded Proxy-Authorization field value, such as
Basic <base64>; CLink copies and zeroizes it for reconnect. No environment or
system proxy setting is consulted. The original HTTPS authority remains the
CONNECT target, HTTP/2 :authority, TLS SNI, and certificate-name identity.
Plaintext h2c targets and HTTPS proxy endpoints are deliberately rejected by
this first surface.
For explicit operational diagnostics, initialize
clink_grpc_observer_options and use clink_grpc_client_open_observed or
clink_grpc_server_open_observed; pass null TLS options for http:// and the
ordinary verified TLS options for https://. A null callback still enables
session counters, which are copied with clink_grpc_session_get_diagnostics.
The optional callback receives numeric stage/kind/status fields only, runs on
the synchronous open caller or session pump owner after native locks are
released, and must return promptly without pumping or closing that same session.
The 16-event internal callback queue reports saturation through
callback_events_dropped; counters saturate rather than wrap. Ordinary open
operations do not enable the diagnostic path.
The server opens the h2c or TLS variant, accepts one independent RPC session
with clink_grpc_server_accept_wait, registers the generated unary methods,
and drives that session from one pump owner. Incoming call events expose the
decoded protobuf payload and metadata through the ordinary native RPC lease.
The handler writes one response and completes with
clink_grpc_server_complete; accepted sessions and the reusable listener have
independent close ownership.
The Preview native-C surface covers unary and all three streaming shapes,
ordered metadata/trailers, deadlines/cancellation, and identity or explicit
per-message gzip. One opened session owns at most one active HTTP/2 connection
generation: GOAWAY drains accepted streams, and opt-in bounded recovery may
give a later fresh call a replacement generation without replaying any
in-flight call. Automatic RPC retries/hedging, discovery, automatic/system
proxy policy, and public C#/Rust/JavaScript gRPC channels remain unsupported.
Explicit native-C HTTP CONNECT is the opt-in exception. Exact runnable
client/server mechanics are kept in
src/native/tests/grpc_h2c_interop_host.c; generated registration is also
compiled by the generated-C native fixture. The automated official-peer gate
is OfficialGrpcLocalInteropFixture: it boots real ASP.NET Core/Kestrel,
Grpc.Net, tonic, and generated native peers over HTTPS in all required
directions. The current 105-case gate covers unary/streaming, compression,
metadata/status, cancellation, pressure, Health, and reflection controls;
connection-management, target-native, proxy/cloud, and release rows remain in
the ledger.
Compression is gRPC-only and explicit. Initialize clink_grpc_message_options
and select CLINK_GRPC_COMPRESSION_GZIP on clink_grpc_call_start or
clink_grpc_server_write_response. A streaming request may choose gzip at call
start and then opt individual messages back to identity; it cannot introduce
gzip after immutable request headers were sent. The adapter rejects gzip when
the peer did not negotiate it. Decompressed output is bounded by the session's
max_payload_bytes, compressed input is bounded by the same framed-message
limit, CRC/truncation/trailing-byte failures are rejected, and no expansion-ratio
limit rejects otherwise valid highly compressible protobuf. Inflate/deflate is
synchronous on the pump owner, so cancellation is observed between messages,
not partway through one zlib operation; applications should keep payload bounds
appropriate for their latency budget.
The native SDK also ships examples/grpc/operator: a wire-compatible C
projection and recipe for opt-in Health plus reflection v1/v1alpha application
services. Register the canonical standard paths explicitly. Health can use
clink_grpc_health.h; reflection must use an explicit descriptor allow-list,
authorize before lookup, validate one selector, reject an oversized registry
before serving, and cap encoded responses. Opening a listener publishes none of
these services automatically.
See the current results and exact commands in the executable feature-parity scoreboard.
Closing Rules
- Close connections before sessions, sessions before listeners, and listeners before runtimes when ownership is explicit.
- A wait may return
PeerClosed,Timeout,Busy,MessageTooLarge, or anotherclink_status/CLinkStatus; treat the status as the operation result. maxPacketSizeis enforced by the backend that accepted the URL. Oversized messages returnMessageTooLarge.- Managed
TimeSpanwaits accept non-negative durations or exactlyTimeout.InfiniteTimeSpan. Other negative values throw, and long finite durations are capped below the native infinite sentinel.
Showing the top 20 packages that depend on EyeAuras.CLink.
| Packages | Downloads |
|---|---|
|
EyeAuras.CLink.Rpc
Proto-first CLink RPC runtime for EyeAuras (see https://eyeauras.net)
|
40 |
|
EyeAuras.CLink.Rpc
Proto-first CLink RPC runtime for EyeAuras (see https://eyeauras.net)
|
36 |
|
EyeAuras.CLink.Rpc
Proto-first CLink RPC runtime for EyeAuras (see https://eyeauras.net)
|
34 |
|
EyeAuras.CLink.Rpc
Proto-first CLink RPC runtime for EyeAuras (see https://eyeauras.net)
|
33 |
|
EyeAuras.CLink.Rpc
Proto-first CLink RPC runtime for EyeAuras (see https://eyeauras.net)
|
32 |
|
EyeAuras.CLink.Rpc
Proto-first CLink RPC runtime for EyeAuras (see https://eyeauras.net)
|
31 |
|
EyeAuras.CLink.Rpc
Proto-first CLink RPC runtime for EyeAuras (see https://eyeauras.net)
|
30 |
|
EyeAuras.CLink.Rpc
Proto-first CLink RPC runtime for EyeAuras (see https://eyeauras.net)
|
29 |
|
EyeAuras.CLink.Rpc
Proto-first CLink RPC runtime for EyeAuras (see https://eyeauras.net)
|
27 |
|
EyeAuras.CLink.Rpc
Proto-first CLink RPC runtime for EyeAuras (see https://eyeauras.net)
|
26 |
|
EyeAuras.CLink.Rpc
Proto-first CLink RPC runtime for EyeAuras (see https://eyeauras.net)
|
24 |
|
EyeAuras.CLink.Rpc
Proto-first CLink RPC runtime for EyeAuras (see https://eyeauras.net)
|
21 |
|
EyeAuras.CLink.Rpc
Proto-first CLink RPC runtime for EyeAuras (see https://eyeauras.net)
|
19 |
.NET 8.0
- JetBrains.Annotations (>= 2022.3.1)
| Version | Downloads | Last updated |
|---|---|---|
| 0.6.2 | 31 | 07/15/2026 |
| 0.6.1 | 27 | 07/11/2026 |
| 0.6.0 | 24 | 07/11/2026 |
| 0.1.45 | 27 | 06/07/2026 |
| 0.1.44 | 33 | 06/07/2026 |
| 0.1.43 | 28 | 06/07/2026 |
| 0.1.42 | 27 | 06/07/2026 |
| 0.1.41 | 31 | 06/06/2026 |
| 0.1.40 | 30 | 06/06/2026 |
| 0.1.39 | 33 | 06/06/2026 |
| 0.1.38 | 34 | 06/06/2026 |
| 0.1.37 | 33 | 06/06/2026 |
| 0.1.36 | 35 | 06/06/2026 |
| 0.1.35 | 35 | 06/06/2026 |
| 0.1.34 | 33 | 06/06/2026 |
| 0.1.33 | 31 | 06/06/2026 |
| 0.1.29 | 34 | 06/06/2026 |
| 0.1.28 | 33 | 06/06/2026 |
| 0.1.22 | 32 | 06/06/2026 |
| 0.1.11 | 43 | 05/10/2026 |