From 19bddd603f8916a132624a7ef59dae766a718f6b Mon Sep 17 00:00:00 2001 From: fanyang Date: Sun, 5 Apr 2026 22:40:06 +0800 Subject: [PATCH 1/4] chore: update kcp submodule --- kcp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kcp b/kcp index 7f98058..f4f3a89 160000 --- a/kcp +++ b/kcp @@ -1 +1 @@ -Subproject commit 7f9805887b0909c52c825925f123e7a84da37167 +Subproject commit f4f3a89cc632647dabdcb146932d2afd5591e62e From df8fba416f20cf8105021e854bea9a73c3ea1e62 Mon Sep 17 00:00:00 2001 From: fanyang Date: Sun, 5 Apr 2026 22:30:00 +0800 Subject: [PATCH 2/4] chore(lint): format code and fix clippy warnings --- src/endpoint.rs | 64 ++++++++++++++++++++++++++--------------------- src/ffi.rs | 1 + src/ffi_safe.rs | 18 ++++++------- src/packet_def.rs | 17 +++++++------ src/stream.rs | 2 +- 5 files changed, 56 insertions(+), 46 deletions(-) diff --git a/src/endpoint.rs b/src/endpoint.rs index e55a4e3..1ee8ae8 100644 --- a/src/endpoint.rs +++ b/src/endpoint.rs @@ -226,7 +226,7 @@ impl KcpConnection { tracing::trace!("recv data ({}): {:?}", buf.len(), buf); assert_ne!(0, buf.len()); let send_ret = recv_sender.send(buf.split()).await; - if let Err(_) = send_ret { + if send_ret.is_err() { break; } } @@ -341,7 +341,7 @@ impl KcpConnectionState { fn handle_packet(&mut self, packet: &KcpPacket) -> Result, Error> { self.notify_pong(); let mut out_packet = None; - let old_state = self.fsm.clone(); + let old_state = self.fsm; let _ = self.fsm.handle_packet(packet, &mut out_packet); if old_state != self.fsm { self.notify.notify_one(); @@ -431,6 +431,12 @@ impl std::fmt::Debug for KcpEndpoint { } } +impl Default for KcpEndpoint { + fn default() -> Self { + Self::new() + } +} + impl KcpEndpoint { pub fn new() -> Self { let (input_sender, input_receiver) = tokio::sync::mpsc::channel(1024); @@ -450,7 +456,7 @@ impl KcpEndpoint { new_conn_sender, new_conn_receiver: Arc::new(tokio::sync::Mutex::new(new_conn_receiver)), - kcp_config_factory: Box::new(|conv| KcpConfig::new_turbo(conv)), + kcp_config_factory: Box::new(KcpConfig::new_turbo), tasks: JoinSet::new(), } @@ -514,7 +520,7 @@ impl KcpEndpoint { } let conv = ConnId::from(&packet); - if packet.header().is_data() && packet.payload().len() > 0 { + if packet.header().is_data() && !packet.payload().is_empty() { if let Some(mut conn) = data.conn_map.get_mut(&conv) { if let Err(e) = conn.handle_input(&packet) { tracing::error!(?e, ?conv, "handle input on connection failed"); @@ -533,32 +539,12 @@ impl KcpEndpoint { let mut state_ref = data.state_map.get_mut(&conv); let state = state_ref.as_deref_mut(); let mut out_packet: Option = None; - if state.is_none() { - if packet.header().is_rst() { - tracing::debug!(?conv, "reset packet for conn, but no state"); - continue; - } - let mut tmp_fsm = KcpConnectionFSM::listen(); - let res = tmp_fsm.handle_packet(&packet, &mut out_packet); - tracing::trace!( - ?conv, - ?state, - ?out_packet, - "handle first packet for conn, ret: {:?}", - res - ); - if res.is_ok() { - let mut conn_state = KcpConnectionState::new(tmp_fsm); - conn_state.set_data(packet.payload().to_vec().into()); - data.state_map.insert(conv, conn_state); - } - } else { - let state = state.unwrap(); + if let Some(state) = state { let prev_established = state.is_established(); let ret = state.handle_packet(&packet); tracing::trace!(?conv, ?state, "handle packet for conn, ret: {:?}", ret); - if ret.is_ok() { - out_packet = ret.unwrap(); + if let Ok(pkt) = ret { + out_packet = pkt; } if !prev_established && state.is_established() { @@ -567,7 +553,9 @@ impl KcpEndpoint { if state.is_peer_closed() { tracing::debug!(?conv, "peer half closed, close recv"); - data.conn_map.get_mut(&conv).map(|conn| conn.close_recv()); + if let Some(conn) = data.conn_map.get_mut(&conv) { + conn.close_recv() + } } if state.is_closed() { @@ -575,6 +563,24 @@ impl KcpEndpoint { tracing::debug!(?conv, "connection closed, remove state"); data.conn_map.remove(&conv); } + } else { + if packet.header().is_rst() { + tracing::debug!(?conv, "reset packet for conn, but no state"); + continue; + } + let mut tmp_fsm = KcpConnectionFSM::listen(); + let res = tmp_fsm.handle_packet(&packet, &mut out_packet); + tracing::trace!( + ?conv, + ?out_packet, + "handle first packet for conn, ret: {:?}", + res + ); + if res.is_ok() { + let mut conn_state = KcpConnectionState::new(tmp_fsm); + conn_state.set_data(packet.payload().to_vec().into()); + data.state_map.insert(conv, conn_state); + } } drop(state_ref); @@ -661,7 +667,7 @@ impl KcpEndpoint { }; let close_ret = state.fsm.close(&mut out_packet); - let cur_state = state.fsm.clone(); + let cur_state = state.fsm; let is_closed = state.is_closed(); drop(state); match close_ret { diff --git a/src/ffi.rs b/src/ffi.rs index 81258b6..aca891b 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -2,5 +2,6 @@ #![allow(non_upper_case_globals)] #![allow(non_camel_case_types)] #![allow(non_snake_case)] +#![allow(clippy::all)] include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/src/ffi_safe.rs b/src/ffi_safe.rs index 2470bf1..a927bea 100644 --- a/src/ffi_safe.rs +++ b/src/ffi_safe.rs @@ -51,7 +51,7 @@ pub struct Kcp { kcp: *mut ikcpcb, config: KcpConfig, now: Instant, - output_cb: Option Result<(), Error>>>, + output_cb: Option, _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>, } @@ -102,7 +102,7 @@ impl Kcp { ret.apply_config()?; - return Ok(ret); + Ok(ret) } } @@ -113,9 +113,9 @@ impl Kcp { pub fn handle_input(&mut self, data: &[u8]) -> Result<(), Error> { let ret = unsafe { ikcp_input(self.kcp, data.as_ptr() as *const _, data.len() as _) }; if ret < 0 { - return Err(anyhow::anyhow!("input failed, return: {}", ret).into()); + Err(anyhow::anyhow!("input failed, return: {}", ret).into()) } else { - return Ok(()); + Ok(()) } } @@ -134,9 +134,9 @@ impl Kcp { pub fn send(&mut self, data: Bytes) -> Result { let ret = unsafe { ikcp_send(self.kcp, data.as_ptr() as *const _, data.len() as _) }; if ret < 0 { - return Err(anyhow::anyhow!("send failed, return: {}", ret).into()); + Err(anyhow::anyhow!("send failed, return: {}", ret).into()) } else { - return Ok(ret as usize); + Ok(ret as usize) } } @@ -153,12 +153,12 @@ impl Kcp { pub fn recv(&mut self, buf: &mut BytesMut) -> Result<(), Error> { let ret = unsafe { ikcp_recv(self.kcp, buf.as_mut_ptr() as *mut _, buf.capacity() as _) }; if ret < 0 { - return Err(anyhow::anyhow!("recv failed, return: {}", ret).into()); + Err(anyhow::anyhow!("recv failed, return: {}", ret).into()) } else { unsafe { buf.set_len(ret as usize); } - return Ok(()); + Ok(()) } } @@ -209,7 +209,7 @@ impl Kcp { } } - return Ok(()); + Ok(()) } } diff --git a/src/packet_def.rs b/src/packet_def.rs index c77c258..3ef9c1c 100644 --- a/src/packet_def.rs +++ b/src/packet_def.rs @@ -189,7 +189,6 @@ impl std::fmt::Debug for KcpPacketHeader { } } - #[derive(Clone)] pub struct KcpPacket { inner: BytesMut, @@ -216,15 +215,15 @@ impl std::fmt::Debug for KcpPacket { } } -impl Into for KcpPacket { - fn into(self) -> BytesMut { - self.inner +impl From for BytesMut { + fn from(val: KcpPacket) -> Self { + val.inner } } -impl Into for KcpPacket { - fn into(self) -> Bytes { - self.inner.freeze() +impl From for Bytes { + fn from(val: KcpPacket) -> Self { + val.inner.freeze() } } @@ -262,4 +261,8 @@ impl KcpPacket { pub fn len(&self) -> usize { self.inner.len() } + + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } } diff --git a/src/stream.rs b/src/stream.rs index 0704620..ca56a05 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -57,7 +57,7 @@ impl AsyncRead for KcpStream { ) -> Poll> { let mut partial_recved = false; if let Some(partial_recv_buf) = &mut self.partial_recv_buf { - assert!(partial_recv_buf.len() > 0); + assert!(!partial_recv_buf.is_empty()); partial_recved = true; let len = std::cmp::min(buf.remaining(), partial_recv_buf.len()); From 264e691582aa4a526d35c01fca238a00563c2a79 Mon Sep 17 00:00:00 2001 From: fanyang Date: Sun, 5 Apr 2026 22:05:32 +0800 Subject: [PATCH 3/4] fix(build): support zigbuild cross-compilation and llvm-ar Rework build.rs to auto-detect cargo-zigbuild via `CC/CC_{target}` env vars, forward zig system include paths to bindgen for cross-compilation, and use `llvm-ar` on macOS to avoid `ar -D` warning. --- Cargo.toml | 18 ++++--- build.rs | 152 +++++++++++++++++++++++++++++++++++++++++++++++------ wrapper.h | 2 +- 3 files changed, 147 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1dc2707..bcfae81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,20 +10,22 @@ license = "MIT" keywords = ["kcp", "bindings"] [dependencies] +anyhow = "1.0.95" +auto_impl = "1.2.1" +bitflags = "2.8.0" bytes = "1" -tokio = { version = "1", features = ["full"] } -tokio-util = { version = "0.7.13" } dashmap = "6.1.0" -zerocopy = { version = "0.7", features = ["derive", "simd"] } -bitflags = "2.8.0" parking_lot = "0.12.3" -auto_impl = "1.2.1" +rand = "0.8.5" thiserror = "2.0.11" -anyhow = "1.0.95" +tokio = { version = "1", features = ["full"] } +tokio-util = { version = "0.7.13" } tracing = "0.1.41" tracing-subscriber = "0.3.19" -rand = "0.8.5" +zerocopy = { version = "0.7", features = ["derive", "simd"] } [build-dependencies] -bindgen = { version="0.72.1", default-features=false, features=["runtime"]} +bindgen = { version = "0.72.1", default-features = false, features = [ + "runtime", +] } cc = "1.2.10" diff --git a/build.rs b/build.rs index 68ed2e0..3c49354 100644 --- a/build.rs +++ b/build.rs @@ -1,12 +1,144 @@ use std::env; use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +fn zig_target(target: &str) -> String { + let mut parts = target.split('-'); + let arch = parts.next().unwrap_or(target); + let _vendor = parts.next(); + let rest = parts.collect::>(); + + if rest.is_empty() { + target.to_owned() + } else { + format!("{arch}-{}", rest.join("-")) + } +} + +fn zig_include_dirs(target: &str) -> Option> { + let output = Command::new("zig") + .args([ + "cc", + &format!("--target={}", zig_target(target)), + "-E", + "-x", + "c", + "-", + "-v", + ]) + .stdin(Stdio::null()) + .output() + .ok()?; + + if !output.status.success() { + return None; + } + + let stderr = String::from_utf8(output.stderr).ok()?; + let start = stderr.find("#include <...> search starts here:")?; + let end = stderr[start..].find("End of search list.")? + start; + let include_block = &stderr[start..end]; + + let include_dirs = include_block + .lines() + .skip(1) + .map(str::trim) + .filter(|line| !line.is_empty() && !line.starts_with("#include")) + .map(ToOwned::to_owned) + .collect::>(); + + Some(include_dirs) +} + +fn is_zigbuild(target: &str) -> bool { + let cc_key = format!("CC_{}", target.replace('-', "_")); + if let Ok(cc) = env::var(&cc_key) { + return cc.contains("zigcc"); + } + if let Ok(cc) = env::var("CC") { + return cc.contains("zigcc"); + } + false +} + +fn generate_bindings(target: &str) { + let extra_header_path = env::var("KCP_SYS_EXTRA_HEADER_PATH").unwrap_or_default(); + let extra_header_paths = extra_header_path + .split(':') + .filter(|s| !s.is_empty()) + .map(|p| format!("-I{p}")); + + let mut bindings = bindgen::Builder::default() + .header("wrapper.h") + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .allowlist_function("ikcp_.*") + .use_core(); + + bindings = bindings.clang_args(extra_header_paths); + + let host = env::var("HOST").unwrap(); + if target != host { + if is_zigbuild(target) { + bindings = bindings.clang_arg(format!("--target={}", zig_target(target))); + + if let Some(include_dirs) = zig_include_dirs(target) { + for include_dir in include_dirs { + bindings = bindings.clang_arg("-isystem").clang_arg(include_dir); + } + } + } else { + bindings = bindings.clang_arg(format!("--target={target}")); + } + } + + let bindings = bindings.generate().expect("Unable to generate bindings"); + + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("bindings.rs"); + bindings + .write_to_file(out_path) + .expect("Couldn't write bindings!"); +} + +fn find_llvm_ar() -> Option { + // ARM Mac (Apple Silicon) + let arm_path = PathBuf::from("/opt/homebrew/opt/llvm/bin/llvm-ar"); + if arm_path.exists() { + return Some(arm_path); + } + // Intel Mac + let intel_path = PathBuf::from("/usr/local/opt/llvm/bin/llvm-ar"); + if intel_path.exists() { + return Some(intel_path); + } + // Fallback: check PATH (MacPorts, Nix, manual installs, etc.) + Command::new("which") + .arg("llvm-ar") + .output() + .ok() + .filter(|o| o.status.success()) + .and_then(|o| { + let path = String::from_utf8(o.stdout).ok()?; + let path = path.trim(); + if path.is_empty() { + None + } else { + Some(PathBuf::from(path)) + } + }) +} fn main() { println!("cargo:rustc-link-lib=kcp"); let dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let fulldir = Path::new(&dir).join("kcp"); + let target = env::var("TARGET").unwrap(); let mut config = cc::Build::new(); + if target.contains("apple-darwin") { + if let Some(llvm_ar) = find_llvm_ar() { + config.archiver(llvm_ar); + } + } config.include(fulldir.clone()); config.file(fulldir.join("ikcp.c")); config.opt_level(3); @@ -17,21 +149,9 @@ fn main() { println!("cargo:rerun-if-changed=kcp/ikcp.h"); println!("cargo:rerun-if-changed=kcp/ikcp.c"); println!("cargo:rerun-if-changed=wrapper.h"); + println!("cargo:rerun-if-env-changed=KCP_SYS_EXTRA_HEADER_PATH"); + println!("cargo:rerun-if-env-changed=CC_{}", target.replace('-', "_")); + println!("cargo:rerun-if-env-changed=CC"); - let extra_header_path = std::env::var("KCP_SYS_EXTRA_HEADER_PATH").unwrap_or_default(); - let extra_header_paths = extra_header_path.split(":").filter(|s| !s.is_empty()).collect::>(); - - let bindings = bindgen::Builder::default() - .header("wrapper.h") - .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) - .clang_args(extra_header_paths.iter().map(|p| format!("-I{}", p))) - .allowlist_function("ikcp_.*") - .use_core() - .generate() - .expect("Unable to generate bindings"); - - let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); - bindings - .write_to_file(out_path.join("bindings.rs")) - .expect("Couldn't write bindings!"); + generate_bindings(&target); } diff --git a/wrapper.h b/wrapper.h index 7adbb0f..1e07a04 100644 --- a/wrapper.h +++ b/wrapper.h @@ -1 +1 @@ -#include "kcp/ikcp.h" \ No newline at end of file +#include "kcp/ikcp.h" From 6c6f68723dd24d0d1a597c653327087e37ee82f0 Mon Sep 17 00:00:00 2001 From: fanyang Date: Sun, 5 Apr 2026 22:25:21 +0800 Subject: [PATCH 4/4] ci: add full build matrix across all GitHub-hosted runners --- .github/workflows/rust.yml | 178 ++++++++++++++++++++++++++++++++++--- 1 file changed, 165 insertions(+), 13 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f662b77..5e3a263 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,26 +1,178 @@ -name: Rust +name: CI on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] + workflow_dispatch: env: CARGO_TERM_COLOR: always jobs: + # ────────────────────────────────────────────── + # Lint: rustfmt + clippy + # ────────────────────────────────────────────── + lint: + name: Lint + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - uses: Swatinem/rust-cache@v2 + + - name: Check formatting + run: cargo fmt --check + + - name: Clippy + run: cargo clippy -- -D warnings + + # ────────────────────────────────────────────── + # Native build + test matrix + # 6 runners × 2 profiles = 12 jobs + # ────────────────────────────────────────────── build: + name: ${{ matrix.name }} (${{ matrix.profile }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + # Linux x64 + - name: Linux x64 + runner: ubuntu-24.04 + target: x86_64-unknown-linux-gnu + profile: debug + - name: Linux x64 + runner: ubuntu-24.04 + target: x86_64-unknown-linux-gnu + profile: release + + # Linux arm64 + - name: Linux arm64 + runner: ubuntu-24.04-arm + target: aarch64-unknown-linux-gnu + profile: debug + - name: Linux arm64 + runner: ubuntu-24.04-arm + target: aarch64-unknown-linux-gnu + profile: release + + # Windows x64 + - name: Windows x64 + runner: windows-latest + target: x86_64-pc-windows-msvc + profile: debug + - name: Windows x64 + runner: windows-latest + target: x86_64-pc-windows-msvc + profile: release + + # Windows arm64 + - name: Windows arm64 + runner: windows-11-arm + target: aarch64-pc-windows-msvc + profile: debug + - name: Windows arm64 + runner: windows-11-arm + target: aarch64-pc-windows-msvc + profile: release + + # macOS x64 (Intel) + - name: macOS x64 + runner: macos-15-intel + target: x86_64-apple-darwin + profile: debug + - name: macOS x64 + runner: macos-15-intel + target: x86_64-apple-darwin + profile: release + + # macOS arm64 (Apple Silicon) + - name: macOS arm64 + runner: macos-15 + target: aarch64-apple-darwin + profile: debug + - name: macOS arm64 + runner: macos-15 + target: aarch64-apple-darwin + profile: release + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.target }}-${{ matrix.profile }} + + # Linux: install libclang for bindgen + - name: Install libclang (Linux) + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y libclang-dev + + - name: Build + run: cargo build --target ${{ matrix.target }} --verbose ${{ matrix.profile == 'release' && '--release' || '' }} + + - name: Test + run: cargo test --target ${{ matrix.target }} --verbose ${{ matrix.profile == 'release' && '--release' || '' }} - runs-on: ubuntu-latest + - name: Build KCP native test + run: | + cmake -S kcp -B kcp/build -DBUILD_TESTING=ON + cmake --build kcp/build --target kcp_test + + # ────────────────────────────────────────────── + # Cross-compilation via zigbuild (musl targets) + # 2 targets × 2 profiles = 4 jobs + # ────────────────────────────────────────────── + cross: + name: Cross ${{ matrix.target }} (${{ matrix.profile }}) + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - target: aarch64-unknown-linux-musl + profile: debug + - target: aarch64-unknown-linux-musl + profile: release + - target: x86_64-unknown-linux-musl + profile: debug + - target: x86_64-unknown-linux-musl + profile: release steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - uses: mlugg/setup-zig@v2 + + - uses: Swatinem/rust-cache@v2 + with: + key: cross-${{ matrix.target }}-${{ matrix.profile }} + + - name: Install libclang and cargo-zigbuild + run: | + sudo apt-get update && sudo apt-get install -y libclang-dev + cargo install cargo-zigbuild + + - name: Build (zigbuild) + run: cargo zigbuild --target ${{ matrix.target }} --verbose ${{ matrix.profile == 'release' && '--release' || '' }}