Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
884ef1e
feat(go): add Go bindings phase 2 — services, actions, graph introspe…
YuanYuYuan Mar 13, 2026
27dc375
docs(go): minimise crate READMEs to book-pointer pattern
YuanYuYuan Mar 13, 2026
c19e4d9
fix(go): harden CGO safety invariants in service and action servers
YuanYuYuan Mar 13, 2026
ca6a6fe
fix(go): correct module path and remove dead binary suppressions
YuanYuYuan Mar 13, 2026
0522088
refactor(go): rewrite action_server example to use BuildTypedActionSe…
YuanYuYuan Mar 13, 2026
6d23158
fix(go): remove redundant unsafe blocks in ffi/action.rs
YuanYuYuan Mar 18, 2026
2e1c8b2
fix(go): fix unsafe-op-in-unsafe-fn and dead_code in ffi/action.rs
YuanYuYuan Mar 18, 2026
d27ca68
fix(go): add codegen step to interop CI and fix codegen-bundled recipe
YuanYuYuan Mar 18, 2026
dc978d9
fix(go): use GITHUB_WORKSPACE instead of git rev-parse in interop CI
YuanYuYuan Mar 18, 2026
8840b82
fix(go): build staticlib explicitly with --lib in interop CI
YuanYuYuan Mar 18, 2026
e81abf5
fix(go): build zenoh_router example for Go interop tests in CI
YuanYuYuan Mar 18, 2026
b6102bd
fix(go): include action_msgs and unique_identifier_msgs in Go codegen…
YuanYuYuan Mar 18, 2026
a77d0d9
test(go): increase CI timing margins for action interop tests
YuanYuYuan Mar 18, 2026
3b1bb34
fix(go): add --endpoint flag to z_srvcli and deterministic service re…
YuanYuYuan Mar 18, 2026
bfd01b9
fix(go): replace fixed sleeps with deterministic readiness checks in …
YuanYuYuan Mar 18, 2026
c85eadc
fix(ci): build FFI library with distro feature flag for Go interop tests
YuanYuYuan Mar 18, 2026
5a69c75
fix(go): apply go-api-audit fixes to rosz bindings
YuanYuYuan Mar 19, 2026
1838a15
fix(go): fix CGO unsafe.Pointer and atomic.Pointer vet errors
YuanYuYuan Mar 19, 2026
bf77691
ci(go): add go vet to test script and wire into CI
YuanYuYuan Mar 19, 2026
98af4cc
fix(ci): install nushell from binary release in ROS container job
YuanYuYuan Mar 19, 2026
e4a63c6
fix(go): update interop tests to use CallTyped and fix yamllint line …
YuanYuYuan Mar 19, 2026
d691ae2
fix(test): fix two flaky CI failures on macOS and in ROS container
YuanYuYuan Mar 23, 2026
ece95ea
chore(ci): exclude ffi layer from Codecov patch coverage
YuanYuYuan Mar 23, 2026
5e4ff31
fix(ci): fix Codecov patch coverage for Go FFI bindings
YuanYuYuan Mar 23, 2026
a0238c3
fix(ci): remove --features ffi from coverage run to fix duplicate sym…
YuanYuYuan Mar 23, 2026
92e7245
docs(go): add networking link to zenohd prerequisite
YuanYuYuan Mar 24, 2026
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
18 changes: 13 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ jobs:
source /tmp/nix-dev-env.sh
cargo llvm-cov \
-p ros-z -p ros-z-codegen -p ros-z-cdr -p ros-z-protocol -p ros-z-schema \
-p ros-z-tests \
-j4 \
--lcov --output-path lcov.info
shell: bash
Expand Down Expand Up @@ -380,12 +381,19 @@ jobs:
with:
shared-key: ${{ runner.os }}-go-ffi

- name: Install just
uses: extractions/setup-just@v2
- name: Install Nushell
uses: hustcer/setup-nu@v3
with:
version: '0.102'

- name: Go vet (static analysis)
run: nu scripts/test-go.nu --vet-only

- name: Go pure tests (codegen + testdata)
run: nu scripts/test-go.nu --codegen-only

- name: Run Go tests
working-directory: crates/ros-z-go
run: just test-go
- name: Go FFI tests (rosz unit tests)
run: nu scripts/test-go.nu --ffi-only

