After months of work, I've just opened a PR to Rust's package manager Cargo! 🦀🎉
A TUI for cargo-tree soon!
Explore your Rust dependency graph interactively 🌳
➡️ Try it: cargo install cargo-tree-tui
Repository: orhun/cargo-tree-tui
PR: cargo#17673
#rustlang #ratatui #terminal
30 likes · 6 reposts
After months of work, I've just opened a PR to Rust's package manager Cargo! 🦀🎉
A TUI for cargo-tree soon!
Explore your Rust dependency graph interactively! 🌳
➡️ Try it: cargo install cargo-tree-tui
Repository: orhun/cargo-tree-tui
PR: cargo#17673
#rustlang #ratatui #terminal #cargo
57 likes · 6 reposts · 1,486 views
Open Source Didn't Ban AI Code. 83% of Its AI Policies Allow It, With One Catch
- Book: Prompt Engineering Pocket Guide
- Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go
- My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub
---
July was the month open source "banned AI." On July 22, Codeberg members voted 358 to 144 to bar projects built mostly from AI-generated code (It's FOSS). About a week later, the GCC Steering Committee said it won't take a legally significant contribution if any part of it traces back to an LLM (It's FOSS).
Read the GCC rule to the end, though. Contributions under the legal-significance threshold (roughly 15 lines) can still come in if they are clearly marked, test cases are exempt, and AI-assisted work carries an Assisted-by: tag in the commit message. Even the "ban" is mostly a set of conditions.
Then look past the two headlines at the whole field. In September, Andre Hora, Romain Robbes and Stefano Zacchiroli published ["We Permit the Use of AI, but [...]"](arxiv.org), a study of 281 AI policies from the 2,000 most-starred GitHub repositories plus 36 well-known projects and organizations such as the Linux kernel, LLVM and the Apache Software Foundation. The title is the finding.
The numbers say "yes, but"
From the paper's results:
- 83.3% of policies permit or encourage AI in code contributions (76.2% permit, 7.1% encourage).
- 14.9% forbid AI in code. Only 5.3% forbid it for every kind of contribution.
- 67.3% require a high level of human involvement.
- 43.4% make the contributor accountable for the result. None of the 281 say accountability is optional.
- 48.8% require you to disclose AI use. Of the disclosure rules that say where, 64.7% want it in the PR description and 26.7% in the commit message.
So a ban is the minority position. What the majority asks for is a human who was there: someone who directed the work, read it, and will answer for it. The paper quotes the policy of psf/requests on this: "you must remove any LLM co-author tags and take full ownership of every line."
One thing the study leaves open, and the authors list it as future work: whether anyone actually follows these policies.
What maintainers actually do with slop
The paper counts 188 countermeasures against low-quality AI contributions across the policies. The breakdown:
| Countermeasure | Share |
|---|---|
| Close the PR | 48.4% |
| Ban or block the user | 20.8% |
| Disallow autonomous agents | 13.8% |
| Restrict new or external users | 4.8% |
| Everything else (six measures) | 12.2% |
Nearly half of all measures are the same move: close it. The rqlite policy says slop PRs "may be closed without comment," per the paper. rust-analyzer goes further and says it does not allow autonomous agents to open pull requests or issues at all.
Some projects now address the agent directly. The paper finds policies in AGENTS.md and CLAUDE.md as well as CONTRIBUTING.md and AI_POLICY.md. The contributing guide of llama.cpp tells fully autonomous agents not to contribute and to exclude the repo from their configuration.
My take: they banned the unread diff, not the model
Put the two findings side by side. Most projects accept AI code. The most common response to bad AI code is closing the PR. The deciding factor between those two outcomes is whether a human read the change before a maintainer had to.
A closed PR is a maintainer telling you the review didn't happen on your side, so it isn't going to happen on theirs. Reviewing a large agent diff costs real time, and maintainers are often volunteers. The policies push that cost back to the sender, which is fair. The diff can be cheap to generate. Someone still has to understand it, and the 67.3% requiring high human involvement is the field agreeing on who that someone is.
That moves the review upstream. If the reading has to happen, it should happen on your machine, before the PR exists. Once you open it, every unread line is a maintainer's problem.
Three steps before you open the PR
1. Read the project's policy first. It might live in a file you don't expect:
ls AI_POLICY.md LLM_POLICY.md AGENTS.md CLAUDE.md \
CONTRIBUTING.md 2>/dev/null
grep -inwE "ai|llm|agent|copilot" CONTRIBUTING.md \
2>/dev/null
Then follow its format. There isn't one standard. The paper notes RustPython asks for Assisted-by and not Co-authored-by, Nextcloud requires an Assisted-by trailer, and psf/requests wants LLM co-author tags gone entirely.
2. Read the change step by step. One big diff hides the step where the agent rewrote a file you didn't ask about. Start with the summary, then walk the branch commit by commit, oldest first:
git diff main...HEAD --stat
git log -p --reverse main..HEAD
The --stat line shows the file you didn't ask about. The log shows each step in order. If you can't explain a hunk, ask the agent to undo it or redo it smaller. "Full ownership of every line" starts here.
3. Disclose where the project asks. Git has a flag for trailers (git docs):
git commit -m "Fix off-by-one in pagination" \
--trailer "Assisted-by: Claude Code"
Then say it in the PR description too, since that's where most disclosure rules point: which agent, what it did, what you checked by hand.
Doing the reading in Hermes IDE
I build Hermes IDE, so weigh this section with that in mind. Everything above works with plain git. Hermes is built around that order of operations: the agent works and you read before you land.
Every agent turn is saved, so you can diff or restore any single turn, with undo. The Review Desk (⌘G, or Ctrl+Shift+G on Windows and Linux) shows the changes by turn or by file, with risk flags, and a line comment goes back to the agent that wrote that line. That's the "read it turn by turn" step without scrolling one long diff (README).
!The Hermes Review Desk in By turn view, listing two turns of a Claude Code task with turn 1's diff of src/db.ts and a drafted commit message
The Review Desk (⌘G) in By turn view: two turns of a Claude Code task, turn 1's diff of src/db.ts, the changed files, and a commit message drafted from the turns.
Only after that does Land come in. It commits, opens a pull request or merges locally, into the branch the task started from, and it can be undone. Check that the commit message carries the trailer your project asks for before you land. Each task also runs on its own hermes/ branch and worktree, so the PR holds one task's work and nothing from the agent next door.
You can download Hermes. It is source-available under BSL 1.1 and runs on macOS, Windows and Linux. It runs Claude Code, Codex and other agents in their own terminals, with your own sign-ins. It isn't affiliated with Nous Research or its Hermes Agent. It can't make an agent's change good, but it gives you a place to check the change first.
Where do you draw the line?
If you maintain a project, does your policy say what "human involvement" means in practice? If you contribute, do you disclose the agent by default, or only when a project asks? Tell me in the comments.
---
If this was useful
Projects now write their AI rules into AGENTS.md and CLAUDE.md, which means a contribution policy is also a prompt. The Prompt Engineering Pocket Guide covers how to write instructions an agent actually follows, whether you're the maintainer writing them or the contributor whose agent reads them. If you'd rather review agent work turn by turn, Hermes IDE is the tool I build for exactly that.

[quote="mtimmerm, post:12, topic:143414"]
But it's convenient.
[/quote]
First off, that's a good "why" - considering downstream users experience. Probably a footgun in this case though.
Even as a polyglot, when my brain is in "rust mode" it's a really expensive operation to notice "ah - that's a [insert other language here] idiom, switch modes for a moment ..." That's why working with ffis is a "different mode" for me and ringfenced-in-code to a rust-API-wrapper.
So overall an idiomatic rust API will likely make your users lives easier overall, even if it misses some of the benefits from somewhere else.
----
As for idiomatic approaches, I can think of 3:
Fixed state type
You've already ruled this out, just including for completeness.
Probably the simplest design but leaves no flexibility for alternative state implementations
pub struct ElementState {
...
}
pub trait Stateful {
fn get_state(&self) -> ElementState;
}
-> impl trait / associated type
Think IntoIterator & Iterator
Two very similar options, for cases where most user will want the flexibility of defining their state type, probably for functional reasons.
(There's also a middle ground between associated type and the third option (below) where you provide a default type, but that is currently unstable via #![feature(associated_type_defaults)])
pub trait Stateful {
fn get_state(&self) -> impl UiElementState;
}
// or
pub trait Stateful {
type: State: UiElementState;
fn get_state(&self) -> Self::State;
}
pub trait UiElementState {
// implementors need to provide basic getters & non-verifying setters
fn size(&self) -> u32;
fn max_size(&self) -> u32;
// borderline case: do you make these unsafe due to lack of verification?
fn set_size_unchecked(&mut self, new_size: u32) -> u32;
// default verifications, calculations etc are provided but can
// be overridden
fn resize(&mut self, new_size: u32) -> UiResult<u32> {
let max_size = self.max_size();
match self.size() {
new_size => Ok(new_size), // no-op
0..=max_size => {
let resized = self.set_size_unchecked(new_size);
Ok(resized)
}
_ => Err(InvalidSize),
}
}
Default generic
Think Vec<T, A = Global> where A: Allocator
The rare one, for cases where most will not want to override but some may for technical reasons.
pub struct ElementState {
...
}
pub trait Stateful<S = ElementState>
where
S: UiElementState
{
fn get_state(&self) -> S;
}
pub trait UiElementState {
// as above ...
}
impl<E: Stateful> UiElementState for E {
fn size(&self) -> u32 {
self.get_state().size()
}
// be nice to users and give them all the methods on their Stateful type...
}
2 likes · 1 comment
[quote="mtimmerm, post:1, topic:143414"]
Now, what I really wanted to do at this point was to provide a blanket implementation of MyTrait for anything that implements MyTraitImpl,which would have completed the solution. Unfortunately, coherence rules disallow this
[/quote]
You can do that (if I've interpreted your sketch accurately). But it sounds like you want it to be overrideable.
[quote="mtimmerm, post:5, topic:143414"]
IF a subtrait could provide default implementations for supertrait methods, then that would solve the problem, but rust doesn't allow this.
[/quote]
Supertrait item shadowing might be able to hack it when it lands? (I haven't played with it recently.)
Oh wow, looks like Rust 1.100 will stabilise Allocator generics! This is huge! Looks like allocator_api2 may get a significant drop in usage.
rust#163811
13 likes
How to Learn Rust Ownership and Borrowing (Without Fighting the Borrow Checker Forever)
My first week with Rust, I spent most of an evening on one error. I had a Vec<String>, I passed it to a function that printed it, and then I tried to print it again in main. The compiler told me the value had been moved. I added & in random places until it compiled, felt clever for ten minutes, and hit the same wall the next day with a struct.
That's the usual story. People don't quit Rust because of syntax. They quit because ownership feels like a set of arbitrary rules the compiler enforces with error messages, and they try to memorize the fixes instead of the model behind them.
Here's the order I'd learn it in if I started again.
Get the three rules into your head first
The Rust Book states them plainly, and everything else follows from them:
1. Each value has one owner.
2. There can only be one owner at a time.
3. When the owner goes out of scope, the value is dropped.
That's it. Moves, borrows and lifetimes are all consequences. When an error confuses you, ask which of the three rules you just broke. Nine times out of ten the answer is obvious once you look.
Understand moves before you touch references
This is the code that got me:
fn print_all(items: Vec<String>) {
for item in &items {
println!("{item}");
}
}
fn main() {
let names = vec![String::from("Ada"), String::from("Linus")];
print_all(names);
println!("{:?}", names); // error: borrow of moved value
}
Passing names to print_all hands ownership to the function. When print_all returns, its parameter goes out of scope and the vector is dropped. There's nothing left in main to print.
Notice that the same thing does not happen with an i32. Integers, booleans, char and a few others implement Copy, so passing them copies the bits and the original stays usable. String and Vec own heap memory, so they move. Once that difference clicks, a lot of "why does this work here but not there" goes away.
Then learn borrowing as two kinds of permission
The fix for the code above is to borrow instead of move:
fn print_all(items: &[String]) {
for item in items {
println!("{item}");
}
}
fn main() {
let names = vec![String::from("Ada"), String::from("Linus")];
print_all(&names);
println!("{:?}", names); // fine
}
I find it easier to think of references as permissions rather than pointers. &T means "you can look." &mut T means "you can change it, and nobody else can even look while you hold this." You can have as many look-only borrows as you want, or exactly one mutable borrow, never both at the same moment.
The classic error that teaches this:
let mut scores = vec![10, 20, 30];
let first = &scores[0];
scores.push(40); // error: cannot borrow as mutable
println!("{first}");
It feels pedantic until you realize why. push can reallocate the vector, which would leave first pointing at freed memory. In C that compiles and crashes later. Rust refuses up front.
Since the 2018 edition, borrows end at their last use, not at the end of the block (this is called non-lexical lifetimes). Move the println! above the push and it compiles. That one detail explains a lot of code that "shouldn't" work but does.
Leave lifetimes for later
Beginners often try to learn lifetime annotations in week one, and it's a mistake. Most of the time the compiler infers them. You only need to write 'a when a function returns a reference and the compiler can't tell which input it came from:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
Read that as "the result lives at least as long as both inputs." Learn it when you actually hit the error. Not before.
Read the error messages slowly
Rust's compiler errors are unusually good. They point at where the value was moved, where it was borrowed and where you tried to use it again, often with a suggested fix. I used to skim to the red line and start guessing. Reading the whole message, including the notes, fixed more bugs for me than any blog post.
rustc --explain E0382 (or whatever code you got) prints a longer explanation with examples. Worth doing for the first few errors you see repeatedly.
Practice on small programs that force the issue
Toy exercises help. Rustlings has a whole section on move semantics, and it's the fastest way to get reps. After that, build something small where data has to be shared:
- A to-do CLI that loads tasks from a file into a Vec<Task> and lets you edit them.
- A word counter that returns a HashMap<&str, usize> borrowing from the input text.
- A tiny linked list. It's painful on purpose, and it teaches you why Box, Rc and RefCell exist.
When you reach for .clone() to make an error go away, write down why. Sometimes cloning is the right call. Often it's a sign you wanted a borrow.
Make the model stick
My real problem wasn't understanding ownership once. I'd get it after a weekend, take two weeks off for work, and come back to the same moved-value error as if I'd never seen it. The rules are short, but the instinct for applying them fades fast if you don't revisit it.
What helped was keeping a short list of the ideas that kept tripping me up (moves vs Copy, shared vs mutable borrows, borrows ending at last use, when lifetimes need writing) and going back over them after a day, then a few days, then a couple of weeks. That's spaced repetition, and for a mental model like this it beats rereading chapter four every time you get stuck.
!Recl's spaced repetition schedule
I build Recl, and this is the kind of learning it's for. You save a Rust talk from YouTube, a chapter of the Book or a blog post about the borrow checker, it writes a summary of the key points, files it under a topic like "Rust", and brings it back on a spaced repetition schedule so you get quizzed before you forget. A plain notes file and calendar reminders do the same job if you'd rather not use an app. The point is to see it again before the compiler has to remind you.
---
I build Recl, an AI learning app that saves the videos, reels and articles you learn from, summarizes them, sorts them into topics, and uses spaced repetition so you actually remember them.
RustでTCP/UDP通信する
約15年間、積ん読していた 『TCP/IPソケットプログラミング C言語編』 を、最近ようやく読みました。1
一方、昨年から Rust を勉強してて、入門書は何冊か読んだものの、実装するネタがなくて困っていました。ChatGPT に相談したら「せっかくソケットプログラミングの本を読んだんだから Rust で実装してみたら」と勧められました。
そこで、本に書かれている C のコードを Rust へ移植してみました。最初は自分で全部書き、書き終わったら cargo clippy や ChatGPT にチェックしてもらって、より Rust ぽい書き方を教えてもらいました。2
内容的には初歩の初歩だと思いますが、メモ代わりに残しておきます。
環境
OS: macOS Tahoe 26.6.2
Rust: 1.99.1
TCP
『TCP/IPソケットプログラミング C言語編』に倣って、エコーサーバを作ります。エコーサーバとは、受信したメッセージをオウム返しするものです。
TCP サーバ
引数で指定されたポート番号でリッスンし、受信メッセージをクライアントへ返します。
変数 buf は、後述の実験のため、意図的に8バイトにしています。
use std::env;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
eprintln!("Usage: {} <port>", args[0]);
return Ok(());
}
let port: u16 = args[1].parse()?;
let listener = TcpListener::bind(("127.0.0.1", port))?;
println!("Listening on port {}", port);
for stream in listener.incoming() {
handle_connection(stream?)?;
}
Ok(())
}
fn handle_connection(mut stream: TcpStream) -> Result<(), std::io::Error> {
println!("Handling client {}", stream.peer_addr()?);
loop {
let mut buf = [0; 8];
let bytes_read = stream.read(&mut buf)?;
if bytes_read == 0 {
break;
}
let received = &buf[..bytes_read];
println!("Received: {}", String::from_utf8_lossy(received));
stream.write_all(received)?;
}
Ok(())
}
TCP クライアント
引数で指定されたサーバへメッセージを送ります。サーバからメッセージを受信したら、画面へ出力します。
use std::env;
use std::io::{Read, Write};
use std::net::TcpStream;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
if args.len() < 3 || args.len() > 4 {
eprintln!(
"Usage: {} <server address> <message> [<server port>]",
args[0]
);
return Ok(());
}
let server_addr = args[1].as_str();
let message = args[2].as_bytes();
let server_port: u16 = match args.get(3) {
Some(port) => port.parse()?,
None => 7,
};
let mut stream = TcpStream::connect((server_addr, server_port))?;
stream.write_all(message)?;
let mut received_bytes = 0;
let expected_bytes = message.len();
while received_bytes < expected_bytes {
let mut buf = [0; 32];
let bytes_read = stream.read(&mut buf)?;
if bytes_read == 0 {
break;
}
println!("Received: {}", String::from_utf8_lossy(&buf[..bytes_read]));
received_bytes += bytes_read;
}
Ok(())
}
TCP 動作確認
ポート 9000 でサーバを起動しておき、 Example123 というメッセージを送ってみます。
サーバ
$ cargo run 9000
Listening on port 9000
Handling client 127.0.0.1:63718
Received: Example1
Received: 23
クライアント
$ cargo run '127.0.0.1' 'Example123' 9000
Received: Example123
下図は、Wireshark でパケットキャプチャした結果をシーケンス図にしてみたものです。3
クライアントからサーバへは、1回だけメッセージを送信しています。サーバで Received: が2行になったのは、受信バッファから8バイトずつ読み取っているためです。
let mut buf = [0; 8];
let bytes_read = stream.read(&mut buf)?;
一方、サーバからクライアントへは2回送信しましたが、クライアントでは Received: は1行になっています。これは偶然で、1行になることもあれば、2行になることもあります。
2行になる場合
$ cargo run '127.0.0.1' 'Example123' 9000
Received: Example1
Received: 23
TCP はメッセージの境界を保持しません。 Example1 と 23 を受信したとき、1つのメッセージが2つに分けて送られてきたのか、別々のメッセージが送られてきたのかを判断するのは受信側のアプリケーションの責務です。4
UDP
UDP でもエコーサーバを作ってみます。
UDP サーバ
TCP サーバと同じです。引数で指定されたポート番号でリッスンし、受信メッセージをクライアントへ返します。
今回も、変数 buf は、意図的に8バイトにしています。実際には、十分なサイズを確保する必要があります。
use std::env;
use std::net::UdpSocket;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
eprintln!("Usage: {} <port>", args[0]);
return Ok(());
}
let port: u16 = args[1].parse()?;
let socket = UdpSocket::bind(("127.0.0.1", port))?;
println!("Listening on port {}", port);
loop {
let mut buf = [0; 8];
let (bytes_read, src_addr) = socket.recv_from(&mut buf)?;
println!("Handling client {}", src_addr);
println!("Received: {}", String::from_utf8_lossy(&buf[..bytes_read]));
socket.send_to(&buf, src_addr)?;
}
}
UDP クライアント
TCP クライアントと同じです。引数で指定されたサーバへメッセージを送ります。サーバからメッセージを受信したら、画面へ出力します。
use std::env;
use std::net::UdpSocket;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
if args.len() < 3 || args.len() > 4 {
eprintln!(
"Usage: {} <server address> <message> [<server port>]",
args[0]
);
return Ok(());
}
let server_addr = args[1].as_str();
let message = args[2].as_bytes();
let server_port: u16 = match args.get(3) {
Some(port) => port.parse()?,
None => 7,
};
let socket = UdpSocket::bind("0.0.0.0:0")?; // 自動割り当て
socket.connect((server_addr, server_port))?;
socket.send(message)?;
let mut buf = [0; 32];
let bytes_read = socket.recv(&mut buf)?;
println!("Received: {}", String::from_utf8_lossy(&buf[..bytes_read]));
Ok(())
}
C と異なり、Rust ではクライアントでも bind を呼び出す必要があります。 0.0.0.0:0 を指定し、OS に自動的に選択させています。
Why UdpSocket in Rust must be created using the bind with the address?
Is there a way to create an UdpSocket without bind() for just send_to()?
また、UDP では、あらゆる送信元からメッセージを受信する可能性があります。 connect を使うと、指定したアドレスに送信元を制限できます。5 なお、 connect を呼び出した後は、 send_to や recv_from ではなく、 send や recv を使うことができます。
UDP 動作確認
ポート 9000 でサーバを起動しておき、 Example123 というメッセージを送ってみます。
サーバ
$ cargo run 9000
Listening on port 9000
Handling client 127.0.0.1:59798
Received: Example1
クライアント
$ cargo run '127.0.0.1' 'Example123' 9000
Received: Example1
クライアントからは Example123 を送信したのに、サーバからは Example1 しか返ってきません。
TCP と異なり、UDP はメッセージの境界を保持します。1個のデータグラムが、1個のメッセージです。データグラムが分割されることはありません。
recv_from でメッセージを取得するときは、1回の呼び出しで、メッセージ全体を取得できなければなりません。今回、変数 buf は8バイトしかないため、 recv_from は先頭8バイトしか取得できません。残りは破棄されてしまいます。
このため、実際には、変数 buf は十分なサイズを確保しておく必要があります。6
タイムアウト
何らかの理由でサーバからレスポンスが返ってこないことは、意外とよくあります。デフォルトでは永遠に待ち続けますが、待ち続けるのではなく、どこかで諦めをつけてリトライするほうが効率的でしょう。
そこで、一定時間が経過してもレスポンスが返ってこなかったらリトライし、それでもダメだったらエラー終了させてみます。
サーバ
UDP サーバとほぼ同じです。クライアントへデータを送る際、60秒スリープさせている点のみが異なります。
use std::env;
use std::net::UdpSocket;
use std::{thread, time};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
eprintln!("Usage: {} <port>", args[0]);
return Ok(());
}
let port: u16 = args[1].parse()?;
let socket = UdpSocket::bind(("127.0.0.1", port))?;
println!("Listening on port {}", port);
loop {
let mut buf = [0; 8];
let (bytes_read, src_addr) = socket.recv_from(&mut buf)?;
println!("Handling client {}", src_addr);
let received = &buf[..bytes_read];
println!("Received: {}", String::from_utf8_lossy(received));
// タイムアウトさせるため60秒スリープさせる
let sleep_sec = time::Duration::from_secs(60);
thread::sleep(sleep_sec);
socket.send_to(&buf, src_addr)?;
}
}
クライアント
10秒待ってもレスポンスが返ってこなかったらリトライします。もし3回リトライしてもレスポンスが返ってこなかったらエラー終了します。
use std::env;
use std::io::ErrorKind;
use std::net::UdpSocket;
use std::time::Duration;
const MAX_RETRIES: u32 = 3;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
if args.len() < 3 || args.len() > 4 {
eprintln!(
"Usage: {} <server address> <message> [<server port>]",
args[0]
);
return Ok(());
}
let server_addr = args[1].as_str();
let message = args[2].as_bytes();
let server_port: u16 = match args.get(3) {
Some(port) => port.parse()?,
None => 7,
};
let socket = UdpSocket::bind("0.0.0.0:0")?; // 自動割り当て
socket.connect((server_addr, server_port))?;
socket.set_read_timeout(Some(Duration::from_secs(10)))?; // タイムアウト
for attempt in 0..=MAX_RETRIES {
socket.send(message)?;
let mut buf = [0; 32];
match socket.recv(&mut buf) {
Ok(bytes_read) => {
println!("Received: {}", String::from_utf8_lossy(&buf[..bytes_read]));
return Ok(());
}
Err(err) => {
match err.kind() {
ErrorKind::WouldBlock | ErrorKind::TimedOut => {
// タイムアウトしたのでリトライする。
if attempt == MAX_RETRIES {
eprintln!("Retry attempt is exceeded");
return Err(Box::new(err));
}
println!("Retry attempt: {}", attempt + 1);
}
_ => {
return Err(Box::new(err));
}
}
}
};
}
Ok(())
}
タイムアウトは set_read_timeout を呼び出して設定します。内部的には setsockopt に SO_RCVTIMEO を設定しています。7
github.com/rust-lang/rust/blob/1.99.1/library/std…
リファレンスによると、タイムアウトになったときのエラーは環境ごとに異なります。Unix では WouldBlock が、Windows では TimedOut が発生するそうです。
Platform-specific behavior
Platforms may return a different error code whenever a read times out as a result of setting this option. For example Unix typically returns an error of the kind WouldBlock, but Windows may return TimedOut.
動作確認
サーバは60秒おきに処理します。一方、クライアントは10秒以内にレスポンスが返ってこないのでリトライしています。3回リトライしても時間内に返ってこないのでエラー終了します。
サーバ
$ cargo run 9000
Listening on port 9000
Handling client 127.0.0.1:51513
Received: Example
Handling client 127.0.0.1:51513
Received: Example
Handling client 127.0.0.1:51513
Received: Example
Handling client 127.0.0.1:51513
Received: Example
クライアント
$ cargo run '127.0.0.1' 'Example' 9000
Retry attempt: 1
Retry attempt: 2
Retry attempt: 3
Retry attempt is exceeded
Error: Os { code: 35, kind: WouldBlock, message: "Resource temporarily unavailable" }
ブロードキャスト
ここでも『TCP/IPソケットプログラミング C言語編』に倣って、ブロードキャストを実装してみます。
送信側
引数で指定されたメッセージを3秒おきにブロードキャストします。
use std::env;
use std::net::UdpSocket;
use std::{thread, time};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
if args.len() != 4 {
eprintln!("Usage: {} <ip address> <port> <message>", args[0]);
return Ok(());
}
let broadcast_ip = args[1].as_str();
let broadcast_port: u16 = args[2].parse()?;
let message = args[3].as_bytes();
let socket = UdpSocket::bind("0.0.0.0:0")?;
socket.set_broadcast(true)?;
loop {
let bytes_sent = socket.send_to(message, (broadcast_ip, broadcast_port))?;
println!("Sent {bytes_sent} bytes");
let sleep_sec = time::Duration::from_secs(3);
thread::sleep(sleep_sec);
}
}
受信側
メッセージを受信したら、画面へ出力します。
use std::env;
use std::net::UdpSocket;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
eprintln!("Usage: {} <port>", args[0]);
return Ok(());
}
let broadcast_port: u16 = args[1].parse()?;
let socket = UdpSocket::bind(format!("0.0.0.0:{}", broadcast_port))?; // 自動割り当て
let mut buf = [0; 32];
let (bytes_read, _) = socket.recv_from(&mut buf)?;
println!("Received: {}", String::from_utf8_lossy(&buf[..bytes_read]));
Ok(())
}
動作確認
255.255.255.255 へ送信すると、同じ LAN にある全てのホストへメッセージを送信できます。
送信側
$ cargo run '255.255.255.255' 9000 'Example'
Sent 7 bytes
Sent 7 bytes
受信側
$ cargo run 9000
Received: Example
マルチキャスト
最後に、マルチキャストを実装してみます。
送信側
ブロードキャストと同じです。引数で指定されたメッセージを3秒おきにマルチキャストします。
マルチキャストでは set_multicast_ttl_v4 を使って TTL を設定できます。引数で指定されたときは、その値を TTL に設定します。指定されなかったときはデフォルト値 1 を設定します。
use std::env;
use std::net::UdpSocket;
use std::{thread, time};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
if args.len() < 4 || args.len() > 5 {
eprintln!("Usage: {} <ip address> <port> <message> [<ttl>]", args[0]);
return Ok(());
}
let multicast_ip = args[1].as_str();
let multicast_port: u16 = args[2].parse()?;
let message = args[3].as_bytes();
let multicast_ttl: u32 = match args.get(4) {
Some(ttl) => ttl.parse()?,
None => 1,
};
let socket = UdpSocket::bind("0.0.0.0:0")?;
socket.set_multicast_ttl_v4(multicast_ttl)?; // TTL
loop {
let bytes_sent = socket.send_to(message, (multicast_ip, multicast_port))?;
println!("Sent {bytes_sent} bytes");
let sleep_sec = time::Duration::from_secs(3);
thread::sleep(sleep_sec);
}
}
受信側
メッセージを受信したら、画面へ出力します。
マルチキャストでは、join_multicast_v4 を使ってマルチキャストグループへ参加しておく必要があります。
use std::env;
use std::net::Ipv4Addr;
use std::net::UdpSocket;
use std::str::FromStr;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
if args.len() != 3 {
eprintln!("Usage: {} <ip address> <port>", args[0]);
return Ok(());
}
let multicast_ip = args[1].as_str();
let multicast_port: u16 = args[2].parse()?;
let socket = UdpSocket::bind(format!("0.0.0.0:{}", multicast_port))?;
let multicast_addr = Ipv4Addr::from_str(multicast_ip)?;
socket.join_multicast_v4(&multicast_addr, &Ipv4Addr::UNSPECIFIED)?;
let mut buf = [0; 32];
let (bytes_read, _) = socket.recv_from(&mut buf)?;
println!("Received: {}", String::from_utf8_lossy(&buf[..bytes_read]));
Ok(())
}
動作確認
マルチキャストアドレス(例: 239.1.2.3 )へ送信すると、マルチキャストグループに参加しているホストへメッセージを送信できます。
なお、マルチキャストアドレスについては下記参照。
マルチキャストIPアドレス - ネットワークエンジニアとして
送信側
$ cargo run '239.1.2.3' 9000 'Example'
Sent 7 bytes
Sent 7 bytes
受信側
$ cargo run '239.1.2.3' 9000
Received: Example
補足
iTerm2
iTerm2 でブロードキャストやマルチキャストを試そうとすると、 No route to host というエラーが表示されます。
送信側
$ cargo run '255.255.255.255' 9000 'Example'
Error: Os { code: 65, kind: HostUnreachable, message: "No route to host" }
これは Mac のセキュリティ制限によるものです。7:1
No route to host on iterm2 only
解決方法は2つあります。8
1つは、Mac 標準の「ターミナル」を使うことです。
もう1つは、 システム設定 > プライバシーとセキュリティ > ローカルネットワーク で iTerm を許可することです。
クロスコンパイル
ブロードキャストやマルチキャストは、ローカルでやってても楽しくありません。古い Mac が家にあったので、そこへ送信することにしました。
しかし、開発環境は Apple Silicon なのに対し、古い Mac は(当然ですが) Intel Mac でした。思いがけずクロスコンパイルしないといけなくなりました。
クロスコンパイルは、場合によってはかなり面倒なことにもなるようですが、幸い、今回は2行でクロスコンパイルできました。
$ rustup target add x86_64-apple-darwin
$ cargo build --release --target x86_64-apple-darwin
脚注
「前書き」の「対象読者」(p.7)に、読者を脅かすようなコードが書かれていますが、実際は C の入門書を1冊仕上げたことがあれば容易に理解できるレベルでした。C 以外では、UNIX のシグナルやプロセスなどの知識があったほうが良いと思います(私は OS の教科書を直前に読んでいたので困りませんでした)。API の使い方の説明がメインなので、プロトコルの解説は マスタリングTCP/IP 入門編 などで補う必要があります。また、原著は2002年の出版で IPv6 には対応していません(他にも古いところがありそう)。そんなに古くても、コードが今でもそのまま動くのは、さすが C 言語と言うべきでしょうか。 ↩︎
cargo clippy は、たとえば if args.len() < 1 と書くと、 is_empty を使えることをサジェストしてくれます。些細なことですが感動しましたね。 ↩︎
「3ウェイハンドシェイク」に続けて、サーバからクライアントへ ACK しています。これは「ウィンドウスケーリング」で、受信ウィンドウサイズを拡大したことをクライアントへ通知しています。 『ハイパフォーマンス ブラウザネットワーキング』 の「2.2.1 フロー制御」のコラム参照。 ↩︎
『TCP/IPソケットプログラミング C言語編』(p.58)には、次の方法が一般的と書かれています。「特定の文字(スペース文字など)、あるいは文字シーケンスをフィールドの区切り文字として使用し、メッセージを既知の数のフィールドで構成する」。 ↩︎
『TCP/IPソケットプログラミング C言語編』(p.66)では recvfrom で送信元アドレスを取得してチェックしています。Rust の recv_from でも同様にチェックできますが、 connect のほうが簡単だと思いました。 ↩︎
『TCP/IPソケットプログラミング C言語編』(p.70)では、この他に MSG_PEEK フラグを使う方法が紹介されています。 ↩︎
あっさり書きましたが、実際には ChatGPT の力を借りながら、この原因に辿り着くまでに数時間を溶かしました。 No route to host というエラーメッセージからは、一見、単なるネットワークの問題に見えます。しかし、実際は Mac のセキュリティ機能により、 iTerm2 によるブロードキャストやマルチキャストがブロックされていたのです。 ↩︎ ↩︎
他に .plist ファイルを編集して Mac のデフォルト設定を変更してしまう方法もあるようですが、手順が煩雑でリスクもありそうだったのでやめました。 ↩︎
Rust in agentic workflows sure consumes a lot of disk space, right?
If you want this fixed, here's something you and your employer should fund 👇
goals#830
106 likes · 26 reposts
> However this unfortunately abuses Poll::Pending, whose docs state: "When a function returns Pending, the function must also ensure that the current task is scheduled to be awoken when progress can be made." This definition is incompatible with how Fuse uses Pending, and so bringing it into the stdlib would require redefining Pending to mean both “pending” and “done”.
This came up in issue #163678. To me it doesn't seem like there's any contradiction between that documented rule and a future that returns Ready once and then Pending forever after that. (Or for that matter, a future that only ever returns Pending.) If there's no further progress to be made, then returning Pending without scheduling any wakeups is reasonable.
Specifically, an async fn future currently panics if you poll it again after it returned Ready. That seems like a fine way to catch mistakes early, at the cost of compiling in the panic machinery like you mentioned. But I don't think there would be anything fundamentally wrong with changing async fn futures to return Pending forever in that case instead. If I understand correctly, that's what the new/proposed compiler switch in that issue is doing. Seems like a good way to solve the problem?
> The compiler can guarantee that calling .await never polls after getting Ready, and so it doesn’t need to handle the Done state.
In this model can the compiler really _guarantee_ that .await is never going to see Done? An incorrect future could return Done before returning Ready. It presumably wouldn't be unsafe/unsound to do so? My intuition is that we'd need to lower .await to something that panicked in that case, which means we're not solving the problem we set out to solve. (Waiting forever on a future that returns Pending forever makes sense, but waiting forever on a future that returns Done doesn't make sense to me.)
> This leads to a rule that we can apply recursively to every future: “When a future is polled, it must also poll all child futures that have had their waker called.” If every future follows this rule, no future will ever be snoozed.
Incidentally this is now an RFC: rfcs#4019
6 points
Mentions
After months of work, I've just opened a PR to Rust's package manager Cargo! 🦀🎉 A TUI for cargo-tree soon! Explore your Rust dependency graph interactively 🌳 ➡️ Try it: cargo install cargo-tree-tui Repository: orhun/cargo-tree-tui PR: cargo#17673 #rustlang #ratatui #terminal
After months of work, I've just opened a PR to Rust's package manager Cargo! 🦀🎉 A TUI for cargo-tree soon! Explore your Rust dependency graph interactively! 🌳 ➡️ Try it: cargo install cargo-tree-tui Repository: orhun/cargo-tree-tui PR: cargo#17673 #rustlang #ratatui #terminal #cargo
Open Source Didn't Ban AI Code. 83% of Its AI Policies Allow It, With One Catch - Book: Prompt Engineering Pocket Guide - Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- July was the month open source "banned AI." On July 22, Codeberg members voted 358 to 144 to bar projects built mostly from AI-generated code (It's FOSS). About a week later, the GCC Steering Committee said it won't take a legally significant contribution if any part of it traces back to an LLM (It's FOSS). Read the GCC rule to the end, though. Contributions under the legal-significance threshold (roughly 15 lines) can still come in if they are clearly marked, test cases are exempt, and AI-assisted work carries an Assisted-by: tag in the commit message. Even the "ban" is mostly a set of conditions. Then look past the two headlines at the whole field. In September, Andre Hora, Romain Robbes and Stefano Zacchiroli published ["We Permit the Use of AI, but [...]"](arxiv.org), a study of 281 AI policies from the 2,000 most-starred GitHub repositories plus 36 well-known projects and organizations such as the Linux kernel, LLVM and the Apache Software Foundation. The title is the finding. The numbers say "yes, but" From the paper's results: - 83.3% of policies permit or encourage AI in code contributions (76.2% permit, 7.1% encourage). - 14.9% forbid AI in code. Only 5.3% forbid it for every kind of contribution. - 67.3% require a high level of human involvement. - 43.4% make the contributor accountable for the result. None of the 281 say accountability is optional. - 48.8% require you to disclose AI use. Of the disclosure rules that say where, 64.7% want it in the PR description and 26.7% in the commit message. So a ban is the minority position. What the majority asks for is a human who was there: someone who directed the work, read it, and will answer for it. The paper quotes the policy of psf/requests on this: "you must remove any LLM co-author tags and take full ownership of every line." One thing the study leaves open, and the authors list it as future work: whether anyone actually follows these policies. What maintainers actually do with slop The paper counts 188 countermeasures against low-quality AI contributions across the policies. The breakdown: | Countermeasure | Share | |---|---| | Close the PR | 48.4% | | Ban or block the user | 20.8% | | Disallow autonomous agents | 13.8% | | Restrict new or external users | 4.8% | | Everything else (six measures) | 12.2% | Nearly half of all measures are the same move: close it. The rqlite policy says slop PRs "may be closed without comment," per the paper. rust-analyzer goes further and says it does not allow autonomous agents to open pull requests or issues at all. Some projects now address the agent directly. The paper finds policies in AGENTS.md and CLAUDE.md as well as CONTRIBUTING.md and AI_POLICY.md. The contributing guide of llama.cpp tells fully autonomous agents not to contribute and to exclude the repo from their configuration. My take: they banned the unread diff, not the model Put the two findings side by side. Most projects accept AI code. The most common response to bad AI code is closing the PR. The deciding factor between those two outcomes is whether a human read the change before a maintainer had to. A closed PR is a maintainer telling you the review didn't happen on your side, so it isn't going to happen on theirs. Reviewing a large agent diff costs real time, and maintainers are often volunteers. The policies push that cost back to the sender, which is fair. The diff can be cheap to generate. Someone still has to understand it, and the 67.3% requiring high human involvement is the field agreeing on who that someone is. That moves the review upstream. If the reading has to happen, it should happen on your machine, before the PR exists. Once you open it, every unread line is a maintainer's problem. Three steps before you open the PR 1. Read the project's policy first. It might live in a file you don't expect: ls AI_POLICY.md LLM_POLICY.md AGENTS.md CLAUDE.md \ CONTRIBUTING.md 2>/dev/null grep -inwE "ai|llm|agent|copilot" CONTRIBUTING.md \ 2>/dev/null Then follow its format. There isn't one standard. The paper notes RustPython asks for Assisted-by and not Co-authored-by, Nextcloud requires an Assisted-by trailer, and psf/requests wants LLM co-author tags gone entirely. 2. Read the change step by step. One big diff hides the step where the agent rewrote a file you didn't ask about. Start with the summary, then walk the branch commit by commit, oldest first: git diff main...HEAD --stat git log -p --reverse main..HEAD The --stat line shows the file you didn't ask about. The log shows each step in order. If you can't explain a hunk, ask the agent to undo it or redo it smaller. "Full ownership of every line" starts here. 3. Disclose where the project asks. Git has a flag for trailers (git docs): git commit -m "Fix off-by-one in pagination" \ --trailer "Assisted-by: Claude Code" Then say it in the PR description too, since that's where most disclosure rules point: which agent, what it did, what you checked by hand. Doing the reading in Hermes IDE I build Hermes IDE, so weigh this section with that in mind. Everything above works with plain git. Hermes is built around that order of operations: the agent works and you read before you land. Every agent turn is saved, so you can diff or restore any single turn, with undo. The Review Desk (⌘G, or Ctrl+Shift+G on Windows and Linux) shows the changes by turn or by file, with risk flags, and a line comment goes back to the agent that wrote that line. That's the "read it turn by turn" step without scrolling one long diff (README). !The Hermes Review Desk in By turn view, listing two turns of a Claude Code task with turn 1's diff of src/db.ts and a drafted commit message The Review Desk (⌘G) in By turn view: two turns of a Claude Code task, turn 1's diff of src/db.ts, the changed files, and a commit message drafted from the turns. Only after that does Land come in. It commits, opens a pull request or merges locally, into the branch the task started from, and it can be undone. Check that the commit message carries the trailer your project asks for before you land. Each task also runs on its own hermes/ branch and worktree, so the PR holds one task's work and nothing from the agent next door. You can download Hermes. It is source-available under BSL 1.1 and runs on macOS, Windows and Linux. It runs Claude Code, Codex and other agents in their own terminals, with your own sign-ins. It isn't affiliated with Nous Research or its Hermes Agent. It can't make an agent's change good, but it gives you a place to check the change first. Where do you draw the line? If you maintain a project, does your policy say what "human involvement" means in practice? If you contribute, do you disclose the agent by default, or only when a project asks? Tell me in the comments. --- If this was useful Projects now write their AI rules into AGENTS.md and CLAUDE.md, which means a contribution policy is also a prompt. The Prompt Engineering Pocket Guide covers how to write instructions an agent actually follows, whether you're the maintainer writing them or the contributor whose agent reads them. If you'd rather review agent work turn by turn, Hermes IDE is the tool I build for exactly that. 
[quote="mtimmerm, post:12, topic:143414"] But it's convenient. [/quote] First off, that's a good "why" - considering downstream users experience. Probably a footgun in this case though. Even as a polyglot, when my brain is in "rust mode" it's a really expensive operation to notice "ah - that's a [insert other language here] idiom, switch modes for a moment ..." That's why working with ffis is a "different mode" for me and ringfenced-in-code to a rust-API-wrapper. So overall an idiomatic rust API will likely make your users lives easier overall, even if it misses some of the benefits from somewhere else. ---- As for idiomatic approaches, I can think of 3: Fixed state type You've already ruled this out, just including for completeness. Probably the simplest design but leaves no flexibility for alternative state implementations pub struct ElementState { ... } pub trait Stateful { fn get_state(&self) -> ElementState; } -> impl trait / associated type Think IntoIterator & Iterator Two very similar options, for cases where most user will want the flexibility of defining their state type, probably for functional reasons. (There's also a middle ground between associated type and the third option (below) where you provide a default type, but that is currently unstable via #![feature(associated_type_defaults)]) pub trait Stateful { fn get_state(&self) -> impl UiElementState; } // or pub trait Stateful { type: State: UiElementState; fn get_state(&self) -> Self::State; } pub trait UiElementState { // implementors need to provide basic getters & non-verifying setters fn size(&self) -> u32; fn max_size(&self) -> u32; // borderline case: do you make these unsafe due to lack of verification? fn set_size_unchecked(&mut self, new_size: u32) -> u32; // default verifications, calculations etc are provided but can // be overridden fn resize(&mut self, new_size: u32) -> UiResult<u32> { let max_size = self.max_size(); match self.size() { new_size => Ok(new_size), // no-op 0..=max_size => { let resized = self.set_size_unchecked(new_size); Ok(resized) } _ => Err(InvalidSize), } } Default generic Think Vec<T, A = Global> where A: Allocator The rare one, for cases where most will not want to override but some may for technical reasons. pub struct ElementState { ... } pub trait Stateful<S = ElementState> where S: UiElementState { fn get_state(&self) -> S; } pub trait UiElementState { // as above ... } impl<E: Stateful> UiElementState for E { fn size(&self) -> u32 { self.get_state().size() } // be nice to users and give them all the methods on their Stateful type... }
[quote="mtimmerm, post:1, topic:143414"] Now, what I really wanted to do at this point was to provide a blanket implementation of MyTrait for anything that implements MyTraitImpl,which would have completed the solution. Unfortunately, coherence rules disallow this [/quote] You can do that (if I've interpreted your sketch accurately). But it sounds like you want it to be overrideable. [quote="mtimmerm, post:5, topic:143414"] IF a subtrait could provide default implementations for supertrait methods, then that would solve the problem, but rust doesn't allow this. [/quote] Supertrait item shadowing might be able to hack it when it lands? (I haven't played with it recently.)
Oh wow, looks like Rust 1.100 will stabilise Allocator generics! This is huge! Looks like allocator_api2 may get a significant drop in usage. rust#163811
How to Learn Rust Ownership and Borrowing (Without Fighting the Borrow Checker Forever) My first week with Rust, I spent most of an evening on one error. I had a Vec<String>, I passed it to a function that printed it, and then I tried to print it again in main. The compiler told me the value had been moved. I added & in random places until it compiled, felt clever for ten minutes, and hit the same wall the next day with a struct. That's the usual story. People don't quit Rust because of syntax. They quit because ownership feels like a set of arbitrary rules the compiler enforces with error messages, and they try to memorize the fixes instead of the model behind them. Here's the order I'd learn it in if I started again. Get the three rules into your head first The Rust Book states them plainly, and everything else follows from them: 1. Each value has one owner. 2. There can only be one owner at a time. 3. When the owner goes out of scope, the value is dropped. That's it. Moves, borrows and lifetimes are all consequences. When an error confuses you, ask which of the three rules you just broke. Nine times out of ten the answer is obvious once you look. Understand moves before you touch references This is the code that got me: fn print_all(items: Vec<String>) { for item in &items { println!("{item}"); } } fn main() { let names = vec![String::from("Ada"), String::from("Linus")]; print_all(names); println!("{:?}", names); // error: borrow of moved value } Passing names to print_all hands ownership to the function. When print_all returns, its parameter goes out of scope and the vector is dropped. There's nothing left in main to print. Notice that the same thing does not happen with an i32. Integers, booleans, char and a few others implement Copy, so passing them copies the bits and the original stays usable. String and Vec own heap memory, so they move. Once that difference clicks, a lot of "why does this work here but not there" goes away. Then learn borrowing as two kinds of permission The fix for the code above is to borrow instead of move: fn print_all(items: &[String]) { for item in items { println!("{item}"); } } fn main() { let names = vec![String::from("Ada"), String::from("Linus")]; print_all(&names); println!("{:?}", names); // fine } I find it easier to think of references as permissions rather than pointers. &T means "you can look." &mut T means "you can change it, and nobody else can even look while you hold this." You can have as many look-only borrows as you want, or exactly one mutable borrow, never both at the same moment. The classic error that teaches this: let mut scores = vec![10, 20, 30]; let first = &scores[0]; scores.push(40); // error: cannot borrow as mutable println!("{first}"); It feels pedantic until you realize why. push can reallocate the vector, which would leave first pointing at freed memory. In C that compiles and crashes later. Rust refuses up front. Since the 2018 edition, borrows end at their last use, not at the end of the block (this is called non-lexical lifetimes). Move the println! above the push and it compiles. That one detail explains a lot of code that "shouldn't" work but does. Leave lifetimes for later Beginners often try to learn lifetime annotations in week one, and it's a mistake. Most of the time the compiler infers them. You only need to write 'a when a function returns a reference and the compiler can't tell which input it came from: fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() > b.len() { a } else { b } } Read that as "the result lives at least as long as both inputs." Learn it when you actually hit the error. Not before. Read the error messages slowly Rust's compiler errors are unusually good. They point at where the value was moved, where it was borrowed and where you tried to use it again, often with a suggested fix. I used to skim to the red line and start guessing. Reading the whole message, including the notes, fixed more bugs for me than any blog post. rustc --explain E0382 (or whatever code you got) prints a longer explanation with examples. Worth doing for the first few errors you see repeatedly. Practice on small programs that force the issue Toy exercises help. Rustlings has a whole section on move semantics, and it's the fastest way to get reps. After that, build something small where data has to be shared: - A to-do CLI that loads tasks from a file into a Vec<Task> and lets you edit them. - A word counter that returns a HashMap<&str, usize> borrowing from the input text. - A tiny linked list. It's painful on purpose, and it teaches you why Box, Rc and RefCell exist. When you reach for .clone() to make an error go away, write down why. Sometimes cloning is the right call. Often it's a sign you wanted a borrow. Make the model stick My real problem wasn't understanding ownership once. I'd get it after a weekend, take two weeks off for work, and come back to the same moved-value error as if I'd never seen it. The rules are short, but the instinct for applying them fades fast if you don't revisit it. What helped was keeping a short list of the ideas that kept tripping me up (moves vs Copy, shared vs mutable borrows, borrows ending at last use, when lifetimes need writing) and going back over them after a day, then a few days, then a couple of weeks. That's spaced repetition, and for a mental model like this it beats rereading chapter four every time you get stuck. !Recl's spaced repetition schedule I build Recl, and this is the kind of learning it's for. You save a Rust talk from YouTube, a chapter of the Book or a blog post about the borrow checker, it writes a summary of the key points, files it under a topic like "Rust", and brings it back on a spaced repetition schedule so you get quizzed before you forget. A plain notes file and calendar reminders do the same job if you'd rather not use an app. The point is to see it again before the compiler has to remind you. --- I build Recl, an AI learning app that saves the videos, reels and articles you learn from, summarizes them, sorts them into topics, and uses spaced repetition so you actually remember them.
RustでTCP/UDP通信する 約15年間、積ん読していた 『TCP/IPソケットプログラミング C言語編』 を、最近ようやく読みました。1 一方、昨年から Rust を勉強してて、入門書は何冊か読んだものの、実装するネタがなくて困っていました。ChatGPT に相談したら「せっかくソケットプログラミングの本を読んだんだから Rust で実装してみたら」と勧められました。 そこで、本に書かれている C のコードを Rust へ移植してみました。最初は自分で全部書き、書き終わったら cargo clippy や ChatGPT にチェックしてもらって、より Rust ぽい書き方を教えてもらいました。2 内容的には初歩の初歩だと思いますが、メモ代わりに残しておきます。 環境 OS: macOS Tahoe 26.6.2 Rust: 1.99.1 TCP 『TCP/IPソケットプログラミング C言語編』に倣って、エコーサーバを作ります。エコーサーバとは、受信したメッセージをオウム返しするものです。 TCP サーバ 引数で指定されたポート番号でリッスンし、受信メッセージをクライアントへ返します。 変数 buf は、後述の実験のため、意図的に8バイトにしています。 use std::env; use std::io::{Read, Write}; use std::net::{TcpListener, TcpStream}; fn main() -> Result<(), Box<dyn std::error::Error>> { let args: Vec<String> = env::args().collect(); if args.len() != 2 { eprintln!("Usage: {} <port>", args[0]); return Ok(()); } let port: u16 = args[1].parse()?; let listener = TcpListener::bind(("127.0.0.1", port))?; println!("Listening on port {}", port); for stream in listener.incoming() { handle_connection(stream?)?; } Ok(()) } fn handle_connection(mut stream: TcpStream) -> Result<(), std::io::Error> { println!("Handling client {}", stream.peer_addr()?); loop { let mut buf = [0; 8]; let bytes_read = stream.read(&mut buf)?; if bytes_read == 0 { break; } let received = &buf[..bytes_read]; println!("Received: {}", String::from_utf8_lossy(received)); stream.write_all(received)?; } Ok(()) } TCP クライアント 引数で指定されたサーバへメッセージを送ります。サーバからメッセージを受信したら、画面へ出力します。 use std::env; use std::io::{Read, Write}; use std::net::TcpStream; fn main() -> Result<(), Box<dyn std::error::Error>> { let args: Vec<String> = env::args().collect(); if args.len() < 3 || args.len() > 4 { eprintln!( "Usage: {} <server address> <message> [<server port>]", args[0] ); return Ok(()); } let server_addr = args[1].as_str(); let message = args[2].as_bytes(); let server_port: u16 = match args.get(3) { Some(port) => port.parse()?, None => 7, }; let mut stream = TcpStream::connect((server_addr, server_port))?; stream.write_all(message)?; let mut received_bytes = 0; let expected_bytes = message.len(); while received_bytes < expected_bytes { let mut buf = [0; 32]; let bytes_read = stream.read(&mut buf)?; if bytes_read == 0 { break; } println!("Received: {}", String::from_utf8_lossy(&buf[..bytes_read])); received_bytes += bytes_read; } Ok(()) } TCP 動作確認 ポート 9000 でサーバを起動しておき、 Example123 というメッセージを送ってみます。 サーバ $ cargo run 9000 Listening on port 9000 Handling client 127.0.0.1:63718 Received: Example1 Received: 23 クライアント $ cargo run '127.0.0.1' 'Example123' 9000 Received: Example123 下図は、Wireshark でパケットキャプチャした結果をシーケンス図にしてみたものです。3 クライアントからサーバへは、1回だけメッセージを送信しています。サーバで Received: が2行になったのは、受信バッファから8バイトずつ読み取っているためです。 let mut buf = [0; 8]; let bytes_read = stream.read(&mut buf)?; 一方、サーバからクライアントへは2回送信しましたが、クライアントでは Received: は1行になっています。これは偶然で、1行になることもあれば、2行になることもあります。 2行になる場合 $ cargo run '127.0.0.1' 'Example123' 9000 Received: Example1 Received: 23 TCP はメッセージの境界を保持しません。 Example1 と 23 を受信したとき、1つのメッセージが2つに分けて送られてきたのか、別々のメッセージが送られてきたのかを判断するのは受信側のアプリケーションの責務です。4 UDP UDP でもエコーサーバを作ってみます。 UDP サーバ TCP サーバと同じです。引数で指定されたポート番号でリッスンし、受信メッセージをクライアントへ返します。 今回も、変数 buf は、意図的に8バイトにしています。実際には、十分なサイズを確保する必要があります。 use std::env; use std::net::UdpSocket; fn main() -> Result<(), Box<dyn std::error::Error>> { let args: Vec<String> = env::args().collect(); if args.len() != 2 { eprintln!("Usage: {} <port>", args[0]); return Ok(()); } let port: u16 = args[1].parse()?; let socket = UdpSocket::bind(("127.0.0.1", port))?; println!("Listening on port {}", port); loop { let mut buf = [0; 8]; let (bytes_read, src_addr) = socket.recv_from(&mut buf)?; println!("Handling client {}", src_addr); println!("Received: {}", String::from_utf8_lossy(&buf[..bytes_read])); socket.send_to(&buf, src_addr)?; } } UDP クライアント TCP クライアントと同じです。引数で指定されたサーバへメッセージを送ります。サーバからメッセージを受信したら、画面へ出力します。 use std::env; use std::net::UdpSocket; fn main() -> Result<(), Box<dyn std::error::Error>> { let args: Vec<String> = env::args().collect(); if args.len() < 3 || args.len() > 4 { eprintln!( "Usage: {} <server address> <message> [<server port>]", args[0] ); return Ok(()); } let server_addr = args[1].as_str(); let message = args[2].as_bytes(); let server_port: u16 = match args.get(3) { Some(port) => port.parse()?, None => 7, }; let socket = UdpSocket::bind("0.0.0.0:0")?; // 自動割り当て socket.connect((server_addr, server_port))?; socket.send(message)?; let mut buf = [0; 32]; let bytes_read = socket.recv(&mut buf)?; println!("Received: {}", String::from_utf8_lossy(&buf[..bytes_read])); Ok(()) } C と異なり、Rust ではクライアントでも bind を呼び出す必要があります。 0.0.0.0:0 を指定し、OS に自動的に選択させています。 Why UdpSocket in Rust must be created using the bind with the address? Is there a way to create an UdpSocket without bind() for just send_to()? また、UDP では、あらゆる送信元からメッセージを受信する可能性があります。 connect を使うと、指定したアドレスに送信元を制限できます。5 なお、 connect を呼び出した後は、 send_to や recv_from ではなく、 send や recv を使うことができます。 UDP 動作確認 ポート 9000 でサーバを起動しておき、 Example123 というメッセージを送ってみます。 サーバ $ cargo run 9000 Listening on port 9000 Handling client 127.0.0.1:59798 Received: Example1 クライアント $ cargo run '127.0.0.1' 'Example123' 9000 Received: Example1 クライアントからは Example123 を送信したのに、サーバからは Example1 しか返ってきません。 TCP と異なり、UDP はメッセージの境界を保持します。1個のデータグラムが、1個のメッセージです。データグラムが分割されることはありません。 recv_from でメッセージを取得するときは、1回の呼び出しで、メッセージ全体を取得できなければなりません。今回、変数 buf は8バイトしかないため、 recv_from は先頭8バイトしか取得できません。残りは破棄されてしまいます。 このため、実際には、変数 buf は十分なサイズを確保しておく必要があります。6 タイムアウト 何らかの理由でサーバからレスポンスが返ってこないことは、意外とよくあります。デフォルトでは永遠に待ち続けますが、待ち続けるのではなく、どこかで諦めをつけてリトライするほうが効率的でしょう。 そこで、一定時間が経過してもレスポンスが返ってこなかったらリトライし、それでもダメだったらエラー終了させてみます。 サーバ UDP サーバとほぼ同じです。クライアントへデータを送る際、60秒スリープさせている点のみが異なります。 use std::env; use std::net::UdpSocket; use std::{thread, time}; fn main() -> Result<(), Box<dyn std::error::Error>> { let args: Vec<String> = env::args().collect(); if args.len() != 2 { eprintln!("Usage: {} <port>", args[0]); return Ok(()); } let port: u16 = args[1].parse()?; let socket = UdpSocket::bind(("127.0.0.1", port))?; println!("Listening on port {}", port); loop { let mut buf = [0; 8]; let (bytes_read, src_addr) = socket.recv_from(&mut buf)?; println!("Handling client {}", src_addr); let received = &buf[..bytes_read]; println!("Received: {}", String::from_utf8_lossy(received)); // タイムアウトさせるため60秒スリープさせる let sleep_sec = time::Duration::from_secs(60); thread::sleep(sleep_sec); socket.send_to(&buf, src_addr)?; } } クライアント 10秒待ってもレスポンスが返ってこなかったらリトライします。もし3回リトライしてもレスポンスが返ってこなかったらエラー終了します。 use std::env; use std::io::ErrorKind; use std::net::UdpSocket; use std::time::Duration; const MAX_RETRIES: u32 = 3; fn main() -> Result<(), Box<dyn std::error::Error>> { let args: Vec<String> = env::args().collect(); if args.len() < 3 || args.len() > 4 { eprintln!( "Usage: {} <server address> <message> [<server port>]", args[0] ); return Ok(()); } let server_addr = args[1].as_str(); let message = args[2].as_bytes(); let server_port: u16 = match args.get(3) { Some(port) => port.parse()?, None => 7, }; let socket = UdpSocket::bind("0.0.0.0:0")?; // 自動割り当て socket.connect((server_addr, server_port))?; socket.set_read_timeout(Some(Duration::from_secs(10)))?; // タイムアウト for attempt in 0..=MAX_RETRIES { socket.send(message)?; let mut buf = [0; 32]; match socket.recv(&mut buf) { Ok(bytes_read) => { println!("Received: {}", String::from_utf8_lossy(&buf[..bytes_read])); return Ok(()); } Err(err) => { match err.kind() { ErrorKind::WouldBlock | ErrorKind::TimedOut => { // タイムアウトしたのでリトライする。 if attempt == MAX_RETRIES { eprintln!("Retry attempt is exceeded"); return Err(Box::new(err)); } println!("Retry attempt: {}", attempt + 1); } _ => { return Err(Box::new(err)); } } } }; } Ok(()) } タイムアウトは set_read_timeout を呼び出して設定します。内部的には setsockopt に SO_RCVTIMEO を設定しています。7 github.com/rust-lang/rust/blob/1.99.1/library/std… リファレンスによると、タイムアウトになったときのエラーは環境ごとに異なります。Unix では WouldBlock が、Windows では TimedOut が発生するそうです。 Platform-specific behavior Platforms may return a different error code whenever a read times out as a result of setting this option. For example Unix typically returns an error of the kind WouldBlock, but Windows may return TimedOut. 動作確認 サーバは60秒おきに処理します。一方、クライアントは10秒以内にレスポンスが返ってこないのでリトライしています。3回リトライしても時間内に返ってこないのでエラー終了します。 サーバ $ cargo run 9000 Listening on port 9000 Handling client 127.0.0.1:51513 Received: Example Handling client 127.0.0.1:51513 Received: Example Handling client 127.0.0.1:51513 Received: Example Handling client 127.0.0.1:51513 Received: Example クライアント $ cargo run '127.0.0.1' 'Example' 9000 Retry attempt: 1 Retry attempt: 2 Retry attempt: 3 Retry attempt is exceeded Error: Os { code: 35, kind: WouldBlock, message: "Resource temporarily unavailable" } ブロードキャスト ここでも『TCP/IPソケットプログラミング C言語編』に倣って、ブロードキャストを実装してみます。 送信側 引数で指定されたメッセージを3秒おきにブロードキャストします。 use std::env; use std::net::UdpSocket; use std::{thread, time}; fn main() -> Result<(), Box<dyn std::error::Error>> { let args: Vec<String> = env::args().collect(); if args.len() != 4 { eprintln!("Usage: {} <ip address> <port> <message>", args[0]); return Ok(()); } let broadcast_ip = args[1].as_str(); let broadcast_port: u16 = args[2].parse()?; let message = args[3].as_bytes(); let socket = UdpSocket::bind("0.0.0.0:0")?; socket.set_broadcast(true)?; loop { let bytes_sent = socket.send_to(message, (broadcast_ip, broadcast_port))?; println!("Sent {bytes_sent} bytes"); let sleep_sec = time::Duration::from_secs(3); thread::sleep(sleep_sec); } } 受信側 メッセージを受信したら、画面へ出力します。 use std::env; use std::net::UdpSocket; fn main() -> Result<(), Box<dyn std::error::Error>> { let args: Vec<String> = env::args().collect(); if args.len() != 2 { eprintln!("Usage: {} <port>", args[0]); return Ok(()); } let broadcast_port: u16 = args[1].parse()?; let socket = UdpSocket::bind(format!("0.0.0.0:{}", broadcast_port))?; // 自動割り当て let mut buf = [0; 32]; let (bytes_read, _) = socket.recv_from(&mut buf)?; println!("Received: {}", String::from_utf8_lossy(&buf[..bytes_read])); Ok(()) } 動作確認 255.255.255.255 へ送信すると、同じ LAN にある全てのホストへメッセージを送信できます。 送信側 $ cargo run '255.255.255.255' 9000 'Example' Sent 7 bytes Sent 7 bytes 受信側 $ cargo run 9000 Received: Example マルチキャスト 最後に、マルチキャストを実装してみます。 送信側 ブロードキャストと同じです。引数で指定されたメッセージを3秒おきにマルチキャストします。 マルチキャストでは set_multicast_ttl_v4 を使って TTL を設定できます。引数で指定されたときは、その値を TTL に設定します。指定されなかったときはデフォルト値 1 を設定します。 use std::env; use std::net::UdpSocket; use std::{thread, time}; fn main() -> Result<(), Box<dyn std::error::Error>> { let args: Vec<String> = env::args().collect(); if args.len() < 4 || args.len() > 5 { eprintln!("Usage: {} <ip address> <port> <message> [<ttl>]", args[0]); return Ok(()); } let multicast_ip = args[1].as_str(); let multicast_port: u16 = args[2].parse()?; let message = args[3].as_bytes(); let multicast_ttl: u32 = match args.get(4) { Some(ttl) => ttl.parse()?, None => 1, }; let socket = UdpSocket::bind("0.0.0.0:0")?; socket.set_multicast_ttl_v4(multicast_ttl)?; // TTL loop { let bytes_sent = socket.send_to(message, (multicast_ip, multicast_port))?; println!("Sent {bytes_sent} bytes"); let sleep_sec = time::Duration::from_secs(3); thread::sleep(sleep_sec); } } 受信側 メッセージを受信したら、画面へ出力します。 マルチキャストでは、join_multicast_v4 を使ってマルチキャストグループへ参加しておく必要があります。 use std::env; use std::net::Ipv4Addr; use std::net::UdpSocket; use std::str::FromStr; fn main() -> Result<(), Box<dyn std::error::Error>> { let args: Vec<String> = env::args().collect(); if args.len() != 3 { eprintln!("Usage: {} <ip address> <port>", args[0]); return Ok(()); } let multicast_ip = args[1].as_str(); let multicast_port: u16 = args[2].parse()?; let socket = UdpSocket::bind(format!("0.0.0.0:{}", multicast_port))?; let multicast_addr = Ipv4Addr::from_str(multicast_ip)?; socket.join_multicast_v4(&multicast_addr, &Ipv4Addr::UNSPECIFIED)?; let mut buf = [0; 32]; let (bytes_read, _) = socket.recv_from(&mut buf)?; println!("Received: {}", String::from_utf8_lossy(&buf[..bytes_read])); Ok(()) } 動作確認 マルチキャストアドレス(例: 239.1.2.3 )へ送信すると、マルチキャストグループに参加しているホストへメッセージを送信できます。 なお、マルチキャストアドレスについては下記参照。 マルチキャストIPアドレス - ネットワークエンジニアとして 送信側 $ cargo run '239.1.2.3' 9000 'Example' Sent 7 bytes Sent 7 bytes 受信側 $ cargo run '239.1.2.3' 9000 Received: Example 補足 iTerm2 iTerm2 でブロードキャストやマルチキャストを試そうとすると、 No route to host というエラーが表示されます。 送信側 $ cargo run '255.255.255.255' 9000 'Example' Error: Os { code: 65, kind: HostUnreachable, message: "No route to host" } これは Mac のセキュリティ制限によるものです。7:1 No route to host on iterm2 only 解決方法は2つあります。8 1つは、Mac 標準の「ターミナル」を使うことです。 もう1つは、 システム設定 > プライバシーとセキュリティ > ローカルネットワーク で iTerm を許可することです。 クロスコンパイル ブロードキャストやマルチキャストは、ローカルでやってても楽しくありません。古い Mac が家にあったので、そこへ送信することにしました。 しかし、開発環境は Apple Silicon なのに対し、古い Mac は(当然ですが) Intel Mac でした。思いがけずクロスコンパイルしないといけなくなりました。 クロスコンパイルは、場合によってはかなり面倒なことにもなるようですが、幸い、今回は2行でクロスコンパイルできました。 $ rustup target add x86_64-apple-darwin $ cargo build --release --target x86_64-apple-darwin 脚注 「前書き」の「対象読者」(p.7)に、読者を脅かすようなコードが書かれていますが、実際は C の入門書を1冊仕上げたことがあれば容易に理解できるレベルでした。C 以外では、UNIX のシグナルやプロセスなどの知識があったほうが良いと思います(私は OS の教科書を直前に読んでいたので困りませんでした)。API の使い方の説明がメインなので、プロトコルの解説は マスタリングTCP/IP 入門編 などで補う必要があります。また、原著は2002年の出版で IPv6 には対応していません(他にも古いところがありそう)。そんなに古くても、コードが今でもそのまま動くのは、さすが C 言語と言うべきでしょうか。 ↩︎ cargo clippy は、たとえば if args.len() < 1 と書くと、 is_empty を使えることをサジェストしてくれます。些細なことですが感動しましたね。 ↩︎ 「3ウェイハンドシェイク」に続けて、サーバからクライアントへ ACK しています。これは「ウィンドウスケーリング」で、受信ウィンドウサイズを拡大したことをクライアントへ通知しています。 『ハイパフォーマンス ブラウザネットワーキング』 の「2.2.1 フロー制御」のコラム参照。 ↩︎ 『TCP/IPソケットプログラミング C言語編』(p.58)には、次の方法が一般的と書かれています。「特定の文字(スペース文字など)、あるいは文字シーケンスをフィールドの区切り文字として使用し、メッセージを既知の数のフィールドで構成する」。 ↩︎ 『TCP/IPソケットプログラミング C言語編』(p.66)では recvfrom で送信元アドレスを取得してチェックしています。Rust の recv_from でも同様にチェックできますが、 connect のほうが簡単だと思いました。 ↩︎ 『TCP/IPソケットプログラミング C言語編』(p.70)では、この他に MSG_PEEK フラグを使う方法が紹介されています。 ↩︎ あっさり書きましたが、実際には ChatGPT の力を借りながら、この原因に辿り着くまでに数時間を溶かしました。 No route to host というエラーメッセージからは、一見、単なるネットワークの問題に見えます。しかし、実際は Mac のセキュリティ機能により、 iTerm2 によるブロードキャストやマルチキャストがブロックされていたのです。 ↩︎ ↩︎ 他に .plist ファイルを編集して Mac のデフォルト設定を変更してしまう方法もあるようですが、手順が煩雑でリスクもありそうだったのでやめました。 ↩︎
Rust in agentic workflows sure consumes a lot of disk space, right? If you want this fixed, here's something you and your employer should fund 👇 goals#830
> However this unfortunately abuses Poll::Pending, whose docs state: "When a function returns Pending, the function must also ensure that the current task is scheduled to be awoken when progress can be made." This definition is incompatible with how Fuse uses Pending, and so bringing it into the stdlib would require redefining Pending to mean both “pending” and “done”. This came up in issue #163678. To me it doesn't seem like there's any contradiction between that documented rule and a future that returns Ready once and then Pending forever after that. (Or for that matter, a future that only ever returns Pending.) If there's no further progress to be made, then returning Pending without scheduling any wakeups is reasonable. Specifically, an async fn future currently panics if you poll it again after it returned Ready. That seems like a fine way to catch mistakes early, at the cost of compiling in the panic machinery like you mentioned. But I don't think there would be anything fundamentally wrong with changing async fn futures to return Pending forever in that case instead. If I understand correctly, that's what the new/proposed compiler switch in that issue is doing. Seems like a good way to solve the problem? > The compiler can guarantee that calling .await never polls after getting Ready, and so it doesn’t need to handle the Done state. In this model can the compiler really _guarantee_ that .await is never going to see Done? An incorrect future could return Done before returning Ready. It presumably wouldn't be unsafe/unsound to do so? My intuition is that we'd need to lower .await to something that panicked in that case, which means we're not solving the problem we set out to solve. (Waiting forever on a future that returns Pending forever makes sense, but waiting forever on a future that returns Done doesn't make sense to me.) > This leads to a rule that we can apply recursively to every future: “When a future is polled, it must also poll all child futures that have had their waker called.” If every future follows this rule, no future will ever be snoozed. Incidentally this is now an RFC: rfcs#4019