// Copyright 2026 Versity Software // This file is licensed under the Apache License, Version 2.0 // (the "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. //go:build linux && amd64 && cgo && rdma // Package rdma provides Go bindings to libcuobjserver via CGO. package rdma /* #cgo CFLAGS: -I${SRCDIR}/../include -I${SRCDIR}/../cuwrapper // Links against cuObjServer 2.x: the resulting binary records a // libcuobjserver.so.2 NEEDED entry and will not run against a 1.x install. #cgo LDFLAGS: -L${SRCDIR} -l:libcuobjwrapper.a -L/usr/lib64 -lcuobjserver -lstdc++ #include "cuobjserver_wrapper.h" #include */ import "C" import ( "errors" "fmt" "sync" "sync/atomic" "syscall" "unsafe" ) const ( cuobjLogPathInfo = 0x0001 cuobjLogPathDebug = 0x0002 cuobjLogPathError = 0x0004 // cuobjProtoRDMADCV1 is the CUOBJ_PROTO_RDMA_DC_V1 protocol identifier. cuobjProtoRDMADCV1 = 1001 ) var debugTelemetryEnabled atomic.Bool // ConfigureTelemetry controls cuObjServer telemetry logging. // When debug is true, enable info+debug+error logs to stderr/stdout path. func ConfigureTelemetry(debug bool) { debugTelemetryEnabled.Store(debug) C.cuobj_server_setup_telemetry(0) if debug { C.cuobj_server_set_telem_flags(C.uint(cuobjLogPathInfo | cuobjLogPathDebug | cuobjLogPathError)) return } C.cuobj_server_set_telem_flags(C.uint(cuobjLogPathError)) } // DebugTelemetryEnabled reports whether verbose RDMA diagnostics are enabled. func DebugTelemetryEnabled() bool { return debugTelemetryEnabled.Load() } // Buffer wraps an RDMA-registered memory region. type Buffer struct { cbuf *C.cuobj_rdma_buffer_t hostPtr unsafe.Pointer size int } // HostPtr returns the underlying host memory pointer. func (b *Buffer) HostPtr() unsafe.Pointer { return b.hostPtr } // Size returns the buffer size in bytes. func (b *Buffer) Size() int { return b.size } // Slice returns the buffer contents as a Go byte slice backed by C-allocated // memory. Returns nil after DeregisterBuffer. func (b *Buffer) Slice() []byte { return unsafe.Slice((*byte)(b.hostPtr), b.size) } // Server wraps a cuObjServer instance. type Server struct { csrv *C.cuobj_server_t sessionOpen bool mu sync.Mutex } // tunablesToC converts a RDMATunables value to the equivalent C struct. func tunablesToC(t RDMATunables) C.cuobj_rdma_tunables_t { var ct C.cuobj_rdma_tunables_t ct.num_dcis = C.int(t.NumDCIs) ct.cq_depth = C.uint(t.CQDepth) ct.dc_key = C.ulong(t.DCKey) ct.service_level = C.int(t.ServiceLevel) ct.timeout = C.uint8_t(t.Timeout) ct.hop_limit = C.uint(t.HopLimit) ct.pkey_index = C.int(t.PKeyIndex) ct.delay_interval = C.uint32_t(t.DelayInterval) ct.delay_mode = C.int(t.DelayMode) ct.retry_cnt = C.uint8_t(t.RetryCount) if t.QPResetOnFailure { ct.qp_reset_on_failure = 1 } ct.traffic_class = C.uint(t.TrafficClass) ct.max_rd_atomic = C.int(t.MaxRdAtomic) return ct } // NewServer creates a cuObjServer bound to the given RDMA IP and port. // Uses CUOBJ_PROTO_RDMA_DC_V1 (1001). If tunables is non-nil, the 4-argument // constructor is used so the tunable parameters apply to the initial session // started by the constructor. Pass nil to use library defaults. // // The constructor is the only point at which tunables can be applied: // libcuobjserver starts the RDMA session inside it, and cuObjServer 2.0.0 // removed the after-the-fact initRDMAConfigParams() entry point. func NewServer(ip string, port uint16, tunables *RDMATunables) (*Server, error) { cip := C.CString(ip) defer C.free(unsafe.Pointer(cip)) var csrv *C.cuobj_server_t if tunables != nil { ct := tunablesToC(*tunables) csrv = C.cuobj_server_create_with_config(cip, C.ushort(port), cuobjProtoRDMADCV1, &ct) } else { csrv = C.cuobj_server_create(cip, C.ushort(port), cuobjProtoRDMADCV1) } if csrv == nil { return nil, fmt.Errorf("rdma: failed to create cuObjServer on %s:%d", ip, port) } srv := &Server{csrv: csrv} // The library starts the session as part of construction; record that // readiness so StartSession can be a no-op and Close/CloseSession use a // consistent ownership model. if srv.IsConnected() { srv.sessionOpen = true } return srv, nil } // StartSession reports whether the RDMA session is up. // // libcuobjserver starts the session inside the cuObjServer constructor and has // never exposed an explicit start entry point, so this verifies the session // came up rather than initiating it. It is kept as an explicit lifecycle step // so callers can fail fast on a server that constructed but did not connect. // StartSession must not be called concurrently with CloseSession or Close. func (s *Server) StartSession() error { s.mu.Lock() alreadyOpen := s.sessionOpen s.mu.Unlock() if alreadyOpen { return nil } rc := C.cuobj_server_start_session(s.csrv) if rc != 0 { if s.IsConnected() { s.mu.Lock() s.sessionOpen = true s.mu.Unlock() return nil } return fmt.Errorf("rdma: RDMA session is not connected (rc=%d)", rc) } s.mu.Lock() s.sessionOpen = true s.mu.Unlock() return nil } // IsConnected returns the RDMA connection status. func (s *Server) IsConnected() bool { return C.cuobj_server_is_connected(s.csrv) != 0 } // AllocHostBuffer allocates a 4KB-aligned host buffer of the given size. func (s *Server) AllocHostBuffer(size int) (unsafe.Pointer, error) { if size <= 0 { return nil, fmt.Errorf("rdma: allocHostBuffer size %d must be positive", size) } ptr := C.cuobj_server_alloc_host_buffer(s.csrv, C.size_t(size)) if ptr == nil { return nil, fmt.Errorf("rdma: allocHostBuffer(%d) failed", size) } return ptr, nil } // FreeHostBuffer releases a buffer previously allocated by AllocHostBuffer. func (s *Server) FreeHostBuffer(ptr unsafe.Pointer) { if ptr != nil { C.cuobj_server_free_host_buffer(ptr) } } // RegisterBuffer registers a host memory region for RDMA and returns a Buffer handle. func (s *Server) RegisterBuffer(ptr unsafe.Pointer, size int) (*Buffer, error) { if ptr == nil { return nil, errors.New("rdma: registerBuffer ptr must not be nil") } if size <= 0 { return nil, fmt.Errorf("rdma: registerBuffer size %d must be positive", size) } cbuf := C.cuobj_server_register_buffer(s.csrv, ptr, C.size_t(size)) if cbuf == nil { return nil, errors.New("rdma: registerBuffer failed") } return &Buffer{cbuf: cbuf, hostPtr: ptr, size: size}, nil } // DeregisterBuffer deregisters a previously registered RDMA buffer. func (s *Server) DeregisterBuffer(buf *Buffer) { if buf != nil && buf.cbuf != nil { C.cuobj_server_deregister_buffer(s.csrv, buf.cbuf) buf.cbuf = nil buf.hostPtr = nil buf.size = 0 } } // AllocateChannel obtains a unique channel ID for concurrent RDMA operations. func (s *Server) AllocateChannel() (uint16, error) { ch := C.cuobj_server_allocate_channel(s.csrv) if ch == C.UINT16_MAX { return 0, errors.New("rdma: no free channel IDs") } return uint16(ch), nil } // FreeChannel releases a previously allocated channel ID. func (s *Server) FreeChannel(id uint16) { C.cuobj_server_free_channel(s.csrv, C.uint16_t(id)) } // HandleGet performs an RDMA WRITE (server→client) to serve a GET request. // The local buffer must already contain the data to send. // Returns bytes transferred. // // On failure the library returns a negative errno, which is wrapped into the // returned error: a malformed RDMA descriptor, or one whose prefix does not // match the server's protocol, reports syscall.EPROTO and can be matched with // errors.Is. That distinguishes a bad client-supplied descriptor from a // transport fault. func (s *Server) HandleGet(key string, buf *Buffer, remoteStart uint64, size int64, rdmaDescr string, channel uint16) (int64, error) { if buf == nil || buf.cbuf == nil { return 0, errors.New("rdma: invalid or deregistered buffer") } if size <= 0 { return 0, fmt.Errorf("rdma: transfer size %d must be positive", size) } if size > MaxTransferSize { return 0, fmt.Errorf("rdma: transfer size %d exceeds max %d", size, MaxTransferSize) } ckey := C.CString(key) defer C.free(unsafe.Pointer(ckey)) cdescr := C.CString(rdmaDescr) defer C.free(unsafe.Pointer(cdescr)) n := C.cuobj_server_handle_get(s.csrv, ckey, buf.cbuf, C.uint64_t(remoteStart), C.size_t(size), cdescr, C.uint16_t(channel)) if n < 0 { return 0, fmt.Errorf("rdma: handleGetObject failed (rc=%d): %w", n, syscall.Errno(-int64(n))) } return int64(n), nil } // HandlePut performs an RDMA READ (client→server) to serve a PUT request. // After return, the local buffer contains the data read from the client. // Returns bytes transferred. // // On failure the library returns a negative errno, which is wrapped into the // returned error: a malformed RDMA descriptor, or one whose prefix does not // match the server's protocol, reports syscall.EPROTO and can be matched with // errors.Is. That distinguishes a bad client-supplied descriptor from a // transport fault. func (s *Server) HandlePut(key string, buf *Buffer, remoteStart uint64, size int64, rdmaDescr string, channel uint16) (int64, error) { if buf == nil || buf.cbuf == nil { return 0, errors.New("rdma: invalid or deregistered buffer") } if size <= 0 { return 0, fmt.Errorf("rdma: transfer size %d must be positive", size) } if size > MaxTransferSize { return 0, fmt.Errorf("rdma: transfer size %d exceeds max %d", size, MaxTransferSize) } ckey := C.CString(key) defer C.free(unsafe.Pointer(ckey)) cdescr := C.CString(rdmaDescr) defer C.free(unsafe.Pointer(cdescr)) n := C.cuobj_server_handle_put(s.csrv, ckey, buf.cbuf, C.uint64_t(remoteStart), C.size_t(size), cdescr, C.uint16_t(channel)) if n < 0 { return 0, fmt.Errorf("rdma: handlePutObject failed (rc=%d): %w", n, syscall.Errno(-int64(n))) } return int64(n), nil } // CloseSession marks the session closed. The underlying library ties session // teardown to the cuObjServer destructor, so the session is not actually torn // down until Close is called. func (s *Server) CloseSession() { s.mu.Lock() defer s.mu.Unlock() if s.csrv != nil && s.sessionOpen { C.cuobj_server_close_session(s.csrv) s.sessionOpen = false } } // Close destroys the cuObjServer instance. The Server must not be used afterward. func (s *Server) Close() { s.mu.Lock() defer s.mu.Unlock() if s.csrv != nil { if s.sessionOpen { C.cuobj_server_close_session(s.csrv) s.sessionOpen = false } C.cuobj_server_destroy(s.csrv) s.csrv = nil } }