python-tests:
name: Python Tests (ros-z-py) (${{ matrix.distro }})
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ jobs:
- name: Install system dependencies
run: apt-get update && apt-get install -y nodejs

- name: Install Nushell
run: |
NU_VERSION=0.102.0
ARCH=$(uname -m | sed 's/x86_64/x86_64/;s/aarch64/aarch64/')
NU_TARBALL="nu-${NU_VERSION}-${ARCH}-unknown-linux-gnu"
curl -fsSL "https://github.com/nushell/nushell/releases/download/${NU_VERSION}/${NU_TARBALL}.tar.gz" \
| tar -xz -C /usr/local/bin --strip-components=1 "${NU_TARBALL}/nu"

- name: Install ROS dependencies
run: |
apt-get install -y \
Expand Down Expand Up @@ -118,3 +126,36 @@ jobs:
cargo nextest run -p ros-z-console \
--features ros-interop --release
fi

- uses: actions/setup-go@v5
with:
go-version: '1.23'
cache: false

- name: Build Rust FFI library + example binaries
run: |
FEATURES="ffi,${{ matrix.ros_z_feature }},rmw-zenoh"
cargo build --release --no-default-features --features "$FEATURES" \
--lib --example z_pubsub --example z_srvcli --example zenoh_router

- name: Generate Go message types (bundled IDL)
run: |
mkdir -p _tmp crates/ros-z-go/generated
cargo run -p ros-z-codegen --bin export_json -- \
--assets crates/ros-z-codegen/assets/jazzy \
--output _tmp/go-manifest.json \
--packages builtin_interfaces,service_msgs,action_msgs,unique_identifier_msgs,std_msgs,example_interfaces
cd crates/ros-z-codegen-go && go run . \
-input ../../_tmp/go-manifest.json \
-output ../ros-z-go/generated \
-prefix github.com/ZettaScaleLabs/ros-z/crates/ros-z-go

- name: Go vet (static analysis)
run: nu scripts/test-go.nu --vet-only

- name: Run Go integration tests (Go↔Go + Go↔Rust + Go↔ROS2)
shell: bash
run: |
source /opt/ros/${{ matrix.distro }}/setup.bash
export RMW_IMPLEMENTATION=rmw_zenoh_cpp
nu scripts/test-go.nu --integration
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,31 @@ ros-z-msgs/python/uv.lock
# Generated Go message packages (codegen test output)
/builtin_interfaces/
/std_msgs/
/action_msgs/
/example_interfaces/
/service_msgs/
/unique_identifier_msgs/
/crates/ros-z-codegen-go/builtin_interfaces/
/crates/ros-z-codegen-go/std_msgs/
/crates/ros-z-codegen-go/action_msgs/
/crates/ros-z-codegen-go/example_interfaces/
/crates/ros-z-codegen-go/service_msgs/
/crates/ros-z-codegen-go/unique_identifier_msgs/
/crates/ros-z-go/generated/

# Built Go example binaries
/crates/ros-z-go/action_client
/crates/ros-z-go/action_client_errors
/crates/ros-z-go/action_server
/crates/ros-z-go/publisher
/crates/ros-z-go/subscriber
/crates/ros-z-go/subscriber_channel
/crates/ros-z-go/service_client
/crates/ros-z-go/service_client_errors
/crates/ros-z-go/service_server
/crates/ros-z-go/examples/production_service/client
/crates/ros-z-go/examples/production_service/server

# Core dumps
core

Expand Down
1 change: 1 addition & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
- [Code Generation Internals](./chapters/python_codegen.md)
- [Go Bindings](./chapters/go_bindings.md)
- [Quick Start](./chapters/go_quick_start.md)
- [Interop Tests](./chapters/go_interop_tests.md)

# Experimental

Expand Down
152 changes: 143 additions & 9 deletions book/src/chapters/go_bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ The `#cgo LDFLAGS` in `rosz/context.go` resolves the library via `${SRCDIR}` —
**Generate message types** (no ROS 2 install needed for bundled types):

```bash
just -f crates/ros-z-go/justfile codegen-bundled # std_msgs, geometry_msgs
just -f crates/ros-z-go/justfile codegen-bundled # std_msgs, geometry_msgs, example_interfaces
just -f crates/ros-z-go/justfile codegen # full set from a ROS 2 installation
```

Expand Down Expand Up @@ -118,13 +118,87 @@ Apply QoS with `.WithQoS(rosz.QosSensorData())` on either builder.

