C++ 바인딩 가이드 (zlink::)¶
이 장의 계약 소유 문서 — C++ bindings 스펙이 다룬다. 이 장은 그 계약을 실제 샘플 코드로 보여준다.
C 코어를 RAII 래퍼로 감싼 바인딩입니다(header-only는 아니며 컴파일 후 링크). C++에서 zlink를 쓰는 방법을 실제 샘플 코드 중심으로 설명합니다. 자세한 메시징 개념은 코어 가이드가 다룹니다.
설치¶
C++ 바인딩은 CMake로 제공합니다.
- C++20 이상 (coroutine, concepts 사용).
- 네이티브 코어가 함께 링크됩니다.
5분 예제¶
#include <zlink.hpp>
// 서버
zlink::context_t ctx;
zlink::pair_socket_t server (ctx);
server.bind ("tcp://127.0.0.1:5555");
zlink::received_t inbound;
server.recv (inbound);
std::printf ("%s\n", inbound.parts ()[0].to_string ().c_str ()); // PING
inbound.close ();
zlink::message_t ack = zlink::message_t::from ("ACK");
server.send ().message (ack).submit ();
// 클라이언트
zlink::context_t ctx;
zlink::pair_socket_t client (ctx);
client.connect ("tcp://127.0.0.1:5555");
zlink::message_t ping = zlink::message_t::from ("PING");
client.send ().message (ping).submit ();
zlink::received_t inbound;
client.recv (inbound);
std::printf ("%s\n", inbound.parts ()[0].to_string ().c_str ()); // ACK
inbound.close ();
핵심 타입¶
컨텍스트¶
context_t는 RAII로 관리합니다. 소멸자에서 자동으로 종료됩니다.
메시지¶
message_t는 페이로드 프레임 하나를 소유합니다. send로 전달하면 소유권이
이전(move)되고 이후 사용 시 무효 상태가 됩니다.
// 문자열에서 생성
zlink::message_t msg = zlink::message_t::from ("payload");
// 바이트에서 생성
std::vector<uint8_t> bytes = {0x01, 0x02};
zlink::message_t msg = zlink::message_t::from (bytes);
// 크기 지정 빈 프레임
zlink::message_t msg = zlink::message_t::allocate (256);
std::memcpy (msg.data (), src, 256);
// 전송 — msg는 여기서 move됨
socket.send ().message (msg).submit ();
// 전송 후 msg는 무효 — 다시 쓰지 말 것
HWM 대기 가능 send에는 동기와 비동기 종결자가 모두 있습니다. plain thread에서는
Core의 blocking admission 경로를 따르는 submit()을 사용합니다. Coroutine에서는
DONTWAIT을 사용하고 socket completion queue에서 완료되는 async()를 co_await합니다.
socket.send ().message (msg).submit (); // 동기, 기본은 HWM admission까지 blocking
co_await socket.send ().message (msg).async (); // 비동기, 호출 thread를 막지 않음
Request도 두 terminal style을 사용합니다. submit()은 reply까지 blocking하고,
async()는 socket completion queue에서 완료되는 awaitable을 반환합니다. Reply는 이
terminal의 결과이며 별도 DATA receive로 받지 않습니다.
접수된 pre-admission operation의 retry는 Core가 소유합니다. Caller retry queue를 만들거나
같은 payload를 다시 submit하지 않습니다. 공용 native
ZLINK_OPT_PENDING_MAX_MSGS/BYTES 제한은 pending SEND와 REQUEST에 함께 적용되며,
send 전용 pending option 이름은 없습니다. Completion은 local admission을 뜻할 뿐 peer
delivery나 application acknowledgement가 아닙니다.
소비하지 않은 async_result_t를 파괴하면 caller waiter만 detach됩니다. Core submit 전에는
Core를 호출하지 않고 language operation을 중단할 수 있지만, successful submit 뒤에는 Core가
계속 admission할 수 있고 socket owner가 늦게 온 completion을 drain합니다. STREAM은
bind/connect 전에 stream_recv_mode_t::raw 또는 packet을 고른 뒤 각각 recv() 또는
recv_packet()을 사용합니다.
public poller가 socket의 poll_event_t::completion owner이면 blocking request나 awaitable이
남아 있는 동안 다른 thread가 wait()를 계속 호출해야 합니다. Wait가 native completion을
drain해 binding state를 settle/cleanup하므로, 같은 thread가 wait 사이에서 blocking terminal을
호출하면 진행이 멈출 수 있습니다.
수신된 메시지 읽기:
const zlink::message_t &part = inbound.parts ()[0];
std::string text = part.to_string (); // 문자열 복사
std::span<const std::byte> bytes = part.bytes (); // 뷰 (메시지 수명 동안만)
size_t size = part.size ();
received_t — 수신 봉투¶
zlink::received_t inbound;
int rc = socket.recv (inbound); // 0 = 성공
// 또는 플래그 지정
socket.recv (inbound, zlink::recv_flags_t::none);
auto parts = inbound.parts (); // const vector
auto rid = inbound.routing_id (); // optional<routing_id_t>
auto token = inbound.reply_token (); // optional<reply_token_t>
inbound.close (); // 명시적 해제 (또는 소멸자)
라우팅 ID¶
auto rid = zlink::routing_id_t::from (
reinterpret_cast<const uint8_t*> (text.data ()), text.size ());
socket.set_routing_id (rid);
소유권과 수명¶
| 상황 | 규칙 |
|---|---|
submit() 성공 |
message_t가 move됨 — 이후 사용 무효 |
async() |
Core completion 대기 동안 operation이 move된 message를 소유 |
submit() 기타 실패 |
예외(submit_error_t) 발생, 메시지 소유권 유지 |
recv() |
received_t&로 in-place 수신, close() 또는 소멸자로 해제 |
| 비동기 요청 | 회신 std::vector<message_t> 소유, 벡터 소멸 시 자동 해제 |
try {
zlink::message_t msg = zlink::message_t::from ("data");
socket.send ().message (msg).submit (); // 성공 시 msg move
} catch (const zlink::submit_error_t &e) {
// 전송 실패 처리
}
공유·이전·복제 (Copy / Move / Clone)¶
payload를 다룰 때 세 가지 명시적 동작이 있습니다. 이름과 의미는 모든 바인딩에서
동일하며 Core C API(zlink_msg_copy/zlink_msg_move)와 1:1로 대응합니다.
| 동작 | 시그니처 | 의미 | 언제 |
|---|---|---|---|
copy() |
message_t copy() const |
ref-count 공유 — 같은 버퍼를 가리키는 새 값 반환, 원본 유효 유지 | 같은 payload를 보관하며 원본도 계속 써야 할 때 |
move(dest) |
void move(message_t& dest) |
소유권 이전 — dest로 넘기고 호출자는 empty |
받은 메시지를 사본 없이 그대로 다시 보낼 때(relay/echo) |
clone() |
message_t clone() const |
깊은 복사 — 독립 버퍼 | 복제 후 payload를 독립적으로 수정할 때 |
// Copy: 같은 버퍼를 공유하는 새 핸들. 둘 다 각자 닫는다(refcount).
zlink::message_t shared = msg.copy ();
socket.send ().message (shared).submit (); // shared는 move됨
// msg는 여전히 유효 — 재사용/close 가능
// Move: 받은 메시지를 사본 없이 그대로 echo (가장 효율적)
zlink::message_t out;
received_part.move (out); // received_part는 empty가 됨
socket.send (routing_id).message (out).submit ();
// Clone: 독립 복제 후 수정
zlink::message_t dup = msg.clone ();
std::memcpy (dup.data (), patch, len); // msg에는 영향 없음
copy()는 mutation 격리를 보장하지 않습니다(공유 버퍼) — 독립 수정이 필요하면
clone()을 쓰세요.
에러 처리¶
C++ 바인딩은 zlink::binding_error_t를 상속하는 작업별 예외를 던집니다.
try {
zlink::message_t msg = zlink::message_t::from ("data");
socket.send ().message (msg).submit ();
} catch (const zlink::submit_error_t &e) {
// blocking admission timeout/backpressure 결과도 포함
// application policy를 적용하기 전에 e.result() 확인
}
예외 타입:
| 예외 | 발생 시점 | result() 타입 |
|---|---|---|
submit_error_t |
전송/발행 실패 | submit_result_t |
request_error_t |
요청 실패 | request_result_t |
recv_error_t |
수신 실패 | recv_result_t |
bind_error_t |
바인드 실패 | bind_result_t |
connect_error_t |
연결 실패 | connect_result_t |
config_error_t |
옵션 설정 실패 | config_result_t |
close_error_t |
닫기 실패 | close_result_t |
handler_error_t |
핸들러 등록 실패 | handler_result_t |
모두 binding_error_t를 상속하며 code(), internal_errno()로 네이티브 코드를
확인할 수 있습니다. 일부 recv API는 예외 대신 recv_result_t 정수 코드를 반환합니다
(예제 참고).
C API 대응표¶
| C API | C++ API |
|---|---|
zlink_ctx_new() |
zlink::context_t{} |
zlink_ctx_term() |
소멸자 또는 ctx.term() |
zlink_socket(ctx, type) |
zlink::pair_socket_t{ctx} 등 |
zlink_bind(s, ep) |
socket.bind(ep) |
zlink_connect(s, ep) |
socket.connect(ep) |
blocking zlink_send(..., parts, count, ...) / zlink_send_rid(..., parts, count, ...) |
socket.send().message(m).submit() |
| DONTWAIT send + completion pull | co_await socket.send().message(m).async() |
zlink_recv(..., parts_out, capacity, count_out, ...) |
socket.recv(received) |
zlink_msg_data(msg) |
part.data() / part.bytes() |
zlink_msg_size(msg) |
part.size() |
zlink_routing_id_t |
zlink::routing_id_t |
zlink_socket_monitor_open(...) |
socket.monitor_open(...) |
zlink_poller_new() |
zlink::poller_t{} |
zlink_timer_new() |
zlink::timer_t{} |
네이티브 라이브러리 / 배포¶
int major, minor, patch;
zlink::version (major, minor, patch);
std::printf ("zlink %d.%d.%d\n", major, minor, patch);
if (zlink::has ("draft")) {
// draft API 지원
}
스레딩 규칙:
| 항목 | 규칙 |
|---|---|
context_t |
스레드 간 공유 가능 |
| 소켓 | 하나의 스레드에서만 사용. 동시 접근 금지 |
| Completion과 receive 전달 | caller-owned terminal 또는 pull loop에서 관찰 |
message_t::bytes() |
메시지 수명 동안만 유효한 span |
flag 없는 동기 submit()은 HWM admission을 기다리는 동안 호출 thread를 멈춥니다.
plain thread에서는 그 thread만 대기합니다. 다른 작업을 계속해야 하는 coroutine에서는
co_await async()를 사용합니다. Managed send는 public DONTWAIT flag terminal을
노출하지 않습니다.
// 올바른 패턴: 소켓 per-스레드
std::thread worker ([&ctx] {
zlink::dealer_socket_t socket (ctx);
socket.connect ("tcp://...");
// 이 스레드에서만 socket 사용
});
샘플¶
bindings/cpp/samples/ 디렉터리에 검증된 샘플이 있습니다.
| 파일 | 설명 |
|---|---|
pair_recv_sample.cpp |
PAIR 송수신 |
dealer_router_recv_sample.cpp |
DEALER/ROUTER 송수신(요청/응답) |
pubsub_recv_sample.cpp |
XPUB/SUB 발행·구독 |
stream_recv_sample.cpp |
STREAM 원시 TCP |
stream_packet_pull_sample.cpp |
STREAM PACKET pull |
monitor_recv_sample.cpp |
모니터 이벤트 수신 |
request_reply_async_sample.cpp |
ROUTER/DEALER 비동기 요청/응답 |
SPOT·Actor 예제는 core 바인딩이 아니라 framework C++ 샘플이 다룬다 — Bingo 등
framework/languages/cpp/samples/를 본다.
cd bindings/cpp
# 샘플은 ZLINK_CPP_BUILD_SAMPLES=ON일 때만 빌드된다
cmake -B build -DZLINK_CPP_BUILD_SAMPLES=ON && cmake --build build
./build/sample_cpp_pair_recv_sample
# 또는 일괄 실행: ./samples/run_samples.sh