Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .github/workflows/rust-bindings.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Rust bindings

on:
push:
tags:
- 'bindings/v*'
pull_request:
paths:
- 'rust/**'
- 'src/**'
- 'include/**'
- '.github/workflows/rust-bindings.yml'

jobs:
test:
name: Test
runs-on: ubuntu-latest
defaults:
run:
working-directory: rust
steps:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt

- name: Format
run: cargo fmt --check

- name: Clippy
run: cargo clippy --all-targets -- -D warnings

- name: Test
run: cargo test

publish:
name: Publish to crates.int.luxonis.com
if: startsWith(github.ref, 'refs/tags/bindings/v')
needs: test
runs-on: ubuntu-latest
defaults:
run:
working-directory: rust
env:
# Override the credential providers from rust/.cargo/config.toml:
# cargo:libsecret needs libsecret-1.so.0 which CI runners don't have.
CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS: cargo:token
CARGO_REGISTRIES_LUXONIS_TOKEN: ${{ secrets.CARGO_REGISTRIES_LUXONIS_TOKEN }}
steps:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Check that the crate version matches the tag
run: |
version="${GITHUB_REF_NAME#bindings/v}"
grep -q "^version = \"${version}\"$" Cargo.toml || {
echo "Tag ${GITHUB_REF_NAME} does not match workspace version:"
grep "^version = " Cargo.toml
exit 1
}

- name: Vendor XLink C/C++ sources into xlink-sys
# A published .crate archive can only contain files inside the crate
# directory; xlink-sys/build.rs prefers this vendored copy over ../../
run: |
mkdir -p xlink-sys/xlink-src
cp -r ../src ../include xlink-sys/xlink-src/

- name: Publish xlink-sys
run: cargo publish -p xlink-sys --registry luxonis --allow-dirty

- name: Publish xlink
# Retry a few times, the just-published xlink-sys may take a moment
# to appear in the registry index.
run: |
for i in $(seq 1 5); do
cargo publish -p xlink --registry luxonis --allow-dirty && exit 0
echo "Publish attempt ${i} failed, retrying in 15s..."
sleep 15
done
exit 1
5 changes: 5 additions & 0 deletions rust/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[registry]
global-credential-providers = ["cargo:token", "cargo:libsecret", "cargo:macos-keychain", "cargo:wincred"]

[registries.luxonis]
index = "sparse+https://crates.int.luxonis.com/api/v1/crates/"
4 changes: 4 additions & 0 deletions rust/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/target
Cargo.lock
# vendored by CI when publishing, see .github/workflows/rust-bindings.yml
xlink-sys/xlink-src/
13 changes: 13 additions & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[workspace]
resolver = "2"
members = ["xlink-sys", "xlink"]

[workspace.package]
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
repository = "https://github.com/luxonis/XLink"
# Only publish to the internal Luxonis registry (never crates.io).
# Publishing is done by CI on `bindings/vX.Y.Z` tags, see
# .github/workflows/rust-bindings.yml
publish = ["luxonis"]
33 changes: 33 additions & 0 deletions rust/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# XLink Rust bindings

Rust bindings to XLink, split into two crates:

- `xlink-sys` — raw FFI bindings. Compiles the XLink C/C++ sources from this
repository directly via the [`cc`](https://crates.io/crates/cc) crate (no
CMake or Hunter required). By default the USB (libusb) protocol is disabled,
so only TCP/IP discovery is available and there are no external native
dependencies. Enable the `libusb` feature to compile the USB protocol
(requires libusb-1.0, discovered via pkg-config).
- `xlink` — safe, idiomatic wrapper. Currently covers library initialization
and device discovery (`find_devices`), which is enough to enumerate RVC2
devices over the network (and over USB with the `libusb` feature).

## Usage

```toml
[dependencies]
xlink = { git = "https://github.com/luxonis/XLink" }
```

```rust
let devices = xlink::find_tcpip_devices()?;
for device in devices {
println!("{} {} ({})", device.mxid, device.name, device.state);
}
```

## Example

```sh
cargo run --example list_devices
```
23 changes: 23 additions & 0 deletions rust/xlink-sys/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "xlink-sys"
description = "Raw FFI bindings to the XLink library, built from source"
links = "XLink"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
# Explicit include list: `xlink-src/` (the C/C++ sources vendored by CI before
# publishing) is gitignored, and cargo would otherwise exclude it from the
# published .crate archive.
include = ["/src", "/build.rs", "/xlink-src"]

[features]
default = []
# Enable the USB (VSC) protocol. Requires libusb-1.0 to be available on the
# system (discovered via pkg-config on unix).
libusb = []

[build-dependencies]
cc = { version = "1.2", features = ["parallel"] }
pkg-config = "0.3"
193 changes: 193 additions & 0 deletions rust/xlink-sys/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
use std::env;
use std::path::{Path, PathBuf};

/// Collects sources with the given extension from a directory (non recursive),
/// mirroring the file(GLOB ...) logic from cmake/XLink.cmake.
fn glob_sources(dir: &Path, extension: &str, exclude: &[&str]) -> Vec<PathBuf> {
let mut sources: Vec<PathBuf> = std::fs::read_dir(dir)
.unwrap_or_else(|err| panic!("failed to read {}: {err}", dir.display()))
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|path| path.extension().is_some_and(|ext| ext == extension))
.filter(|path| {
let name = path.file_name().unwrap().to_string_lossy();
!exclude.contains(&name.as_ref())
})
.collect();
sources.sort();
sources
}

fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
// When building from a published crate, the C/C++ sources are vendored
// into `xlink-src/` (see .github/workflows/rust-bindings.yml). When
// building from the repository, they live at the repository root.
let vendored = manifest_dir.join("xlink-src");
let root = if vendored.join("src").is_dir() {
vendored
} else {
// <XLink root>/rust/xlink-sys -> <XLink root>
manifest_dir
.ancestors()
.nth(2)
.expect("xlink-sys must live in <XLink root>/rust/xlink-sys")
.to_path_buf()
};

let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();
let libusb_enabled = env::var("CARGO_FEATURE_LIBUSB").is_ok();

let src_pc = root.join("src/pc");
let src_protocols = root.join("src/pc/protocols");
let src_shared = root.join("src/shared");

println!("cargo:rerun-if-changed={}", root.join("src").display());
println!("cargo:rerun-if-changed={}", root.join("include").display());

let mut usb_excludes: Vec<&str> = Vec::new();
if !libusb_enabled {
// Same removal as cmake/XLink.cmake when XLINK_ENABLE_LIBUSB=OFF
usb_excludes.push("usb_host.cpp");
usb_excludes.push("win_usb_host.cpp");
}

let mut c_sources = Vec::new();
let mut cpp_sources = Vec::new();

c_sources.extend(glob_sources(&src_pc, "c", &usb_excludes));
cpp_sources.extend(glob_sources(&src_pc, "cpp", &usb_excludes));
c_sources.extend(glob_sources(&src_protocols, "c", &usb_excludes));
cpp_sources.extend(glob_sources(&src_protocols, "cpp", &usb_excludes));
// file(GLOB_RECURSE ...) on src/shared; the directory is currently flat
c_sources.extend(glob_sources(&src_shared, "c", &[]));
cpp_sources.extend(glob_sources(&src_shared, "cpp", &[]));

let mut platform_include: Option<PathBuf> = None;
match target_os.as_str() {
"windows" => {
platform_include = Some(root.join("src/pc/Win/include"));
c_sources.extend(glob_sources(
&root.join("src/pc/Win/src"),
"c",
&usb_excludes,
));
cpp_sources.extend(glob_sources(
&root.join("src/pc/Win/src"),
"cpp",
&usb_excludes,
));
}
"macos" | "ios" => {
platform_include = Some(root.join("src/pc/MacOS"));
c_sources.push(root.join("src/pc/MacOS/pthread_semaphore.c"));
}
_ => {}
}

// Stub for the header normally produced by CMake's generate_export_header.
// We always build a static library, so the macros expand to nothing.
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let export_header_dir = out_dir.join("generated-include");
std::fs::create_dir_all(&export_header_dir).unwrap();
std::fs::write(
export_header_dir.join("XLinkExport.h"),
"#ifndef XLINK_EXPORT_H\n\
#define XLINK_EXPORT_H\n\
#define XLINK_EXPORT\n\
#define XLINK_NO_EXPORT\n\
#define XLINK_DEPRECATED\n\
#endif\n",
)
.unwrap();

let configure_common = |build: &mut cc::Build| {
build
.include(root.join("include"))
.include(root.join("include/XLink"))
.include(&export_header_dir)
.include(&src_protocols)
.define("HAVE_STRUCT_TIMESPEC", None)
.define("_CRT_SECURE_NO_WARNINGS", None)
.define("USE_USB_VSC", None)
.define("USE_TCP_IP", None)
.warnings(false);

if let Some(include) = &platform_include {
build.include(include);
}

if libusb_enabled {
build.define("XLINK_ENABLE_LIBUSB", None);
}

match target_os.as_str() {
"windows" => {
build.define("WIN32_LEAN_AND_MEAN", None);
}
"linux" | "android" => {
build.define("_GNU_SOURCE", None);
// pthread_getname_np is available on glibc >= 2.12; musl gained
// it in 1.2.3 but keep the conservative glibc-only default.
if target_env == "gnu" {
build.define("HAVE_PTHREAD_GETNAME_NP", None);
}
}
"macos" | "ios" => {
build.define("HAVE_PTHREAD_GETNAME_NP", None);
}
_ => {}
}
};

let mut usb_include: Option<PathBuf> = None;
if libusb_enabled {
#[allow(unused_mut)]
let mut probed = pkg_config::Config::new()
.atleast_version("1.0")
.probe("libusb-1.0");
match probed {
Ok(library) => {
usb_include = library.include_paths.first().cloned();
}
Err(err) => {
panic!("the `libusb` feature requires libusb-1.0 (pkg-config lookup failed: {err})")
}
}
}

// C sources (C99, gnu extensions like the CMake build)
let mut c_build = cc::Build::new();
configure_common(&mut c_build);
if let Some(include) = &usb_include {
c_build.include(include);
}
c_build.std("gnu99").files(&c_sources).compile("xlink_c");

// C++ sources (C++11)
let mut cpp_build = cc::Build::new();
configure_common(&mut cpp_build);
if let Some(include) = &usb_include {
cpp_build.include(include);
}
cpp_build
.cpp(true)
.std("c++11")
.files(&cpp_sources)
.compile("xlink_cpp");

match target_os.as_str() {
"windows" => {
println!("cargo:rustc-link-lib=ws2_32");
println!("cargo:rustc-link-lib=iphlpapi");
}
"macos" | "ios" => {}
"android" => {
println!("cargo:rustc-link-lib=log");
}
_ => {
println!("cargo:rustc-link-lib=pthread");
}
}
}
Loading
Loading