## Services

> **Coming in v0.2.** Service client and server support will be added in the next release.
```go
// Server
server, err := node.CreateServiceServer("add_two_ints").
Build(&example_interfaces.AddTwoInts{}, func(reqBytes []byte) ([]byte, error) {
var req example_interfaces.AddTwoIntsRequest
req.DeserializeCDR(reqBytes)
return (&example_interfaces.AddTwoIntsResponse{Sum: req.A + req.B}).SerializeCDR()
})

// Client
client, err := node.CreateServiceClient("add_two_ints").
Build(&example_interfaces.AddTwoInts{})
respBytes, err := client.Call(&example_interfaces.AddTwoIntsRequest{A: 5, B: 3})
// client.CallWithTimeout(req, 10*time.Second) for a custom timeout
var resp example_interfaces.AddTwoIntsResponse
if err := resp.DeserializeCDR(respBytes); err != nil {
// handle error
}
// use resp.Sum
```

---

## Actions

> **Coming in v0.2.** Action client and server support, including cooperative cancellation and feedback streaming, will be added in the next release.
```go
// Server
server, err := node.CreateActionServer("fibonacci").Build(
&example_interfaces.Fibonacci{},
func(goalBytes []byte) bool { // accept / reject
var g example_interfaces.FibonacciGoal
g.DeserializeCDR(goalBytes)
return g.Order > 0
},
func(h *rosz.ServerGoalHandle, goalBytes []byte) ([]byte, error) {
var g example_interfaces.FibonacciGoal
g.DeserializeCDR(goalBytes)
seq := []int32{0, 1}
for i := 2; i < int(g.Order); i++ {
seq = append(seq, seq[i-1]+seq[i-2])
h.PublishFeedback(&example_interfaces.FibonacciFeedback{Sequence: seq})
}
return (&example_interfaces.FibonacciResult{Sequence: seq}).SerializeCDR()
},
)

// Client
client, err := node.CreateActionClient("fibonacci").
Build(&example_interfaces.Fibonacci{})
handle, err := client.SendGoal(&example_interfaces.FibonacciGoal{Order: 10})
resultBytes, err := handle.GetResult()
```

### Goal status

```go
handle.GetStatus() // GoalStatusAccepted → GoalStatusSucceeded
handle.IsActive() // true while executing
handle.IsTerminal() // true when finished
handle.Cancel() // request cancellation
```

### Cooperative cancellation

The execute callback runs in its own goroutine. Poll `IsCancelRequested()` at safe points:

```go
func(h *rosz.ServerGoalHandle, goalBytes []byte) ([]byte, error) {
// ...
for i := 2; i < int(g.Order); i++ {
if h.IsCancelRequested() {
// return partial result and acknowledge cancel
h.Canceled(&example_interfaces.FibonacciResult{Sequence: seq})
return (&example_interfaces.FibonacciResult{Sequence: seq}).SerializeCDR()
}
// ... compute step
time.Sleep(50 * time.Millisecond)
}
// ...
}
```

---

Expand Down Expand Up @@ -152,11 +226,47 @@ Use `rosz.NewRingChannel` (drops oldest on full) or `rosz.NewFifoChannel` (block

### Service

> **Coming in v0.2.**
```go
// Typed server
server, err := rosz.BuildTypedServiceServer(
node.CreateServiceServer("add_two_ints"),
&example_interfaces.AddTwoInts{},
func(req *example_interfaces.AddTwoIntsRequest) (*example_interfaces.AddTwoIntsResponse, error) {
return &example_interfaces.AddTwoIntsResponse{Sum: req.A + req.B}, nil
},
)

// Typed call
resp := &example_interfaces.AddTwoIntsResponse{}
err := rosz.CallTyped(client, &example_interfaces.AddTwoIntsRequest{A: 5, B: 3}, resp)
// rosz.CallTypedWithTimeout(client, req, resp, 10*time.Second)
```

### Action

> **Coming in v0.2.**
```go
// Typed server
server, err := rosz.BuildTypedActionServer(
node.CreateActionServer("fibonacci"), &example_interfaces.Fibonacci{},
func(g *example_interfaces.FibonacciGoal) bool { return g.Order > 0 },
func(h *rosz.ServerGoalHandle, g *example_interfaces.FibonacciGoal) (*example_interfaces.FibonacciResult, error) {
seq := []int32{0, 1}
for i := 2; i < int(g.Order); i++ {
if h.IsCancelRequested() {
return &example_interfaces.FibonacciResult{Sequence: seq}, nil
}
seq = append(seq, seq[i-1]+seq[i-2])
}
return &example_interfaces.FibonacciResult{Sequence: seq}, nil
},
)

// Typed client
client, err := rosz.BuildTypedActionClient(node.CreateActionClient("fibonacci"), &example_interfaces.Fibonacci{})
handle, err := rosz.SendTypedGoal(client, &example_interfaces.FibonacciGoal{Order: 10})
result := &example_interfaces.FibonacciResult{}
err = rosz.GetTypedResult(handle, result)
```

---

Expand All @@ -165,6 +275,7 @@ Use `rosz.NewRingChannel` (drops oldest on full) or `rosz.NewFifoChannel` (block
```go
rosz.QosDefault() // Reliable, Volatile, KeepLast(10)
rosz.QosSensorData() // BestEffort, Volatile, KeepLast(5) — high-rate streams
rosz.QosServicesDefault() // Reliable, Volatile, KeepLast(10)
rosz.QosTransientLocal() // Reliable, TransientLocal, KeepLast(1) — /tf_static, /robot_description
rosz.QosKeepAll() // Reliable, Volatile, KeepAll
```
Expand All @@ -180,9 +291,10 @@ Match QoS profiles on both sides. BestEffort publisher + Reliable subscriber wil
## Graph Introspection

```go
topics, _ := node.GetTopicNamesAndTypes() // []TopicInfo
names, _ := node.GetNodeNames() // []NodeInfo
exists, _ := node.NodeExists("talker", "/")
topics, _ := node.GetTopicNamesAndTypes() // map[string][]string
names, _ := node.GetNodeNames() // []string
services, _ := node.GetServiceNamesAndTypes()
exists, _ := node.NodeExists("talker")
```

Requires a Zenoh router and a brief settling time after nodes come online.
Expand All @@ -196,17 +308,27 @@ All `Build()` calls and operations return `error`. Use `errors.Is()` with sentin
| Sentinel | When raised |
|----------|-------------|
| `rosz.ErrBuildFailed` | `Build()` failed — FFI returned nil |
| `rosz.ErrTimeout` | Service call timed out |
| `rosz.ErrGoalRejected` | Action server rejected the goal |
| `rosz.ErrResultFailed` | Could not retrieve action result |
| `rosz.ErrCancelFailed` | Cancellation request failed |

```go
if errors.Is(err, rosz.ErrBuildFailed) { /* construction failed */ }
if errors.Is(err, rosz.ErrTimeout) { /* no server responded */ }
```

Inspect the error code directly when you need fine-grained handling:

```go
var e rosz.RoszError
if errors.As(err, &e) {
log.Fatalf("FFI error %d: %s", e.Code(), e.Message())
switch e.Code() {
case rosz.ErrorCodeServiceTimeout:
// retry
default:
log.Fatalf("FFI error %d: %s", e.Code(), e.Message())
}
}
```

Expand Down Expand Up @@ -238,14 +360,26 @@ Logs go to stderr via Go's `log/slog` structured text format. For Zenoh-level tr
| `publisher/` | Basic publish loop |
| `subscriber/` | Typed callback subscriber |
| `subscriber_channel/` | Channel-based, `range`-friendly |
| `service_server/` | AddTwoInts server |
| `service_client/` | Synchronous call |
| `service_client_errors/` | Timeouts, retries, sentinel errors |
| `action_server/` | Fibonacci server with feedback |
| `action_client/` | Send goal · monitor feedback · get result |
| `action_client_errors/` | Rejected goals, cancellation |
| `production_service/` | Graceful shutdown, circuit breaker, metrics |

```bash
just -f crates/ros-z-go/justfile run-example <name>
```

See `crates/ros-z-go/examples/production_service/README.md` for the production walkthrough.

---

## Further Reading

- **[Pub/Sub](./pubsub.md)** — QoS theory, ROS 2 interop requirements
- **[Services](./services.md)** — Service concepts
- **[Actions](./actions.md)** — Action lifecycle
- **[Message Generation](./message_generation.md)** — Generating types from IDL
- **[Interop Tests](./go_interop_tests.md)** — Test suite details
Loading
Loading