Add: windows mvp - transparent bugs not fixed
This commit is contained in:
95
skills/coding-guidelines/SKILL.md
Normal file
95
skills/coding-guidelines/SKILL.md
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
name: coding-guidelines
|
||||
description: "Use when asking about Rust code style or best practices. Keywords: naming, formatting, comment, clippy, rustfmt, lint, code style, best practice, P.NAM, G.FMT, code review, naming convention, variable naming, function naming, type naming, 命名规范, 代码风格, 格式化, 最佳实践, 代码审查, 怎么命名"
|
||||
source: https://rust-coding-guidelines.github.io/rust-coding-guidelines-zh/
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Rust Coding Guidelines (50 Core Rules)
|
||||
|
||||
## Naming (Rust-Specific)
|
||||
|
||||
| Rule | Guideline |
|
||||
|------|-----------|
|
||||
| No `get_` prefix | `fn name()` not `fn get_name()` |
|
||||
| Iterator convention | `iter()` / `iter_mut()` / `into_iter()` |
|
||||
| Conversion naming | `as_` (cheap &), `to_` (expensive), `into_` (ownership) |
|
||||
| Static var prefix | `G_CONFIG` for `static`, no prefix for `const` |
|
||||
|
||||
## Data Types
|
||||
|
||||
| Rule | Guideline |
|
||||
|------|-----------|
|
||||
| Use newtypes | `struct Email(String)` for domain semantics |
|
||||
| Prefer slice patterns | `if let [first, .., last] = slice` |
|
||||
| Pre-allocate | `Vec::with_capacity()`, `String::with_capacity()` |
|
||||
| Avoid Vec abuse | Use arrays for fixed sizes |
|
||||
|
||||
## Strings
|
||||
|
||||
| Rule | Guideline |
|
||||
|------|-----------|
|
||||
| Prefer bytes | `s.bytes()` over `s.chars()` when ASCII |
|
||||
| Use `Cow<str>` | When might modify borrowed data |
|
||||
| Use `format!` | Over string concatenation with `+` |
|
||||
| Avoid nested iteration | `contains()` on string is O(n*m) |
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Rule | Guideline |
|
||||
|------|-----------|
|
||||
| Use `?` propagation | Not `try!()` macro |
|
||||
| `expect()` over `unwrap()` | When value guaranteed |
|
||||
| Assertions for invariants | `assert!` at function entry |
|
||||
|
||||
## Memory
|
||||
|
||||
| Rule | Guideline |
|
||||
|------|-----------|
|
||||
| Meaningful lifetimes | `'src`, `'ctx` not just `'a` |
|
||||
| `try_borrow()` for RefCell | Avoid panic |
|
||||
| Shadowing for transformation | `let x = x.parse()?` |
|
||||
|
||||
## Concurrency
|
||||
|
||||
| Rule | Guideline |
|
||||
|------|-----------|
|
||||
| Identify lock ordering | Prevent deadlocks |
|
||||
| Atomics for primitives | Not Mutex for bool/usize |
|
||||
| Choose memory order carefully | Relaxed/Acquire/Release/SeqCst |
|
||||
|
||||
## Async
|
||||
|
||||
| Rule | Guideline |
|
||||
|------|-----------|
|
||||
| Sync for CPU-bound | Async is for I/O |
|
||||
| Don't hold locks across await | Use scoped guards |
|
||||
|
||||
## Macros
|
||||
|
||||
| Rule | Guideline |
|
||||
|------|-----------|
|
||||
| Avoid unless necessary | Prefer functions/generics |
|
||||
| Follow Rust syntax | Macro input should look like Rust |
|
||||
|
||||
## Deprecated → Better
|
||||
|
||||
| Deprecated | Better | Since |
|
||||
|------------|--------|-------|
|
||||
| `lazy_static!` | `std::sync::OnceLock` | 1.70 |
|
||||
| `once_cell::Lazy` | `std::sync::LazyLock` | 1.80 |
|
||||
| `std::sync::mpsc` | `crossbeam::channel` | - |
|
||||
| `std::sync::Mutex` | `parking_lot::Mutex` | - |
|
||||
| `failure`/`error-chain` | `thiserror`/`anyhow` | - |
|
||||
| `try!()` | `?` operator | 2018 |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```
|
||||
Naming: snake_case (fn/var), CamelCase (type), SCREAMING_CASE (const)
|
||||
Format: rustfmt (just use it)
|
||||
Docs: /// for public items, //! for module docs
|
||||
Lint: #![warn(clippy::all)]
|
||||
```
|
||||
|
||||
Claude knows Rust conventions well. These are the non-obvious Rust-specific rules.
|
||||
16
skills/coding-guidelines/clippy-lints/_index.md
Normal file
16
skills/coding-guidelines/clippy-lints/_index.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Clippy Lint → Rule Mapping
|
||||
|
||||
| Clippy Lint | Category | Fix |
|
||||
|-------------|----------|-----|
|
||||
| `unwrap_used` | Error | Use `?` or `expect()` |
|
||||
| `needless_clone` | Perf | Use reference |
|
||||
| `await_holding_lock` | Async | Scope guard before await |
|
||||
| `linkedlist` | Perf | Use Vec/VecDeque |
|
||||
| `wildcard_imports` | Style | Explicit imports |
|
||||
| `missing_safety_doc` | Safety | Add `# Safety` doc |
|
||||
| `undocumented_unsafe_blocks` | Safety | Add `// SAFETY:` |
|
||||
| `transmute_ptr_to_ptr` | Safety | Use `pointer::cast()` |
|
||||
| `large_stack_arrays` | Mem | Use Vec or Box |
|
||||
| `too_many_arguments` | Design | Use struct params |
|
||||
|
||||
For unsafe-related lints → see `unsafe-checker` skill.
|
||||
6
skills/coding-guidelines/index/rules-index.md
Normal file
6
skills/coding-guidelines/index/rules-index.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Complete Rules Reference
|
||||
|
||||
For the full 500+ rules, see:
|
||||
- Source: https://rust-coding-guidelines.github.io/rust-coding-guidelines-zh/
|
||||
|
||||
Core rules are in `../SKILL.md`.
|
||||
49
skills/core-actionbook/SKILL.md
Normal file
49
skills/core-actionbook/SKILL.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: core-actionbook
|
||||
# Internal tool - no description to prevent auto-triggering
|
||||
# Used by: rust-learner agents for pre-computed selectors
|
||||
---
|
||||
|
||||
# Actionbook
|
||||
|
||||
Pre-computed action manuals for browser automation. Agents receive structured page information instead of parsing entire HTML.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **search_actions** - Search by keyword, returns URL-based action IDs with content previews
|
||||
2. **get_action_by_id** - Get full action manual with page details, DOM structure, and element selectors
|
||||
3. **Execute** - Use returned selectors with your browser automation tool
|
||||
|
||||
## MCP Tools
|
||||
|
||||
- `search_actions` - Search by keyword. Returns: URL-based action IDs, content previews, relevance scores
|
||||
- `get_action_by_id` - Get full action details. Returns: action content, page element selectors (CSS/XPath), element types, allowed methods (click, type, extract), document metadata
|
||||
|
||||
### Parameters
|
||||
|
||||
**search_actions**:
|
||||
- `query` (required): Search keyword (e.g., "airbnb search", "google login")
|
||||
- `type`: `vector` | `fulltext` | `hybrid` (default)
|
||||
- `limit`: Max results (default: 5)
|
||||
- `sourceIds`: Filter by source IDs (comma-separated)
|
||||
- `minScore`: Minimum relevance score (0-1)
|
||||
|
||||
**get_action_by_id**:
|
||||
- `id` (required): URL-based action ID (e.g., `example.com/page`)
|
||||
|
||||
## Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Airbnb Search",
|
||||
"url": "www.airbnb.com/search",
|
||||
"elements": [
|
||||
{
|
||||
"name": "location_input",
|
||||
"selector": "input[data-testid='structured-search-input-field-query']",
|
||||
"type": "textbox",
|
||||
"methods": ["type", "fill"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
115
skills/core-agent-browser/SKILL.md
Normal file
115
skills/core-agent-browser/SKILL.md
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
name: core-agent-browser
|
||||
# Internal tool - no description to prevent auto-triggering
|
||||
# Used by: rust-learner, docs-researcher, crate-researcher agents
|
||||
---
|
||||
|
||||
# Browser Automation with agent-browser
|
||||
|
||||
## Priority Note
|
||||
|
||||
For fetching Rust/crate information, use this priority order:
|
||||
1. **rust-learner skill** - Orchestrates actionbook + browser-fetcher
|
||||
2. **actionbook MCP** - Pre-computed selectors for known sites
|
||||
3. **agent-browser CLI** - Direct browser automation (last resort)
|
||||
|
||||
Use agent-browser directly only when:
|
||||
- actionbook has no pre-computed selectors for the target site
|
||||
- You need interactive browser testing/automation
|
||||
- You need screenshots or form filling
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
agent-browser open <url> # Navigate to page
|
||||
agent-browser snapshot -i # Get interactive elements with refs
|
||||
agent-browser click @e1 # Click element by ref
|
||||
agent-browser fill @e2 "text" # Fill input by ref
|
||||
agent-browser close # Close browser
|
||||
```
|
||||
|
||||
## Core workflow
|
||||
|
||||
1. Navigate: `agent-browser open <url>`
|
||||
2. Snapshot: `agent-browser snapshot -i` (returns elements with refs like `@e1`, `@e2`)
|
||||
3. Interact using refs from the snapshot
|
||||
4. Re-snapshot after navigation or significant DOM changes
|
||||
|
||||
## Commands
|
||||
|
||||
### Navigation
|
||||
```bash
|
||||
agent-browser open <url> # Navigate to URL
|
||||
agent-browser back # Go back
|
||||
agent-browser forward # Go forward
|
||||
agent-browser reload # Reload page
|
||||
agent-browser close # Close browser
|
||||
```
|
||||
|
||||
### Snapshot (page analysis)
|
||||
```bash
|
||||
agent-browser snapshot # Full accessibility tree
|
||||
agent-browser snapshot -i # Interactive elements only (recommended)
|
||||
agent-browser snapshot -c # Compact output
|
||||
agent-browser snapshot -d 3 # Limit depth to 3
|
||||
```
|
||||
|
||||
### Interactions (use @refs from snapshot)
|
||||
```bash
|
||||
agent-browser click @e1 # Click
|
||||
agent-browser dblclick @e1 # Double-click
|
||||
agent-browser fill @e2 "text" # Clear and type
|
||||
agent-browser type @e2 "text" # Type without clearing
|
||||
agent-browser press Enter # Press key
|
||||
agent-browser press Control+a # Key combination
|
||||
agent-browser hover @e1 # Hover
|
||||
agent-browser check @e1 # Check checkbox
|
||||
agent-browser uncheck @e1 # Uncheck checkbox
|
||||
agent-browser select @e1 "value" # Select dropdown
|
||||
agent-browser scroll down 500 # Scroll page
|
||||
agent-browser scrollintoview @e1 # Scroll element into view
|
||||
```
|
||||
|
||||
### Get information
|
||||
```bash
|
||||
agent-browser get text @e1 # Get element text
|
||||
agent-browser get value @e1 # Get input value
|
||||
agent-browser get title # Get page title
|
||||
agent-browser get url # Get current URL
|
||||
```
|
||||
|
||||
### Screenshots
|
||||
```bash
|
||||
agent-browser screenshot # Screenshot to stdout
|
||||
agent-browser screenshot path.png # Save to file
|
||||
agent-browser screenshot --full # Full page
|
||||
```
|
||||
|
||||
### Wait
|
||||
```bash
|
||||
agent-browser wait @e1 # Wait for element
|
||||
agent-browser wait 2000 # Wait milliseconds
|
||||
agent-browser wait --text "Success" # Wait for text
|
||||
agent-browser wait --load networkidle # Wait for network idle
|
||||
```
|
||||
|
||||
### Semantic locators (alternative to refs)
|
||||
```bash
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find text "Sign In" click
|
||||
agent-browser find label "Email" fill "user@test.com"
|
||||
```
|
||||
|
||||
## Example: Form submission
|
||||
|
||||
```bash
|
||||
agent-browser open https://example.com/form
|
||||
agent-browser snapshot -i
|
||||
# Output shows: textbox "Email" [ref=e1], textbox "Password" [ref=e2], button "Submit" [ref=e3]
|
||||
|
||||
agent-browser fill @e1 "user@example.com"
|
||||
agent-browser fill @e2 "password123"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --load networkidle
|
||||
agent-browser snapshot -i # Check result
|
||||
```
|
||||
216
skills/core-dynamic-skills/SKILL.md
Normal file
216
skills/core-dynamic-skills/SKILL.md
Normal file
@@ -0,0 +1,216 @@
|
||||
---
|
||||
name: core-dynamic-skills
|
||||
# Command-based tool - no description to prevent auto-triggering
|
||||
# Triggered by: /sync-crate-skills, /clean-crate-skills, /update-crate-skill
|
||||
argument-hint: "[--force] | <crate_name>"
|
||||
context: fork
|
||||
agent: general-purpose
|
||||
---
|
||||
|
||||
# Dynamic Skills Manager
|
||||
|
||||
> **Version:** 2.1.0 | **Last Updated:** 2025-01-27
|
||||
|
||||
Orchestrates on-demand generation of crate-specific skills based on project dependencies.
|
||||
|
||||
## Concept
|
||||
|
||||
Dynamic skills are:
|
||||
- Generated locally at `~/.claude/skills/`
|
||||
- Based on Cargo.toml dependencies
|
||||
- Created using llms.txt from docs.rs
|
||||
- Versioned and updatable
|
||||
- Not committed to the rust-skills repository
|
||||
|
||||
## Trigger Scenarios
|
||||
|
||||
### Prompt-on-Open
|
||||
|
||||
When entering a directory with Cargo.toml:
|
||||
1. Detect Cargo.toml (single or workspace)
|
||||
2. Parse dependencies list
|
||||
3. Check which crates are missing skills
|
||||
4. If missing: "Found X dependencies without skills. Sync now?"
|
||||
5. If confirmed: run `/sync-crate-skills`
|
||||
|
||||
### Manual Commands
|
||||
|
||||
- `/sync-crate-skills` - Sync all dependencies
|
||||
- `/clean-crate-skills [crate]` - Remove skills
|
||||
- `/update-crate-skill <crate>` - Update specific skill
|
||||
|
||||
## Execution Mode Detection
|
||||
|
||||
**CRITICAL: Check if agent and command infrastructure is available.**
|
||||
|
||||
Try to read: `../../agents/` directory
|
||||
Check if `/create-llms-for-skills` and `/create-skills-via-llms` commands work.
|
||||
|
||||
---
|
||||
|
||||
## Agent Mode (Plugin Install)
|
||||
|
||||
**When full plugin infrastructure is available:**
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
Cargo.toml
|
||||
↓
|
||||
Parse dependencies
|
||||
↓
|
||||
For each crate:
|
||||
├─ Check ~/.claude/skills/{crate}/
|
||||
├─ If missing: Check actionbook for llms.txt
|
||||
│ ├─ Found: /create-skills-via-llms
|
||||
│ └─ Not found: /create-llms-for-skills first
|
||||
└─ Load skill
|
||||
```
|
||||
|
||||
### Workflow Priority
|
||||
|
||||
1. **actionbook MCP** - Check for pre-generated llms.txt
|
||||
2. **/create-llms-for-skills** - Generate llms.txt from docs.rs
|
||||
3. **/create-skills-via-llms** - Create skills from llms.txt
|
||||
|
||||
### Sync Command
|
||||
|
||||
```bash
|
||||
/sync-crate-skills [--force]
|
||||
```
|
||||
|
||||
1. Parse Cargo.toml for dependencies
|
||||
2. For each dependency:
|
||||
- Check if skill exists at `~/.claude/skills/{crate}/`
|
||||
- If missing (or --force): generate skill
|
||||
3. Report results
|
||||
|
||||
---
|
||||
|
||||
## Inline Mode (Skills-only Install)
|
||||
|
||||
**When agent/command infrastructure is NOT available, execute manually:**
|
||||
|
||||
### Step 1: Parse Cargo.toml
|
||||
|
||||
```bash
|
||||
# Read dependencies
|
||||
cat Cargo.toml | grep -A 100 '\[dependencies\]' | grep -E '^[a-zA-Z]'
|
||||
```
|
||||
|
||||
Or use Read tool to parse Cargo.toml and extract:
|
||||
- `[dependencies]` section
|
||||
- `[dev-dependencies]` section (optional)
|
||||
- Workspace members (if workspace project)
|
||||
|
||||
### Step 2: Check Existing Skills
|
||||
|
||||
```bash
|
||||
# List existing skills
|
||||
ls ~/.claude/skills/
|
||||
```
|
||||
|
||||
Compare with dependencies to find missing skills.
|
||||
|
||||
### Step 3: Generate Missing Skills
|
||||
|
||||
For each missing crate:
|
||||
|
||||
```bash
|
||||
# 1. Fetch crate documentation
|
||||
agent-browser open "https://docs.rs/{crate}/latest/{crate}/"
|
||||
agent-browser get text ".docblock"
|
||||
# Save content
|
||||
|
||||
# 2. Create skill directory
|
||||
mkdir -p ~/.claude/skills/{crate}
|
||||
mkdir -p ~/.claude/skills/{crate}/references
|
||||
|
||||
# 3. Create SKILL.md
|
||||
# Use template from rust-skill-creator inline mode
|
||||
|
||||
# 4. Create reference files for key modules
|
||||
agent-browser open "https://docs.rs/{crate}/latest/{crate}/{module}/"
|
||||
agent-browser get text ".docblock"
|
||||
# Save to ~/.claude/skills/{crate}/references/{module}.md
|
||||
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
**WebFetch fallback:**
|
||||
```
|
||||
WebFetch("https://docs.rs/{crate}/latest/{crate}/", "Extract API documentation overview, key types, and usage examples")
|
||||
```
|
||||
|
||||
### Step 4: Workspace Support
|
||||
|
||||
For Cargo workspace projects:
|
||||
|
||||
```bash
|
||||
# 1. Parse root Cargo.toml for workspace members
|
||||
cat Cargo.toml | grep -A 10 '\[workspace\]'
|
||||
|
||||
# 2. For each member, parse their Cargo.toml
|
||||
for member in members; do
|
||||
cat ${member}/Cargo.toml | grep -A 100 '\[dependencies\]'
|
||||
done
|
||||
|
||||
# 3. Aggregate and deduplicate dependencies
|
||||
# 4. Generate skills for missing crates
|
||||
```
|
||||
|
||||
### Clean Command (Inline)
|
||||
|
||||
```bash
|
||||
# Clean specific crate
|
||||
rm -rf ~/.claude/skills/{crate_name}
|
||||
|
||||
# Clean all generated skills
|
||||
rm -rf ~/.claude/skills/*
|
||||
```
|
||||
|
||||
### Update Command (Inline)
|
||||
|
||||
```bash
|
||||
# Remove old skill
|
||||
rm -rf ~/.claude/skills/{crate_name}
|
||||
|
||||
# Re-generate (same as sync for single crate)
|
||||
# Follow Step 3 above for the specific crate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Local Skills Directory
|
||||
|
||||
```
|
||||
~/.claude/skills/
|
||||
├── tokio/
|
||||
│ ├── SKILL.md
|
||||
│ └── references/
|
||||
├── serde/
|
||||
│ ├── SKILL.md
|
||||
│ └── references/
|
||||
└── axum/
|
||||
├── SKILL.md
|
||||
└── references/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Commands
|
||||
|
||||
- `/sync-crate-skills` - Main sync command
|
||||
- `/clean-crate-skills` - Cleanup command
|
||||
- `/update-crate-skill` - Update command
|
||||
- `/create-llms-for-skills` - Generate llms.txt (Agent Mode only)
|
||||
- `/create-skills-via-llms` - Create skills from llms.txt (Agent Mode only)
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| Commands not found | Skills-only install | Use inline mode |
|
||||
| Cargo.toml not found | Not in Rust project | Navigate to project root |
|
||||
| docs.rs unavailable | Network issue | Retry or skip crate |
|
||||
| Permission denied | Directory issue | Check ~/.claude/skills/ permissions |
|
||||
249
skills/core-fix-skill-docs/SKILL.md
Normal file
249
skills/core-fix-skill-docs/SKILL.md
Normal file
@@ -0,0 +1,249 @@
|
||||
---
|
||||
name: core-fix-skill-docs
|
||||
# Internal maintenance tool - no description to prevent auto-triggering
|
||||
# Triggered by: /fix-skill-docs command
|
||||
argument-hint: "[crate_name] [--check-only]"
|
||||
context: fork
|
||||
agent: general-purpose
|
||||
---
|
||||
|
||||
# Fix Skill Documentation
|
||||
|
||||
> **Version:** 2.1.0 | **Last Updated:** 2025-01-27
|
||||
|
||||
Check and fix missing reference files in dynamic skills.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/fix-skill-docs [crate_name] [--check-only] [--remove-invalid]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `crate_name`: Specific crate to check (optional, defaults to all)
|
||||
- `--check-only`: Only report issues, don't fix
|
||||
- `--remove-invalid`: Remove invalid references instead of creating files
|
||||
|
||||
## Execution Mode Detection
|
||||
|
||||
**CRITICAL: Check if agent infrastructure is available.**
|
||||
|
||||
This skill can run in two modes:
|
||||
- **Agent Mode**: Uses background agents for documentation fetching
|
||||
- **Inline Mode**: Executes directly using agent-browser CLI or WebFetch
|
||||
|
||||
---
|
||||
|
||||
## Agent Mode (Plugin Install)
|
||||
|
||||
**When agent infrastructure is available, use background agents for fetching:**
|
||||
|
||||
### Instructions
|
||||
|
||||
#### 1. Scan Skills Directory
|
||||
|
||||
```bash
|
||||
# If crate_name provided
|
||||
skill_dir=~/.claude/skills/{crate_name}
|
||||
|
||||
# Otherwise scan all
|
||||
for dir in ~/.claude/skills/*/; do
|
||||
# Process each skill
|
||||
done
|
||||
```
|
||||
|
||||
#### 2. Parse SKILL.md for References
|
||||
|
||||
Extract referenced files from Documentation section:
|
||||
|
||||
```markdown
|
||||
## Documentation
|
||||
- `./references/file1.md` - Description
|
||||
```
|
||||
|
||||
#### 3. Check File Existence
|
||||
|
||||
```bash
|
||||
if [ ! -f "{skill_dir}/references/{filename}" ]; then
|
||||
echo "MISSING: {filename}"
|
||||
fi
|
||||
```
|
||||
|
||||
#### 4. Report Status
|
||||
|
||||
```
|
||||
=== {crate_name} ===
|
||||
SKILL.md: OK
|
||||
references/:
|
||||
- sync.md: OK
|
||||
- runtime.md: MISSING
|
||||
|
||||
Action needed: 1 file missing
|
||||
```
|
||||
|
||||
#### 5. Fix Missing Files (Agent Mode)
|
||||
|
||||
Launch background agent to fetch documentation:
|
||||
|
||||
```
|
||||
Task(
|
||||
subagent_type: "general-purpose",
|
||||
run_in_background: true,
|
||||
prompt: "Fetch documentation for {crate_name}/{module} from docs.rs.
|
||||
Use agent-browser CLI to navigate to https://docs.rs/{crate_name}/latest/{crate_name}/{module}/
|
||||
Extract the main documentation and save to ~/.claude/skills/{crate_name}/references/{module}.md"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Inline Mode (Skills-only Install)
|
||||
|
||||
**When agent infrastructure is NOT available, execute directly:**
|
||||
|
||||
### Step 1: Scan Skills Directory
|
||||
|
||||
```bash
|
||||
# List all skills
|
||||
ls ~/.claude/skills/
|
||||
|
||||
# Or check specific skill
|
||||
ls ~/.claude/skills/{crate_name}/
|
||||
```
|
||||
|
||||
### Step 2: Parse SKILL.md for References
|
||||
|
||||
Read SKILL.md and extract all `./references/*.md` patterns:
|
||||
|
||||
```bash
|
||||
# Using Read tool
|
||||
Read("~/.claude/skills/{crate_name}/SKILL.md")
|
||||
|
||||
# Look for lines like:
|
||||
# - `./references/sync.md` - Sync primitives
|
||||
# - `./references/runtime.md` - Runtime configuration
|
||||
```
|
||||
|
||||
### Step 3: Check File Existence
|
||||
|
||||
```bash
|
||||
# Check each referenced file
|
||||
for ref in references; do
|
||||
if [ ! -f "~/.claude/skills/{crate_name}/references/${ref}.md" ]; then
|
||||
echo "MISSING: ${ref}.md"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### Step 4: Report Status
|
||||
|
||||
Output format:
|
||||
```
|
||||
=== {crate_name} ===
|
||||
SKILL.md: OK
|
||||
references/:
|
||||
- sync.md: OK
|
||||
- runtime.md: MISSING
|
||||
|
||||
Action needed: 1 file missing
|
||||
```
|
||||
|
||||
### Step 5: Fix Missing Files (Inline)
|
||||
|
||||
For each missing file:
|
||||
|
||||
**Using agent-browser CLI:**
|
||||
```bash
|
||||
agent-browser open "https://docs.rs/{crate_name}/latest/{crate_name}/{module}/"
|
||||
agent-browser get text ".docblock"
|
||||
# Save output to ~/.claude/skills/{crate_name}/references/{module}.md
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
**Using WebFetch fallback:**
|
||||
```
|
||||
WebFetch("https://docs.rs/{crate_name}/latest/{crate_name}/{module}/",
|
||||
"Extract the main documentation content for this module")
|
||||
```
|
||||
|
||||
Then write the content:
|
||||
```bash
|
||||
Write("~/.claude/skills/{crate_name}/references/{module}.md", <fetched_content>)
|
||||
```
|
||||
|
||||
### Step 6: Update SKILL.md (if --remove-invalid)
|
||||
|
||||
If `--remove-invalid` flag is set and file cannot be fetched:
|
||||
|
||||
```bash
|
||||
# Read current SKILL.md
|
||||
Read("~/.claude/skills/{crate_name}/SKILL.md")
|
||||
|
||||
# Remove the invalid reference line
|
||||
Edit("~/.claude/skills/{crate_name}/SKILL.md",
|
||||
old_string="- `./references/{invalid_file}.md` - Description",
|
||||
new_string="")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool Priority
|
||||
|
||||
1. **agent-browser CLI** - Primary tool for fetching documentation
|
||||
2. **WebFetch** - Fallback if agent-browser unavailable
|
||||
3. **Edit SKILL.md** - For removing invalid references (--remove-invalid only)
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Check All Skills (--check-only)
|
||||
|
||||
```bash
|
||||
/fix-skill-docs --check-only
|
||||
|
||||
# Output:
|
||||
=== tokio ===
|
||||
SKILL.md: OK
|
||||
references/:
|
||||
- sync.md: OK
|
||||
- runtime.md: MISSING
|
||||
- task.md: OK
|
||||
|
||||
=== serde ===
|
||||
SKILL.md: OK
|
||||
references/:
|
||||
- derive.md: OK
|
||||
|
||||
Summary: 1 file missing in 1 skill
|
||||
```
|
||||
|
||||
### Fix Specific Crate
|
||||
|
||||
```bash
|
||||
/fix-skill-docs tokio
|
||||
|
||||
# Fetches missing runtime.md from docs.rs
|
||||
# Reports success
|
||||
```
|
||||
|
||||
### Remove Invalid References
|
||||
|
||||
```bash
|
||||
/fix-skill-docs tokio --remove-invalid
|
||||
|
||||
# If runtime.md cannot be fetched:
|
||||
# Removes reference from SKILL.md instead
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| Agent not available | Skills-only install | Use inline mode |
|
||||
| Skills directory empty | No skills installed | Run /sync-crate-skills first |
|
||||
| docs.rs unavailable | Network issue | Retry or use --remove-invalid |
|
||||
| Permission denied | Directory issue | Check ~/.claude/skills/ permissions |
|
||||
| Invalid SKILL.md format | Corrupted skill | Re-generate skill |
|
||||
161
skills/domain-cli/SKILL.md
Normal file
161
skills/domain-cli/SKILL.md
Normal file
@@ -0,0 +1,161 @@
|
||||
---
|
||||
name: domain-cli
|
||||
description: "Use when building CLI tools. Keywords: CLI, command line, terminal, clap, structopt, argument parsing, subcommand, interactive, TUI, ratatui, crossterm, indicatif, progress bar, colored output, shell completion, config file, environment variable, 命令行, 终端应用, 参数解析"
|
||||
globs: ["**/Cargo.toml"]
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# CLI Domain
|
||||
|
||||
> **Layer 3: Domain Constraints**
|
||||
|
||||
## Domain Constraints → Design Implications
|
||||
|
||||
| Domain Rule | Design Constraint | Rust Implication |
|
||||
|-------------|-------------------|------------------|
|
||||
| User ergonomics | Clear help, errors | clap derive macros |
|
||||
| Config precedence | CLI > env > file | Layered config loading |
|
||||
| Exit codes | Non-zero on error | Proper Result handling |
|
||||
| Stdout/stderr | Data vs errors | eprintln! for errors |
|
||||
| Interruptible | Handle Ctrl+C | Signal handling |
|
||||
|
||||
---
|
||||
|
||||
## Critical Constraints
|
||||
|
||||
### User Communication
|
||||
|
||||
```
|
||||
RULE: Errors to stderr, data to stdout
|
||||
WHY: Pipeable output, scriptability
|
||||
RUST: eprintln! for errors, println! for data
|
||||
```
|
||||
|
||||
### Configuration Priority
|
||||
|
||||
```
|
||||
RULE: CLI args > env vars > config file > defaults
|
||||
WHY: User expectation, override capability
|
||||
RUST: Layered config with clap + figment/config
|
||||
```
|
||||
|
||||
### Exit Codes
|
||||
|
||||
```
|
||||
RULE: Return non-zero on any error
|
||||
WHY: Script integration, automation
|
||||
RUST: main() -> Result<(), Error> or explicit exit()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From constraints to design (Layer 2):
|
||||
|
||||
```
|
||||
"Need argument parsing"
|
||||
↓ m05-type-driven: Derive structs for args
|
||||
↓ clap: #[derive(Parser)]
|
||||
|
||||
"Need config layering"
|
||||
↓ m09-domain: Config as domain object
|
||||
↓ figment/config: Layer sources
|
||||
|
||||
"Need progress display"
|
||||
↓ m12-lifecycle: Progress bar as RAII
|
||||
↓ indicatif: ProgressBar
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Crates
|
||||
|
||||
| Purpose | Crate |
|
||||
|---------|-------|
|
||||
| Argument parsing | clap |
|
||||
| Interactive prompts | dialoguer |
|
||||
| Progress bars | indicatif |
|
||||
| Colored output | colored |
|
||||
| Terminal UI | ratatui |
|
||||
| Terminal control | crossterm |
|
||||
| Console utilities | console |
|
||||
|
||||
## Design Patterns
|
||||
|
||||
| Pattern | Purpose | Implementation |
|
||||
|---------|---------|----------------|
|
||||
| Args struct | Type-safe args | `#[derive(Parser)]` |
|
||||
| Subcommands | Command hierarchy | `#[derive(Subcommand)]` |
|
||||
| Config layers | Override precedence | CLI > env > file |
|
||||
| Progress | User feedback | `ProgressBar::new(len)` |
|
||||
|
||||
## Code Pattern: CLI Structure
|
||||
|
||||
```rust
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "myapp", about = "My CLI tool")]
|
||||
struct Cli {
|
||||
/// Enable verbose output
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Initialize a new project
|
||||
Init { name: String },
|
||||
/// Run the application
|
||||
Run {
|
||||
#[arg(short, long)]
|
||||
port: Option<u16>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
Commands::Init { name } => init_project(&name)?,
|
||||
Commands::Run { port } => run_server(port.unwrap_or(8080))?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Domain Violation | Fix |
|
||||
|---------|-----------------|-----|
|
||||
| Errors to stdout | Breaks piping | eprintln! |
|
||||
| No help text | Poor UX | #[arg(help = "...")] |
|
||||
| Panic on error | Bad exit code | Result + proper handling |
|
||||
| No progress for long ops | User uncertainty | indicatif |
|
||||
|
||||
---
|
||||
|
||||
## Trace to Layer 1
|
||||
|
||||
| Constraint | Layer 2 Pattern | Layer 1 Implementation |
|
||||
|------------|-----------------|------------------------|
|
||||
| Type-safe args | Derive macros | clap Parser |
|
||||
| Error handling | Result propagation | anyhow + exit codes |
|
||||
| User feedback | Progress RAII | indicatif ProgressBar |
|
||||
| Config precedence | Builder pattern | Layered sources |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Error handling | m06-error-handling |
|
||||
| Type-driven args | m05-type-driven |
|
||||
| Progress lifecycle | m12-lifecycle |
|
||||
| Async CLI | m07-concurrency |
|
||||
166
skills/domain-cloud-native/SKILL.md
Normal file
166
skills/domain-cloud-native/SKILL.md
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
name: domain-cloud-native
|
||||
description: "Use when building cloud-native apps. Keywords: kubernetes, k8s, docker, container, grpc, tonic, microservice, service mesh, observability, tracing, metrics, health check, cloud, deployment, 云原生, 微服务, 容器"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Cloud-Native Domain
|
||||
|
||||
> **Layer 3: Domain Constraints**
|
||||
|
||||
## Domain Constraints → Design Implications
|
||||
|
||||
| Domain Rule | Design Constraint | Rust Implication |
|
||||
|-------------|-------------------|------------------|
|
||||
| 12-Factor | Config from env | Environment-based config |
|
||||
| Observability | Metrics + traces | tracing + opentelemetry |
|
||||
| Health checks | Liveness/readiness | Dedicated endpoints |
|
||||
| Graceful shutdown | Clean termination | Signal handling |
|
||||
| Horizontal scale | Stateless design | No local state |
|
||||
| Container-friendly | Small binaries | Release optimization |
|
||||
|
||||
---
|
||||
|
||||
## Critical Constraints
|
||||
|
||||
### Stateless Design
|
||||
|
||||
```
|
||||
RULE: No local persistent state
|
||||
WHY: Pods can be killed/rescheduled anytime
|
||||
RUST: External state (Redis, DB), no static mut
|
||||
```
|
||||
|
||||
### Graceful Shutdown
|
||||
|
||||
```
|
||||
RULE: Handle SIGTERM, drain connections
|
||||
WHY: Zero-downtime deployments
|
||||
RUST: tokio::signal + graceful shutdown
|
||||
```
|
||||
|
||||
### Observability
|
||||
|
||||
```
|
||||
RULE: Every request must be traceable
|
||||
WHY: Debugging distributed systems
|
||||
RUST: tracing spans, opentelemetry export
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From constraints to design (Layer 2):
|
||||
|
||||
```
|
||||
"Need distributed tracing"
|
||||
↓ m12-lifecycle: Span lifecycle
|
||||
↓ tracing + opentelemetry
|
||||
|
||||
"Need graceful shutdown"
|
||||
↓ m07-concurrency: Signal handling
|
||||
↓ m12-lifecycle: Connection draining
|
||||
|
||||
"Need health checks"
|
||||
↓ domain-web: HTTP endpoints
|
||||
↓ m06-error-handling: Health status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Crates
|
||||
|
||||
| Purpose | Crate |
|
||||
|---------|-------|
|
||||
| gRPC | tonic |
|
||||
| Kubernetes | kube, kube-runtime |
|
||||
| Docker | bollard |
|
||||
| Tracing | tracing, opentelemetry |
|
||||
| Metrics | prometheus, metrics |
|
||||
| Config | config, figment |
|
||||
| Health | HTTP endpoints |
|
||||
|
||||
## Design Patterns
|
||||
|
||||
| Pattern | Purpose | Implementation |
|
||||
|---------|---------|----------------|
|
||||
| gRPC services | Service mesh | tonic + tower |
|
||||
| K8s operators | Custom resources | kube-runtime Controller |
|
||||
| Observability | Debugging | tracing + OTEL |
|
||||
| Health checks | Orchestration | `/health`, `/ready` |
|
||||
| Config | 12-factor | Env vars + secrets |
|
||||
|
||||
## Code Pattern: Graceful Shutdown
|
||||
|
||||
```rust
|
||||
use tokio::signal;
|
||||
|
||||
async fn run_server() -> anyhow::Result<()> {
|
||||
let app = Router::new()
|
||||
.route("/health", get(health))
|
||||
.route("/ready", get(ready));
|
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
|
||||
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
signal::ctrl_c().await.expect("failed to listen for ctrl+c");
|
||||
tracing::info!("shutdown signal received");
|
||||
}
|
||||
```
|
||||
|
||||
## Health Check Pattern
|
||||
|
||||
```rust
|
||||
async fn health() -> StatusCode {
|
||||
StatusCode::OK
|
||||
}
|
||||
|
||||
async fn ready(State(db): State<Arc<DbPool>>) -> StatusCode {
|
||||
match db.ping().await {
|
||||
Ok(_) => StatusCode::OK,
|
||||
Err(_) => StatusCode::SERVICE_UNAVAILABLE,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Domain Violation | Fix |
|
||||
|---------|-----------------|-----|
|
||||
| Local file state | Not stateless | External storage |
|
||||
| No SIGTERM handling | Hard kills | Graceful shutdown |
|
||||
| No tracing | Can't debug | tracing spans |
|
||||
| Static config | Not 12-factor | Env vars |
|
||||
|
||||
---
|
||||
|
||||
## Trace to Layer 1
|
||||
|
||||
| Constraint | Layer 2 Pattern | Layer 1 Implementation |
|
||||
|------------|-----------------|------------------------|
|
||||
| Stateless | External state | Arc<Client> for external |
|
||||
| Graceful shutdown | Signal handling | tokio::signal |
|
||||
| Tracing | Span lifecycle | tracing + OTEL |
|
||||
| Health checks | HTTP endpoints | Dedicated routes |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Async patterns | m07-concurrency |
|
||||
| HTTP endpoints | domain-web |
|
||||
| Error handling | m13-domain-error |
|
||||
| Resource lifecycle | m12-lifecycle |
|
||||
178
skills/domain-embedded/SKILL.md
Normal file
178
skills/domain-embedded/SKILL.md
Normal file
@@ -0,0 +1,178 @@
|
||||
---
|
||||
name: domain-embedded
|
||||
description: "Use when developing embedded/no_std Rust. Keywords: embedded, no_std, microcontroller, MCU, ARM, RISC-V, bare metal, firmware, HAL, PAC, RTIC, embassy, interrupt, DMA, peripheral, GPIO, SPI, I2C, UART, embedded-hal, cortex-m, esp32, stm32, nrf, 嵌入式, 单片机, 固件, 裸机"
|
||||
globs: ["**/Cargo.toml", "**/.cargo/config.toml"]
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
## Project Context (Auto-Injected)
|
||||
|
||||
**Target configuration:**
|
||||
!`cat .cargo/config.toml 2>/dev/null || echo "No .cargo/config.toml found"`
|
||||
|
||||
---
|
||||
|
||||
# Embedded Domain
|
||||
|
||||
> **Layer 3: Domain Constraints**
|
||||
|
||||
## Domain Constraints → Design Implications
|
||||
|
||||
| Domain Rule | Design Constraint | Rust Implication |
|
||||
|-------------|-------------------|------------------|
|
||||
| No heap | Stack allocation | heapless, no Box/Vec |
|
||||
| No std | Core only | #![no_std] |
|
||||
| Real-time | Predictable timing | No dynamic alloc |
|
||||
| Resource limited | Minimal memory | Static buffers |
|
||||
| Hardware safety | Safe peripheral access | HAL + ownership |
|
||||
| Interrupt safe | No blocking in ISR | Atomic, critical sections |
|
||||
|
||||
---
|
||||
|
||||
## Critical Constraints
|
||||
|
||||
### No Dynamic Allocation
|
||||
|
||||
```
|
||||
RULE: Cannot use heap (no allocator)
|
||||
WHY: Deterministic memory, no OOM
|
||||
RUST: heapless::Vec<T, N>, arrays
|
||||
```
|
||||
|
||||
### Interrupt Safety
|
||||
|
||||
```
|
||||
RULE: Shared state must be interrupt-safe
|
||||
WHY: ISR can preempt at any time
|
||||
RUST: Mutex<RefCell<T>> + critical section
|
||||
```
|
||||
|
||||
### Hardware Ownership
|
||||
|
||||
```
|
||||
RULE: Peripherals must have clear ownership
|
||||
WHY: Prevent conflicting access
|
||||
RUST: HAL takes ownership, singletons
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From constraints to design (Layer 2):
|
||||
|
||||
```
|
||||
"Need no_std compatible data structures"
|
||||
↓ m02-resource: heapless collections
|
||||
↓ Static sizing: heapless::Vec<T, N>
|
||||
|
||||
"Need interrupt-safe state"
|
||||
↓ m03-mutability: Mutex<RefCell<Option<T>>>
|
||||
↓ m07-concurrency: Critical sections
|
||||
|
||||
"Need peripheral ownership"
|
||||
↓ m01-ownership: Singleton pattern
|
||||
↓ m12-lifecycle: RAII for hardware
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layer Stack
|
||||
|
||||
| Layer | Examples | Purpose |
|
||||
|-------|----------|---------|
|
||||
| PAC | stm32f4, esp32c3 | Register access |
|
||||
| HAL | stm32f4xx-hal | Hardware abstraction |
|
||||
| Framework | RTIC, Embassy | Concurrency |
|
||||
| Traits | embedded-hal | Portable drivers |
|
||||
|
||||
## Framework Comparison
|
||||
|
||||
| Framework | Style | Best For |
|
||||
|-----------|-------|----------|
|
||||
| RTIC | Priority-based | Interrupt-driven apps |
|
||||
| Embassy | Async | Complex state machines |
|
||||
| Bare metal | Manual | Simple apps |
|
||||
|
||||
## Key Crates
|
||||
|
||||
| Purpose | Crate |
|
||||
|---------|-------|
|
||||
| Runtime (ARM) | cortex-m-rt |
|
||||
| Panic handler | panic-halt, panic-probe |
|
||||
| Collections | heapless |
|
||||
| HAL traits | embedded-hal |
|
||||
| Logging | defmt |
|
||||
| Flash/debug | probe-run |
|
||||
|
||||
## Design Patterns
|
||||
|
||||
| Pattern | Purpose | Implementation |
|
||||
|---------|---------|----------------|
|
||||
| no_std setup | Bare metal | `#![no_std]` + `#![no_main]` |
|
||||
| Entry point | Startup | `#[entry]` or embassy |
|
||||
| Static state | ISR access | `Mutex<RefCell<Option<T>>>` |
|
||||
| Fixed buffers | No heap | `heapless::Vec<T, N>` |
|
||||
|
||||
## Code Pattern: Static Peripheral
|
||||
|
||||
```rust
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use cortex_m::interrupt::{self, Mutex};
|
||||
use core::cell::RefCell;
|
||||
|
||||
static LED: Mutex<RefCell<Option<Led>>> = Mutex::new(RefCell::new(None));
|
||||
|
||||
#[entry]
|
||||
fn main() -> ! {
|
||||
let dp = pac::Peripherals::take().unwrap();
|
||||
let led = Led::new(dp.GPIOA);
|
||||
|
||||
interrupt::free(|cs| {
|
||||
LED.borrow(cs).replace(Some(led));
|
||||
});
|
||||
|
||||
loop {
|
||||
interrupt::free(|cs| {
|
||||
if let Some(led) = LED.borrow(cs).borrow_mut().as_mut() {
|
||||
led.toggle();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Domain Violation | Fix |
|
||||
|---------|-----------------|-----|
|
||||
| Using Vec | Heap allocation | heapless::Vec |
|
||||
| No critical section | Race with ISR | Mutex + interrupt::free |
|
||||
| Blocking in ISR | Missed interrupts | Defer to main loop |
|
||||
| Unsafe peripheral | Hardware conflict | HAL ownership |
|
||||
|
||||
---
|
||||
|
||||
## Trace to Layer 1
|
||||
|
||||
| Constraint | Layer 2 Pattern | Layer 1 Implementation |
|
||||
|------------|-----------------|------------------------|
|
||||
| No heap | Static collections | heapless::Vec<T, N> |
|
||||
| ISR safety | Critical sections | Mutex<RefCell<T>> |
|
||||
| Hardware ownership | Singleton | take().unwrap() |
|
||||
| no_std | Core-only | #![no_std], #![no_main] |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Static memory | m02-resource |
|
||||
| Interior mutability | m03-mutability |
|
||||
| Interrupt patterns | m07-concurrency |
|
||||
| Unsafe for hardware | unsafe-checker |
|
||||
146
skills/domain-fintech/SKILL.md
Normal file
146
skills/domain-fintech/SKILL.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
name: domain-fintech
|
||||
description: "Use when building fintech apps. Keywords: fintech, trading, decimal, currency, financial, money, transaction, ledger, payment, exchange rate, precision, rounding, accounting, 金融, 交易系统, 货币, 支付"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# FinTech Domain
|
||||
|
||||
> **Layer 3: Domain Constraints**
|
||||
|
||||
## Domain Constraints → Design Implications
|
||||
|
||||
| Domain Rule | Design Constraint | Rust Implication |
|
||||
|-------------|-------------------|------------------|
|
||||
| Audit trail | Immutable records | Arc<T>, no mutation |
|
||||
| Precision | No floating point | rust_decimal |
|
||||
| Consistency | Transaction boundaries | Clear ownership |
|
||||
| Compliance | Complete logging | Structured tracing |
|
||||
| Reproducibility | Deterministic execution | No race conditions |
|
||||
|
||||
---
|
||||
|
||||
## Critical Constraints
|
||||
|
||||
### Financial Precision
|
||||
|
||||
```
|
||||
RULE: Never use f64 for money
|
||||
WHY: Floating point loses precision
|
||||
RUST: Use rust_decimal::Decimal
|
||||
```
|
||||
|
||||
### Audit Requirements
|
||||
|
||||
```
|
||||
RULE: All transactions must be immutable and traceable
|
||||
WHY: Regulatory compliance, dispute resolution
|
||||
RUST: Arc<T> for sharing, event sourcing pattern
|
||||
```
|
||||
|
||||
### Consistency
|
||||
|
||||
```
|
||||
RULE: Money can't disappear or appear
|
||||
WHY: Double-entry accounting principles
|
||||
RUST: Transaction types with validated totals
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From constraints to design (Layer 2):
|
||||
|
||||
```
|
||||
"Need immutable transaction records"
|
||||
↓ m09-domain: Model as Value Objects
|
||||
↓ m01-ownership: Use Arc for shared immutable data
|
||||
|
||||
"Need precise decimal math"
|
||||
↓ m05-type-driven: Newtype for Currency/Amount
|
||||
↓ rust_decimal: Use Decimal type
|
||||
|
||||
"Need transaction boundaries"
|
||||
↓ m12-lifecycle: RAII for transaction scope
|
||||
↓ m09-domain: Aggregate boundaries
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Crates
|
||||
|
||||
| Purpose | Crate |
|
||||
|---------|-------|
|
||||
| Decimal math | rust_decimal |
|
||||
| Date/time | chrono, time |
|
||||
| UUID | uuid |
|
||||
| Serialization | serde |
|
||||
| Validation | validator |
|
||||
|
||||
## Design Patterns
|
||||
|
||||
| Pattern | Purpose | Implementation |
|
||||
|---------|---------|----------------|
|
||||
| Currency newtype | Type safety | `struct Amount(Decimal);` |
|
||||
| Transaction | Atomic operations | Event sourcing |
|
||||
| Audit log | Traceability | Structured logging with trace IDs |
|
||||
| Ledger | Double-entry | Debit/credit balance |
|
||||
|
||||
## Code Pattern: Currency Type
|
||||
|
||||
```rust
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Amount {
|
||||
value: Decimal,
|
||||
currency: Currency,
|
||||
}
|
||||
|
||||
impl Amount {
|
||||
pub fn new(value: Decimal, currency: Currency) -> Self {
|
||||
Self { value, currency }
|
||||
}
|
||||
|
||||
pub fn add(&self, other: &Amount) -> Result<Amount, CurrencyMismatch> {
|
||||
if self.currency != other.currency {
|
||||
return Err(CurrencyMismatch);
|
||||
}
|
||||
Ok(Amount::new(self.value + other.value, self.currency))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Domain Violation | Fix |
|
||||
|---------|-----------------|-----|
|
||||
| Using f64 | Precision loss | rust_decimal |
|
||||
| Mutable transaction | Audit trail broken | Immutable + events |
|
||||
| String for amount | No validation | Validated newtype |
|
||||
| Silent overflow | Money disappears | Checked arithmetic |
|
||||
|
||||
---
|
||||
|
||||
## Trace to Layer 1
|
||||
|
||||
| Constraint | Layer 2 Pattern | Layer 1 Implementation |
|
||||
|------------|-----------------|------------------------|
|
||||
| Immutable records | Event sourcing | Arc<T>, Clone |
|
||||
| Transaction scope | Aggregate | Owned children |
|
||||
| Precision | Value Object | rust_decimal newtype |
|
||||
| Thread-safe sharing | Shared immutable | Arc (not Rc) |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Value Object design | m09-domain |
|
||||
| Ownership for immutable | m01-ownership |
|
||||
| Arc for sharing | m02-resource |
|
||||
| Error handling | m13-domain-error |
|
||||
168
skills/domain-iot/SKILL.md
Normal file
168
skills/domain-iot/SKILL.md
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
name: domain-iot
|
||||
description: "Use when building IoT apps. Keywords: IoT, Internet of Things, sensor, MQTT, device, edge computing, telemetry, actuator, smart home, gateway, protocol, 物联网, 传感器, 边缘计算, 智能家居"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# IoT Domain
|
||||
|
||||
> **Layer 3: Domain Constraints**
|
||||
|
||||
## Domain Constraints → Design Implications
|
||||
|
||||
| Domain Rule | Design Constraint | Rust Implication |
|
||||
|-------------|-------------------|------------------|
|
||||
| Unreliable network | Offline-first | Local buffering |
|
||||
| Power constraints | Efficient code | Sleep modes, minimal alloc |
|
||||
| Resource limits | Small footprint | no_std where needed |
|
||||
| Security | Encrypted comms | TLS, signed firmware |
|
||||
| Reliability | Self-recovery | Watchdog, error handling |
|
||||
| OTA updates | Safe upgrades | Rollback capability |
|
||||
|
||||
---
|
||||
|
||||
## Critical Constraints
|
||||
|
||||
### Network Unreliability
|
||||
|
||||
```
|
||||
RULE: Network can fail at any time
|
||||
WHY: Wireless, remote locations
|
||||
RUST: Local queue, retry with backoff
|
||||
```
|
||||
|
||||
### Power Management
|
||||
|
||||
```
|
||||
RULE: Minimize power consumption
|
||||
WHY: Battery life, energy costs
|
||||
RUST: Sleep modes, efficient algorithms
|
||||
```
|
||||
|
||||
### Device Security
|
||||
|
||||
```
|
||||
RULE: All communication encrypted
|
||||
WHY: Physical access possible
|
||||
RUST: TLS, signed messages
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From constraints to design (Layer 2):
|
||||
|
||||
```
|
||||
"Need offline-first design"
|
||||
↓ m12-lifecycle: Local buffer with persistence
|
||||
↓ m13-domain-error: Retry with backoff
|
||||
|
||||
"Need power efficiency"
|
||||
↓ domain-embedded: no_std patterns
|
||||
↓ m10-performance: Minimal allocations
|
||||
|
||||
"Need reliable messaging"
|
||||
↓ m07-concurrency: Async with timeout
|
||||
↓ MQTT: QoS levels
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Comparison
|
||||
|
||||
| Environment | Stack | Crates |
|
||||
|-------------|-------|--------|
|
||||
| Linux gateway | tokio + std | rumqttc, reqwest |
|
||||
| MCU device | embassy + no_std | embedded-hal |
|
||||
| Hybrid | Split workloads | Both |
|
||||
|
||||
## Key Crates
|
||||
|
||||
| Purpose | Crate |
|
||||
|---------|-------|
|
||||
| MQTT (std) | rumqttc, paho-mqtt |
|
||||
| Embedded | embedded-hal, embassy |
|
||||
| Async (std) | tokio |
|
||||
| Async (no_std) | embassy |
|
||||
| Logging (no_std) | defmt |
|
||||
| Logging (std) | tracing |
|
||||
|
||||
## Design Patterns
|
||||
|
||||
| Pattern | Purpose | Implementation |
|
||||
|---------|---------|----------------|
|
||||
| Pub/Sub | Device comms | MQTT topics |
|
||||
| Edge compute | Local processing | Filter before upload |
|
||||
| OTA updates | Firmware upgrade | Signed + rollback |
|
||||
| Power mgmt | Battery life | Sleep + wake events |
|
||||
| Store & forward | Network reliability | Local queue |
|
||||
|
||||
## Code Pattern: MQTT Client
|
||||
|
||||
```rust
|
||||
use rumqttc::{AsyncClient, MqttOptions, QoS};
|
||||
|
||||
async fn run_mqtt() -> anyhow::Result<()> {
|
||||
let mut options = MqttOptions::new("device-1", "broker.example.com", 1883);
|
||||
options.set_keep_alive(Duration::from_secs(30));
|
||||
|
||||
let (client, mut eventloop) = AsyncClient::new(options, 10);
|
||||
|
||||
// Subscribe to commands
|
||||
client.subscribe("devices/device-1/commands", QoS::AtLeastOnce).await?;
|
||||
|
||||
// Publish telemetry
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let data = read_sensor().await;
|
||||
client.publish("devices/device-1/telemetry", QoS::AtLeastOnce, false, data).await.ok();
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
});
|
||||
|
||||
// Process events
|
||||
loop {
|
||||
match eventloop.poll().await {
|
||||
Ok(event) => handle_event(event).await,
|
||||
Err(e) => {
|
||||
tracing::error!("MQTT error: {}", e);
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Domain Violation | Fix |
|
||||
|---------|-----------------|-----|
|
||||
| No retry logic | Lost data | Exponential backoff |
|
||||
| Always-on radio | Battery drain | Sleep between sends |
|
||||
| Unencrypted MQTT | Security risk | TLS |
|
||||
| No local buffer | Network outage = data loss | Persist locally |
|
||||
|
||||
---
|
||||
|
||||
## Trace to Layer 1
|
||||
|
||||
| Constraint | Layer 2 Pattern | Layer 1 Implementation |
|
||||
|------------|-----------------|------------------------|
|
||||
| Offline-first | Store & forward | Local queue + flush |
|
||||
| Power efficiency | Sleep patterns | Timer-based wake |
|
||||
| Network reliability | Retry | tokio-retry, backoff |
|
||||
| Security | TLS | rustls, native-tls |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Embedded patterns | domain-embedded |
|
||||
| Async patterns | m07-concurrency |
|
||||
| Error recovery | m13-domain-error |
|
||||
| Performance | m10-performance |
|
||||
181
skills/domain-ml/SKILL.md
Normal file
181
skills/domain-ml/SKILL.md
Normal file
@@ -0,0 +1,181 @@
|
||||
---
|
||||
name: domain-ml
|
||||
description: "Use when building ML/AI apps in Rust. Keywords: machine learning, ML, AI, tensor, model, inference, neural network, deep learning, training, prediction, ndarray, tch-rs, burn, candle, 机器学习, 人工智能, 模型推理"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Machine Learning Domain
|
||||
|
||||
> **Layer 3: Domain Constraints**
|
||||
|
||||
## Domain Constraints → Design Implications
|
||||
|
||||
| Domain Rule | Design Constraint | Rust Implication |
|
||||
|-------------|-------------------|------------------|
|
||||
| Large data | Efficient memory | Zero-copy, streaming |
|
||||
| GPU acceleration | CUDA/Metal support | candle, tch-rs |
|
||||
| Model portability | Standard formats | ONNX |
|
||||
| Batch processing | Throughput over latency | Batched inference |
|
||||
| Numerical precision | Float handling | ndarray, careful f32/f64 |
|
||||
| Reproducibility | Deterministic | Seeded random, versioning |
|
||||
|
||||
---
|
||||
|
||||
## Critical Constraints
|
||||
|
||||
### Memory Efficiency
|
||||
|
||||
```
|
||||
RULE: Avoid copying large tensors
|
||||
WHY: Memory bandwidth is bottleneck
|
||||
RUST: References, views, in-place ops
|
||||
```
|
||||
|
||||
### GPU Utilization
|
||||
|
||||
```
|
||||
RULE: Batch operations for GPU efficiency
|
||||
WHY: GPU overhead per kernel launch
|
||||
RUST: Batch sizes, async data loading
|
||||
```
|
||||
|
||||
### Model Portability
|
||||
|
||||
```
|
||||
RULE: Use standard model formats
|
||||
WHY: Train in Python, deploy in Rust
|
||||
RUST: ONNX via tract or candle
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From constraints to design (Layer 2):
|
||||
|
||||
```
|
||||
"Need efficient data pipelines"
|
||||
↓ m10-performance: Streaming, batching
|
||||
↓ polars: Lazy evaluation
|
||||
|
||||
"Need GPU inference"
|
||||
↓ m07-concurrency: Async data loading
|
||||
↓ candle/tch-rs: CUDA backend
|
||||
|
||||
"Need model loading"
|
||||
↓ m12-lifecycle: Lazy init, caching
|
||||
↓ tract: ONNX runtime
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use Case → Framework
|
||||
|
||||
| Use Case | Recommended | Why |
|
||||
|----------|-------------|-----|
|
||||
| Inference only | tract (ONNX) | Lightweight, portable |
|
||||
| Training + inference | candle, burn | Pure Rust, GPU |
|
||||
| PyTorch models | tch-rs | Direct bindings |
|
||||
| Data pipelines | polars | Fast, lazy eval |
|
||||
|
||||
## Key Crates
|
||||
|
||||
| Purpose | Crate |
|
||||
|---------|-------|
|
||||
| Tensors | ndarray |
|
||||
| ONNX inference | tract |
|
||||
| ML framework | candle, burn |
|
||||
| PyTorch bindings | tch-rs |
|
||||
| Data processing | polars |
|
||||
| Embeddings | fastembed |
|
||||
|
||||
## Design Patterns
|
||||
|
||||
| Pattern | Purpose | Implementation |
|
||||
|---------|---------|----------------|
|
||||
| Model loading | Once, reuse | `OnceLock<Model>` |
|
||||
| Batching | Throughput | Collect then process |
|
||||
| Streaming | Large data | Iterator-based |
|
||||
| GPU async | Parallelism | Data loading parallel to compute |
|
||||
|
||||
## Code Pattern: Inference Server
|
||||
|
||||
```rust
|
||||
use std::sync::OnceLock;
|
||||
use tract_onnx::prelude::*;
|
||||
|
||||
static MODEL: OnceLock<SimplePlan<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>> = OnceLock::new();
|
||||
|
||||
fn get_model() -> &'static SimplePlan<...> {
|
||||
MODEL.get_or_init(|| {
|
||||
tract_onnx::onnx()
|
||||
.model_for_path("model.onnx")
|
||||
.unwrap()
|
||||
.into_optimized()
|
||||
.unwrap()
|
||||
.into_runnable()
|
||||
.unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
async fn predict(input: Vec<f32>) -> anyhow::Result<Vec<f32>> {
|
||||
let model = get_model();
|
||||
let input = tract_ndarray::arr1(&input).into_shape((1, input.len()))?;
|
||||
let result = model.run(tvec!(input.into()))?;
|
||||
Ok(result[0].to_array_view::<f32>()?.iter().copied().collect())
|
||||
}
|
||||
```
|
||||
|
||||
## Code Pattern: Batched Inference
|
||||
|
||||
```rust
|
||||
async fn batch_predict(inputs: Vec<Vec<f32>>, batch_size: usize) -> Vec<Vec<f32>> {
|
||||
let mut results = Vec::with_capacity(inputs.len());
|
||||
|
||||
for batch in inputs.chunks(batch_size) {
|
||||
// Stack inputs into batch tensor
|
||||
let batch_tensor = stack_inputs(batch);
|
||||
|
||||
// Run inference on batch
|
||||
let batch_output = model.run(batch_tensor).await;
|
||||
|
||||
// Unstack results
|
||||
results.extend(unstack_outputs(batch_output));
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Domain Violation | Fix |
|
||||
|---------|-----------------|-----|
|
||||
| Clone tensors | Memory waste | Use views |
|
||||
| Single inference | GPU underutilized | Batch processing |
|
||||
| Load model per request | Slow | Singleton pattern |
|
||||
| Sync data loading | GPU idle | Async pipeline |
|
||||
|
||||
---
|
||||
|
||||
## Trace to Layer 1
|
||||
|
||||
| Constraint | Layer 2 Pattern | Layer 1 Implementation |
|
||||
|------------|-----------------|------------------------|
|
||||
| Memory efficiency | Zero-copy | ndarray views |
|
||||
| Model singleton | Lazy init | OnceLock<Model> |
|
||||
| Batch processing | Chunked iteration | chunks() + parallel |
|
||||
| GPU async | Concurrent loading | tokio::spawn + GPU |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Performance | m10-performance |
|
||||
| Lazy initialization | m12-lifecycle |
|
||||
| Async patterns | m07-concurrency |
|
||||
| Memory efficiency | m01-ownership |
|
||||
156
skills/domain-web/SKILL.md
Normal file
156
skills/domain-web/SKILL.md
Normal file
@@ -0,0 +1,156 @@
|
||||
---
|
||||
name: domain-web
|
||||
description: "Use when building web services. Keywords: web server, HTTP, REST API, GraphQL, WebSocket, axum, actix, warp, rocket, tower, hyper, reqwest, middleware, router, handler, extractor, state management, authentication, authorization, JWT, session, cookie, CORS, rate limiting, web 开发, HTTP 服务, API 设计, 中间件, 路由"
|
||||
globs: ["**/Cargo.toml"]
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Web Domain
|
||||
|
||||
> **Layer 3: Domain Constraints**
|
||||
|
||||
## Domain Constraints → Design Implications
|
||||
|
||||
| Domain Rule | Design Constraint | Rust Implication |
|
||||
|-------------|-------------------|------------------|
|
||||
| Stateless HTTP | No request-local globals | State in extractors |
|
||||
| Concurrency | Handle many connections | Async, Send + Sync |
|
||||
| Latency SLA | Fast response | Efficient ownership |
|
||||
| Security | Input validation | Type-safe extractors |
|
||||
| Observability | Request tracing | tracing + tower layers |
|
||||
|
||||
---
|
||||
|
||||
## Critical Constraints
|
||||
|
||||
### Async by Default
|
||||
|
||||
```
|
||||
RULE: Web handlers must not block
|
||||
WHY: Block one task = block many requests
|
||||
RUST: async/await, spawn_blocking for CPU work
|
||||
```
|
||||
|
||||
### State Management
|
||||
|
||||
```
|
||||
RULE: Shared state must be thread-safe
|
||||
WHY: Handlers run on any thread
|
||||
RUST: Arc<T>, Arc<RwLock<T>> for mutable
|
||||
```
|
||||
|
||||
### Request Lifecycle
|
||||
|
||||
```
|
||||
RULE: Resources live only for request duration
|
||||
WHY: Memory management, no leaks
|
||||
RUST: Extractors, proper ownership
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From constraints to design (Layer 2):
|
||||
|
||||
```
|
||||
"Need shared application state"
|
||||
↓ m07-concurrency: Use Arc for thread-safe sharing
|
||||
↓ m02-resource: Arc<RwLock<T>> for mutable state
|
||||
|
||||
"Need request validation"
|
||||
↓ m05-type-driven: Validated extractors
|
||||
↓ m06-error-handling: IntoResponse for errors
|
||||
|
||||
"Need middleware stack"
|
||||
↓ m12-lifecycle: Tower layers
|
||||
↓ m04-zero-cost: Trait-based composition
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Framework Comparison
|
||||
|
||||
| Framework | Style | Best For |
|
||||
|-----------|-------|----------|
|
||||
| axum | Functional, tower | Modern APIs |
|
||||
| actix-web | Actor-based | High performance |
|
||||
| warp | Filter composition | Composable APIs |
|
||||
| rocket | Macro-driven | Rapid development |
|
||||
|
||||
## Key Crates
|
||||
|
||||
| Purpose | Crate |
|
||||
|---------|-------|
|
||||
| HTTP server | axum, actix-web |
|
||||
| HTTP client | reqwest |
|
||||
| JSON | serde_json |
|
||||
| Auth/JWT | jsonwebtoken |
|
||||
| Session | tower-sessions |
|
||||
| Database | sqlx, diesel |
|
||||
| Middleware | tower |
|
||||
|
||||
## Design Patterns
|
||||
|
||||
| Pattern | Purpose | Implementation |
|
||||
|---------|---------|----------------|
|
||||
| Extractors | Request parsing | `State(db)`, `Json(payload)` |
|
||||
| Error response | Unified errors | `impl IntoResponse` |
|
||||
| Middleware | Cross-cutting | Tower layers |
|
||||
| Shared state | App config | `Arc<AppState>` |
|
||||
|
||||
## Code Pattern: Axum Handler
|
||||
|
||||
```rust
|
||||
async fn handler(
|
||||
State(db): State<Arc<DbPool>>,
|
||||
Json(payload): Json<CreateUser>,
|
||||
) -> Result<Json<User>, AppError> {
|
||||
let user = db.create_user(&payload).await?;
|
||||
Ok(Json(user))
|
||||
}
|
||||
|
||||
// Error handling
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, message) = match self {
|
||||
Self::NotFound => (StatusCode::NOT_FOUND, "Not found"),
|
||||
Self::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Internal error"),
|
||||
};
|
||||
(status, Json(json!({"error": message}))).into_response()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Domain Violation | Fix |
|
||||
|---------|-----------------|-----|
|
||||
| Blocking in handler | Latency spike | spawn_blocking |
|
||||
| Rc in state | Not Send + Sync | Use Arc |
|
||||
| No validation | Security risk | Type-safe extractors |
|
||||
| No error response | Bad UX | IntoResponse impl |
|
||||
|
||||
---
|
||||
|
||||
## Trace to Layer 1
|
||||
|
||||
| Constraint | Layer 2 Pattern | Layer 1 Implementation |
|
||||
|------------|-----------------|------------------------|
|
||||
| Async handlers | Async/await | tokio runtime |
|
||||
| Thread-safe state | Shared state | Arc<T>, Arc<RwLock<T>> |
|
||||
| Request lifecycle | Extractors | Ownership via From<Request> |
|
||||
| Middleware | Tower layers | Trait-based composition |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Async patterns | m07-concurrency |
|
||||
| State management | m02-resource |
|
||||
| Error handling | m06-error-handling |
|
||||
| Middleware design | m12-lifecycle |
|
||||
134
skills/m01-ownership/SKILL.md
Normal file
134
skills/m01-ownership/SKILL.md
Normal file
@@ -0,0 +1,134 @@
|
||||
---
|
||||
name: m01-ownership
|
||||
description: "CRITICAL: Use for ownership/borrow/lifetime issues. Triggers: E0382, E0597, E0506, E0507, E0515, E0716, E0106, value moved, borrowed value does not live long enough, cannot move out of, use of moved value, ownership, borrow, lifetime, 'a, 'static, move, clone, Copy, 所有权, 借用, 生命周期"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Ownership & Lifetimes
|
||||
|
||||
> **Layer 1: Language Mechanics**
|
||||
|
||||
## Core Question
|
||||
|
||||
**Who should own this data, and for how long?**
|
||||
|
||||
Before fixing ownership errors, understand the data's role:
|
||||
- Is it shared or exclusive?
|
||||
- Is it short-lived or long-lived?
|
||||
- Is it transformed or just read?
|
||||
|
||||
---
|
||||
|
||||
## Error → Design Question
|
||||
|
||||
| Error | Don't Just Say | Ask Instead |
|
||||
|-------|----------------|-------------|
|
||||
| E0382 | "Clone it" | Who should own this data? |
|
||||
| E0597 | "Extend lifetime" | Is the scope boundary correct? |
|
||||
| E0506 | "End borrow first" | Should mutation happen elsewhere? |
|
||||
| E0507 | "Clone before move" | Why are we moving from a reference? |
|
||||
| E0515 | "Return owned" | Should caller own the data? |
|
||||
| E0716 | "Bind to variable" | Why is this temporary? |
|
||||
| E0106 | "Add 'a" | What is the actual lifetime relationship? |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before fixing an ownership error, ask:
|
||||
|
||||
1. **What is this data's domain role?**
|
||||
- Entity (unique identity) → owned
|
||||
- Value Object (interchangeable) → clone/copy OK
|
||||
- Temporary (computation result) → maybe restructure
|
||||
|
||||
2. **Is the ownership design intentional?**
|
||||
- By design → work within constraints
|
||||
- Accidental → consider redesign
|
||||
|
||||
3. **Fix symptom or redesign?**
|
||||
- If Strike 3 (3rd attempt) → escalate to Layer 2
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
When errors persist, trace to design layer:
|
||||
|
||||
```
|
||||
E0382 (moved value)
|
||||
↑ Ask: What design choice led to this ownership pattern?
|
||||
↑ Check: m09-domain (is this Entity or Value Object?)
|
||||
↑ Check: domain-* (what constraints apply?)
|
||||
```
|
||||
|
||||
| Persistent Error | Trace To | Question |
|
||||
|-----------------|----------|----------|
|
||||
| E0382 repeated | m02-resource | Should use Arc/Rc for sharing? |
|
||||
| E0597 repeated | m09-domain | Is scope boundary at right place? |
|
||||
| E0506/E0507 | m03-mutability | Should use interior mutability? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From design decisions to implementation:
|
||||
|
||||
```
|
||||
"Data needs to be shared immutably"
|
||||
↓ Use: Arc<T> (multi-thread) or Rc<T> (single-thread)
|
||||
|
||||
"Data needs exclusive ownership"
|
||||
↓ Use: move semantics, take ownership
|
||||
|
||||
"Data is read-only view"
|
||||
↓ Use: &T (immutable borrow)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Ownership | Cost | Use When |
|
||||
|---------|-----------|------|----------|
|
||||
| Move | Transfer | Zero | Caller doesn't need data |
|
||||
| `&T` | Borrow | Zero | Read-only access |
|
||||
| `&mut T` | Exclusive borrow | Zero | Need to modify |
|
||||
| `clone()` | Duplicate | Alloc + copy | Actually need a copy |
|
||||
| `Rc<T>` | Shared (single) | Ref count | Single-thread sharing |
|
||||
| `Arc<T>` | Shared (multi) | Atomic ref count | Multi-thread sharing |
|
||||
| `Cow<T>` | Clone-on-write | Alloc if mutated | Might modify |
|
||||
|
||||
## Error Code Reference
|
||||
|
||||
| Error | Cause | Quick Fix |
|
||||
|-------|-------|-----------|
|
||||
| E0382 | Value moved | Clone, reference, or redesign ownership |
|
||||
| E0597 | Reference outlives owner | Extend owner scope or restructure |
|
||||
| E0506 | Assign while borrowed | End borrow before mutation |
|
||||
| E0507 | Move out of borrowed | Clone or use reference |
|
||||
| E0515 | Return local reference | Return owned value |
|
||||
| E0716 | Temporary dropped | Bind to variable |
|
||||
| E0106 | Missing lifetime | Add `'a` annotation |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| `.clone()` everywhere | Hides design issues | Design ownership properly |
|
||||
| Fight borrow checker | Increases complexity | Work with the compiler |
|
||||
| `'static` for everything | Restricts flexibility | Use appropriate lifetimes |
|
||||
| Leak with `Box::leak` | Memory leak | Proper lifetime design |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Need smart pointers | m02-resource |
|
||||
| Need interior mutability | m03-mutability |
|
||||
| Data is domain entity | m09-domain |
|
||||
| Learning ownership concepts | m14-mental-model |
|
||||
222
skills/m01-ownership/comparison.md
Normal file
222
skills/m01-ownership/comparison.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# Ownership: Comparison with Other Languages
|
||||
|
||||
## Rust vs C++
|
||||
|
||||
### Memory Management
|
||||
|
||||
| Aspect | Rust | C++ |
|
||||
|--------|------|-----|
|
||||
| Default | Move semantics | Copy semantics (pre-C++11) |
|
||||
| Move | `let b = a;` (a invalidated) | `auto b = std::move(a);` (a valid but unspecified) |
|
||||
| Copy | `let b = a.clone();` | `auto b = a;` |
|
||||
| Safety | Compile-time enforcement | Runtime responsibility |
|
||||
|
||||
### Rust Move vs C++ Move
|
||||
|
||||
```rust
|
||||
// Rust: after move, 'a' is INVALID
|
||||
let a = String::from("hello");
|
||||
let b = a; // a moved
|
||||
// println!("{}", a); // COMPILE ERROR
|
||||
|
||||
// Equivalent in C++:
|
||||
// std::string a = "hello";
|
||||
// std::string b = std::move(a);
|
||||
// std::cout << a; // UNDEFINED (compiles but buggy)
|
||||
```
|
||||
|
||||
### Smart Pointers
|
||||
|
||||
| Rust | C++ | Purpose |
|
||||
|------|-----|---------|
|
||||
| `Box<T>` | `std::unique_ptr<T>` | Unique ownership |
|
||||
| `Rc<T>` | `std::shared_ptr<T>` | Shared ownership |
|
||||
| `Arc<T>` | `std::shared_ptr<T>` + atomic | Thread-safe shared |
|
||||
| `RefCell<T>` | (manual runtime checks) | Interior mutability |
|
||||
|
||||
---
|
||||
|
||||
## Rust vs Go
|
||||
|
||||
### Memory Model
|
||||
|
||||
| Aspect | Rust | Go |
|
||||
|--------|------|-----|
|
||||
| Memory | Stack + heap, explicit | GC manages all |
|
||||
| Ownership | Enforced at compile-time | None (GC handles) |
|
||||
| Null | `Option<T>` | `nil` for pointers |
|
||||
| Concurrency | `Send`/`Sync` traits | Channels (less strict) |
|
||||
|
||||
### Sharing Data
|
||||
|
||||
```rust
|
||||
// Rust: explicit about sharing
|
||||
use std::sync::Arc;
|
||||
let data = Arc::new(vec![1, 2, 3]);
|
||||
let data_clone = Arc::clone(&data);
|
||||
std::thread::spawn(move || {
|
||||
println!("{:?}", data_clone);
|
||||
});
|
||||
|
||||
// Go: implicit sharing
|
||||
// data := []int{1, 2, 3}
|
||||
// go func() {
|
||||
// fmt.Println(data) // potential race condition
|
||||
// }()
|
||||
```
|
||||
|
||||
### Why No GC in Rust
|
||||
|
||||
1. **Deterministic destruction**: Resources freed exactly when scope ends
|
||||
2. **Zero-cost**: No GC pauses or overhead
|
||||
3. **Embeddable**: Works in OS kernels, embedded systems
|
||||
4. **Predictable latency**: Critical for real-time systems
|
||||
|
||||
---
|
||||
|
||||
## Rust vs Java/C#
|
||||
|
||||
### Reference Semantics
|
||||
|
||||
| Aspect | Rust | Java/C# |
|
||||
|--------|------|---------|
|
||||
| Objects | Owned by default | Reference by default |
|
||||
| Null | `Option<T>` | `null` (nullable) |
|
||||
| Immutability | Default | Must use `final`/`readonly` |
|
||||
| Copy | Explicit `.clone()` | Reference copy (shallow) |
|
||||
|
||||
### Comparison
|
||||
|
||||
```rust
|
||||
// Rust: clear ownership
|
||||
fn process(data: Vec<i32>) { // takes ownership
|
||||
// data is ours, will be freed at end
|
||||
}
|
||||
|
||||
let numbers = vec![1, 2, 3];
|
||||
process(numbers);
|
||||
// numbers is invalid here
|
||||
|
||||
// Java: ambiguous ownership
|
||||
// void process(List<Integer> data) {
|
||||
// // Who owns data? Caller? Callee? Both?
|
||||
// // Can caller still use it?
|
||||
// }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rust vs Python
|
||||
|
||||
### Memory Model
|
||||
|
||||
| Aspect | Rust | Python |
|
||||
|--------|------|--------|
|
||||
| Typing | Static, compile-time | Dynamic, runtime |
|
||||
| Memory | Ownership-based | Reference counting + GC |
|
||||
| Mutability | Default immutable | Default mutable |
|
||||
| Performance | Native, zero-cost | Interpreted, higher overhead |
|
||||
|
||||
### Common Pattern Translation
|
||||
|
||||
```rust
|
||||
// Rust: borrowing iteration
|
||||
let items = vec!["a", "b", "c"];
|
||||
for item in &items {
|
||||
println!("{}", item);
|
||||
}
|
||||
// items still usable
|
||||
|
||||
// Python: iteration doesn't consume
|
||||
// items = ["a", "b", "c"]
|
||||
// for item in items:
|
||||
// print(item)
|
||||
// items still usable (different reason - ref counting)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Unique Rust Concepts
|
||||
|
||||
### Concepts Other Languages Lack
|
||||
|
||||
1. **Borrow Checker**: No other mainstream language has compile-time borrow checking
|
||||
2. **Lifetimes**: Explicit annotation of reference validity
|
||||
3. **Move by Default**: Values move, not copy
|
||||
4. **No Null**: `Option<T>` instead of null pointers
|
||||
5. **Affine Types**: Values can be used at most once
|
||||
|
||||
### Learning Curve Areas
|
||||
|
||||
| Concept | Coming From | Key Insight |
|
||||
|---------|-------------|-------------|
|
||||
| Ownership | GC languages | Think about who "owns" data |
|
||||
| Borrowing | C/C++ | Like references but checked |
|
||||
| Lifetimes | Any | Explicit scope of validity |
|
||||
| Move | C++ | Move is default, not copy |
|
||||
|
||||
---
|
||||
|
||||
## Mental Model Shifts
|
||||
|
||||
### From GC Languages (Java, Go, Python)
|
||||
|
||||
```
|
||||
Before: "Memory just works, GC handles it"
|
||||
After: "I explicitly decide who owns data and when it's freed"
|
||||
```
|
||||
|
||||
Key shifts:
|
||||
- Think about ownership at design time
|
||||
- Returning references requires lifetime thinking
|
||||
- No more `null` - use `Option<T>`
|
||||
|
||||
### From C/C++
|
||||
|
||||
```
|
||||
Before: "I manually manage memory and hope I get it right"
|
||||
After: "Compiler enforces correctness, I fight the borrow checker"
|
||||
```
|
||||
|
||||
Key shifts:
|
||||
- Trust the compiler's errors
|
||||
- Move is the default (unlike C++ copy)
|
||||
- Smart pointers are idiomatic, not overhead
|
||||
|
||||
### From Functional Languages (Haskell, ML)
|
||||
|
||||
```
|
||||
Before: "Everything is immutable, copying is fine"
|
||||
After: "Mutability is explicit, ownership prevents aliasing"
|
||||
```
|
||||
|
||||
Key shifts:
|
||||
- Mutability is safe because of ownership rules
|
||||
- No persistent data structures needed (usually)
|
||||
- Performance characteristics are explicit
|
||||
|
||||
---
|
||||
|
||||
## Performance Trade-offs
|
||||
|
||||
| Language | Memory Overhead | Latency | Throughput |
|
||||
|----------|-----------------|---------|------------|
|
||||
| Rust | Minimal (no GC) | Predictable | Excellent |
|
||||
| C++ | Minimal | Predictable | Excellent |
|
||||
| Go | GC overhead | GC pauses | Good |
|
||||
| Java | GC overhead | GC pauses | Good |
|
||||
| Python | High (ref counting + GC) | Variable | Lower |
|
||||
|
||||
### When Rust Ownership Wins
|
||||
|
||||
1. **Real-time systems**: No GC pauses
|
||||
2. **Embedded**: No runtime overhead
|
||||
3. **High-performance**: Zero-cost abstractions
|
||||
4. **Concurrent**: Data races prevented at compile time
|
||||
|
||||
### When GC Might Be Preferable
|
||||
|
||||
1. **Rapid prototyping**: Less mental overhead
|
||||
2. **Complex object graphs**: Cycles are tricky in Rust
|
||||
3. **GUI applications**: Object lifetimes are dynamic
|
||||
4. **Small programs**: Overhead doesn't matter
|
||||
339
skills/m01-ownership/examples/best-practices.md
Normal file
339
skills/m01-ownership/examples/best-practices.md
Normal file
@@ -0,0 +1,339 @@
|
||||
# Ownership Best Practices
|
||||
|
||||
## API Design Patterns
|
||||
|
||||
### 1. Prefer Borrowing Over Ownership
|
||||
|
||||
```rust
|
||||
// BAD: takes ownership unnecessarily
|
||||
fn print_name(name: String) {
|
||||
println!("Name: {}", name);
|
||||
}
|
||||
|
||||
// GOOD: borrows instead
|
||||
fn print_name(name: &str) {
|
||||
println!("Name: {}", name);
|
||||
}
|
||||
|
||||
// Caller benefits:
|
||||
let name = String::from("Alice");
|
||||
print_name(&name); // can reuse name
|
||||
print_name(&name); // still valid
|
||||
```
|
||||
|
||||
### 2. Return Owned Values from Constructors
|
||||
|
||||
```rust
|
||||
// GOOD: return owned value
|
||||
impl User {
|
||||
fn new(name: &str) -> Self {
|
||||
User {
|
||||
name: name.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: accept Into<String> for flexibility
|
||||
impl User {
|
||||
fn new(name: impl Into<String>) -> Self {
|
||||
User {
|
||||
name: name.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
let u1 = User::new("Alice"); // &str
|
||||
let u2 = User::new(String::from("Bob")); // String
|
||||
```
|
||||
|
||||
### 3. Use AsRef for Generic Borrowing
|
||||
|
||||
```rust
|
||||
// GOOD: accepts both &str and String
|
||||
fn process<S: AsRef<str>>(input: S) {
|
||||
let s = input.as_ref();
|
||||
println!("{}", s);
|
||||
}
|
||||
|
||||
process("literal"); // &str
|
||||
process(String::from("owned")); // String
|
||||
process(&String::from("ref")); // &String
|
||||
```
|
||||
|
||||
### 4. Cow for Clone-on-Write
|
||||
|
||||
```rust
|
||||
use std::borrow::Cow;
|
||||
|
||||
// Return borrowed when possible, owned when needed
|
||||
fn maybe_modify(s: &str, uppercase: bool) -> Cow<'_, str> {
|
||||
if uppercase {
|
||||
Cow::Owned(s.to_uppercase()) // allocates
|
||||
} else {
|
||||
Cow::Borrowed(s) // zero-cost
|
||||
}
|
||||
}
|
||||
|
||||
let input = "hello";
|
||||
let result = maybe_modify(input, false);
|
||||
// result is borrowed, no allocation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Struct Design Patterns
|
||||
|
||||
### 1. Owned Fields vs References
|
||||
|
||||
```rust
|
||||
// Use owned fields for most cases
|
||||
struct User {
|
||||
name: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
// Use references only when lifetime is clear
|
||||
struct UserView<'a> {
|
||||
name: &'a str,
|
||||
email: &'a str,
|
||||
}
|
||||
|
||||
// Pattern: owned data + view for efficiency
|
||||
impl User {
|
||||
fn view(&self) -> UserView<'_> {
|
||||
UserView {
|
||||
name: &self.name,
|
||||
email: &self.email,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Builder Pattern with Ownership
|
||||
|
||||
```rust
|
||||
#[derive(Default)]
|
||||
struct RequestBuilder {
|
||||
url: Option<String>,
|
||||
method: Option<String>,
|
||||
body: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl RequestBuilder {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
// Take self by value for chaining
|
||||
fn url(mut self, url: impl Into<String>) -> Self {
|
||||
self.url = Some(url.into());
|
||||
self
|
||||
}
|
||||
|
||||
fn method(mut self, method: impl Into<String>) -> Self {
|
||||
self.method = Some(method.into());
|
||||
self
|
||||
}
|
||||
|
||||
fn build(self) -> Result<Request, Error> {
|
||||
Ok(Request {
|
||||
url: self.url.ok_or(Error::MissingUrl)?,
|
||||
method: self.method.unwrap_or_else(|| "GET".to_string()),
|
||||
body: self.body.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
let req = RequestBuilder::new()
|
||||
.url("https://example.com")
|
||||
.method("POST")
|
||||
.build()?;
|
||||
```
|
||||
|
||||
### 3. Interior Mutability When Needed
|
||||
|
||||
```rust
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
// Shared mutable state in single-threaded context
|
||||
struct Counter {
|
||||
value: Rc<RefCell<u32>>,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
fn new() -> Self {
|
||||
Counter {
|
||||
value: Rc::new(RefCell::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
fn increment(&self) {
|
||||
*self.value.borrow_mut() += 1;
|
||||
}
|
||||
|
||||
fn get(&self) -> u32 {
|
||||
*self.value.borrow()
|
||||
}
|
||||
|
||||
fn clone_handle(&self) -> Self {
|
||||
Counter {
|
||||
value: Rc::clone(&self.value),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Collection Patterns
|
||||
|
||||
### 1. Efficient Iteration
|
||||
|
||||
```rust
|
||||
let items = vec![1, 2, 3, 4, 5];
|
||||
|
||||
// Iterate by reference (no move)
|
||||
for item in &items {
|
||||
println!("{}", item);
|
||||
}
|
||||
|
||||
// Iterate by mutable reference
|
||||
for item in &mut items.clone() {
|
||||
*item *= 2;
|
||||
}
|
||||
|
||||
// Consume with into_iter when done
|
||||
let sum: i32 = items.into_iter().sum();
|
||||
```
|
||||
|
||||
### 2. Collecting Results
|
||||
|
||||
```rust
|
||||
// Collect into owned collection
|
||||
let strings: Vec<String> = (0..5)
|
||||
.map(|i| format!("item_{}", i))
|
||||
.collect();
|
||||
|
||||
// Collect references
|
||||
let refs: Vec<&str> = strings.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
// Collect with transformation
|
||||
let result: Result<Vec<i32>, _> = ["1", "2", "3"]
|
||||
.iter()
|
||||
.map(|s| s.parse::<i32>())
|
||||
.collect();
|
||||
```
|
||||
|
||||
### 3. Entry API for Maps
|
||||
|
||||
```rust
|
||||
use std::collections::HashMap;
|
||||
|
||||
let mut map: HashMap<String, Vec<i32>> = HashMap::new();
|
||||
|
||||
// Efficient: don't search twice
|
||||
map.entry("key".to_string())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(42);
|
||||
|
||||
// With entry modification
|
||||
map.entry("key".to_string())
|
||||
.and_modify(|v| v.push(43))
|
||||
.or_insert_with(|| vec![43]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling with Ownership
|
||||
|
||||
### 1. Preserve Context in Errors
|
||||
|
||||
```rust
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ParseError {
|
||||
input: String, // owns the problematic input
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "Failed to parse '{}': {}", self.input, self.message)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(input: &str) -> Result<i32, ParseError> {
|
||||
input.parse().map_err(|_| ParseError {
|
||||
input: input.to_string(), // clone for error context
|
||||
message: "not a valid integer".to_string(),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Ownership in Result Chains
|
||||
|
||||
```rust
|
||||
fn process_data(path: &str) -> Result<ProcessedData, Error> {
|
||||
let content = std::fs::read_to_string(path)?; // owned String
|
||||
let parsed = parse_content(&content)?; // borrow
|
||||
let processed = transform(parsed)?; // ownership moves
|
||||
Ok(processed) // return owned
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### 1. Avoid Unnecessary Clones
|
||||
|
||||
```rust
|
||||
// BAD: cloning just to compare
|
||||
fn contains_item(items: &[String], target: &str) -> bool {
|
||||
items.iter().any(|s| s.clone() == target) // unnecessary clone
|
||||
}
|
||||
|
||||
// GOOD: compare references
|
||||
fn contains_item(items: &[String], target: &str) -> bool {
|
||||
items.iter().any(|s| s == target) // String implements PartialEq<str>
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Slices for Flexibility
|
||||
|
||||
```rust
|
||||
// BAD: requires Vec
|
||||
fn sum(numbers: &Vec<i32>) -> i32 {
|
||||
numbers.iter().sum()
|
||||
}
|
||||
|
||||
// GOOD: accepts any slice
|
||||
fn sum(numbers: &[i32]) -> i32 {
|
||||
numbers.iter().sum()
|
||||
}
|
||||
|
||||
// Now works with:
|
||||
sum(&vec![1, 2, 3]); // Vec
|
||||
sum(&[1, 2, 3]); // array
|
||||
sum(&array[1..3]); // slice
|
||||
```
|
||||
|
||||
### 3. In-Place Mutation
|
||||
|
||||
```rust
|
||||
// BAD: allocates new String
|
||||
fn make_uppercase(s: &str) -> String {
|
||||
s.to_uppercase()
|
||||
}
|
||||
|
||||
// GOOD when you own the data: mutate in place
|
||||
fn make_uppercase(mut s: String) -> String {
|
||||
s.make_ascii_uppercase(); // in-place for ASCII
|
||||
s
|
||||
}
|
||||
```
|
||||
265
skills/m01-ownership/patterns/common-errors.md
Normal file
265
skills/m01-ownership/patterns/common-errors.md
Normal file
@@ -0,0 +1,265 @@
|
||||
# Common Ownership Errors & Fixes
|
||||
|
||||
## E0382: Use of Moved Value
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
let s = String::from("hello");
|
||||
let s2 = s; // s moved here
|
||||
println!("{}", s); // ERROR: value borrowed after move
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Clone (if ownership not needed)**
|
||||
```rust
|
||||
let s = String::from("hello");
|
||||
let s2 = s.clone(); // s is cloned
|
||||
println!("{}", s); // OK: s still valid
|
||||
```
|
||||
|
||||
**Option 2: Borrow (if modification not needed)**
|
||||
```rust
|
||||
let s = String::from("hello");
|
||||
let s2 = &s; // borrow, not move
|
||||
println!("{}", s); // OK
|
||||
println!("{}", s2); // OK
|
||||
```
|
||||
|
||||
**Option 3: Use Rc/Arc (for shared ownership)**
|
||||
```rust
|
||||
use std::rc::Rc;
|
||||
let s = Rc::new(String::from("hello"));
|
||||
let s2 = Rc::clone(&s); // shared ownership
|
||||
println!("{}", s); // OK
|
||||
println!("{}", s2); // OK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E0597: Borrowed Value Does Not Live Long Enough
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
fn get_str() -> &str {
|
||||
let s = String::from("hello");
|
||||
&s // ERROR: s dropped here, but reference returned
|
||||
}
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Return owned value**
|
||||
```rust
|
||||
fn get_str() -> String {
|
||||
String::from("hello") // return owned value
|
||||
}
|
||||
```
|
||||
|
||||
**Option 2: Use 'static lifetime**
|
||||
```rust
|
||||
fn get_str() -> &'static str {
|
||||
"hello" // string literal has 'static lifetime
|
||||
}
|
||||
```
|
||||
|
||||
**Option 3: Accept reference parameter**
|
||||
```rust
|
||||
fn get_str<'a>(s: &'a str) -> &'a str {
|
||||
s // return reference with same lifetime as input
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E0499: Cannot Borrow as Mutable More Than Once
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
let mut s = String::from("hello");
|
||||
let r1 = &mut s;
|
||||
let r2 = &mut s; // ERROR: second mutable borrow
|
||||
println!("{}, {}", r1, r2);
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Sequential borrows**
|
||||
```rust
|
||||
let mut s = String::from("hello");
|
||||
{
|
||||
let r1 = &mut s;
|
||||
r1.push_str(" world");
|
||||
} // r1 goes out of scope
|
||||
let r2 = &mut s; // OK: r1 no longer exists
|
||||
```
|
||||
|
||||
**Option 2: Use RefCell for interior mutability**
|
||||
```rust
|
||||
use std::cell::RefCell;
|
||||
let s = RefCell::new(String::from("hello"));
|
||||
let mut r1 = s.borrow_mut();
|
||||
// drop r1 before borrowing again
|
||||
drop(r1);
|
||||
let mut r2 = s.borrow_mut();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E0502: Cannot Borrow as Mutable While Immutable Borrow Exists
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
let mut v = vec![1, 2, 3];
|
||||
let first = &v[0]; // immutable borrow
|
||||
v.push(4); // ERROR: mutable borrow while immutable exists
|
||||
println!("{}", first);
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Finish using immutable borrow first**
|
||||
```rust
|
||||
let mut v = vec![1, 2, 3];
|
||||
let first = v[0]; // copy value, not borrow
|
||||
v.push(4); // OK
|
||||
println!("{}", first); // OK: using copied value
|
||||
```
|
||||
|
||||
**Option 2: Clone before mutating**
|
||||
```rust
|
||||
let mut v = vec![1, 2, 3];
|
||||
let first = v[0].clone(); // if T: Clone
|
||||
v.push(4);
|
||||
println!("{}", first);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E0507: Cannot Move Out of Borrowed Content
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
fn take_string(s: &String) {
|
||||
let moved = *s; // ERROR: cannot move out of borrowed content
|
||||
}
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Clone**
|
||||
```rust
|
||||
fn take_string(s: &String) {
|
||||
let cloned = s.clone();
|
||||
}
|
||||
```
|
||||
|
||||
**Option 2: Take ownership in function signature**
|
||||
```rust
|
||||
fn take_string(s: String) { // take ownership
|
||||
let moved = s;
|
||||
}
|
||||
```
|
||||
|
||||
**Option 3: Use mem::take for Option/Default types**
|
||||
```rust
|
||||
fn take_from_option(opt: &mut Option<String>) -> Option<String> {
|
||||
std::mem::take(opt) // replaces with None, returns owned value
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E0515: Return Local Reference
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
fn create_string() -> &String {
|
||||
let s = String::from("hello");
|
||||
&s // ERROR: cannot return reference to local variable
|
||||
}
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Return owned value**
|
||||
```rust
|
||||
fn create_string() -> String {
|
||||
String::from("hello")
|
||||
}
|
||||
```
|
||||
|
||||
**Option 2: Use static/const**
|
||||
```rust
|
||||
fn get_static_str() -> &'static str {
|
||||
"hello"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E0716: Temporary Value Dropped While Borrowed
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
let r: &str = &String::from("hello"); // ERROR: temporary dropped
|
||||
println!("{}", r);
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Bind to variable first**
|
||||
```rust
|
||||
let s = String::from("hello");
|
||||
let r: &str = &s;
|
||||
println!("{}", r);
|
||||
```
|
||||
|
||||
**Option 2: Use let binding with reference**
|
||||
```rust
|
||||
let r: &str = {
|
||||
let s = String::from("hello");
|
||||
// s.as_str() // ERROR: still temporary
|
||||
Box::leak(s.into_boxed_str()) // extreme: leak for 'static
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Loop Ownership Issues
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
let strings = vec![String::from("a"), String::from("b")];
|
||||
for s in strings {
|
||||
println!("{}", s);
|
||||
}
|
||||
// ERROR: strings moved into loop
|
||||
println!("{:?}", strings);
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Iterate by reference**
|
||||
```rust
|
||||
let strings = vec![String::from("a"), String::from("b")];
|
||||
for s in &strings {
|
||||
println!("{}", s);
|
||||
}
|
||||
println!("{:?}", strings); // OK
|
||||
```
|
||||
|
||||
**Option 2: Use iter()**
|
||||
```rust
|
||||
for s in strings.iter() {
|
||||
println!("{}", s);
|
||||
}
|
||||
```
|
||||
|
||||
**Option 3: Clone if needed**
|
||||
```rust
|
||||
for s in strings.clone() {
|
||||
// consumes cloned vec
|
||||
}
|
||||
println!("{:?}", strings); // original still available
|
||||
```
|
||||
229
skills/m01-ownership/patterns/lifetime-patterns.md
Normal file
229
skills/m01-ownership/patterns/lifetime-patterns.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# Lifetime Patterns
|
||||
|
||||
## Basic Lifetime Annotation
|
||||
|
||||
### When Required
|
||||
```rust
|
||||
// ERROR: missing lifetime specifier
|
||||
fn longest(x: &str, y: &str) -> &str {
|
||||
if x.len() > y.len() { x } else { y }
|
||||
}
|
||||
|
||||
// FIX: explicit lifetime
|
||||
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
||||
if x.len() > y.len() { x } else { y }
|
||||
}
|
||||
```
|
||||
|
||||
### Lifetime Elision Rules
|
||||
1. Each input reference gets its own lifetime
|
||||
2. If one input lifetime, output uses same
|
||||
3. If `&self` or `&mut self`, output uses self's lifetime
|
||||
|
||||
```rust
|
||||
// These are equivalent (elision applies):
|
||||
fn first_word(s: &str) -> &str { ... }
|
||||
fn first_word<'a>(s: &'a str) -> &'a str { ... }
|
||||
|
||||
// Method with self (elision applies):
|
||||
impl MyStruct {
|
||||
fn get_ref(&self) -> &str { ... }
|
||||
// Equivalent to:
|
||||
fn get_ref<'a>(&'a self) -> &'a str { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Struct Lifetimes
|
||||
|
||||
### Struct Holding References
|
||||
```rust
|
||||
// Struct must declare lifetime for references
|
||||
struct Excerpt<'a> {
|
||||
part: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Excerpt<'a> {
|
||||
fn level(&self) -> i32 { 3 }
|
||||
|
||||
// Return reference tied to self's lifetime
|
||||
fn get_part(&self) -> &str {
|
||||
self.part
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Lifetimes in Struct
|
||||
```rust
|
||||
struct Multi<'a, 'b> {
|
||||
x: &'a str,
|
||||
y: &'b str,
|
||||
}
|
||||
|
||||
// Use when references may have different lifetimes
|
||||
fn make_multi<'a, 'b>(x: &'a str, y: &'b str) -> Multi<'a, 'b> {
|
||||
Multi { x, y }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 'static Lifetime
|
||||
|
||||
### When to Use
|
||||
```rust
|
||||
// String literals are 'static
|
||||
let s: &'static str = "hello";
|
||||
|
||||
// Owned data can be leaked to 'static
|
||||
let leaked: &'static str = Box::leak(String::from("hello").into_boxed_str());
|
||||
|
||||
// Thread spawn requires 'static or move
|
||||
std::thread::spawn(move || {
|
||||
// closure owns data, satisfies 'static
|
||||
});
|
||||
```
|
||||
|
||||
### Avoid Overusing 'static
|
||||
```rust
|
||||
// BAD: requires 'static unnecessarily
|
||||
fn process(s: &'static str) { ... }
|
||||
|
||||
// GOOD: use generic lifetime
|
||||
fn process<'a>(s: &'a str) { ... }
|
||||
// or
|
||||
fn process(s: &str) { ... } // lifetime elision
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Higher-Ranked Trait Bounds (HRTB)
|
||||
|
||||
### for<'a> Syntax
|
||||
```rust
|
||||
// Function that works with any lifetime
|
||||
fn apply_to_ref<F>(f: F)
|
||||
where
|
||||
F: for<'a> Fn(&'a str) -> &'a str,
|
||||
{
|
||||
let s = String::from("hello");
|
||||
let result = f(&s);
|
||||
println!("{}", result);
|
||||
}
|
||||
```
|
||||
|
||||
### Common Use: Closure Bounds
|
||||
```rust
|
||||
// Closure that borrows any lifetime
|
||||
fn filter_refs<F>(items: &[&str], pred: F) -> Vec<&str>
|
||||
where
|
||||
F: for<'a> Fn(&'a str) -> bool,
|
||||
{
|
||||
items.iter().copied().filter(|s| pred(s)).collect()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lifetime Bounds
|
||||
|
||||
### 'a: 'b (Outlives)
|
||||
```rust
|
||||
// 'a must live at least as long as 'b
|
||||
fn coerce<'a, 'b>(x: &'a str) -> &'b str
|
||||
where
|
||||
'a: 'b,
|
||||
{
|
||||
x
|
||||
}
|
||||
```
|
||||
|
||||
### T: 'a (Type Outlives Lifetime)
|
||||
```rust
|
||||
// T must live at least as long as 'a
|
||||
struct Wrapper<'a, T: 'a> {
|
||||
value: &'a T,
|
||||
}
|
||||
|
||||
// Common pattern with trait objects
|
||||
fn use_trait<'a, T: MyTrait + 'a>(t: &'a T) { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Lifetime Mistakes
|
||||
|
||||
### Mistake 1: Returning Reference to Local
|
||||
```rust
|
||||
// WRONG
|
||||
fn dangle() -> &String {
|
||||
let s = String::from("hello");
|
||||
&s // s dropped, reference invalid
|
||||
}
|
||||
|
||||
// RIGHT
|
||||
fn no_dangle() -> String {
|
||||
String::from("hello")
|
||||
}
|
||||
```
|
||||
|
||||
### Mistake 2: Conflicting Lifetimes
|
||||
```rust
|
||||
// WRONG: might return reference to y which has shorter lifetime
|
||||
fn wrong<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
|
||||
y // ERROR: 'b might not live as long as 'a
|
||||
}
|
||||
|
||||
// RIGHT: use same lifetime or add bound
|
||||
fn right<'a>(x: &'a str, y: &'a str) -> &'a str {
|
||||
y // OK: both have lifetime 'a
|
||||
}
|
||||
```
|
||||
|
||||
### Mistake 3: Struct Outlives Reference
|
||||
```rust
|
||||
// WRONG: s might outlive the string it references
|
||||
let r;
|
||||
{
|
||||
let s = String::from("hello");
|
||||
r = Excerpt { part: &s }; // ERROR
|
||||
}
|
||||
println!("{}", r.part); // s already dropped
|
||||
|
||||
// RIGHT: ensure source outlives struct
|
||||
let s = String::from("hello");
|
||||
let r = Excerpt { part: &s };
|
||||
println!("{}", r.part); // OK: s still in scope
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Subtyping and Variance
|
||||
|
||||
### Covariance
|
||||
```rust
|
||||
// &'a T is covariant in 'a
|
||||
// Can use &'long where &'short expected
|
||||
fn example<'short, 'long: 'short>(long_ref: &'long str) {
|
||||
let short_ref: &'short str = long_ref; // OK: covariance
|
||||
}
|
||||
```
|
||||
|
||||
### Invariance
|
||||
```rust
|
||||
// &'a mut T is invariant in 'a
|
||||
fn example<'a, 'b>(x: &'a mut &'b str, y: &'b str) {
|
||||
*x = y; // ERROR if 'a and 'b are different
|
||||
}
|
||||
```
|
||||
|
||||
### Practical Impact
|
||||
```rust
|
||||
// This works due to covariance
|
||||
fn accept_any<'a>(s: &'a str) { ... }
|
||||
|
||||
let s = String::from("hello");
|
||||
let long_lived: &str = &s;
|
||||
accept_any(long_lived); // 'long coerces to 'short
|
||||
```
|
||||
159
skills/m02-resource/SKILL.md
Normal file
159
skills/m02-resource/SKILL.md
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
name: m02-resource
|
||||
description: "CRITICAL: Use for smart pointers and resource management. Triggers: Box, Rc, Arc, Weak, RefCell, Cell, smart pointer, heap allocation, reference counting, RAII, Drop, should I use Box or Rc, when to use Arc vs Rc, 智能指针, 引用计数, 堆分配"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Resource Management
|
||||
|
||||
> **Layer 1: Language Mechanics**
|
||||
|
||||
## Core Question
|
||||
|
||||
**What ownership pattern does this resource need?**
|
||||
|
||||
Before choosing a smart pointer, understand:
|
||||
- Is ownership single or shared?
|
||||
- Is access single-threaded or multi-threaded?
|
||||
- Are there potential cycles?
|
||||
|
||||
---
|
||||
|
||||
## Error → Design Question
|
||||
|
||||
| Error | Don't Just Say | Ask Instead |
|
||||
|-------|----------------|-------------|
|
||||
| "Need heap allocation" | "Use Box" | Why can't this be on stack? |
|
||||
| Rc memory leak | "Use Weak" | Is the cycle necessary in design? |
|
||||
| RefCell panic | "Use try_borrow" | Is runtime check the right approach? |
|
||||
| Arc overhead complaint | "Accept it" | Is multi-thread access actually needed? |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before choosing a smart pointer:
|
||||
|
||||
1. **What's the ownership model?**
|
||||
- Single owner → Box or owned value
|
||||
- Shared ownership → Rc/Arc
|
||||
- Weak reference → Weak
|
||||
|
||||
2. **What's the thread context?**
|
||||
- Single-thread → Rc, Cell, RefCell
|
||||
- Multi-thread → Arc, Mutex, RwLock
|
||||
|
||||
3. **Are there cycles?**
|
||||
- Yes → One direction must be Weak
|
||||
- No → Regular Rc/Arc is fine
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
When pointer choice is unclear, trace to design:
|
||||
|
||||
```
|
||||
"Should I use Arc or Rc?"
|
||||
↑ Ask: Is this data shared across threads?
|
||||
↑ Check: m07-concurrency (thread model)
|
||||
↑ Check: domain-* (performance constraints)
|
||||
```
|
||||
|
||||
| Situation | Trace To | Question |
|
||||
|-----------|----------|----------|
|
||||
| Rc vs Arc confusion | m07-concurrency | What's the concurrency model? |
|
||||
| RefCell panics | m03-mutability | Is interior mutability right here? |
|
||||
| Memory leaks | m12-lifecycle | Where should cleanup happen? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From design to implementation:
|
||||
|
||||
```
|
||||
"Need single-owner heap data"
|
||||
↓ Use: Box<T>
|
||||
|
||||
"Need shared immutable data (single-thread)"
|
||||
↓ Use: Rc<T>
|
||||
|
||||
"Need shared immutable data (multi-thread)"
|
||||
↓ Use: Arc<T>
|
||||
|
||||
"Need to break reference cycle"
|
||||
↓ Use: Weak<T>
|
||||
|
||||
"Need shared mutable data"
|
||||
↓ Single-thread: Rc<RefCell<T>>
|
||||
↓ Multi-thread: Arc<Mutex<T>> or Arc<RwLock<T>>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Type | Ownership | Thread-Safe | Use When |
|
||||
|------|-----------|-------------|----------|
|
||||
| `Box<T>` | Single | Yes | Heap allocation, recursive types |
|
||||
| `Rc<T>` | Shared | No | Single-thread shared ownership |
|
||||
| `Arc<T>` | Shared | Yes | Multi-thread shared ownership |
|
||||
| `Weak<T>` | Weak ref | Same as Rc/Arc | Break reference cycles |
|
||||
| `Cell<T>` | Single | No | Interior mutability (Copy types) |
|
||||
| `RefCell<T>` | Single | No | Interior mutability (runtime check) |
|
||||
|
||||
## Decision Flowchart
|
||||
|
||||
```
|
||||
Need heap allocation?
|
||||
├─ Yes → Single owner?
|
||||
│ ├─ Yes → Box<T>
|
||||
│ └─ No → Multi-thread?
|
||||
│ ├─ Yes → Arc<T>
|
||||
│ └─ No → Rc<T>
|
||||
└─ No → Stack allocation (default)
|
||||
|
||||
Have reference cycles?
|
||||
├─ Yes → Use Weak for one direction
|
||||
└─ No → Regular Rc/Arc
|
||||
|
||||
Need interior mutability?
|
||||
├─ Yes → Thread-safe needed?
|
||||
│ ├─ Yes → Mutex<T> or RwLock<T>
|
||||
│ └─ No → T: Copy? → Cell<T> : RefCell<T>
|
||||
└─ No → Use &mut T
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Problem | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| Rc cycle leak | Mutual strong refs | Use Weak for one direction |
|
||||
| RefCell panic | Borrow conflict at runtime | Use try_borrow or restructure |
|
||||
| Arc overhead | Atomic ops in hot path | Consider Rc if single-threaded |
|
||||
| Box unnecessary | Data fits on stack | Remove Box |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| Arc everywhere | Unnecessary atomic overhead | Use Rc for single-thread |
|
||||
| RefCell everywhere | Runtime panics | Design clear ownership |
|
||||
| Box for small types | Unnecessary allocation | Stack allocation |
|
||||
| Ignore Weak for cycles | Memory leaks | Design parent-child with Weak |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Ownership errors | m01-ownership |
|
||||
| Interior mutability details | m03-mutability |
|
||||
| Multi-thread context | m07-concurrency |
|
||||
| Resource lifecycle | m12-lifecycle |
|
||||
153
skills/m03-mutability/SKILL.md
Normal file
153
skills/m03-mutability/SKILL.md
Normal file
@@ -0,0 +1,153 @@
|
||||
---
|
||||
name: m03-mutability
|
||||
description: "CRITICAL: Use for mutability issues. Triggers: E0596, E0499, E0502, cannot borrow as mutable, already borrowed as immutable, mut, &mut, interior mutability, Cell, RefCell, Mutex, RwLock, 可变性, 内部可变性, 借用冲突"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Mutability
|
||||
|
||||
> **Layer 1: Language Mechanics**
|
||||
|
||||
## Core Question
|
||||
|
||||
**Why does this data need to change, and who can change it?**
|
||||
|
||||
Before adding interior mutability, understand:
|
||||
- Is mutation essential or accidental complexity?
|
||||
- Who should control mutation?
|
||||
- Is the mutation pattern safe?
|
||||
|
||||
---
|
||||
|
||||
## Error → Design Question
|
||||
|
||||
| Error | Don't Just Say | Ask Instead |
|
||||
|-------|----------------|-------------|
|
||||
| E0596 | "Add mut" | Should this really be mutable? |
|
||||
| E0499 | "Split borrows" | Is the data structure right? |
|
||||
| E0502 | "Separate scopes" | Why do we need both borrows? |
|
||||
| RefCell panic | "Use try_borrow" | Is runtime check appropriate? |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before adding mutability:
|
||||
|
||||
1. **Is mutation necessary?**
|
||||
- Maybe transform → return new value
|
||||
- Maybe builder → construct immutably
|
||||
|
||||
2. **Who controls mutation?**
|
||||
- External caller → `&mut T`
|
||||
- Internal logic → interior mutability
|
||||
- Concurrent access → synchronized mutability
|
||||
|
||||
3. **What's the thread context?**
|
||||
- Single-thread → Cell/RefCell
|
||||
- Multi-thread → Mutex/RwLock/Atomic
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
When mutability conflicts persist:
|
||||
|
||||
```
|
||||
E0499/E0502 (borrow conflicts)
|
||||
↑ Ask: Is the data structure designed correctly?
|
||||
↑ Check: m09-domain (should data be split?)
|
||||
↑ Check: m07-concurrency (is async involved?)
|
||||
```
|
||||
|
||||
| Persistent Error | Trace To | Question |
|
||||
|-----------------|----------|----------|
|
||||
| Repeated borrow conflicts | m09-domain | Should data be restructured? |
|
||||
| RefCell in async | m07-concurrency | Is Send/Sync needed? |
|
||||
| Mutex deadlocks | m07-concurrency | Is the lock design right? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From design to implementation:
|
||||
|
||||
```
|
||||
"Need mutable access from &self"
|
||||
↓ T: Copy → Cell<T>
|
||||
↓ T: !Copy → RefCell<T>
|
||||
|
||||
"Need thread-safe mutation"
|
||||
↓ Simple counters → AtomicXxx
|
||||
↓ Complex data → Mutex<T> or RwLock<T>
|
||||
|
||||
"Need shared mutable state"
|
||||
↓ Single-thread: Rc<RefCell<T>>
|
||||
↓ Multi-thread: Arc<Mutex<T>>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Borrow Rules
|
||||
|
||||
```
|
||||
At any time, you can have EITHER:
|
||||
├─ Multiple &T (immutable borrows)
|
||||
└─ OR one &mut T (mutable borrow)
|
||||
|
||||
Never both simultaneously.
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Thread-Safe | Runtime Cost | Use When |
|
||||
|---------|-------------|--------------|----------|
|
||||
| `&mut T` | N/A | Zero | Exclusive mutable access |
|
||||
| `Cell<T>` | No | Zero | Copy types, no refs needed |
|
||||
| `RefCell<T>` | No | Runtime check | Non-Copy, need runtime borrow |
|
||||
| `Mutex<T>` | Yes | Lock contention | Thread-safe mutation |
|
||||
| `RwLock<T>` | Yes | Lock contention | Many readers, few writers |
|
||||
| `Atomic*` | Yes | Minimal | Simple types (bool, usize) |
|
||||
|
||||
## Error Code Reference
|
||||
|
||||
| Error | Cause | Quick Fix |
|
||||
|-------|-------|-----------|
|
||||
| E0596 | Borrowing immutable as mutable | Add `mut` or redesign |
|
||||
| E0499 | Multiple mutable borrows | Restructure code flow |
|
||||
| E0502 | &mut while & exists | Separate borrow scopes |
|
||||
|
||||
---
|
||||
|
||||
## Interior Mutability Decision
|
||||
|
||||
| Scenario | Choose |
|
||||
|----------|--------|
|
||||
| T: Copy, single-thread | `Cell<T>` |
|
||||
| T: !Copy, single-thread | `RefCell<T>` |
|
||||
| T: Copy, multi-thread | `AtomicXxx` |
|
||||
| T: !Copy, multi-thread | `Mutex<T>` or `RwLock<T>` |
|
||||
| Read-heavy, multi-thread | `RwLock<T>` |
|
||||
| Simple flags/counters | `AtomicBool`, `AtomicUsize` |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| RefCell everywhere | Runtime panics | Clear ownership design |
|
||||
| Mutex for single-thread | Unnecessary overhead | RefCell |
|
||||
| Ignore RefCell panic | Hard to debug | Handle or restructure |
|
||||
| Lock inside hot loop | Performance killer | Batch operations |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Smart pointer choice | m02-resource |
|
||||
| Thread safety | m07-concurrency |
|
||||
| Data structure design | m09-domain |
|
||||
| Anti-patterns | m15-anti-pattern |
|
||||
165
skills/m04-zero-cost/SKILL.md
Normal file
165
skills/m04-zero-cost/SKILL.md
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
name: m04-zero-cost
|
||||
description: "CRITICAL: Use for generics, traits, zero-cost abstraction. Triggers: E0277, E0308, E0599, generic, trait, impl, dyn, where, monomorphization, static dispatch, dynamic dispatch, impl Trait, trait bound not satisfied, 泛型, 特征, 零成本抽象, 单态化"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Zero-Cost Abstraction
|
||||
|
||||
> **Layer 1: Language Mechanics**
|
||||
|
||||
## Core Question
|
||||
|
||||
**Do we need compile-time or runtime polymorphism?**
|
||||
|
||||
Before choosing between generics and trait objects:
|
||||
- Is the type known at compile time?
|
||||
- Is a heterogeneous collection needed?
|
||||
- What's the performance priority?
|
||||
|
||||
---
|
||||
|
||||
## Error → Design Question
|
||||
|
||||
| Error | Don't Just Say | Ask Instead |
|
||||
|-------|----------------|-------------|
|
||||
| E0277 | "Add trait bound" | Is this abstraction at the right level? |
|
||||
| E0308 | "Fix the type" | Should types be unified or distinct? |
|
||||
| E0599 | "Import the trait" | Is the trait the right abstraction? |
|
||||
| E0038 | "Make object-safe" | Do we really need dynamic dispatch? |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before adding trait bounds:
|
||||
|
||||
1. **What abstraction is needed?**
|
||||
- Same behavior, different types → trait
|
||||
- Different behavior, same type → enum
|
||||
- No abstraction needed → concrete type
|
||||
|
||||
2. **When is type known?**
|
||||
- Compile time → generics (static dispatch)
|
||||
- Runtime → trait objects (dynamic dispatch)
|
||||
|
||||
3. **What's the trade-off priority?**
|
||||
- Performance → generics
|
||||
- Compile time → trait objects
|
||||
- Flexibility → depends
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
When type system fights back:
|
||||
|
||||
```
|
||||
E0277 (trait bound not satisfied)
|
||||
↑ Ask: Is the abstraction level correct?
|
||||
↑ Check: m09-domain (what behavior is being abstracted?)
|
||||
↑ Check: m05-type-driven (should use newtype?)
|
||||
```
|
||||
|
||||
| Persistent Error | Trace To | Question |
|
||||
|-----------------|----------|----------|
|
||||
| Complex trait bounds | m09-domain | Is the abstraction right? |
|
||||
| Object safety issues | m05-type-driven | Can typestate help? |
|
||||
| Type explosion | m10-performance | Accept dyn overhead? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From design to implementation:
|
||||
|
||||
```
|
||||
"Need to abstract over types with same behavior"
|
||||
↓ Types known at compile time → impl Trait or generics
|
||||
↓ Types determined at runtime → dyn Trait
|
||||
|
||||
"Need collection of different types"
|
||||
↓ Closed set → enum
|
||||
↓ Open set → Vec<Box<dyn Trait>>
|
||||
|
||||
"Need to return different types"
|
||||
↓ Same type → impl Trait
|
||||
↓ Different types → Box<dyn Trait>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Dispatch | Code Size | Runtime Cost |
|
||||
|---------|----------|-----------|--------------|
|
||||
| `fn foo<T: Trait>()` | Static | +bloat | Zero |
|
||||
| `fn foo(x: &dyn Trait)` | Dynamic | Minimal | vtable lookup |
|
||||
| `impl Trait` return | Static | +bloat | Zero |
|
||||
| `Box<dyn Trait>` | Dynamic | Minimal | Allocation + vtable |
|
||||
|
||||
## Syntax Comparison
|
||||
|
||||
```rust
|
||||
// Static dispatch - type known at compile time
|
||||
fn process(x: impl Display) { } // argument position
|
||||
fn process<T: Display>(x: T) { } // explicit generic
|
||||
fn get() -> impl Display { } // return position
|
||||
|
||||
// Dynamic dispatch - type determined at runtime
|
||||
fn process(x: &dyn Display) { } // reference
|
||||
fn process(x: Box<dyn Display>) { } // owned
|
||||
```
|
||||
|
||||
## Error Code Reference
|
||||
|
||||
| Error | Cause | Quick Fix |
|
||||
|-------|-------|-----------|
|
||||
| E0277 | Type doesn't impl trait | Add impl or change bound |
|
||||
| E0308 | Type mismatch | Check generic params |
|
||||
| E0599 | No method found | Import trait with `use` |
|
||||
| E0038 | Trait not object-safe | Use generics or redesign |
|
||||
|
||||
---
|
||||
|
||||
## Decision Guide
|
||||
|
||||
| Scenario | Choose | Why |
|
||||
|----------|--------|-----|
|
||||
| Performance critical | Generics | Zero runtime cost |
|
||||
| Heterogeneous collection | `dyn Trait` | Different types at runtime |
|
||||
| Plugin architecture | `dyn Trait` | Unknown types at compile |
|
||||
| Reduce compile time | `dyn Trait` | Less monomorphization |
|
||||
| Small, known type set | `enum` | No indirection |
|
||||
|
||||
---
|
||||
|
||||
## Object Safety
|
||||
|
||||
A trait is object-safe if it:
|
||||
- Doesn't have `Self: Sized` bound
|
||||
- Doesn't return `Self`
|
||||
- Doesn't have generic methods
|
||||
- Uses `where Self: Sized` for non-object-safe methods
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| Over-generic everything | Compile time, complexity | Concrete types when possible |
|
||||
| `dyn` for known types | Unnecessary indirection | Generics |
|
||||
| Complex trait hierarchies | Hard to understand | Simpler design |
|
||||
| Ignore object safety | Limits flexibility | Plan for dyn if needed |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Type-driven design | m05-type-driven |
|
||||
| Domain abstraction | m09-domain |
|
||||
| Performance concerns | m10-performance |
|
||||
| Send/Sync bounds | m07-concurrency |
|
||||
175
skills/m05-type-driven/SKILL.md
Normal file
175
skills/m05-type-driven/SKILL.md
Normal file
@@ -0,0 +1,175 @@
|
||||
---
|
||||
name: m05-type-driven
|
||||
description: "CRITICAL: Use for type-driven design. Triggers: type state, PhantomData, newtype, marker trait, builder pattern, make invalid states unrepresentable, compile-time validation, sealed trait, ZST, 类型状态, 新类型模式, 类型驱动设计"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Type-Driven Design
|
||||
|
||||
> **Layer 1: Language Mechanics**
|
||||
|
||||
## Core Question
|
||||
|
||||
**How can the type system prevent invalid states?**
|
||||
|
||||
Before reaching for runtime checks:
|
||||
- Can the compiler catch this error?
|
||||
- Can invalid states be unrepresentable?
|
||||
- Can the type encode the invariant?
|
||||
|
||||
---
|
||||
|
||||
## Error → Design Question
|
||||
|
||||
| Pattern | Don't Just Say | Ask Instead |
|
||||
|---------|----------------|-------------|
|
||||
| Primitive obsession | "It's just a string" | What does this value represent? |
|
||||
| Boolean flags | "Add an is_valid flag" | Can states be types? |
|
||||
| Optional everywhere | "Check for None" | Is absence really possible? |
|
||||
| Validation at runtime | "Return Err if invalid" | Can we validate at construction? |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before adding runtime validation:
|
||||
|
||||
1. **Can the type encode the constraint?**
|
||||
- Numeric range → bounded types or newtypes
|
||||
- Valid states → type state pattern
|
||||
- Semantic meaning → newtype
|
||||
|
||||
2. **When is validation possible?**
|
||||
- At construction → validated newtype
|
||||
- At state transition → type state
|
||||
- Only at runtime → Result with clear error
|
||||
|
||||
3. **Who needs to know the invariant?**
|
||||
- Compiler → type-level encoding
|
||||
- API users → clear type signatures
|
||||
- Runtime only → documentation
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
When type design is unclear:
|
||||
|
||||
```
|
||||
"Need to validate email format"
|
||||
↑ Ask: Is this a domain value object?
|
||||
↑ Check: m09-domain (Email as Value Object)
|
||||
↑ Check: domain-* (validation requirements)
|
||||
```
|
||||
|
||||
| Situation | Trace To | Question |
|
||||
|-----------|----------|----------|
|
||||
| What types to create | m09-domain | What's the domain model? |
|
||||
| State machine design | m09-domain | What are valid transitions? |
|
||||
| Marker trait usage | m04-zero-cost | Static or dynamic dispatch? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From design to implementation:
|
||||
|
||||
```
|
||||
"Need type-safe wrapper for primitives"
|
||||
↓ Newtype: struct UserId(u64);
|
||||
|
||||
"Need compile-time state validation"
|
||||
↓ Type State: Connection<Connected>
|
||||
|
||||
"Need to track phantom type parameters"
|
||||
↓ PhantomData: PhantomData<T>
|
||||
|
||||
"Need capability markers"
|
||||
↓ Marker Trait: trait Validated {}
|
||||
|
||||
"Need gradual construction"
|
||||
↓ Builder: Builder::new().field(x).build()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Purpose | Example |
|
||||
|---------|---------|---------|
|
||||
| Newtype | Type safety | `struct UserId(u64);` |
|
||||
| Type State | State machine | `Connection<Connected>` |
|
||||
| PhantomData | Variance/lifetime | `PhantomData<&'a T>` |
|
||||
| Marker Trait | Capability flag | `trait Validated {}` |
|
||||
| Builder | Gradual construction | `Builder::new().name("x").build()` |
|
||||
| Sealed Trait | Prevent external impl | `mod private { pub trait Sealed {} }` |
|
||||
|
||||
## Pattern Examples
|
||||
|
||||
### Newtype
|
||||
|
||||
```rust
|
||||
struct Email(String); // Not just any string
|
||||
|
||||
impl Email {
|
||||
pub fn new(s: &str) -> Result<Self, ValidationError> {
|
||||
// Validate once, trust forever
|
||||
validate_email(s)?;
|
||||
Ok(Self(s.to_string()))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Type State
|
||||
|
||||
```rust
|
||||
struct Connection<State>(TcpStream, PhantomData<State>);
|
||||
|
||||
struct Disconnected;
|
||||
struct Connected;
|
||||
struct Authenticated;
|
||||
|
||||
impl Connection<Disconnected> {
|
||||
fn connect(self) -> Connection<Connected> { ... }
|
||||
}
|
||||
|
||||
impl Connection<Connected> {
|
||||
fn authenticate(self) -> Connection<Authenticated> { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decision Guide
|
||||
|
||||
| Need | Pattern |
|
||||
|------|---------|
|
||||
| Type safety for primitives | Newtype |
|
||||
| Compile-time state validation | Type State |
|
||||
| Lifetime/variance markers | PhantomData |
|
||||
| Capability flags | Marker Trait |
|
||||
| Gradual construction | Builder |
|
||||
| Closed set of impls | Sealed Trait |
|
||||
| Zero-sized type marker | ZST struct |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| Boolean flags for states | Runtime errors | Type state |
|
||||
| String for semantic types | No type safety | Newtype |
|
||||
| Option for uninitialized | Unclear invariant | Builder |
|
||||
| Public fields with invariants | Invariant violation | Private + validated new() |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Domain modeling | m09-domain |
|
||||
| Trait design | m04-zero-cost |
|
||||
| Error handling in constructors | m06-error-handling |
|
||||
| Anti-patterns | m15-anti-pattern |
|
||||
166
skills/m06-error-handling/SKILL.md
Normal file
166
skills/m06-error-handling/SKILL.md
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
name: m06-error-handling
|
||||
description: "CRITICAL: Use for error handling. Triggers: Result, Option, Error, ?, unwrap, expect, panic, anyhow, thiserror, when to panic vs return Result, custom error, error propagation, 错误处理, Result 用法, 什么时候用 panic"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Error Handling
|
||||
|
||||
> **Layer 1: Language Mechanics**
|
||||
|
||||
## Core Question
|
||||
|
||||
**Is this failure expected or a bug?**
|
||||
|
||||
Before choosing error handling strategy:
|
||||
- Can this fail in normal operation?
|
||||
- Who should handle this failure?
|
||||
- What context does the caller need?
|
||||
|
||||
---
|
||||
|
||||
## Error → Design Question
|
||||
|
||||
| Pattern | Don't Just Say | Ask Instead |
|
||||
|---------|----------------|-------------|
|
||||
| unwrap panics | "Use ?" | Is None/Err actually possible here? |
|
||||
| Type mismatch on ? | "Use anyhow" | Are error types designed correctly? |
|
||||
| Lost error context | "Add .context()" | What does the caller need to know? |
|
||||
| Too many error variants | "Use Box<dyn Error>" | Is error granularity right? |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before handling an error:
|
||||
|
||||
1. **What kind of failure is this?**
|
||||
- Expected → Result<T, E>
|
||||
- Absence normal → Option<T>
|
||||
- Bug/invariant → panic!
|
||||
- Unrecoverable → panic!
|
||||
|
||||
2. **Who handles this?**
|
||||
- Caller → propagate with ?
|
||||
- Current function → match/if-let
|
||||
- User → friendly error message
|
||||
- Programmer → panic with message
|
||||
|
||||
3. **What context is needed?**
|
||||
- Type of error → thiserror variants
|
||||
- Call chain → anyhow::Context
|
||||
- Debug info → anyhow or tracing
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
When error strategy is unclear:
|
||||
|
||||
```
|
||||
"Should I return Result or Option?"
|
||||
↑ Ask: Is absence/failure normal or exceptional?
|
||||
↑ Check: m09-domain (what does domain say?)
|
||||
↑ Check: domain-* (error handling requirements)
|
||||
```
|
||||
|
||||
| Situation | Trace To | Question |
|
||||
|-----------|----------|----------|
|
||||
| Too many unwraps | m09-domain | Is the data model right? |
|
||||
| Error context design | m13-domain-error | What recovery is needed? |
|
||||
| Library vs app errors | m11-ecosystem | Who are the consumers? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From design to implementation:
|
||||
|
||||
```
|
||||
"Expected failure, library code"
|
||||
↓ Use: thiserror for typed errors
|
||||
|
||||
"Expected failure, application code"
|
||||
↓ Use: anyhow for ergonomic errors
|
||||
|
||||
"Absence is normal (find, get, lookup)"
|
||||
↓ Use: Option<T>
|
||||
|
||||
"Bug or invariant violation"
|
||||
↓ Use: panic!, assert!, unreachable!
|
||||
|
||||
"Need to propagate with context"
|
||||
↓ Use: .context("what was happening")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | When | Example |
|
||||
|---------|------|---------|
|
||||
| `Result<T, E>` | Recoverable error | `fn read() -> Result<String, io::Error>` |
|
||||
| `Option<T>` | Absence is normal | `fn find() -> Option<&Item>` |
|
||||
| `?` | Propagate error | `let data = file.read()?;` |
|
||||
| `unwrap()` | Dev/test only | `config.get("key").unwrap()` |
|
||||
| `expect()` | Invariant holds | `env.get("HOME").expect("HOME set")` |
|
||||
| `panic!` | Unrecoverable | `panic!("critical failure")` |
|
||||
|
||||
## Library vs Application
|
||||
|
||||
| Context | Error Crate | Why |
|
||||
|---------|-------------|-----|
|
||||
| Library | `thiserror` | Typed errors for consumers |
|
||||
| Application | `anyhow` | Ergonomic error handling |
|
||||
| Mixed | Both | thiserror at boundaries, anyhow internally |
|
||||
|
||||
## Decision Flowchart
|
||||
|
||||
```
|
||||
Is failure expected?
|
||||
├─ Yes → Is absence the only "failure"?
|
||||
│ ├─ Yes → Option<T>
|
||||
│ └─ No → Result<T, E>
|
||||
│ ├─ Library → thiserror
|
||||
│ └─ Application → anyhow
|
||||
└─ No → Is it a bug?
|
||||
├─ Yes → panic!, assert!
|
||||
└─ No → Consider if really unrecoverable
|
||||
|
||||
Use ? → Need context?
|
||||
├─ Yes → .context("message")
|
||||
└─ No → Plain ?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| `unwrap()` panic | Unhandled None/Err | Use `?` or match |
|
||||
| Type mismatch | Different error types | Use `anyhow` or `From` |
|
||||
| Lost context | `?` without context | Add `.context()` |
|
||||
| `cannot use ?` | Missing Result return | Return `Result<(), E>` |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| `.unwrap()` everywhere | Panics in production | `.expect("reason")` or `?` |
|
||||
| Ignore errors silently | Bugs hidden | Handle or propagate |
|
||||
| `panic!` for expected errors | Bad UX, no recovery | Result |
|
||||
| Box<dyn Error> everywhere | Lost type info | thiserror |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Domain error strategy | m13-domain-error |
|
||||
| Crate boundaries | m11-ecosystem |
|
||||
| Type-safe errors | m05-type-driven |
|
||||
| Mental models | m14-mental-model |
|
||||
332
skills/m06-error-handling/examples/library-vs-app.md
Normal file
332
skills/m06-error-handling/examples/library-vs-app.md
Normal file
@@ -0,0 +1,332 @@
|
||||
# Error Handling: Library vs Application
|
||||
|
||||
## Library Error Design
|
||||
|
||||
### Principles
|
||||
1. **Define specific error types** - Don't use `anyhow` in libraries
|
||||
2. **Implement std::error::Error** - For compatibility
|
||||
3. **Provide error variants** - Let users match on errors
|
||||
4. **Include source errors** - Enable error chains
|
||||
5. **Be `Send + Sync`** - For async compatibility
|
||||
|
||||
### Example: Library Error Type
|
||||
```rust
|
||||
// lib.rs
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DatabaseError {
|
||||
#[error("connection failed: {host}:{port}")]
|
||||
ConnectionFailed {
|
||||
host: String,
|
||||
port: u16,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
#[error("query failed: {query}")]
|
||||
QueryFailed {
|
||||
query: String,
|
||||
#[source]
|
||||
source: SqlError,
|
||||
},
|
||||
|
||||
#[error("record not found: {table}.{id}")]
|
||||
NotFound { table: String, id: String },
|
||||
|
||||
#[error("constraint violation: {0}")]
|
||||
ConstraintViolation(String),
|
||||
}
|
||||
|
||||
// Public Result alias
|
||||
pub type Result<T> = std::result::Result<T, DatabaseError>;
|
||||
|
||||
// Library functions
|
||||
pub fn connect(host: &str, port: u16) -> Result<Connection> {
|
||||
// ...
|
||||
}
|
||||
|
||||
pub fn query(conn: &Connection, sql: &str) -> Result<Rows> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Library Usage of Errors
|
||||
```rust
|
||||
impl Database {
|
||||
pub fn get_user(&self, id: &str) -> Result<User> {
|
||||
let rows = self.query(&format!("SELECT * FROM users WHERE id = '{}'", id))?;
|
||||
|
||||
rows.first()
|
||||
.cloned()
|
||||
.ok_or_else(|| DatabaseError::NotFound {
|
||||
table: "users".to_string(),
|
||||
id: id.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Application Error Design
|
||||
|
||||
### Principles
|
||||
1. **Use anyhow for convenience** - Or custom unified error
|
||||
2. **Add context liberally** - Help debugging
|
||||
3. **Log at boundaries** - Don't log in libraries
|
||||
4. **Convert to user-friendly messages** - For display
|
||||
|
||||
### Example: Application Error Handling
|
||||
```rust
|
||||
// main.rs
|
||||
use anyhow::{Context, Result};
|
||||
use tracing::{error, info};
|
||||
|
||||
async fn run_server() -> Result<()> {
|
||||
let config = load_config()
|
||||
.context("failed to load configuration")?;
|
||||
|
||||
let db = Database::connect(&config.db_url)
|
||||
.await
|
||||
.context("failed to connect to database")?;
|
||||
|
||||
let server = Server::new(config.port)
|
||||
.context("failed to create server")?;
|
||||
|
||||
info!("Server starting on port {}", config.port);
|
||||
|
||||
server.run(db).await
|
||||
.context("server error")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::init();
|
||||
|
||||
if let Err(e) = run_server().await {
|
||||
error!("Application error: {:#}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Converting Library Errors
|
||||
```rust
|
||||
use mylib::DatabaseError;
|
||||
|
||||
async fn get_user_handler(id: &str) -> Result<Response> {
|
||||
match db.get_user(id).await {
|
||||
Ok(user) => Ok(Response::json(user)),
|
||||
|
||||
Err(DatabaseError::NotFound { .. }) => {
|
||||
Ok(Response::not_found("User not found"))
|
||||
}
|
||||
|
||||
Err(DatabaseError::ConnectionFailed { .. }) => {
|
||||
error!("Database connection failed");
|
||||
Ok(Response::internal_error("Service unavailable"))
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
error!("Database error: {}", e);
|
||||
Err(e.into()) // Convert to anyhow::Error
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling Layers
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Application Layer │
|
||||
│ - Use anyhow or unified error │
|
||||
│ - Add context at boundaries │
|
||||
│ - Log errors │
|
||||
│ - Convert to user messages │
|
||||
└─────────────────────────────────────┘
|
||||
│
|
||||
│ calls
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ Service Layer │
|
||||
│ - Map between error types │
|
||||
│ - Add business context │
|
||||
│ - Handle recoverable errors │
|
||||
└─────────────────────────────────────┘
|
||||
│
|
||||
│ calls
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ Library Layer │
|
||||
│ - Define specific error types │
|
||||
│ - Use thiserror │
|
||||
│ - Include source errors │
|
||||
│ - No logging │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### HTTP API Error Response
|
||||
```rust
|
||||
use axum::{response::IntoResponse, http::StatusCode};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ErrorResponse {
|
||||
error: String,
|
||||
code: String,
|
||||
}
|
||||
|
||||
enum AppError {
|
||||
NotFound(String),
|
||||
BadRequest(String),
|
||||
Internal(anyhow::Error),
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
let (status, error, code) = match self {
|
||||
AppError::NotFound(msg) => {
|
||||
(StatusCode::NOT_FOUND, msg, "NOT_FOUND")
|
||||
}
|
||||
AppError::BadRequest(msg) => {
|
||||
(StatusCode::BAD_REQUEST, msg, "BAD_REQUEST")
|
||||
}
|
||||
AppError::Internal(e) => {
|
||||
tracing::error!("Internal error: {:#}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Internal server error".to_string(),
|
||||
"INTERNAL_ERROR",
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let body = ErrorResponse {
|
||||
error,
|
||||
code: code.to_string(),
|
||||
};
|
||||
|
||||
(status, axum::Json(body)).into_response()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CLI Error Handling
|
||||
```rust
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
#[arg(short, long)]
|
||||
config: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(e) = run() {
|
||||
eprintln!("Error: {:#}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let config = std::fs::read_to_string(&args.config)
|
||||
.context(format!("Failed to read config file: {}", args.config))?;
|
||||
|
||||
let parsed: Config = toml::from_str(&config)
|
||||
.context("Failed to parse config file")?;
|
||||
|
||||
process(parsed)?;
|
||||
|
||||
println!("Done!");
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Error Handling
|
||||
|
||||
### Testing Error Cases
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_not_found_error() {
|
||||
let result = db.get_user("nonexistent");
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(DatabaseError::NotFound { table, id })
|
||||
if table == "users" && id == "nonexistent"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_message() {
|
||||
let err = DatabaseError::NotFound {
|
||||
table: "users".to_string(),
|
||||
id: "123".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(err.to_string(), "record not found: users.123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_chain() {
|
||||
let io_err = std::io::Error::new(
|
||||
std::io::ErrorKind::ConnectionRefused,
|
||||
"connection refused"
|
||||
);
|
||||
|
||||
let err = DatabaseError::ConnectionFailed {
|
||||
host: "localhost".to_string(),
|
||||
port: 5432,
|
||||
source: io_err,
|
||||
};
|
||||
|
||||
// Check source is preserved
|
||||
assert!(err.source().is_some());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing with anyhow
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_with_context() -> anyhow::Result<()> {
|
||||
let result = process("valid input")?;
|
||||
assert_eq!(result, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_context() {
|
||||
let err = process("invalid")
|
||||
.context("processing failed")
|
||||
.unwrap_err();
|
||||
|
||||
// Check error chain contains expected text
|
||||
let chain = format!("{:#}", err);
|
||||
assert!(chain.contains("processing failed"));
|
||||
}
|
||||
}
|
||||
```
|
||||
404
skills/m06-error-handling/patterns/error-patterns.md
Normal file
404
skills/m06-error-handling/patterns/error-patterns.md
Normal file
@@ -0,0 +1,404 @@
|
||||
# Error Handling Patterns
|
||||
|
||||
## The ? Operator
|
||||
|
||||
### Basic Usage
|
||||
```rust
|
||||
fn read_config() -> Result<Config, io::Error> {
|
||||
let content = std::fs::read_to_string("config.toml")?;
|
||||
let config: Config = toml::from_str(&content)?; // needs From impl
|
||||
Ok(config)
|
||||
}
|
||||
```
|
||||
|
||||
### With Different Error Types
|
||||
```rust
|
||||
use std::error::Error;
|
||||
|
||||
// Box<dyn Error> for quick prototyping
|
||||
fn process() -> Result<(), Box<dyn Error>> {
|
||||
let file = std::fs::read_to_string("data.txt")?;
|
||||
let num: i32 = file.trim().parse()?; // different error type
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Conversion with From
|
||||
```rust
|
||||
#[derive(Debug)]
|
||||
enum MyError {
|
||||
Io(std::io::Error),
|
||||
Parse(std::num::ParseIntError),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for MyError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
MyError::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::num::ParseIntError> for MyError {
|
||||
fn from(err: std::num::ParseIntError) -> Self {
|
||||
MyError::Parse(err)
|
||||
}
|
||||
}
|
||||
|
||||
fn process() -> Result<i32, MyError> {
|
||||
let content = std::fs::read_to_string("num.txt")?; // auto-converts
|
||||
let num: i32 = content.trim().parse()?; // auto-converts
|
||||
Ok(num)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Type Design
|
||||
|
||||
### Simple Enum Error
|
||||
```rust
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ConfigError {
|
||||
NotFound,
|
||||
InvalidFormat,
|
||||
MissingField(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ConfigError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ConfigError::NotFound => write!(f, "configuration file not found"),
|
||||
ConfigError::InvalidFormat => write!(f, "invalid configuration format"),
|
||||
ConfigError::MissingField(field) => write!(f, "missing field: {}", field),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ConfigError {}
|
||||
```
|
||||
|
||||
### Error with Source (Wrapping)
|
||||
```rust
|
||||
#[derive(Debug)]
|
||||
pub struct AppError {
|
||||
kind: AppErrorKind,
|
||||
source: Option<Box<dyn std::error::Error + Send + Sync>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum AppErrorKind {
|
||||
Config,
|
||||
Database,
|
||||
Network,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AppError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.kind {
|
||||
AppErrorKind::Config => write!(f, "configuration error"),
|
||||
AppErrorKind::Database => write!(f, "database error"),
|
||||
AppErrorKind::Network => write!(f, "network error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AppError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
self.source.as_ref().map(|e| e.as_ref() as _)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using thiserror
|
||||
|
||||
### Basic Usage
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DataError {
|
||||
#[error("file not found: {path}")]
|
||||
NotFound { path: String },
|
||||
|
||||
#[error("invalid data format")]
|
||||
InvalidFormat,
|
||||
|
||||
#[error("IO error")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("parse error: {0}")]
|
||||
Parse(#[from] std::num::ParseIntError),
|
||||
}
|
||||
|
||||
// Usage
|
||||
fn load_data(path: &str) -> Result<Data, DataError> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.map_err(|_| DataError::NotFound { path: path.to_string() })?;
|
||||
let num: i32 = content.trim().parse()?; // auto-converts with #[from]
|
||||
Ok(Data { value: num })
|
||||
}
|
||||
```
|
||||
|
||||
### Transparent Wrapper
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error(transparent)]
|
||||
pub struct MyError(#[from] InnerError);
|
||||
|
||||
// Useful for newtype error wrappers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using anyhow
|
||||
|
||||
### For Applications
|
||||
```rust
|
||||
use anyhow::{Context, Result, bail, ensure};
|
||||
|
||||
fn process_file(path: &str) -> Result<Data> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.context("failed to read config file")?;
|
||||
|
||||
ensure!(!content.is_empty(), "config file is empty");
|
||||
|
||||
let data: Data = serde_json::from_str(&content)
|
||||
.context("failed to parse JSON")?;
|
||||
|
||||
if data.version < 1 {
|
||||
bail!("unsupported config version: {}", data.version);
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let data = process_file("config.json")
|
||||
.context("failed to load configuration")?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Error Chain
|
||||
```rust
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
fn deep_function() -> Result<()> {
|
||||
std::fs::read_to_string("missing.txt")
|
||||
.context("failed to read file")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn middle_function() -> Result<()> {
|
||||
deep_function()
|
||||
.context("failed in deep function")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn top_function() -> Result<()> {
|
||||
middle_function()
|
||||
.context("failed in middle function")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Error output shows full chain:
|
||||
// Error: failed in middle function
|
||||
// Caused by:
|
||||
// 0: failed in deep function
|
||||
// 1: failed to read file
|
||||
// 2: No such file or directory (os error 2)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Option Handling
|
||||
|
||||
### Converting Option to Result
|
||||
```rust
|
||||
fn find_user(id: u32) -> Option<User> { ... }
|
||||
|
||||
// Using ok_or for static error
|
||||
fn get_user(id: u32) -> Result<User, &'static str> {
|
||||
find_user(id).ok_or("user not found")
|
||||
}
|
||||
|
||||
// Using ok_or_else for dynamic error
|
||||
fn get_user(id: u32) -> Result<User, String> {
|
||||
find_user(id).ok_or_else(|| format!("user {} not found", id))
|
||||
}
|
||||
```
|
||||
|
||||
### Chaining Options
|
||||
```rust
|
||||
fn get_nested_value(data: &Data) -> Option<&str> {
|
||||
data.config
|
||||
.as_ref()?
|
||||
.nested
|
||||
.as_ref()?
|
||||
.value
|
||||
.as_deref()
|
||||
}
|
||||
|
||||
// Equivalent with and_then
|
||||
fn get_nested_value(data: &Data) -> Option<&str> {
|
||||
data.config
|
||||
.as_ref()
|
||||
.and_then(|c| c.nested.as_ref())
|
||||
.and_then(|n| n.value.as_deref())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Result Combinators
|
||||
|
||||
### map and map_err
|
||||
```rust
|
||||
fn parse_port(s: &str) -> Result<u16, ParseError> {
|
||||
s.parse::<u16>()
|
||||
.map_err(|e| ParseError::InvalidPort(e))
|
||||
}
|
||||
|
||||
fn get_url(config: &Config) -> Result<String, Error> {
|
||||
config.url()
|
||||
.map(|u| format!("https://{}", u))
|
||||
}
|
||||
```
|
||||
|
||||
### and_then (flatMap)
|
||||
```rust
|
||||
fn validate_and_save(input: &str) -> Result<(), Error> {
|
||||
validate(input)
|
||||
.and_then(|valid| save(valid))
|
||||
.and_then(|saved| notify(saved))
|
||||
}
|
||||
```
|
||||
|
||||
### unwrap_or and unwrap_or_else
|
||||
```rust
|
||||
// Default value
|
||||
let port = config.port().unwrap_or(8080);
|
||||
|
||||
// Computed default
|
||||
let port = config.port().unwrap_or_else(|| find_free_port());
|
||||
|
||||
// Default for Result
|
||||
let data = load_data().unwrap_or_default();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Early Return vs Combinators
|
||||
|
||||
### Early Return Style
|
||||
```rust
|
||||
fn process(input: &str) -> Result<Output, Error> {
|
||||
let step1 = validate(input)?;
|
||||
if !step1.is_valid {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
|
||||
let step2 = transform(step1)?;
|
||||
let step3 = save(step2)?;
|
||||
|
||||
Ok(step3)
|
||||
}
|
||||
```
|
||||
|
||||
### Combinator Style
|
||||
```rust
|
||||
fn process(input: &str) -> Result<Output, Error> {
|
||||
validate(input)
|
||||
.and_then(|s| {
|
||||
if s.is_valid {
|
||||
Ok(s)
|
||||
} else {
|
||||
Err(Error::Invalid)
|
||||
}
|
||||
})
|
||||
.and_then(transform)
|
||||
.and_then(save)
|
||||
}
|
||||
```
|
||||
|
||||
### When to Use Which
|
||||
|
||||
| Style | Best For |
|
||||
|-------|----------|
|
||||
| Early return (`?`) | Most cases, clearer flow |
|
||||
| Combinators | Functional pipelines, one-liners |
|
||||
| Match | Complex branching on errors |
|
||||
|
||||
---
|
||||
|
||||
## Panic vs Result
|
||||
|
||||
### When to Panic
|
||||
```rust
|
||||
// 1. Unrecoverable programmer error
|
||||
fn get_config() -> &'static Config {
|
||||
CONFIG.get().expect("config must be initialized")
|
||||
}
|
||||
|
||||
// 2. In tests
|
||||
#[test]
|
||||
fn test_parsing() {
|
||||
let result = parse("valid").unwrap(); // OK in tests
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
// 3. Prototype/examples
|
||||
fn main() {
|
||||
let data = load().unwrap(); // OK for quick examples
|
||||
}
|
||||
```
|
||||
|
||||
### When to Return Result
|
||||
```rust
|
||||
// 1. Any I/O operation
|
||||
fn read_file(path: &str) -> Result<String, io::Error>
|
||||
|
||||
// 2. User input validation
|
||||
fn parse_port(s: &str) -> Result<u16, ParseError>
|
||||
|
||||
// 3. Network operations
|
||||
async fn fetch(url: &str) -> Result<Response, Error>
|
||||
|
||||
// 4. Anything that can fail at runtime
|
||||
fn connect(addr: &str) -> Result<Connection, Error>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Context Best Practices
|
||||
|
||||
### Add Context at Boundaries
|
||||
```rust
|
||||
fn load_user_config(user_id: u64) -> Result<Config, Error> {
|
||||
let path = format!("/home/{}/config.toml", user_id);
|
||||
|
||||
std::fs::read_to_string(&path)
|
||||
.context(format!("failed to read config for user {}", user_id))?
|
||||
// NOT: .context("failed to read file") // too generic
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Include Relevant Data
|
||||
```rust
|
||||
// Good: includes the problematic value
|
||||
fn parse_age(s: &str) -> Result<u8, Error> {
|
||||
s.parse()
|
||||
.context(format!("invalid age value: '{}'", s))
|
||||
}
|
||||
|
||||
// Bad: no context about what failed
|
||||
fn parse_age(s: &str) -> Result<u8, Error> {
|
||||
s.parse()
|
||||
.context("parse error")
|
||||
}
|
||||
```
|
||||
222
skills/m07-concurrency/SKILL.md
Normal file
222
skills/m07-concurrency/SKILL.md
Normal file
@@ -0,0 +1,222 @@
|
||||
---
|
||||
name: m07-concurrency
|
||||
description: "CRITICAL: Use for concurrency/async. Triggers: E0277 Send Sync, cannot be sent between threads, thread, spawn, channel, mpsc, Mutex, RwLock, Atomic, async, await, Future, tokio, deadlock, race condition, 并发, 线程, 异步, 死锁"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Concurrency
|
||||
|
||||
> **Layer 1: Language Mechanics**
|
||||
|
||||
## Core Question
|
||||
|
||||
**Is this CPU-bound or I/O-bound, and what's the sharing model?**
|
||||
|
||||
Before choosing concurrency primitives:
|
||||
- What's the workload type?
|
||||
- What data needs to be shared?
|
||||
- What's the thread safety requirement?
|
||||
|
||||
---
|
||||
|
||||
## Error → Design Question
|
||||
|
||||
| Error | Don't Just Say | Ask Instead |
|
||||
|-------|----------------|-------------|
|
||||
| E0277 Send | "Add Send bound" | Should this type cross threads? |
|
||||
| E0277 Sync | "Wrap in Mutex" | Is shared access really needed? |
|
||||
| Future not Send | "Use spawn_local" | Is async the right choice? |
|
||||
| Deadlock | "Reorder locks" | Is the locking design correct? |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before adding concurrency:
|
||||
|
||||
1. **What's the workload?**
|
||||
- CPU-bound → threads (std::thread, rayon)
|
||||
- I/O-bound → async (tokio, async-std)
|
||||
- Mixed → hybrid approach
|
||||
|
||||
2. **What's the sharing model?**
|
||||
- No sharing → message passing (channels)
|
||||
- Immutable sharing → Arc<T>
|
||||
- Mutable sharing → Arc<Mutex<T>> or Arc<RwLock<T>>
|
||||
|
||||
3. **What are the Send/Sync requirements?**
|
||||
- Cross-thread ownership → Send
|
||||
- Cross-thread references → Sync
|
||||
- Single-thread async → spawn_local
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑ (MANDATORY)
|
||||
|
||||
**CRITICAL**: Don't just fix the error. Trace UP to find domain constraints.
|
||||
|
||||
### Domain Detection Table
|
||||
|
||||
| Context Keywords | Load Domain Skill | Key Constraint |
|
||||
|-----------------|-------------------|----------------|
|
||||
| Web API, HTTP, axum, actix, handler | **domain-web** | Handlers run on any thread |
|
||||
| 交易, 支付, trading, payment | **domain-fintech** | Audit + thread safety |
|
||||
| gRPC, kubernetes, microservice | **domain-cloud-native** | Distributed tracing |
|
||||
| CLI, terminal, clap | **domain-cli** | Usually single-thread OK |
|
||||
|
||||
### Example: Web API + Rc Error
|
||||
|
||||
```
|
||||
"Rc cannot be sent between threads" in Web API context
|
||||
↑ DETECT: "Web API" → Load domain-web
|
||||
↑ FIND: domain-web says "Shared state must be thread-safe"
|
||||
↑ FIND: domain-web says "Rc in state" is Common Mistake
|
||||
↓ DESIGN: Use Arc<T> with State extractor
|
||||
↓ IMPL: axum::extract::State<Arc<AppConfig>>
|
||||
```
|
||||
|
||||
### Generic Trace
|
||||
|
||||
```
|
||||
"Send not satisfied for my type"
|
||||
↑ Ask: What domain is this? Load domain-* skill
|
||||
↑ Ask: Does this type need to cross thread boundaries?
|
||||
↑ Check: m09-domain (is the data model correct?)
|
||||
```
|
||||
|
||||
| Situation | Trace To | Question |
|
||||
|-----------|----------|----------|
|
||||
| Send/Sync in Web | **domain-web** | What's the state management pattern? |
|
||||
| Send/Sync in CLI | **domain-cli** | Is multi-thread really needed? |
|
||||
| Mutex vs channels | m09-domain | Shared state or message passing? |
|
||||
| Async vs threads | m10-performance | What's the workload profile? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
From design to implementation:
|
||||
|
||||
```
|
||||
"Need parallelism for CPU work"
|
||||
↓ Use: std::thread or rayon
|
||||
|
||||
"Need concurrency for I/O"
|
||||
↓ Use: async/await with tokio
|
||||
|
||||
"Need to share immutable data across threads"
|
||||
↓ Use: Arc<T>
|
||||
|
||||
"Need to share mutable data across threads"
|
||||
↓ Use: Arc<Mutex<T>> or Arc<RwLock<T>>
|
||||
↓ Or: channels for message passing
|
||||
|
||||
"Need simple atomic operations"
|
||||
↓ Use: AtomicBool, AtomicUsize, etc.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Send/Sync Markers
|
||||
|
||||
| Marker | Meaning | Example |
|
||||
|--------|---------|---------|
|
||||
| `Send` | Can transfer ownership between threads | Most types |
|
||||
| `Sync` | Can share references between threads | `Arc<T>` |
|
||||
| `!Send` | Must stay on one thread | `Rc<T>` |
|
||||
| `!Sync` | No shared refs across threads | `RefCell<T>` |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Thread-Safe | Blocking | Use When |
|
||||
|---------|-------------|----------|----------|
|
||||
| `std::thread` | Yes | Yes | CPU-bound parallelism |
|
||||
| `async/await` | Yes | No | I/O-bound concurrency |
|
||||
| `Mutex<T>` | Yes | Yes | Shared mutable state |
|
||||
| `RwLock<T>` | Yes | Yes | Read-heavy shared state |
|
||||
| `mpsc::channel` | Yes | Optional | Message passing |
|
||||
| `Arc<Mutex<T>>` | Yes | Yes | Shared mutable across threads |
|
||||
|
||||
## Decision Flowchart
|
||||
|
||||
```
|
||||
What type of work?
|
||||
├─ CPU-bound → std::thread or rayon
|
||||
├─ I/O-bound → async/await
|
||||
└─ Mixed → hybrid (spawn_blocking)
|
||||
|
||||
Need to share data?
|
||||
├─ No → message passing (channels)
|
||||
├─ Immutable → Arc<T>
|
||||
└─ Mutable →
|
||||
├─ Read-heavy → Arc<RwLock<T>>
|
||||
└─ Write-heavy → Arc<Mutex<T>>
|
||||
└─ Simple counter → AtomicUsize
|
||||
|
||||
Async context?
|
||||
├─ Type is Send → tokio::spawn
|
||||
├─ Type is !Send → spawn_local
|
||||
└─ Blocking code → spawn_blocking
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| E0277 `Send` not satisfied | Non-Send in async | Use Arc or spawn_local |
|
||||
| E0277 `Sync` not satisfied | Non-Sync shared | Wrap with Mutex |
|
||||
| Deadlock | Lock ordering | Consistent lock order |
|
||||
| `future is not Send` | Non-Send across await | Drop before await |
|
||||
| `MutexGuard` across await | Guard held during suspend | Scope guard properly |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| Arc<Mutex<T>> everywhere | Contention, complexity | Message passing |
|
||||
| thread::sleep in async | Blocks executor | tokio::time::sleep |
|
||||
| Holding locks across await | Blocks other tasks | Scope locks tightly |
|
||||
| Ignoring deadlock risk | Hard to debug | Lock ordering, try_lock |
|
||||
|
||||
---
|
||||
|
||||
## Async-Specific Patterns
|
||||
|
||||
### Avoid MutexGuard Across Await
|
||||
|
||||
```rust
|
||||
// Bad: guard held across await
|
||||
let guard = mutex.lock().await;
|
||||
do_async().await; // guard still held!
|
||||
|
||||
// Good: scope the lock
|
||||
{
|
||||
let guard = mutex.lock().await;
|
||||
// use guard
|
||||
} // guard dropped
|
||||
do_async().await;
|
||||
```
|
||||
|
||||
### Non-Send Types in Async
|
||||
|
||||
```rust
|
||||
// Rc is !Send, can't cross await in spawned task
|
||||
// Option 1: use Arc instead
|
||||
// Option 2: use spawn_local (single-thread runtime)
|
||||
// Option 3: ensure Rc is dropped before .await
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Smart pointer choice | m02-resource |
|
||||
| Interior mutability | m03-mutability |
|
||||
| Performance tuning | m10-performance |
|
||||
| Domain concurrency needs | domain-* |
|
||||
312
skills/m07-concurrency/comparison.md
Normal file
312
skills/m07-concurrency/comparison.md
Normal file
@@ -0,0 +1,312 @@
|
||||
# Concurrency: Comparison with Other Languages
|
||||
|
||||
## Rust vs Go
|
||||
|
||||
### Concurrency Model
|
||||
|
||||
| Aspect | Rust | Go |
|
||||
|--------|------|-----|
|
||||
| Model | Ownership + Send/Sync | CSP (Communicating Sequential Processes) |
|
||||
| Primitives | Arc, Mutex, channels | goroutines, channels |
|
||||
| Safety | Compile-time | Runtime (race detector) |
|
||||
| Async | async/await + runtime | Built-in scheduler |
|
||||
|
||||
### Goroutines vs Rust Tasks
|
||||
|
||||
```rust
|
||||
// Rust: explicit about thread safety
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
let data = Arc::new(Mutex::new(vec![]));
|
||||
let data_clone = Arc::clone(&data);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut guard = data_clone.lock().await;
|
||||
guard.push(1); // Safe: Mutex protects access
|
||||
});
|
||||
|
||||
// Go: implicit sharing (potential race)
|
||||
// data := []int{}
|
||||
// go func() {
|
||||
// data = append(data, 1) // RACE CONDITION!
|
||||
// }()
|
||||
```
|
||||
|
||||
### Channel Comparison
|
||||
|
||||
```rust
|
||||
// Rust: typed channels with ownership
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
let (tx, mut rx) = mpsc::channel::<String>(100);
|
||||
|
||||
tokio::spawn(async move {
|
||||
tx.send("hello".to_string()).await.unwrap();
|
||||
// tx is moved, can't be used elsewhere
|
||||
});
|
||||
|
||||
// Go: channels are more flexible but less safe
|
||||
// ch := make(chan string, 100)
|
||||
// go func() {
|
||||
// ch <- "hello"
|
||||
// // ch can still be used anywhere
|
||||
// }()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rust vs Java
|
||||
|
||||
### Thread Safety Model
|
||||
|
||||
| Aspect | Rust | Java |
|
||||
|--------|------|------|
|
||||
| Safety | Compile-time (Send/Sync) | Runtime (synchronized, volatile) |
|
||||
| Null | No null (Option) | NullPointerException risk |
|
||||
| Locks | RAII (drop releases) | try-finally or try-with-resources |
|
||||
| Memory | No GC | GC with stop-the-world |
|
||||
|
||||
### Synchronization Comparison
|
||||
|
||||
```rust
|
||||
// Rust: lock is tied to data
|
||||
use std::sync::Mutex;
|
||||
|
||||
let data = Mutex::new(vec![1, 2, 3]);
|
||||
{
|
||||
let mut guard = data.lock().unwrap();
|
||||
guard.push(4);
|
||||
} // lock released automatically
|
||||
|
||||
// Java: lock and data are separate
|
||||
// List<Integer> data = new ArrayList<>();
|
||||
// synchronized(data) {
|
||||
// data.add(4);
|
||||
// } // easy to forget synchronization elsewhere
|
||||
```
|
||||
|
||||
### Thread Pool Comparison
|
||||
|
||||
```rust
|
||||
// Rust: rayon for data parallelism
|
||||
use rayon::prelude::*;
|
||||
|
||||
let sum: i32 = (0..1000)
|
||||
.into_par_iter()
|
||||
.map(|x| x * x)
|
||||
.sum();
|
||||
|
||||
// Java: Stream API
|
||||
// int sum = IntStream.range(0, 1000)
|
||||
// .parallel()
|
||||
// .map(x -> x * x)
|
||||
// .sum();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rust vs C++
|
||||
|
||||
### Safety Guarantees
|
||||
|
||||
| Aspect | Rust | C++ |
|
||||
|--------|------|-----|
|
||||
| Data races | Prevented at compile-time | Undefined behavior |
|
||||
| Deadlocks | Not prevented (same as C++) | Not prevented |
|
||||
| Thread safety | Send/Sync traits | Convention only |
|
||||
| Memory ordering | Explicit Ordering enum | memory_order enum |
|
||||
|
||||
### Atomic Comparison
|
||||
|
||||
```rust
|
||||
// Rust: clear memory ordering
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
|
||||
let counter = AtomicI32::new(0);
|
||||
counter.fetch_add(1, Ordering::SeqCst);
|
||||
let value = counter.load(Ordering::Acquire);
|
||||
|
||||
// C++: similar but without safety
|
||||
// std::atomic<int> counter{0};
|
||||
// counter.fetch_add(1, std::memory_order_seq_cst);
|
||||
// int value = counter.load(std::memory_order_acquire);
|
||||
```
|
||||
|
||||
### Mutex Comparison
|
||||
|
||||
```rust
|
||||
// Rust: data protected by Mutex
|
||||
use std::sync::Mutex;
|
||||
|
||||
struct SafeCounter {
|
||||
count: Mutex<i32>, // Mutex contains the data
|
||||
}
|
||||
|
||||
impl SafeCounter {
|
||||
fn increment(&self) {
|
||||
*self.count.lock().unwrap() += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// C++: mutex separate from data (error-prone)
|
||||
// class Counter {
|
||||
// std::mutex mtx;
|
||||
// int count; // NOT protected by type system
|
||||
// public:
|
||||
// void increment() {
|
||||
// std::lock_guard<std::mutex> lock(mtx);
|
||||
// count++;
|
||||
// }
|
||||
// void unsafe_increment() {
|
||||
// count++; // Compiles! But wrong.
|
||||
// }
|
||||
// };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Async Models Comparison
|
||||
|
||||
| Language | Model | Runtime |
|
||||
|----------|-------|---------|
|
||||
| Rust | async/await, zero-cost | tokio, async-std (bring your own) |
|
||||
| Go | goroutines | Built-in scheduler |
|
||||
| JavaScript | async/await, Promises | Event loop (single-threaded) |
|
||||
| Python | async/await | asyncio (single-threaded) |
|
||||
| Java | CompletableFuture, Virtual Threads | ForkJoinPool, Loom |
|
||||
|
||||
### Rust vs JavaScript Async
|
||||
|
||||
```rust
|
||||
// Rust: async requires explicit runtime, can use multiple threads
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let results = tokio::join!(
|
||||
fetch("url1"), // runs concurrently
|
||||
fetch("url2"),
|
||||
);
|
||||
}
|
||||
|
||||
// JavaScript: single-threaded event loop
|
||||
// async function main() {
|
||||
// const results = await Promise.all([
|
||||
// fetch("url1"),
|
||||
// fetch("url2"),
|
||||
// ]);
|
||||
// }
|
||||
```
|
||||
|
||||
### Rust vs Python Async
|
||||
|
||||
```rust
|
||||
// Rust: true parallelism possible
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() {
|
||||
let handles: Vec<_> = urls
|
||||
.into_iter()
|
||||
.map(|url| tokio::spawn(fetch(url))) // spawns on thread pool
|
||||
.collect();
|
||||
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
}
|
||||
|
||||
// Python: asyncio is single-threaded (use ProcessPoolExecutor for CPU)
|
||||
# async def main():
|
||||
# tasks = [asyncio.create_task(fetch(url)) for url in urls]
|
||||
# await asyncio.gather(*tasks) # all on same thread
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Send and Sync: Rust's Unique Feature
|
||||
|
||||
No other mainstream language has compile-time thread safety markers:
|
||||
|
||||
| Trait | Meaning | Auto-impl |
|
||||
|-------|---------|-----------|
|
||||
| `Send` | Safe to transfer between threads | Most types |
|
||||
| `Sync` | Safe to share `&T` between threads | Types with thread-safe `&` |
|
||||
| `!Send` | Must stay on one thread | Rc, raw pointers |
|
||||
| `!Sync` | References can't be shared | RefCell, Cell |
|
||||
|
||||
### Why This Matters
|
||||
|
||||
```rust
|
||||
// Rust PREVENTS this at compile time:
|
||||
use std::rc::Rc;
|
||||
|
||||
let rc = Rc::new(42);
|
||||
std::thread::spawn(move || {
|
||||
println!("{}", rc); // ERROR: Rc is not Send
|
||||
});
|
||||
|
||||
// In other languages, this would be a runtime bug:
|
||||
// - Go: race detector might catch it
|
||||
// - Java: undefined behavior
|
||||
// - Python: GIL usually saves you
|
||||
// - C++: undefined behavior
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
| Aspect | Rust | Go | Java | C++ |
|
||||
|--------|------|-----|------|-----|
|
||||
| Thread overhead | System threads or M:N | M:N (goroutines) | System or virtual | System threads |
|
||||
| Context switch | OS-level or cooperative | Cheap (goroutines) | OS-level | OS-level |
|
||||
| Memory | Predictable (no GC) | GC pauses | GC pauses | Predictable |
|
||||
| Async overhead | Zero-cost futures | Runtime overhead | Boxing overhead | Depends |
|
||||
|
||||
### When to Use What
|
||||
|
||||
| Scenario | Best Choice |
|
||||
|----------|-------------|
|
||||
| CPU-bound parallelism | Rust (rayon), C++ |
|
||||
| I/O-bound concurrency | Rust (tokio), Go, Node.js |
|
||||
| Low latency required | Rust, C++ |
|
||||
| Rapid development | Go, Python |
|
||||
| Complex concurrent state | Rust (compile-time safety) |
|
||||
|
||||
---
|
||||
|
||||
## Mental Model Shifts
|
||||
|
||||
### From Go
|
||||
|
||||
```
|
||||
Before: "Just use goroutines and channels"
|
||||
After: "Explicitly declare what can be shared and how"
|
||||
```
|
||||
|
||||
Key shifts:
|
||||
- `Arc<Mutex<T>>` instead of implicit sharing
|
||||
- Compiler enforces thread safety
|
||||
- Async needs explicit runtime
|
||||
|
||||
### From Java
|
||||
|
||||
```
|
||||
Before: "synchronized everywhere, hope for the best"
|
||||
After: "Types encode thread safety, compiler enforces"
|
||||
```
|
||||
|
||||
Key shifts:
|
||||
- No need for synchronized keyword
|
||||
- Mutex contains data, not separate
|
||||
- No GC pauses in critical sections
|
||||
|
||||
### From C++
|
||||
|
||||
```
|
||||
Before: "Be careful, read the docs, use sanitizers"
|
||||
After: "Compiler catches data races, trust the type system"
|
||||
```
|
||||
|
||||
Key shifts:
|
||||
- Send/Sync replace convention
|
||||
- RAII locks are mandatory, not optional
|
||||
- Much harder to write incorrect concurrent code
|
||||
396
skills/m07-concurrency/examples/thread-patterns.md
Normal file
396
skills/m07-concurrency/examples/thread-patterns.md
Normal file
@@ -0,0 +1,396 @@
|
||||
# Thread-Based Concurrency Patterns
|
||||
|
||||
## Thread Spawning Best Practices
|
||||
|
||||
### Basic Thread Spawn
|
||||
```rust
|
||||
use std::thread;
|
||||
|
||||
fn main() {
|
||||
let handle = thread::spawn(|| {
|
||||
println!("Hello from thread!");
|
||||
42 // return value
|
||||
});
|
||||
|
||||
let result = handle.join().unwrap();
|
||||
println!("Thread returned: {}", result);
|
||||
}
|
||||
```
|
||||
|
||||
### Named Threads for Debugging
|
||||
```rust
|
||||
use std::thread;
|
||||
|
||||
let builder = thread::Builder::new()
|
||||
.name("worker-1".to_string())
|
||||
.stack_size(32 * 1024); // 32KB stack
|
||||
|
||||
let handle = builder.spawn(|| {
|
||||
println!("Thread name: {:?}", thread::current().name());
|
||||
}).unwrap();
|
||||
```
|
||||
|
||||
### Scoped Threads (No 'static Required)
|
||||
```rust
|
||||
use std::thread;
|
||||
|
||||
fn process_data(data: &[u32]) -> Vec<u32> {
|
||||
thread::scope(|s| {
|
||||
let handles: Vec<_> = data
|
||||
.chunks(2)
|
||||
.map(|chunk| {
|
||||
s.spawn(|| {
|
||||
chunk.iter().map(|x| x * 2).collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
handles
|
||||
.into_iter()
|
||||
.flat_map(|h| h.join().unwrap())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let data = vec![1, 2, 3, 4, 5, 6];
|
||||
let result = process_data(&data); // No 'static needed!
|
||||
println!("{:?}", result);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Shared State Patterns
|
||||
|
||||
### Arc + Mutex (Read-Write)
|
||||
```rust
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
fn shared_counter() {
|
||||
let counter = Arc::new(Mutex::new(0));
|
||||
let mut handles = vec![];
|
||||
|
||||
for _ in 0..10 {
|
||||
let counter = Arc::clone(&counter);
|
||||
let handle = thread::spawn(move || {
|
||||
let mut num = counter.lock().unwrap();
|
||||
*num += 1;
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
println!("Result: {}", *counter.lock().unwrap());
|
||||
}
|
||||
```
|
||||
|
||||
### Arc + RwLock (Read-Heavy)
|
||||
```rust
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread;
|
||||
|
||||
fn read_heavy_cache() {
|
||||
let cache = Arc::new(RwLock::new(vec![1, 2, 3]));
|
||||
|
||||
// Many readers
|
||||
for i in 0..5 {
|
||||
let cache = Arc::clone(&cache);
|
||||
thread::spawn(move || {
|
||||
let data = cache.read().unwrap();
|
||||
println!("Reader {}: {:?}", i, *data);
|
||||
});
|
||||
}
|
||||
|
||||
// Occasional writer
|
||||
{
|
||||
let cache = Arc::clone(&cache);
|
||||
thread::spawn(move || {
|
||||
let mut data = cache.write().unwrap();
|
||||
data.push(4);
|
||||
println!("Writer: added element");
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Atomic for Simple Types
|
||||
```rust
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
fn atomic_counter() {
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
let mut handles = vec![];
|
||||
|
||||
for _ in 0..10 {
|
||||
let counter = Arc::clone(&counter);
|
||||
handles.push(thread::spawn(move || {
|
||||
for _ in 0..1000 {
|
||||
counter.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
println!("Result: {}", counter.load(Ordering::SeqCst));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Channel Patterns
|
||||
|
||||
### MPSC Channel
|
||||
```rust
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
|
||||
fn producer_consumer() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
// Multiple producers
|
||||
for i in 0..3 {
|
||||
let tx = tx.clone();
|
||||
thread::spawn(move || {
|
||||
for j in 0..5 {
|
||||
tx.send(format!("msg {}-{}", i, j)).unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
drop(tx); // Drop original sender
|
||||
|
||||
// Single consumer
|
||||
for received in rx {
|
||||
println!("Got: {}", received);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sync Channel (Bounded)
|
||||
```rust
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
|
||||
fn bounded_channel() {
|
||||
let (tx, rx) = mpsc::sync_channel(2); // buffer size 2
|
||||
|
||||
thread::spawn(move || {
|
||||
for i in 0..5 {
|
||||
println!("Sending {}", i);
|
||||
tx.send(i).unwrap(); // blocks if buffer full
|
||||
println!("Sent {}", i);
|
||||
}
|
||||
});
|
||||
|
||||
thread::sleep(std::time::Duration::from_millis(500));
|
||||
for received in rx {
|
||||
println!("Received: {}", received);
|
||||
thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Thread Pool Patterns
|
||||
|
||||
### Using rayon for Parallel Iteration
|
||||
```rust
|
||||
use rayon::prelude::*;
|
||||
|
||||
fn parallel_map() {
|
||||
let numbers: Vec<i32> = (0..1000).collect();
|
||||
|
||||
let squares: Vec<i32> = numbers
|
||||
.par_iter() // parallel iterator
|
||||
.map(|x| x * x)
|
||||
.collect();
|
||||
|
||||
println!("Processed {} items", squares.len());
|
||||
}
|
||||
|
||||
fn parallel_filter_map() {
|
||||
let data: Vec<String> = get_data();
|
||||
|
||||
let results: Vec<_> = data
|
||||
.par_iter()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| expensive_process(s))
|
||||
.collect();
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Thread Pool with crossbeam
|
||||
```rust
|
||||
use crossbeam::channel;
|
||||
use std::thread;
|
||||
|
||||
fn custom_pool(num_workers: usize) {
|
||||
let (tx, rx) = channel::bounded::<Box<dyn FnOnce() + Send>>(100);
|
||||
|
||||
// Spawn workers
|
||||
let workers: Vec<_> = (0..num_workers)
|
||||
.map(|_| {
|
||||
let rx = rx.clone();
|
||||
thread::spawn(move || {
|
||||
while let Ok(task) = rx.recv() {
|
||||
task();
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Submit tasks
|
||||
for i in 0..100 {
|
||||
tx.send(Box::new(move || {
|
||||
println!("Processing task {}", i);
|
||||
})).unwrap();
|
||||
}
|
||||
|
||||
drop(tx); // Close channel
|
||||
|
||||
for worker in workers {
|
||||
worker.join().unwrap();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Synchronization Primitives
|
||||
|
||||
### Barrier (Wait for All)
|
||||
```rust
|
||||
use std::sync::{Arc, Barrier};
|
||||
use std::thread;
|
||||
|
||||
fn barrier_example() {
|
||||
let barrier = Arc::new(Barrier::new(3));
|
||||
let mut handles = vec![];
|
||||
|
||||
for i in 0..3 {
|
||||
let barrier = Arc::clone(&barrier);
|
||||
handles.push(thread::spawn(move || {
|
||||
println!("Thread {} starting", i);
|
||||
thread::sleep(std::time::Duration::from_millis(i as u64 * 100));
|
||||
|
||||
barrier.wait(); // All threads wait here
|
||||
|
||||
println!("Thread {} after barrier", i);
|
||||
}));
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Condvar (Condition Variable)
|
||||
```rust
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::thread;
|
||||
|
||||
fn condvar_example() {
|
||||
let pair = Arc::new((Mutex::new(false), Condvar::new()));
|
||||
let pair_clone = Arc::clone(&pair);
|
||||
|
||||
// Waiter thread
|
||||
let waiter = thread::spawn(move || {
|
||||
let (lock, cvar) = &*pair_clone;
|
||||
let mut started = lock.lock().unwrap();
|
||||
while !*started {
|
||||
started = cvar.wait(started).unwrap();
|
||||
}
|
||||
println!("Waiter: condition met!");
|
||||
});
|
||||
|
||||
// Notifier
|
||||
thread::sleep(std::time::Duration::from_millis(100));
|
||||
let (lock, cvar) = &*pair;
|
||||
{
|
||||
let mut started = lock.lock().unwrap();
|
||||
*started = true;
|
||||
}
|
||||
cvar.notify_one();
|
||||
|
||||
waiter.join().unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
### Once (One-Time Initialization)
|
||||
```rust
|
||||
use std::sync::Once;
|
||||
|
||||
static INIT: Once = Once::new();
|
||||
static mut CONFIG: Option<Config> = None;
|
||||
|
||||
fn get_config() -> &'static Config {
|
||||
INIT.call_once(|| {
|
||||
unsafe {
|
||||
CONFIG = Some(load_config());
|
||||
}
|
||||
});
|
||||
unsafe { CONFIG.as_ref().unwrap() }
|
||||
}
|
||||
|
||||
// Better: use once_cell or lazy_static
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static CONFIG: Lazy<Config> = Lazy::new(|| {
|
||||
load_config()
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling in Threads
|
||||
|
||||
### Handling Panics
|
||||
```rust
|
||||
use std::thread;
|
||||
|
||||
fn handle_panic() {
|
||||
let handle = thread::spawn(|| {
|
||||
panic!("Thread panicked!");
|
||||
});
|
||||
|
||||
match handle.join() {
|
||||
Ok(_) => println!("Thread completed successfully"),
|
||||
Err(e) => {
|
||||
if let Some(s) = e.downcast_ref::<&str>() {
|
||||
println!("Thread panicked with: {}", s);
|
||||
} else if let Some(s) = e.downcast_ref::<String>() {
|
||||
println!("Thread panicked with: {}", s);
|
||||
} else {
|
||||
println!("Thread panicked with unknown error");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Catching Panics
|
||||
```rust
|
||||
use std::panic;
|
||||
|
||||
fn catch_panic() {
|
||||
let result = panic::catch_unwind(|| {
|
||||
risky_operation()
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(value) => println!("Success: {:?}", value),
|
||||
Err(_) => println!("Operation panicked, continuing..."),
|
||||
}
|
||||
}
|
||||
```
|
||||
409
skills/m07-concurrency/patterns/async-patterns.md
Normal file
409
skills/m07-concurrency/patterns/async-patterns.md
Normal file
@@ -0,0 +1,409 @@
|
||||
# Async Patterns in Rust
|
||||
|
||||
## Task Spawning
|
||||
|
||||
### Basic Spawn
|
||||
```rust
|
||||
use tokio::task;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Spawn a task that runs concurrently
|
||||
let handle = task::spawn(async {
|
||||
expensive_computation().await
|
||||
});
|
||||
|
||||
// Do other work while task runs
|
||||
other_work().await;
|
||||
|
||||
// Wait for result
|
||||
let result = handle.await.unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
### Spawn with Shared State
|
||||
```rust
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
async fn process_with_state() {
|
||||
let state = Arc::new(Mutex::new(vec![]));
|
||||
|
||||
let handles: Vec<_> = (0..10)
|
||||
.map(|i| {
|
||||
let state = Arc::clone(&state);
|
||||
tokio::spawn(async move {
|
||||
let mut guard = state.lock().await;
|
||||
guard.push(i);
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Wait for all tasks
|
||||
for handle in handles {
|
||||
handle.await.unwrap();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Select Pattern
|
||||
|
||||
### Racing Multiple Futures
|
||||
```rust
|
||||
use tokio::select;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
async fn first_response() {
|
||||
select! {
|
||||
result = fetch_from_server_a() => {
|
||||
println!("A responded first: {:?}", result);
|
||||
}
|
||||
result = fetch_from_server_b() => {
|
||||
println!("B responded first: {:?}", result);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Select with Timeout
|
||||
```rust
|
||||
use tokio::time::timeout;
|
||||
|
||||
async fn with_timeout() -> Result<Data, Error> {
|
||||
select! {
|
||||
result = fetch_data() => result,
|
||||
_ = sleep(Duration::from_secs(5)) => {
|
||||
Err(Error::Timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Or use timeout directly
|
||||
async fn with_timeout2() -> Result<Data, Error> {
|
||||
timeout(Duration::from_secs(5), fetch_data())
|
||||
.await
|
||||
.map_err(|_| Error::Timeout)?
|
||||
}
|
||||
```
|
||||
|
||||
### Select with Channel
|
||||
```rust
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
async fn process_messages(mut rx: mpsc::Receiver<Message>) {
|
||||
loop {
|
||||
select! {
|
||||
Some(msg) = rx.recv() => {
|
||||
handle_message(msg).await;
|
||||
}
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
println!("Shutting down...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Channel Patterns
|
||||
|
||||
### MPSC (Multi-Producer, Single-Consumer)
|
||||
```rust
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
async fn producer_consumer() {
|
||||
let (tx, mut rx) = mpsc::channel(100);
|
||||
|
||||
// Spawn producers
|
||||
for i in 0..3 {
|
||||
let tx = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
tx.send(format!("Message from {}", i)).await.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
// Drop original sender so channel closes
|
||||
drop(tx);
|
||||
|
||||
// Consume
|
||||
while let Some(msg) = rx.recv().await {
|
||||
println!("Received: {}", msg);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Oneshot (Single-Shot Response)
|
||||
```rust
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
async fn request_response() {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let result = compute_something().await;
|
||||
tx.send(result).unwrap();
|
||||
});
|
||||
|
||||
// Wait for response
|
||||
let response = rx.await.unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
### Broadcast (Multi-Consumer)
|
||||
```rust
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
async fn pub_sub() {
|
||||
let (tx, _) = broadcast::channel(16);
|
||||
|
||||
// Subscribe multiple consumers
|
||||
let mut rx1 = tx.subscribe();
|
||||
let mut rx2 = tx.subscribe();
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Ok(msg) = rx1.recv().await {
|
||||
println!("Consumer 1: {}", msg);
|
||||
}
|
||||
});
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Ok(msg) = rx2.recv().await {
|
||||
println!("Consumer 2: {}", msg);
|
||||
}
|
||||
});
|
||||
|
||||
// Publish
|
||||
tx.send("Hello").unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
### Watch (Single Latest Value)
|
||||
```rust
|
||||
use tokio::sync::watch;
|
||||
|
||||
async fn config_updates() {
|
||||
let (tx, mut rx) = watch::channel(Config::default());
|
||||
|
||||
// Consumer watches for changes
|
||||
tokio::spawn(async move {
|
||||
while rx.changed().await.is_ok() {
|
||||
let config = rx.borrow();
|
||||
apply_config(&config);
|
||||
}
|
||||
});
|
||||
|
||||
// Update config
|
||||
tx.send(Config::new()).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Structured Concurrency
|
||||
|
||||
### JoinSet for Task Groups
|
||||
```rust
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
async fn parallel_fetch(urls: Vec<String>) -> Vec<Result<Response, Error>> {
|
||||
let mut set = JoinSet::new();
|
||||
|
||||
for url in urls {
|
||||
set.spawn(async move {
|
||||
fetch(&url).await
|
||||
});
|
||||
}
|
||||
|
||||
let mut results = vec![];
|
||||
while let Some(res) = set.join_next().await {
|
||||
results.push(res.unwrap());
|
||||
}
|
||||
results
|
||||
}
|
||||
```
|
||||
|
||||
### Scoped Tasks (no 'static)
|
||||
```rust
|
||||
// Using tokio-scoped or async-scoped crate
|
||||
use async_scoped::TokioScope;
|
||||
|
||||
async fn scoped_example(data: &[u32]) {
|
||||
let results = TokioScope::scope_and_block(|scope| {
|
||||
for item in data {
|
||||
scope.spawn(async move {
|
||||
process(item).await
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cancellation Patterns
|
||||
|
||||
### Using CancellationToken
|
||||
```rust
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
async fn cancellable_task(token: CancellationToken) {
|
||||
loop {
|
||||
select! {
|
||||
_ = token.cancelled() => {
|
||||
println!("Task cancelled");
|
||||
break;
|
||||
}
|
||||
_ = do_work() => {
|
||||
// Continue working
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn main_with_cancellation() {
|
||||
let token = CancellationToken::new();
|
||||
let task_token = token.clone();
|
||||
|
||||
let handle = tokio::spawn(cancellable_task(task_token));
|
||||
|
||||
// Cancel after some condition
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
token.cancel();
|
||||
|
||||
handle.await.unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
### Graceful Shutdown
|
||||
```rust
|
||||
async fn serve_with_shutdown(shutdown: impl Future) {
|
||||
let server = TcpListener::bind("0.0.0.0:8080").await.unwrap();
|
||||
|
||||
loop {
|
||||
select! {
|
||||
Ok((socket, _)) = server.accept() => {
|
||||
tokio::spawn(handle_connection(socket));
|
||||
}
|
||||
_ = &mut shutdown => {
|
||||
println!("Shutting down...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let ctrl_c = async {
|
||||
tokio::signal::ctrl_c().await.unwrap();
|
||||
};
|
||||
|
||||
serve_with_shutdown(ctrl_c).await;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backpressure Patterns
|
||||
|
||||
### Bounded Channels
|
||||
```rust
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
async fn with_backpressure() {
|
||||
// Buffer of 10 - producers will wait if full
|
||||
let (tx, mut rx) = mpsc::channel(10);
|
||||
|
||||
let producer = tokio::spawn(async move {
|
||||
for i in 0..1000 {
|
||||
// This will wait if channel is full
|
||||
tx.send(i).await.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
let consumer = tokio::spawn(async move {
|
||||
while let Some(item) = rx.recv().await {
|
||||
// Slow consumer
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
process(item);
|
||||
}
|
||||
});
|
||||
|
||||
let _ = tokio::join!(producer, consumer);
|
||||
}
|
||||
```
|
||||
|
||||
### Semaphore for Rate Limiting
|
||||
```rust
|
||||
use tokio::sync::Semaphore;
|
||||
use std::sync::Arc;
|
||||
|
||||
async fn rate_limited_requests(urls: Vec<String>) {
|
||||
let semaphore = Arc::new(Semaphore::new(10)); // max 10 concurrent
|
||||
|
||||
let handles: Vec<_> = urls
|
||||
.into_iter()
|
||||
.map(|url| {
|
||||
let sem = Arc::clone(&semaphore);
|
||||
tokio::spawn(async move {
|
||||
let _permit = sem.acquire().await.unwrap();
|
||||
fetch(&url).await
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for handle in handles {
|
||||
handle.await.unwrap();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling in Async
|
||||
|
||||
### Propagating Errors
|
||||
```rust
|
||||
async fn fetch_and_parse(url: &str) -> Result<Data, Error> {
|
||||
let response = fetch(url).await?;
|
||||
let data = parse(response).await?;
|
||||
Ok(data)
|
||||
}
|
||||
```
|
||||
|
||||
### Handling Task Panics
|
||||
```rust
|
||||
async fn robust_spawn() {
|
||||
let handle = tokio::spawn(async {
|
||||
risky_operation().await
|
||||
});
|
||||
|
||||
match handle.await {
|
||||
Ok(result) => println!("Success: {:?}", result),
|
||||
Err(e) if e.is_panic() => {
|
||||
println!("Task panicked: {:?}", e);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Task cancelled: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Try-Join for Multiple Results
|
||||
```rust
|
||||
use tokio::try_join;
|
||||
|
||||
async fn fetch_all() -> Result<(A, B, C), Error> {
|
||||
// All must succeed, or first error returned
|
||||
try_join!(
|
||||
fetch_a(),
|
||||
fetch_b(),
|
||||
fetch_c(),
|
||||
)
|
||||
}
|
||||
```
|
||||
331
skills/m07-concurrency/patterns/common-errors.md
Normal file
331
skills/m07-concurrency/patterns/common-errors.md
Normal file
@@ -0,0 +1,331 @@
|
||||
# Common Concurrency Errors & Fixes
|
||||
|
||||
## E0277: Cannot Send Between Threads
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
use std::rc::Rc;
|
||||
|
||||
let data = Rc::new(42);
|
||||
std::thread::spawn(move || {
|
||||
println!("{}", data); // ERROR: Rc<i32> cannot be sent between threads
|
||||
});
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Use Arc instead**
|
||||
```rust
|
||||
use std::sync::Arc;
|
||||
|
||||
let data = Arc::new(42);
|
||||
let data_clone = Arc::clone(&data);
|
||||
std::thread::spawn(move || {
|
||||
println!("{}", data_clone); // OK: Arc is Send
|
||||
});
|
||||
```
|
||||
|
||||
**Option 2: Move owned data**
|
||||
```rust
|
||||
let data = 42; // i32 is Copy and Send
|
||||
std::thread::spawn(move || {
|
||||
println!("{}", data); // OK
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E0277: Cannot Share Between Threads (Not Sync)
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
use std::cell::RefCell;
|
||||
use std::sync::Arc;
|
||||
|
||||
let data = Arc::new(RefCell::new(42));
|
||||
// ERROR: RefCell is not Sync
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Use Mutex for thread-safe interior mutability**
|
||||
```rust
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
let data = Arc::new(Mutex::new(42));
|
||||
let data_clone = Arc::clone(&data);
|
||||
std::thread::spawn(move || {
|
||||
let mut guard = data_clone.lock().unwrap();
|
||||
*guard += 1;
|
||||
});
|
||||
```
|
||||
|
||||
**Option 2: Use RwLock for read-heavy workloads**
|
||||
```rust
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
let data = Arc::new(RwLock::new(42));
|
||||
let data_clone = Arc::clone(&data);
|
||||
std::thread::spawn(move || {
|
||||
let guard = data_clone.read().unwrap();
|
||||
println!("{}", *guard);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deadlock Patterns
|
||||
|
||||
### Pattern 1: Lock Ordering Deadlock
|
||||
```rust
|
||||
// DANGER: potential deadlock
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
let a = Arc::new(Mutex::new(1));
|
||||
let b = Arc::new(Mutex::new(2));
|
||||
|
||||
// Thread 1: locks a then b
|
||||
let a1 = Arc::clone(&a);
|
||||
let b1 = Arc::clone(&b);
|
||||
std::thread::spawn(move || {
|
||||
let _a = a1.lock().unwrap();
|
||||
let _b = b1.lock().unwrap(); // waits for b
|
||||
});
|
||||
|
||||
// Thread 2: locks b then a (opposite order!)
|
||||
let a2 = Arc::clone(&a);
|
||||
let b2 = Arc::clone(&b);
|
||||
std::thread::spawn(move || {
|
||||
let _b = b2.lock().unwrap();
|
||||
let _a = a2.lock().unwrap(); // waits for a - DEADLOCK
|
||||
});
|
||||
```
|
||||
|
||||
### Fix: Consistent Lock Ordering
|
||||
```rust
|
||||
// SAFE: always lock in same order (a before b)
|
||||
std::thread::spawn(move || {
|
||||
let _a = a1.lock().unwrap();
|
||||
let _b = b1.lock().unwrap();
|
||||
});
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let _a = a2.lock().unwrap(); // same order
|
||||
let _b = b2.lock().unwrap();
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Self-Deadlock
|
||||
```rust
|
||||
// DANGER: locking same mutex twice
|
||||
let m = Mutex::new(42);
|
||||
let _g1 = m.lock().unwrap();
|
||||
let _g2 = m.lock().unwrap(); // DEADLOCK on std::Mutex
|
||||
|
||||
// FIX: use parking_lot::ReentrantMutex if needed
|
||||
// or restructure code to avoid double locking
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mutex Guard Across Await
|
||||
|
||||
### Error Pattern
|
||||
```rust
|
||||
use std::sync::Mutex;
|
||||
use tokio::time::sleep;
|
||||
|
||||
async fn bad_async() {
|
||||
let m = Mutex::new(42);
|
||||
let guard = m.lock().unwrap();
|
||||
sleep(Duration::from_secs(1)).await; // WARNING: guard held across await
|
||||
println!("{}", *guard);
|
||||
}
|
||||
```
|
||||
|
||||
### Fix Options
|
||||
|
||||
**Option 1: Scope the lock**
|
||||
```rust
|
||||
async fn good_async() {
|
||||
let m = Mutex::new(42);
|
||||
let value = {
|
||||
let guard = m.lock().unwrap();
|
||||
*guard // copy value
|
||||
}; // guard dropped here
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
println!("{}", value);
|
||||
}
|
||||
```
|
||||
|
||||
**Option 2: Use tokio::sync::Mutex**
|
||||
```rust
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
async fn good_async() {
|
||||
let m = Mutex::new(42);
|
||||
let guard = m.lock().await; // async lock
|
||||
sleep(Duration::from_secs(1)).await; // OK with tokio::Mutex
|
||||
println!("{}", *guard);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Race Prevention
|
||||
|
||||
### Pattern: Missing Synchronization
|
||||
```rust
|
||||
// This WON'T compile - Rust prevents data races
|
||||
use std::sync::Arc;
|
||||
|
||||
let data = Arc::new(0);
|
||||
let d1 = Arc::clone(&data);
|
||||
let d2 = Arc::clone(&data);
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// *d1 += 1; // ERROR: cannot mutate through Arc
|
||||
});
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// *d2 += 1; // ERROR: cannot mutate through Arc
|
||||
});
|
||||
```
|
||||
|
||||
### Fix: Add Synchronization
|
||||
```rust
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
|
||||
// Option 1: Mutex
|
||||
let data = Arc::new(Mutex::new(0));
|
||||
let d1 = Arc::clone(&data);
|
||||
std::thread::spawn(move || {
|
||||
*d1.lock().unwrap() += 1;
|
||||
});
|
||||
|
||||
// Option 2: Atomic (for simple types)
|
||||
let data = Arc::new(AtomicI32::new(0));
|
||||
let d1 = Arc::clone(&data);
|
||||
std::thread::spawn(move || {
|
||||
d1.fetch_add(1, Ordering::SeqCst);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Channel Errors
|
||||
|
||||
### Disconnected Channel
|
||||
```rust
|
||||
use std::sync::mpsc;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
drop(tx); // sender dropped
|
||||
match rx.recv() {
|
||||
Ok(v) => println!("{}", v),
|
||||
Err(_) => println!("channel disconnected"), // this happens
|
||||
}
|
||||
```
|
||||
|
||||
### Fix: Handle Disconnection
|
||||
```rust
|
||||
// Use try_recv for non-blocking
|
||||
loop {
|
||||
match rx.try_recv() {
|
||||
Ok(msg) => handle(msg),
|
||||
Err(TryRecvError::Empty) => continue,
|
||||
Err(TryRecvError::Disconnected) => break,
|
||||
}
|
||||
}
|
||||
|
||||
// Or iterate (stops on disconnect)
|
||||
for msg in rx {
|
||||
handle(msg);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Async Common Errors
|
||||
|
||||
### Forgetting to Spawn
|
||||
```rust
|
||||
// WRONG: future not polled
|
||||
async fn fetch_data() -> Result<Data, Error> { ... }
|
||||
|
||||
fn process() {
|
||||
fetch_data(); // does nothing! returns Future that's dropped
|
||||
}
|
||||
|
||||
// RIGHT: await or spawn
|
||||
async fn process() {
|
||||
let data = fetch_data().await; // awaited
|
||||
}
|
||||
|
||||
fn process_sync() {
|
||||
tokio::spawn(fetch_data()); // spawned
|
||||
}
|
||||
```
|
||||
|
||||
### Blocking in Async Context
|
||||
```rust
|
||||
// WRONG: blocks the executor
|
||||
async fn bad() {
|
||||
std::thread::sleep(Duration::from_secs(1)); // blocks!
|
||||
std::fs::read_to_string("file.txt").unwrap(); // blocks!
|
||||
}
|
||||
|
||||
// RIGHT: use async versions
|
||||
async fn good() {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
tokio::fs::read_to_string("file.txt").await.unwrap();
|
||||
}
|
||||
|
||||
// Or spawn_blocking for CPU-bound work
|
||||
async fn compute() {
|
||||
let result = tokio::task::spawn_blocking(|| {
|
||||
heavy_computation() // OK to block here
|
||||
}).await.unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Thread Panic Handling
|
||||
|
||||
### Unhandled Panic
|
||||
```rust
|
||||
let handle = std::thread::spawn(|| {
|
||||
panic!("oops");
|
||||
});
|
||||
|
||||
// Main thread continues, might miss the error
|
||||
handle.join().unwrap(); // panics here
|
||||
```
|
||||
|
||||
### Proper Error Handling
|
||||
```rust
|
||||
let handle = std::thread::spawn(|| {
|
||||
panic!("oops");
|
||||
});
|
||||
|
||||
match handle.join() {
|
||||
Ok(result) => println!("Success: {:?}", result),
|
||||
Err(e) => println!("Thread panicked: {:?}", e),
|
||||
}
|
||||
|
||||
// For async: use catch_unwind
|
||||
use std::panic;
|
||||
|
||||
async fn safe_task() {
|
||||
let result = panic::catch_unwind(|| {
|
||||
risky_operation()
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(v) => use_value(v),
|
||||
Err(_) => log_error("task panicked"),
|
||||
}
|
||||
}
|
||||
```
|
||||
174
skills/m09-domain/SKILL.md
Normal file
174
skills/m09-domain/SKILL.md
Normal file
@@ -0,0 +1,174 @@
|
||||
---
|
||||
name: m09-domain
|
||||
description: "CRITICAL: Use for domain modeling. Triggers: domain model, DDD, domain-driven design, entity, value object, aggregate, repository pattern, business rules, validation, invariant, 领域模型, 领域驱动设计, 业务规则"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Domain Modeling
|
||||
|
||||
> **Layer 2: Design Choices**
|
||||
|
||||
## Core Question
|
||||
|
||||
**What is this concept's role in the domain?**
|
||||
|
||||
Before modeling in code, understand:
|
||||
- Is it an Entity (identity matters) or Value Object (interchangeable)?
|
||||
- What invariants must be maintained?
|
||||
- Where are the aggregate boundaries?
|
||||
|
||||
---
|
||||
|
||||
## Domain Concept → Rust Pattern
|
||||
|
||||
| Domain Concept | Rust Pattern | Ownership Implication |
|
||||
|----------------|--------------|----------------------|
|
||||
| Entity | struct + Id | Owned, unique identity |
|
||||
| Value Object | struct + Clone/Copy | Shareable, immutable |
|
||||
| Aggregate Root | struct owns children | Clear ownership tree |
|
||||
| Repository | trait | Abstracts persistence |
|
||||
| Domain Event | enum | Captures state changes |
|
||||
| Service | impl block / free fn | Stateless operations |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before creating a domain type:
|
||||
|
||||
1. **What's the concept's identity?**
|
||||
- Needs unique identity → Entity (Id field)
|
||||
- Interchangeable by value → Value Object (Clone/Copy)
|
||||
|
||||
2. **What invariants must hold?**
|
||||
- Always valid → private fields + validated constructor
|
||||
- Transition rules → type state pattern
|
||||
|
||||
3. **Who owns this data?**
|
||||
- Single owner (parent) → owned field
|
||||
- Shared reference → Arc/Rc
|
||||
- Weak reference → Weak
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
To domain constraints (Layer 3):
|
||||
|
||||
```
|
||||
"How should I model a Transaction?"
|
||||
↑ Ask: What domain rules govern transactions?
|
||||
↑ Check: domain-fintech (audit, precision requirements)
|
||||
↑ Check: Business stakeholders (what invariants?)
|
||||
```
|
||||
|
||||
| Design Question | Trace To | Ask |
|
||||
|-----------------|----------|-----|
|
||||
| Entity vs Value Object | domain-* | What makes two instances "the same"? |
|
||||
| Aggregate boundaries | domain-* | What must be consistent together? |
|
||||
| Validation rules | domain-* | What business rules apply? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
To implementation (Layer 1):
|
||||
|
||||
```
|
||||
"Model as Entity"
|
||||
↓ m01-ownership: Owned, unique
|
||||
↓ m05-type-driven: Newtype for Id
|
||||
|
||||
"Model as Value Object"
|
||||
↓ m01-ownership: Clone/Copy OK
|
||||
↓ m05-type-driven: Validate at construction
|
||||
|
||||
"Model as Aggregate"
|
||||
↓ m01-ownership: Parent owns children
|
||||
↓ m02-resource: Consider Rc for shared within aggregate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| DDD Concept | Rust Pattern | Example |
|
||||
|-------------|--------------|---------|
|
||||
| Value Object | Newtype | `struct Email(String);` |
|
||||
| Entity | Struct + ID | `struct User { id: UserId, ... }` |
|
||||
| Aggregate | Module boundary | `mod order { ... }` |
|
||||
| Repository | Trait | `trait UserRepo { fn find(...) }` |
|
||||
| Domain Event | Enum | `enum OrderEvent { Created, ... }` |
|
||||
|
||||
## Pattern Templates
|
||||
|
||||
### Value Object
|
||||
|
||||
```rust
|
||||
struct Email(String);
|
||||
|
||||
impl Email {
|
||||
pub fn new(s: &str) -> Result<Self, ValidationError> {
|
||||
validate_email(s)?;
|
||||
Ok(Self(s.to_string()))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Entity
|
||||
|
||||
```rust
|
||||
struct UserId(Uuid);
|
||||
|
||||
struct User {
|
||||
id: UserId,
|
||||
email: Email,
|
||||
// ... other fields
|
||||
}
|
||||
|
||||
impl PartialEq for User {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id // Identity equality
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Aggregate
|
||||
|
||||
```rust
|
||||
mod order {
|
||||
pub struct Order {
|
||||
id: OrderId,
|
||||
items: Vec<OrderItem>, // Owned children
|
||||
// ...
|
||||
}
|
||||
|
||||
impl Order {
|
||||
pub fn add_item(&mut self, item: OrderItem) {
|
||||
// Enforce aggregate invariants
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Why Wrong | Better |
|
||||
|---------|-----------|--------|
|
||||
| Primitive obsession | No type safety | Newtype wrappers |
|
||||
| Public fields with invariants | Invariants violated | Private + accessor |
|
||||
| Leaked aggregate internals | Broken encapsulation | Methods on root |
|
||||
| String for semantic types | No validation | Validated newtype |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Type-driven implementation | m05-type-driven |
|
||||
| Ownership for aggregates | m01-ownership |
|
||||
| Domain error handling | m13-domain-error |
|
||||
| Specific domain rules | domain-* |
|
||||
157
skills/m10-performance/SKILL.md
Normal file
157
skills/m10-performance/SKILL.md
Normal file
@@ -0,0 +1,157 @@
|
||||
---
|
||||
name: m10-performance
|
||||
description: "CRITICAL: Use for performance optimization. Triggers: performance, optimization, benchmark, profiling, flamegraph, criterion, slow, fast, allocation, cache, SIMD, make it faster, 性能优化, 基准测试"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Performance Optimization
|
||||
|
||||
> **Layer 2: Design Choices**
|
||||
|
||||
## Core Question
|
||||
|
||||
**What's the bottleneck, and is optimization worth it?**
|
||||
|
||||
Before optimizing:
|
||||
- Have you measured? (Don't guess)
|
||||
- What's the acceptable performance?
|
||||
- Will optimization add complexity?
|
||||
|
||||
---
|
||||
|
||||
## Performance Decision → Implementation
|
||||
|
||||
| Goal | Design Choice | Implementation |
|
||||
|------|---------------|----------------|
|
||||
| Reduce allocations | Pre-allocate, reuse | `with_capacity`, object pools |
|
||||
| Improve cache | Contiguous data | `Vec`, `SmallVec` |
|
||||
| Parallelize | Data parallelism | `rayon`, threads |
|
||||
| Avoid copies | Zero-copy | References, `Cow<T>` |
|
||||
| Reduce indirection | Inline data | `smallvec`, arrays |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before optimizing:
|
||||
|
||||
1. **Have you measured?**
|
||||
- Profile first → flamegraph, perf
|
||||
- Benchmark → criterion, cargo bench
|
||||
- Identify actual hotspots
|
||||
|
||||
2. **What's the priority?**
|
||||
- Algorithm (10x-1000x improvement)
|
||||
- Data structure (2x-10x)
|
||||
- Allocation (2x-5x)
|
||||
- Cache (1.5x-3x)
|
||||
|
||||
3. **What's the trade-off?**
|
||||
- Complexity vs speed
|
||||
- Memory vs CPU
|
||||
- Latency vs throughput
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
To domain constraints (Layer 3):
|
||||
|
||||
```
|
||||
"How fast does this need to be?"
|
||||
↑ Ask: What's the performance SLA?
|
||||
↑ Check: domain-* (latency requirements)
|
||||
↑ Check: Business requirements (acceptable response time)
|
||||
```
|
||||
|
||||
| Question | Trace To | Ask |
|
||||
|----------|----------|-----|
|
||||
| Latency requirements | domain-* | What's acceptable response time? |
|
||||
| Throughput needs | domain-* | How many requests per second? |
|
||||
| Memory constraints | domain-* | What's the memory budget? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
To implementation (Layer 1):
|
||||
|
||||
```
|
||||
"Need to reduce allocations"
|
||||
↓ m01-ownership: Use references, avoid clone
|
||||
↓ m02-resource: Pre-allocate with_capacity
|
||||
|
||||
"Need to parallelize"
|
||||
↓ m07-concurrency: Choose rayon or threads
|
||||
↓ m07-concurrency: Consider async for I/O-bound
|
||||
|
||||
"Need cache efficiency"
|
||||
↓ Data layout: Prefer Vec over HashMap when possible
|
||||
↓ Access patterns: Sequential over random access
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `cargo bench` | Micro-benchmarks |
|
||||
| `criterion` | Statistical benchmarks |
|
||||
| `perf` / `flamegraph` | CPU profiling |
|
||||
| `heaptrack` | Allocation tracking |
|
||||
| `valgrind` / `cachegrind` | Cache analysis |
|
||||
|
||||
## Optimization Priority
|
||||
|
||||
```
|
||||
1. Algorithm choice (10x - 1000x)
|
||||
2. Data structure (2x - 10x)
|
||||
3. Allocation reduction (2x - 5x)
|
||||
4. Cache optimization (1.5x - 3x)
|
||||
5. SIMD/Parallelism (2x - 8x)
|
||||
```
|
||||
|
||||
## Common Techniques
|
||||
|
||||
| Technique | When | How |
|
||||
|-----------|------|-----|
|
||||
| Pre-allocation | Known size | `Vec::with_capacity(n)` |
|
||||
| Avoid cloning | Hot paths | Use references or `Cow<T>` |
|
||||
| Batch operations | Many small ops | Collect then process |
|
||||
| SmallVec | Usually small | `smallvec::SmallVec<[T; N]>` |
|
||||
| Inline buffers | Fixed-size data | Arrays over Vec |
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Why Wrong | Better |
|
||||
|---------|-----------|--------|
|
||||
| Optimize without profiling | Wrong target | Profile first |
|
||||
| Benchmark in debug mode | Meaningless | Always `--release` |
|
||||
| Use LinkedList | Cache unfriendly | `Vec` or `VecDeque` |
|
||||
| Hidden `.clone()` | Unnecessary allocs | Use references |
|
||||
| Premature optimization | Wasted effort | Make it work first |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| Clone to avoid lifetimes | Performance cost | Proper ownership |
|
||||
| Box everything | Indirection cost | Stack when possible |
|
||||
| HashMap for small sets | Overhead | Vec with linear search |
|
||||
| String concat in loop | O(n^2) | `String::with_capacity` or `format!` |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Reducing clones | m01-ownership |
|
||||
| Concurrency options | m07-concurrency |
|
||||
| Smart pointer choice | m02-resource |
|
||||
| Domain requirements | domain-* |
|
||||
365
skills/m10-performance/patterns/optimization-guide.md
Normal file
365
skills/m10-performance/patterns/optimization-guide.md
Normal file
@@ -0,0 +1,365 @@
|
||||
# Rust Performance Optimization Guide
|
||||
|
||||
## Profiling First
|
||||
|
||||
### Tools
|
||||
```bash
|
||||
# CPU profiling
|
||||
cargo install flamegraph
|
||||
cargo flamegraph --bin myapp
|
||||
|
||||
# Memory profiling
|
||||
cargo install cargo-instruments # macOS
|
||||
heaptrack ./target/release/myapp # Linux
|
||||
|
||||
# Benchmarking
|
||||
cargo bench # with criterion
|
||||
|
||||
# Cache analysis
|
||||
valgrind --tool=cachegrind ./target/release/myapp
|
||||
```
|
||||
|
||||
### Criterion Benchmarks
|
||||
```rust
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
fn benchmark_parse(c: &mut Criterion) {
|
||||
let input = "test data".repeat(1000);
|
||||
|
||||
c.bench_function("parse_v1", |b| {
|
||||
b.iter(|| parse_v1(&input))
|
||||
});
|
||||
|
||||
c.bench_function("parse_v2", |b| {
|
||||
b.iter(|| parse_v2(&input))
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, benchmark_parse);
|
||||
criterion_main!(benches);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Optimizations
|
||||
|
||||
### 1. Avoid Unnecessary Allocations
|
||||
|
||||
```rust
|
||||
// BAD: allocates on every call
|
||||
fn to_uppercase(s: &str) -> String {
|
||||
s.to_uppercase()
|
||||
}
|
||||
|
||||
// GOOD: return Cow, allocate only if needed
|
||||
use std::borrow::Cow;
|
||||
|
||||
fn to_uppercase(s: &str) -> Cow<'_, str> {
|
||||
if s.chars().all(|c| c.is_uppercase()) {
|
||||
Cow::Borrowed(s)
|
||||
} else {
|
||||
Cow::Owned(s.to_uppercase())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Reuse Allocations
|
||||
|
||||
```rust
|
||||
// BAD: creates new Vec each iteration
|
||||
for item in items {
|
||||
let mut buffer = Vec::new();
|
||||
process(&mut buffer, item);
|
||||
}
|
||||
|
||||
// GOOD: reuse buffer
|
||||
let mut buffer = Vec::new();
|
||||
for item in items {
|
||||
buffer.clear();
|
||||
process(&mut buffer, item);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Appropriate Collections
|
||||
|
||||
| Need | Collection | Notes |
|
||||
|------|------------|-------|
|
||||
| Sequential access | `Vec<T>` | Best cache locality |
|
||||
| Random access by key | `HashMap<K, V>` | O(1) lookup |
|
||||
| Ordered keys | `BTreeMap<K, V>` | O(log n) lookup |
|
||||
| Small sets (<20) | `Vec<T>` + linear search | Lower overhead |
|
||||
| FIFO queue | `VecDeque<T>` | O(1) push/pop both ends |
|
||||
|
||||
### 4. Pre-allocate Capacity
|
||||
|
||||
```rust
|
||||
// BAD: many reallocations
|
||||
let mut v = Vec::new();
|
||||
for i in 0..10000 {
|
||||
v.push(i);
|
||||
}
|
||||
|
||||
// GOOD: single allocation
|
||||
let mut v = Vec::with_capacity(10000);
|
||||
for i in 0..10000 {
|
||||
v.push(i);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## String Optimization
|
||||
|
||||
### Avoid String Concatenation in Loops
|
||||
|
||||
```rust
|
||||
// BAD: O(n²) allocations
|
||||
let mut result = String::new();
|
||||
for s in strings {
|
||||
result = result + &s;
|
||||
}
|
||||
|
||||
// GOOD: O(n) with push_str
|
||||
let mut result = String::new();
|
||||
for s in strings {
|
||||
result.push_str(&s);
|
||||
}
|
||||
|
||||
// BETTER: pre-calculate capacity
|
||||
let total_len: usize = strings.iter().map(|s| s.len()).sum();
|
||||
let mut result = String::with_capacity(total_len);
|
||||
for s in strings {
|
||||
result.push_str(&s);
|
||||
}
|
||||
|
||||
// BEST: use join for simple cases
|
||||
let result = strings.join("");
|
||||
```
|
||||
|
||||
### Use &str When Possible
|
||||
|
||||
```rust
|
||||
// BAD: requires allocation
|
||||
fn greet(name: String) {
|
||||
println!("Hello, {}", name);
|
||||
}
|
||||
|
||||
// GOOD: borrows, no allocation
|
||||
fn greet(name: &str) {
|
||||
println!("Hello, {}", name);
|
||||
}
|
||||
|
||||
// Works with both:
|
||||
greet("world"); // &str
|
||||
greet(&String::from("world")); // &String coerces to &str
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Iterator Optimization
|
||||
|
||||
### Use Iterators Over Indexing
|
||||
|
||||
```rust
|
||||
// BAD: bounds checking on each access
|
||||
let mut sum = 0;
|
||||
for i in 0..vec.len() {
|
||||
sum += vec[i];
|
||||
}
|
||||
|
||||
// GOOD: no bounds checking
|
||||
let sum: i32 = vec.iter().sum();
|
||||
|
||||
// GOOD: when index needed
|
||||
for (i, item) in vec.iter().enumerate() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Lazy Evaluation
|
||||
|
||||
```rust
|
||||
// Iterators are lazy - computation happens at collect
|
||||
let result: Vec<_> = data
|
||||
.iter()
|
||||
.filter(|x| x.is_valid())
|
||||
.map(|x| x.process())
|
||||
.take(10) // stop after 10 items
|
||||
.collect();
|
||||
```
|
||||
|
||||
### Avoid Collecting When Not Needed
|
||||
|
||||
```rust
|
||||
// BAD: unnecessary intermediate allocation
|
||||
let filtered: Vec<_> = items.iter().filter(|x| x.valid).collect();
|
||||
let count = filtered.len();
|
||||
|
||||
// GOOD: no allocation
|
||||
let count = items.iter().filter(|x| x.valid).count();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Parallelism with Rayon
|
||||
|
||||
```rust
|
||||
use rayon::prelude::*;
|
||||
|
||||
// Sequential
|
||||
let sum: i32 = (0..1_000_000).map(|x| x * x).sum();
|
||||
|
||||
// Parallel (automatic work stealing)
|
||||
let sum: i32 = (0..1_000_000).into_par_iter().map(|x| x * x).sum();
|
||||
|
||||
// Parallel with custom chunk size
|
||||
let results: Vec<_> = data
|
||||
.par_chunks(1000)
|
||||
.map(|chunk| process_chunk(chunk))
|
||||
.collect();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Layout
|
||||
|
||||
### Use Appropriate Integer Sizes
|
||||
|
||||
```rust
|
||||
// If values are small, use smaller types
|
||||
struct Item {
|
||||
count: u8, // 0-255, not u64
|
||||
flags: u8, // small enum
|
||||
id: u32, // if 4 billion is enough
|
||||
}
|
||||
```
|
||||
|
||||
### Pack Structs Efficiently
|
||||
|
||||
```rust
|
||||
// BAD: 24 bytes due to padding
|
||||
struct Bad {
|
||||
a: u8, // 1 byte + 7 padding
|
||||
b: u64, // 8 bytes
|
||||
c: u8, // 1 byte + 7 padding
|
||||
}
|
||||
|
||||
// GOOD: 16 bytes (or use #[repr(packed)])
|
||||
struct Good {
|
||||
b: u64, // 8 bytes
|
||||
a: u8, // 1 byte
|
||||
c: u8, // 1 byte + 6 padding
|
||||
}
|
||||
```
|
||||
|
||||
### Box Large Values
|
||||
|
||||
```rust
|
||||
// Large enum variants waste space
|
||||
enum Message {
|
||||
Quit,
|
||||
Data([u8; 10000]), // all variants are 10000+ bytes
|
||||
}
|
||||
|
||||
// Better: box the large variant
|
||||
enum Message {
|
||||
Quit,
|
||||
Data(Box<[u8; 10000]>), // variants are pointer-sized
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Async Performance
|
||||
|
||||
### Avoid Blocking in Async
|
||||
|
||||
```rust
|
||||
// BAD: blocks the executor
|
||||
async fn bad() {
|
||||
std::thread::sleep(Duration::from_secs(1)); // blocking!
|
||||
std::fs::read_to_string("file.txt").unwrap(); // blocking!
|
||||
}
|
||||
|
||||
// GOOD: use async versions
|
||||
async fn good() {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
tokio::fs::read_to_string("file.txt").await.unwrap();
|
||||
}
|
||||
|
||||
// For CPU work: spawn_blocking
|
||||
async fn compute() -> i32 {
|
||||
tokio::task::spawn_blocking(|| {
|
||||
heavy_computation()
|
||||
}).await.unwrap()
|
||||
}
|
||||
```
|
||||
|
||||
### Buffer Async I/O
|
||||
|
||||
```rust
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
|
||||
// BAD: many small reads
|
||||
async fn bad(file: File) {
|
||||
let mut byte = [0u8];
|
||||
while file.read(&mut byte).await.unwrap() > 0 {
|
||||
process(byte[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: buffered reading
|
||||
async fn good(file: File) {
|
||||
let reader = BufReader::new(file);
|
||||
let mut lines = reader.lines();
|
||||
while let Some(line) = lines.next_line().await.unwrap() {
|
||||
process(&line);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Release Build Optimization
|
||||
|
||||
### Cargo.toml Settings
|
||||
|
||||
```toml
|
||||
[profile.release]
|
||||
lto = true # Link-time optimization
|
||||
codegen-units = 1 # Single codegen unit (slower compile, faster code)
|
||||
panic = "abort" # Smaller binary, no unwinding
|
||||
strip = true # Strip symbols
|
||||
|
||||
[profile.release-fast]
|
||||
inherits = "release"
|
||||
opt-level = 3 # Maximum optimization
|
||||
|
||||
[profile.release-small]
|
||||
inherits = "release"
|
||||
opt-level = "s" # Optimize for size
|
||||
```
|
||||
|
||||
### Compile-Time Assertions
|
||||
|
||||
```rust
|
||||
// Zero runtime cost
|
||||
const _: () = assert!(std::mem::size_of::<MyStruct>() <= 64);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
Before optimizing:
|
||||
- [ ] Profile to find actual bottlenecks
|
||||
- [ ] Have benchmarks to measure improvement
|
||||
- [ ] Consider if optimization is worth complexity
|
||||
|
||||
Common wins:
|
||||
- [ ] Reduce allocations (Cow, reuse buffers)
|
||||
- [ ] Use appropriate collections
|
||||
- [ ] Pre-allocate with_capacity
|
||||
- [ ] Use iterators instead of indexing
|
||||
- [ ] Enable LTO for release builds
|
||||
- [ ] Use rayon for parallel workloads
|
||||
162
skills/m11-ecosystem/SKILL.md
Normal file
162
skills/m11-ecosystem/SKILL.md
Normal file
@@ -0,0 +1,162 @@
|
||||
---
|
||||
name: m11-ecosystem
|
||||
description: "Use when integrating crates or ecosystem questions. Keywords: E0425, E0433, E0603, crate, cargo, dependency, feature flag, workspace, which crate to use, using external C libraries, creating Python extensions, PyO3, wasm, WebAssembly, bindgen, cbindgen, napi-rs, cannot find, private, crate recommendation, best crate for, Cargo.toml, features, crate 推荐, 依赖管理, 特性标志, 工作空间, Python 绑定"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
## Current Dependencies (Auto-Injected)
|
||||
|
||||
!`grep -A 100 '^\[dependencies\]' Cargo.toml 2>/dev/null | head -30 || echo "No Cargo.toml found"`
|
||||
|
||||
---
|
||||
|
||||
# Ecosystem Integration
|
||||
|
||||
> **Layer 2: Design Choices**
|
||||
|
||||
## Core Question
|
||||
|
||||
**What's the right crate for this job, and how should it integrate?**
|
||||
|
||||
Before adding dependencies:
|
||||
- Is there a standard solution?
|
||||
- What's the maintenance status?
|
||||
- What's the API stability?
|
||||
|
||||
---
|
||||
|
||||
## Integration Decision → Implementation
|
||||
|
||||
| Need | Choice | Crates |
|
||||
|------|--------|--------|
|
||||
| Serialization | Derive-based | serde, serde_json |
|
||||
| Async runtime | tokio or async-std | tokio (most popular) |
|
||||
| HTTP client | Ergonomic | reqwest |
|
||||
| HTTP server | Modern | axum, actix-web |
|
||||
| Database | SQL or ORM | sqlx, diesel |
|
||||
| CLI parsing | Derive-based | clap |
|
||||
| Error handling | App vs lib | anyhow, thiserror |
|
||||
| Logging | Facade | tracing, log |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before adding a dependency:
|
||||
|
||||
1. **Is it well-maintained?**
|
||||
- Recent commits?
|
||||
- Active issue response?
|
||||
- Breaking changes frequency?
|
||||
|
||||
2. **What's the scope?**
|
||||
- Do you need the full crate or just a feature?
|
||||
- Can feature flags reduce bloat?
|
||||
|
||||
3. **How does it integrate?**
|
||||
- Trait-based or concrete types?
|
||||
- Sync or async?
|
||||
- What bounds does it require?
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
To domain constraints (Layer 3):
|
||||
|
||||
```
|
||||
"Which HTTP framework should I use?"
|
||||
↑ Ask: What are the performance requirements?
|
||||
↑ Check: domain-web (latency, throughput needs)
|
||||
↑ Check: Team expertise (familiarity with framework)
|
||||
```
|
||||
|
||||
| Question | Trace To | Ask |
|
||||
|----------|----------|-----|
|
||||
| Framework choice | domain-* | What constraints matter? |
|
||||
| Library vs build | domain-* | What's the deployment model? |
|
||||
| API design | domain-* | Who are the consumers? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
To implementation (Layer 1):
|
||||
|
||||
```
|
||||
"Integrate external crate"
|
||||
↓ m04-zero-cost: Trait bounds and generics
|
||||
↓ m06-error-handling: Error type compatibility
|
||||
|
||||
"FFI integration"
|
||||
↓ unsafe-checker: Safety requirements
|
||||
↓ m12-lifecycle: Resource cleanup
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Language Interop
|
||||
|
||||
| Integration | Crate/Tool | Use Case |
|
||||
|-------------|------------|----------|
|
||||
| C/C++ → Rust | `bindgen` | Auto-generate bindings |
|
||||
| Rust → C | `cbindgen` | Export C headers |
|
||||
| Python ↔ Rust | `pyo3` | Python extensions |
|
||||
| Node.js ↔ Rust | `napi-rs` | Node addons |
|
||||
| WebAssembly | `wasm-bindgen` | Browser/WASI |
|
||||
|
||||
### Cargo Features
|
||||
|
||||
| Feature | Purpose |
|
||||
|---------|---------|
|
||||
| `[features]` | Optional functionality |
|
||||
| `default = [...]` | Default features |
|
||||
| `feature = "serde"` | Conditional deps |
|
||||
| `[workspace]` | Multi-crate projects |
|
||||
|
||||
## Error Code Reference
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| E0433 | Can't find crate | Add to Cargo.toml |
|
||||
| E0603 | Private item | Check crate docs |
|
||||
| Feature not enabled | Optional feature | Enable in `features` |
|
||||
| Version conflict | Incompatible deps | `cargo update` or pin |
|
||||
| Duplicate types | Different crate versions | Unify in workspace |
|
||||
|
||||
---
|
||||
|
||||
## Crate Selection Criteria
|
||||
|
||||
| Criterion | Good Sign | Warning Sign |
|
||||
|-----------|-----------|--------------|
|
||||
| Maintenance | Recent commits | Years inactive |
|
||||
| Community | Active issues/PRs | No response |
|
||||
| Documentation | Examples, API docs | Minimal docs |
|
||||
| Stability | Semantic versioning | Frequent breaking |
|
||||
| Dependencies | Minimal, well-known | Heavy, obscure |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| `extern crate` | Outdated (2018+) | Just `use` |
|
||||
| `#[macro_use]` | Global pollution | Explicit import |
|
||||
| Wildcard deps `*` | Unpredictable | Specific versions |
|
||||
| Too many deps | Supply chain risk | Evaluate necessity |
|
||||
| Vendoring everything | Maintenance burden | Trust crates.io |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Error type design | m06-error-handling |
|
||||
| Trait integration | m04-zero-cost |
|
||||
| FFI safety | unsafe-checker |
|
||||
| Resource management | m12-lifecycle |
|
||||
177
skills/m12-lifecycle/SKILL.md
Normal file
177
skills/m12-lifecycle/SKILL.md
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
name: m12-lifecycle
|
||||
description: "Use when designing resource lifecycles. Keywords: RAII, Drop, resource lifecycle, connection pool, lazy initialization, connection pool design, resource cleanup patterns, cleanup, scope, OnceCell, Lazy, once_cell, OnceLock, transaction, session management, when is Drop called, cleanup on error, guard pattern, scope guard, 资源生命周期, 连接池, 惰性初始化, 资源清理, RAII 模式"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Resource Lifecycle
|
||||
|
||||
> **Layer 2: Design Choices**
|
||||
|
||||
## Core Question
|
||||
|
||||
**When should this resource be created, used, and cleaned up?**
|
||||
|
||||
Before implementing lifecycle:
|
||||
- What's the resource's scope?
|
||||
- Who owns the cleanup responsibility?
|
||||
- What happens on error?
|
||||
|
||||
---
|
||||
|
||||
## Lifecycle Pattern → Implementation
|
||||
|
||||
| Pattern | When | Implementation |
|
||||
|---------|------|----------------|
|
||||
| RAII | Auto cleanup | `Drop` trait |
|
||||
| Lazy init | Deferred creation | `OnceLock`, `LazyLock` |
|
||||
| Pool | Reuse expensive resources | `r2d2`, `deadpool` |
|
||||
| Guard | Scoped access | `MutexGuard` pattern |
|
||||
| Scope | Transaction boundary | Custom struct + Drop |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before designing lifecycle:
|
||||
|
||||
1. **What's the resource cost?**
|
||||
- Cheap → create per use
|
||||
- Expensive → pool or cache
|
||||
- Global → lazy singleton
|
||||
|
||||
2. **What's the scope?**
|
||||
- Function-local → stack allocation
|
||||
- Request-scoped → passed or extracted
|
||||
- Application-wide → static or Arc
|
||||
|
||||
3. **What about errors?**
|
||||
- Cleanup must happen → Drop
|
||||
- Cleanup is optional → explicit close
|
||||
- Cleanup can fail → Result from close
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
To domain constraints (Layer 3):
|
||||
|
||||
```
|
||||
"How should I manage database connections?"
|
||||
↑ Ask: What's the connection cost?
|
||||
↑ Check: domain-* (latency requirements)
|
||||
↑ Check: Infrastructure (connection limits)
|
||||
```
|
||||
|
||||
| Question | Trace To | Ask |
|
||||
|----------|----------|-----|
|
||||
| Connection pooling | domain-* | What's acceptable latency? |
|
||||
| Resource limits | domain-* | What are infra constraints? |
|
||||
| Transaction scope | domain-* | What must be atomic? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
To implementation (Layer 1):
|
||||
|
||||
```
|
||||
"Need automatic cleanup"
|
||||
↓ m02-resource: Implement Drop
|
||||
↓ m01-ownership: Clear owner for cleanup
|
||||
|
||||
"Need lazy initialization"
|
||||
↓ m03-mutability: OnceLock for thread-safe
|
||||
↓ m07-concurrency: LazyLock for sync
|
||||
|
||||
"Need connection pool"
|
||||
↓ m07-concurrency: Thread-safe pool
|
||||
↓ m02-resource: Arc for sharing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Type | Use Case |
|
||||
|---------|------|----------|
|
||||
| RAII | `Drop` trait | Auto cleanup on scope exit |
|
||||
| Lazy Init | `OnceLock`, `LazyLock` | Deferred initialization |
|
||||
| Pool | `r2d2`, `deadpool` | Connection reuse |
|
||||
| Guard | `MutexGuard` | Scoped lock release |
|
||||
| Scope | Custom struct | Transaction boundaries |
|
||||
|
||||
## Lifecycle Events
|
||||
|
||||
| Event | Rust Mechanism |
|
||||
|-------|----------------|
|
||||
| Creation | `new()`, `Default` |
|
||||
| Lazy Init | `OnceLock::get_or_init` |
|
||||
| Usage | `&self`, `&mut self` |
|
||||
| Cleanup | `Drop::drop()` |
|
||||
|
||||
## Pattern Templates
|
||||
|
||||
### RAII Guard
|
||||
|
||||
```rust
|
||||
struct FileGuard {
|
||||
path: PathBuf,
|
||||
_handle: File,
|
||||
}
|
||||
|
||||
impl Drop for FileGuard {
|
||||
fn drop(&mut self) {
|
||||
// Cleanup: remove temp file
|
||||
let _ = std::fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Lazy Singleton
|
||||
|
||||
```rust
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static CONFIG: OnceLock<Config> = OnceLock::new();
|
||||
|
||||
fn get_config() -> &'static Config {
|
||||
CONFIG.get_or_init(|| {
|
||||
Config::load().expect("config required")
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| Resource leak | Forgot Drop | Implement Drop or RAII wrapper |
|
||||
| Double free | Manual memory | Let Rust handle |
|
||||
| Use after drop | Dangling reference | Check lifetimes |
|
||||
| E0509 move out of Drop | Moving owned field | `Option::take()` |
|
||||
| Pool exhaustion | Not returned | Ensure Drop returns |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| Manual cleanup | Easy to forget | RAII/Drop |
|
||||
| `lazy_static!` | External dep | `std::sync::OnceLock` |
|
||||
| Global mutable state | Thread unsafety | `OnceLock` or proper sync |
|
||||
| Forget to close | Resource leak | Drop impl |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Smart pointers | m02-resource |
|
||||
| Thread-safe init | m07-concurrency |
|
||||
| Domain scopes | m09-domain |
|
||||
| Error in cleanup | m06-error-handling |
|
||||
180
skills/m13-domain-error/SKILL.md
Normal file
180
skills/m13-domain-error/SKILL.md
Normal file
@@ -0,0 +1,180 @@
|
||||
---
|
||||
name: m13-domain-error
|
||||
description: "Use when designing domain error handling. Keywords: domain error, error categorization, recovery strategy, retry, fallback, domain error hierarchy, user-facing vs internal errors, error code design, circuit breaker, graceful degradation, resilience, error context, backoff, retry with backoff, error recovery, transient vs permanent error, 领域错误, 错误分类, 恢复策略, 重试, 熔断器, 优雅降级"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Domain Error Strategy
|
||||
|
||||
> **Layer 2: Design Choices**
|
||||
|
||||
## Core Question
|
||||
|
||||
**Who needs to handle this error, and how should they recover?**
|
||||
|
||||
Before designing error types:
|
||||
- Is this user-facing or internal?
|
||||
- Is recovery possible?
|
||||
- What context is needed for debugging?
|
||||
|
||||
---
|
||||
|
||||
## Error Categorization
|
||||
|
||||
| Error Type | Audience | Recovery | Example |
|
||||
|------------|----------|----------|---------|
|
||||
| User-facing | End users | Guide action | `InvalidEmail`, `NotFound` |
|
||||
| Internal | Developers | Debug info | `DatabaseError`, `ParseError` |
|
||||
| System | Ops/SRE | Monitor/alert | `ConnectionTimeout`, `RateLimited` |
|
||||
| Transient | Automation | Retry | `NetworkError`, `ServiceUnavailable` |
|
||||
| Permanent | Human | Investigate | `ConfigInvalid`, `DataCorrupted` |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
Before designing error types:
|
||||
|
||||
1. **Who sees this error?**
|
||||
- End user → friendly message, actionable
|
||||
- Developer → detailed, debuggable
|
||||
- Ops → structured, alertable
|
||||
|
||||
2. **Can we recover?**
|
||||
- Transient → retry with backoff
|
||||
- Degradable → fallback value
|
||||
- Permanent → fail fast, alert
|
||||
|
||||
3. **What context is needed?**
|
||||
- Call chain → anyhow::Context
|
||||
- Request ID → structured logging
|
||||
- Input data → error payload
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
To domain constraints (Layer 3):
|
||||
|
||||
```
|
||||
"How should I handle payment failures?"
|
||||
↑ Ask: What are the business rules for retries?
|
||||
↑ Check: domain-fintech (transaction requirements)
|
||||
↑ Check: SLA (availability requirements)
|
||||
```
|
||||
|
||||
| Question | Trace To | Ask |
|
||||
|----------|----------|-----|
|
||||
| Retry policy | domain-* | What's acceptable latency for retry? |
|
||||
| User experience | domain-* | What message should users see? |
|
||||
| Compliance | domain-* | What must be logged for audit? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
To implementation (Layer 1):
|
||||
|
||||
```
|
||||
"Need typed errors"
|
||||
↓ m06-error-handling: thiserror for library
|
||||
↓ m04-zero-cost: Error enum design
|
||||
|
||||
"Need error context"
|
||||
↓ m06-error-handling: anyhow::Context
|
||||
↓ Logging: tracing with fields
|
||||
|
||||
"Need retry logic"
|
||||
↓ m07-concurrency: async retry patterns
|
||||
↓ Crates: tokio-retry, backoff
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Recovery Pattern | When | Implementation |
|
||||
|------------------|------|----------------|
|
||||
| Retry | Transient failures | exponential backoff |
|
||||
| Fallback | Degraded mode | cached/default value |
|
||||
| Circuit Breaker | Cascading failures | failsafe-rs |
|
||||
| Timeout | Slow operations | `tokio::time::timeout` |
|
||||
| Bulkhead | Isolation | separate thread pools |
|
||||
|
||||
## Error Hierarchy
|
||||
|
||||
```rust
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum AppError {
|
||||
// User-facing
|
||||
#[error("Invalid input: {0}")]
|
||||
Validation(String),
|
||||
|
||||
// Transient (retryable)
|
||||
#[error("Service temporarily unavailable")]
|
||||
ServiceUnavailable(#[source] reqwest::Error),
|
||||
|
||||
// Internal (log details, show generic)
|
||||
#[error("Internal error")]
|
||||
Internal(#[source] anyhow::Error),
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
pub fn is_retryable(&self) -> bool {
|
||||
matches!(self, Self::ServiceUnavailable(_))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Retry Pattern
|
||||
|
||||
```rust
|
||||
use tokio_retry::{Retry, strategy::ExponentialBackoff};
|
||||
|
||||
async fn with_retry<F, T, E>(f: F) -> Result<T, E>
|
||||
where
|
||||
F: Fn() -> impl Future<Output = Result<T, E>>,
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
let strategy = ExponentialBackoff::from_millis(100)
|
||||
.max_delay(Duration::from_secs(10))
|
||||
.take(5);
|
||||
|
||||
Retry::spawn(strategy, || f()).await
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Why Wrong | Better |
|
||||
|---------|-----------|--------|
|
||||
| Same error for all | No actionability | Categorize by audience |
|
||||
| Retry everything | Wasted resources | Only transient errors |
|
||||
| Infinite retry | DoS self | Max attempts + backoff |
|
||||
| Expose internal errors | Security risk | User-friendly messages |
|
||||
| No context | Hard to debug | .context() everywhere |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| String errors | No structure | thiserror types |
|
||||
| panic! for recoverable | Bad UX | Result with context |
|
||||
| Ignore errors | Silent failures | Log or propagate |
|
||||
| Box<dyn Error> everywhere | Lost type info | thiserror |
|
||||
| Error in happy path | Performance | Early validation |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Error handling basics | m06-error-handling |
|
||||
| Retry implementation | m07-concurrency |
|
||||
| Domain modeling | m09-domain |
|
||||
| User-facing APIs | domain-* |
|
||||
177
skills/m14-mental-model/SKILL.md
Normal file
177
skills/m14-mental-model/SKILL.md
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
name: m14-mental-model
|
||||
description: "Use when learning Rust concepts. Keywords: mental model, how to think about ownership, understanding borrow checker, visualizing memory layout, analogy, misconception, explaining ownership, why does Rust, help me understand, confused about, learning Rust, explain like I'm, ELI5, intuition for, coming from Java, coming from Python, 心智模型, 如何理解所有权, 学习 Rust, Rust 入门, 为什么 Rust"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Mental Models
|
||||
|
||||
> **Layer 2: Design Choices**
|
||||
|
||||
## Core Question
|
||||
|
||||
**What's the right way to think about this Rust concept?**
|
||||
|
||||
When learning or explaining Rust:
|
||||
- What's the correct mental model?
|
||||
- What misconceptions should be avoided?
|
||||
- What analogies help understanding?
|
||||
|
||||
---
|
||||
|
||||
## Key Mental Models
|
||||
|
||||
| Concept | Mental Model | Analogy |
|
||||
|---------|--------------|---------|
|
||||
| Ownership | Unique key | Only one person has the house key |
|
||||
| Move | Key handover | Giving away your key |
|
||||
| `&T` | Lending for reading | Lending a book |
|
||||
| `&mut T` | Exclusive editing | Only you can edit the doc |
|
||||
| Lifetime `'a` | Valid scope | "Ticket valid until..." |
|
||||
| `Box<T>` | Heap pointer | Remote control to TV |
|
||||
| `Rc<T>` | Shared ownership | Multiple remotes, last turns off |
|
||||
| `Arc<T>` | Thread-safe Rc | Remotes from any room |
|
||||
|
||||
---
|
||||
|
||||
## Coming From Other Languages
|
||||
|
||||
| From | Key Shift |
|
||||
|------|-----------|
|
||||
| Java/C# | Values are owned, not references by default |
|
||||
| C/C++ | Compiler enforces safety rules |
|
||||
| Python/Go | No GC, deterministic destruction |
|
||||
| Functional | Mutability is safe via ownership |
|
||||
| JavaScript | No null, use Option instead |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
When confused about Rust:
|
||||
|
||||
1. **What's the ownership model?**
|
||||
- Who owns this data?
|
||||
- How long does it live?
|
||||
- Who can access it?
|
||||
|
||||
2. **What guarantee is Rust providing?**
|
||||
- No data races
|
||||
- No dangling pointers
|
||||
- No use-after-free
|
||||
|
||||
3. **What's the compiler telling me?**
|
||||
- Error = violation of safety rule
|
||||
- Solution = work with the rules
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
To design understanding (Layer 2):
|
||||
|
||||
```
|
||||
"Why can't I do X in Rust?"
|
||||
↑ Ask: What safety guarantee would be violated?
|
||||
↑ Check: m01-m07 for the rule being enforced
|
||||
↑ Ask: What's the intended design pattern?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
To implementation (Layer 1):
|
||||
|
||||
```
|
||||
"I understand the concept, now how do I implement?"
|
||||
↓ m01-ownership: Ownership patterns
|
||||
↓ m02-resource: Smart pointer choice
|
||||
↓ m07-concurrency: Thread safety
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Misconceptions
|
||||
|
||||
| Error | Wrong Model | Correct Model |
|
||||
|-------|-------------|---------------|
|
||||
| E0382 use after move | GC cleans up | Ownership = unique key transfer |
|
||||
| E0502 borrow conflict | Multiple writers OK | Only one writer at a time |
|
||||
| E0499 multiple mut borrows | Aliased mutation | Exclusive access for mutation |
|
||||
| E0106 missing lifetime | Ignoring scope | References have validity scope |
|
||||
| E0507 cannot move from `&T` | Implicit clone | References don't own data |
|
||||
|
||||
## Deprecated Thinking
|
||||
|
||||
| Deprecated | Better |
|
||||
|------------|--------|
|
||||
| "Rust is like C++" | Different ownership model |
|
||||
| "Lifetimes are GC" | Compile-time validity scope |
|
||||
| "Clone solves everything" | Restructure ownership |
|
||||
| "Fight the borrow checker" | Work with the compiler |
|
||||
| "`unsafe` to avoid rules" | Understand safe patterns first |
|
||||
|
||||
---
|
||||
|
||||
## Ownership Visualization
|
||||
|
||||
```
|
||||
Stack Heap
|
||||
+----------------+ +----------------+
|
||||
| main() | | |
|
||||
| s1 ─────────────────────> │ "hello" |
|
||||
| | | |
|
||||
| fn takes(s) { | | |
|
||||
| s2 (moved) ─────────────> │ "hello" |
|
||||
| } | | (s1 invalid) |
|
||||
+----------------+ +----------------+
|
||||
|
||||
After move: s1 is no longer valid
|
||||
```
|
||||
|
||||
## Reference Visualization
|
||||
|
||||
```
|
||||
+----------------+
|
||||
| data: String |────────────> "hello"
|
||||
+----------------+
|
||||
↑
|
||||
│ &data (immutable borrow)
|
||||
│
|
||||
+------+------+
|
||||
| reader1 reader2 (multiple OK)
|
||||
+------+------+
|
||||
|
||||
+----------------+
|
||||
| data: String |────────────> "hello"
|
||||
+----------------+
|
||||
↑
|
||||
│ &mut data (mutable borrow)
|
||||
│
|
||||
+------+
|
||||
| writer (only one)
|
||||
+------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Learning Path
|
||||
|
||||
| Stage | Focus | Skills |
|
||||
|-------|-------|--------|
|
||||
| Beginner | Ownership basics | m01-ownership, m14-mental-model |
|
||||
| Intermediate | Smart pointers, error handling | m02, m06 |
|
||||
| Advanced | Concurrency, unsafe | m07, unsafe-checker |
|
||||
| Expert | Design patterns | m09-m15, domain-* |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Ownership errors | m01-ownership |
|
||||
| Smart pointers | m02-resource |
|
||||
| Concurrency | m07-concurrency |
|
||||
| Anti-patterns | m15-anti-pattern |
|
||||
286
skills/m14-mental-model/patterns/thinking-in-rust.md
Normal file
286
skills/m14-mental-model/patterns/thinking-in-rust.md
Normal file
@@ -0,0 +1,286 @@
|
||||
# Thinking in Rust: Mental Models
|
||||
|
||||
## Core Mental Models
|
||||
|
||||
### 1. Ownership as Resource Management
|
||||
|
||||
```
|
||||
Traditional: "Who has a pointer to this data?"
|
||||
Rust: "Who OWNS this data and is responsible for freeing it?"
|
||||
```
|
||||
|
||||
Key insight: Every value has exactly one owner. When the owner goes out of scope, the value is dropped.
|
||||
|
||||
```rust
|
||||
{
|
||||
let s = String::from("hello"); // s owns the String
|
||||
// use s...
|
||||
} // s goes out of scope, String is dropped (memory freed)
|
||||
```
|
||||
|
||||
### 2. Borrowing as Temporary Access
|
||||
|
||||
```
|
||||
Traditional: "I'll just read from this pointer"
|
||||
Rust: "I'm borrowing this value, owner still responsible for it"
|
||||
```
|
||||
|
||||
Key insight: Borrows are like library books - you can read them, but must return them.
|
||||
|
||||
```rust
|
||||
fn print_length(s: &String) { // borrows s
|
||||
println!("{}", s.len());
|
||||
} // borrow ends, caller still owns s
|
||||
|
||||
let my_string = String::from("hello");
|
||||
print_length(&my_string); // lend to function
|
||||
println!("{}", my_string); // still have it
|
||||
```
|
||||
|
||||
### 3. Lifetimes as Validity Scopes
|
||||
|
||||
```
|
||||
Traditional: "Hope this pointer is still valid"
|
||||
Rust: "Compiler tracks exactly how long references are valid"
|
||||
```
|
||||
|
||||
Key insight: A reference can't outlive the data it points to.
|
||||
|
||||
```rust
|
||||
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
||||
// 'a means: the returned reference is valid as long as BOTH inputs are valid
|
||||
if x.len() > y.len() { x } else { y }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Shifting Perspectives
|
||||
|
||||
### From "Everything is a Reference" (Java/C#)
|
||||
|
||||
Java mental model:
|
||||
```java
|
||||
// Everything is implicitly a reference
|
||||
User user = new User("Alice"); // user is a reference
|
||||
List<User> users = new ArrayList<>();
|
||||
users.add(user); // shares the reference
|
||||
user.setName("Bob"); // affects the list too!
|
||||
```
|
||||
|
||||
Rust mental model:
|
||||
```rust
|
||||
// Values are owned, sharing is explicit
|
||||
let user = User::new("Alice"); // user is owned
|
||||
let mut users = vec![];
|
||||
users.push(user); // user moved into vec, can't use user anymore
|
||||
// user.set_name("Bob"); // ERROR: user was moved
|
||||
|
||||
// If you need sharing:
|
||||
use std::rc::Rc;
|
||||
let user = Rc::new(User::new("Alice"));
|
||||
let user2 = Rc::clone(&user); // explicit shared ownership
|
||||
```
|
||||
|
||||
### From "Manual Memory Management" (C/C++)
|
||||
|
||||
C mental model:
|
||||
```c
|
||||
char* s = malloc(100);
|
||||
// ... must remember to free(s) ...
|
||||
// ... what if we return early? ...
|
||||
// ... what if an exception occurs? ...
|
||||
free(s);
|
||||
```
|
||||
|
||||
Rust mental model:
|
||||
```rust
|
||||
let s = String::with_capacity(100);
|
||||
// ... use s ...
|
||||
// No need to free - Rust drops s automatically when scope ends
|
||||
// Even with early returns, panics, or any control flow
|
||||
```
|
||||
|
||||
### From "Garbage Collection" (Go/Python)
|
||||
|
||||
GC mental model:
|
||||
```python
|
||||
# Create objects, GC will figure it out
|
||||
users = []
|
||||
for name in names:
|
||||
users.append(User(name))
|
||||
# GC runs sometime later, when it feels like it
|
||||
```
|
||||
|
||||
Rust mental model:
|
||||
```rust
|
||||
let users: Vec<User> = names
|
||||
.iter()
|
||||
.map(|name| User::new(name))
|
||||
.collect();
|
||||
// Memory is freed EXACTLY when users goes out of scope
|
||||
// Deterministic, no GC pauses, no unpredictable memory usage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Questions to Ask
|
||||
|
||||
### When Designing Functions
|
||||
|
||||
1. **Does this function need to own the data, or just read it?**
|
||||
- Need to keep it: take ownership (`fn process(data: Vec<T>)`)
|
||||
- Just reading: borrow (`fn process(data: &[T])`)
|
||||
- Need to modify: mutable borrow (`fn process(data: &mut Vec<T>)`)
|
||||
|
||||
2. **Does the return value contain references to inputs?**
|
||||
- Yes: need lifetime annotations
|
||||
- No: lifetime elision usually works
|
||||
|
||||
### When Designing Structs
|
||||
|
||||
1. **Should this struct own its data or reference it?**
|
||||
- Long-lived, independent: own (`name: String`)
|
||||
- Short-lived view: reference (`name: &'a str`)
|
||||
|
||||
2. **Do multiple parts need to access the same data?**
|
||||
- Single-threaded: `Rc<T>` or `Rc<RefCell<T>>`
|
||||
- Multi-threaded: `Arc<T>` or `Arc<Mutex<T>>`
|
||||
|
||||
### When Hitting Borrow Checker Errors
|
||||
|
||||
1. **Am I trying to use a value after moving it?**
|
||||
- Clone it, borrow it, or restructure the code
|
||||
|
||||
2. **Am I trying to have multiple mutable references?**
|
||||
- Scope the mutations, use interior mutability, or redesign
|
||||
|
||||
3. **Does a reference outlive its source?**
|
||||
- Return owned data instead, or use `'static`
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### The Clone Escape Hatch
|
||||
|
||||
When fighting the borrow checker, `.clone()` often works:
|
||||
|
||||
```rust
|
||||
// Can't do this - double borrow
|
||||
let mut map = HashMap::new();
|
||||
for key in map.keys() {
|
||||
map.insert(key.clone(), process(key)); // ERROR: map borrowed twice
|
||||
}
|
||||
|
||||
// Clone to escape
|
||||
let keys: Vec<_> = map.keys().cloned().collect();
|
||||
for key in keys {
|
||||
map.insert(key.clone(), process(&key)); // OK
|
||||
}
|
||||
```
|
||||
|
||||
But ask: "Is there a better design?" Often, restructuring is better than cloning.
|
||||
|
||||
### The "Make It Own" Pattern
|
||||
|
||||
When lifetimes get complex, make the struct own its data:
|
||||
|
||||
```rust
|
||||
// Complex: struct with references
|
||||
struct Parser<'a> {
|
||||
input: &'a str,
|
||||
current: &'a str,
|
||||
}
|
||||
|
||||
// Simpler: struct owns data
|
||||
struct Parser {
|
||||
input: String,
|
||||
position: usize,
|
||||
}
|
||||
```
|
||||
|
||||
### The "Split the Borrow" Pattern
|
||||
|
||||
```rust
|
||||
struct Data {
|
||||
field_a: Vec<i32>,
|
||||
field_b: Vec<i32>,
|
||||
}
|
||||
|
||||
// Can't borrow self mutably twice
|
||||
fn process(&mut self) {
|
||||
// for a in &self.field_a {
|
||||
// self.field_b.push(*a); // ERROR
|
||||
// }
|
||||
|
||||
// Split the borrow
|
||||
let Data { field_a, field_b } = self;
|
||||
for a in field_a.iter() {
|
||||
field_b.push(*a); // OK: separate borrows
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Rust Way
|
||||
|
||||
### Embrace the Type System
|
||||
|
||||
```rust
|
||||
// Don't: stringly-typed
|
||||
fn connect(host: &str, port: &str) { ... }
|
||||
connect("8080", "localhost"); // oops, wrong order
|
||||
|
||||
// Do: strongly-typed
|
||||
struct Host(String);
|
||||
struct Port(u16);
|
||||
fn connect(host: Host, port: Port) { ... }
|
||||
// connect(Port(8080), Host("localhost".into())); // compile error!
|
||||
```
|
||||
|
||||
### Make Invalid States Unrepresentable
|
||||
|
||||
```rust
|
||||
// Don't: runtime checks
|
||||
struct Connection {
|
||||
socket: Option<Socket>,
|
||||
connected: bool,
|
||||
}
|
||||
|
||||
// Do: types enforce states
|
||||
enum Connection {
|
||||
Disconnected,
|
||||
Connected { socket: Socket },
|
||||
}
|
||||
```
|
||||
|
||||
### Let the Compiler Guide You
|
||||
|
||||
```rust
|
||||
// Start with what you want
|
||||
fn process(data: ???) -> ???
|
||||
|
||||
// Let compiler errors tell you:
|
||||
// - What types are needed
|
||||
// - What lifetimes are needed
|
||||
// - What bounds are needed
|
||||
|
||||
// The error messages are documentation!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary: The Rust Mental Model
|
||||
|
||||
1. **Values have owners** - exactly one at a time
|
||||
2. **Borrowing is lending** - temporary access, owner retains responsibility
|
||||
3. **Lifetimes are scopes** - compiler tracks validity
|
||||
4. **Types encode constraints** - use them to prevent bugs
|
||||
5. **The compiler is your friend** - work with it, not against it
|
||||
|
||||
When stuck:
|
||||
- Clone to make progress
|
||||
- Restructure to own instead of borrow
|
||||
- Ask: "What is the compiler trying to tell me?"
|
||||
160
skills/m15-anti-pattern/SKILL.md
Normal file
160
skills/m15-anti-pattern/SKILL.md
Normal file
@@ -0,0 +1,160 @@
|
||||
---
|
||||
name: m15-anti-pattern
|
||||
description: "Use when reviewing code for anti-patterns. Keywords: anti-pattern, common mistake, pitfall, code smell, bad practice, code review, is this an anti-pattern, better way to do this, common mistake to avoid, why is this bad, idiomatic way, beginner mistake, fighting borrow checker, clone everywhere, unwrap in production, should I refactor, 反模式, 常见错误, 代码异味, 最佳实践, 地道写法"
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Anti-Patterns
|
||||
|
||||
> **Layer 2: Design Choices**
|
||||
|
||||
## Core Question
|
||||
|
||||
**Is this pattern hiding a design problem?**
|
||||
|
||||
When reviewing code:
|
||||
- Is this solving the symptom or the cause?
|
||||
- Is there a more idiomatic approach?
|
||||
- Does this fight or flow with Rust?
|
||||
|
||||
---
|
||||
|
||||
## Anti-Pattern → Better Pattern
|
||||
|
||||
| Anti-Pattern | Why Bad | Better |
|
||||
|--------------|---------|--------|
|
||||
| `.clone()` everywhere | Hides ownership issues | Proper references or ownership |
|
||||
| `.unwrap()` in production | Runtime panics | `?`, `expect`, or handling |
|
||||
| `Rc` when single owner | Unnecessary overhead | Simple ownership |
|
||||
| `unsafe` for convenience | UB risk | Find safe pattern |
|
||||
| OOP via `Deref` | Misleading API | Composition, traits |
|
||||
| Giant match arms | Unmaintainable | Extract to methods |
|
||||
| `String` everywhere | Allocation waste | `&str`, `Cow<str>` |
|
||||
| Ignoring `#[must_use]` | Lost errors | Handle or `let _ =` |
|
||||
|
||||
---
|
||||
|
||||
## Thinking Prompt
|
||||
|
||||
When seeing suspicious code:
|
||||
|
||||
1. **Is this symptom or cause?**
|
||||
- Clone to avoid borrow? → Ownership design issue
|
||||
- Unwrap "because it won't fail"? → Unhandled case
|
||||
|
||||
2. **What would idiomatic code look like?**
|
||||
- References instead of clones
|
||||
- Iterators instead of index loops
|
||||
- Pattern matching instead of flags
|
||||
|
||||
3. **Does this fight Rust?**
|
||||
- Fighting borrow checker → restructure
|
||||
- Excessive unsafe → find safe pattern
|
||||
|
||||
---
|
||||
|
||||
## Trace Up ↑
|
||||
|
||||
To design understanding:
|
||||
|
||||
```
|
||||
"Why does my code have so many clones?"
|
||||
↑ Ask: Is the ownership model correct?
|
||||
↑ Check: m09-domain (data flow design)
|
||||
↑ Check: m01-ownership (reference patterns)
|
||||
```
|
||||
|
||||
| Anti-Pattern | Trace To | Question |
|
||||
|--------------|----------|----------|
|
||||
| Clone everywhere | m01-ownership | Who should own this data? |
|
||||
| Unwrap everywhere | m06-error-handling | What's the error strategy? |
|
||||
| Rc everywhere | m09-domain | Is ownership clear? |
|
||||
| Fighting lifetimes | m09-domain | Should data structure change? |
|
||||
|
||||
---
|
||||
|
||||
## Trace Down ↓
|
||||
|
||||
To implementation (Layer 1):
|
||||
|
||||
```
|
||||
"Replace clone with proper ownership"
|
||||
↓ m01-ownership: Reference patterns
|
||||
↓ m02-resource: Smart pointer if needed
|
||||
|
||||
"Replace unwrap with proper handling"
|
||||
↓ m06-error-handling: ? operator
|
||||
↓ m06-error-handling: expect with message
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Top 5 Beginner Mistakes
|
||||
|
||||
| Rank | Mistake | Fix |
|
||||
|------|---------|-----|
|
||||
| 1 | Clone to escape borrow checker | Use references |
|
||||
| 2 | Unwrap in production | Propagate with `?` |
|
||||
| 3 | String for everything | Use `&str` |
|
||||
| 4 | Index loops | Use iterators |
|
||||
| 5 | Fighting lifetimes | Restructure to own data |
|
||||
|
||||
## Code Smell → Refactoring
|
||||
|
||||
| Smell | Indicates | Refactoring |
|
||||
|-------|-----------|-------------|
|
||||
| Many `.clone()` | Ownership unclear | Clarify data flow |
|
||||
| Many `.unwrap()` | Error handling missing | Add proper handling |
|
||||
| Many `pub` fields | Encapsulation broken | Private + accessors |
|
||||
| Deep nesting | Complex logic | Extract methods |
|
||||
| Long functions | Multiple responsibilities | Split |
|
||||
| Giant enums | Missing abstraction | Trait + types |
|
||||
|
||||
---
|
||||
|
||||
## Common Error Patterns
|
||||
|
||||
| Error | Anti-Pattern Cause | Fix |
|
||||
|-------|-------------------|-----|
|
||||
| E0382 use after move | Cloning vs ownership | Proper references |
|
||||
| Panic in production | Unwrap everywhere | ?, matching |
|
||||
| Slow performance | String for all text | &str, Cow |
|
||||
| Borrow checker fights | Wrong structure | Restructure |
|
||||
| Memory bloat | Rc/Arc everywhere | Simple ownership |
|
||||
|
||||
---
|
||||
|
||||
## Deprecated → Better
|
||||
|
||||
| Deprecated | Better |
|
||||
|------------|--------|
|
||||
| Index-based loops | `.iter()`, `.enumerate()` |
|
||||
| `collect::<Vec<_>>()` then iterate | Chain iterators |
|
||||
| Manual unsafe cell | `Cell`, `RefCell` |
|
||||
| `mem::transmute` for casts | `as` or `TryFrom` |
|
||||
| Custom linked list | `Vec`, `VecDeque` |
|
||||
| `lazy_static!` | `std::sync::OnceLock` |
|
||||
|
||||
---
|
||||
|
||||
## Quick Review Checklist
|
||||
|
||||
- [ ] No `.clone()` without justification
|
||||
- [ ] No `.unwrap()` in library code
|
||||
- [ ] No `pub` fields with invariants
|
||||
- [ ] No index loops when iterator works
|
||||
- [ ] No `String` where `&str` suffices
|
||||
- [ ] No ignored `#[must_use]` warnings
|
||||
- [ ] No `unsafe` without SAFETY comment
|
||||
- [ ] No giant functions (>50 lines)
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Ownership patterns | m01-ownership |
|
||||
| Error handling | m06-error-handling |
|
||||
| Mental models | m14-mental-model |
|
||||
| Performance | m10-performance |
|
||||
421
skills/m15-anti-pattern/patterns/common-mistakes.md
Normal file
421
skills/m15-anti-pattern/patterns/common-mistakes.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# Common Rust Anti-Patterns & Mistakes
|
||||
|
||||
## Ownership Anti-Patterns
|
||||
|
||||
### 1. Clone Everything
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: clone to avoid borrow checker
|
||||
fn process(data: Vec<String>) {
|
||||
for item in data.clone() { // unnecessary clone
|
||||
println!("{}", item);
|
||||
}
|
||||
use_data(data);
|
||||
}
|
||||
|
||||
// BETTER: borrow when you don't need ownership
|
||||
fn process(data: Vec<String>) {
|
||||
for item in &data { // borrow instead
|
||||
println!("{}", item);
|
||||
}
|
||||
use_data(data);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Unnecessary Box
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: boxing everything
|
||||
fn get_value() -> Box<String> {
|
||||
Box::new(String::from("hello"))
|
||||
}
|
||||
|
||||
// BETTER: return value directly
|
||||
fn get_value() -> String {
|
||||
String::from("hello")
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Holding References Too Long
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: borrow prevents mutation
|
||||
let mut data = vec![1, 2, 3];
|
||||
let first = &data[0];
|
||||
data.push(4); // ERROR: data is borrowed
|
||||
println!("{}", first);
|
||||
|
||||
// BETTER: scope the borrow
|
||||
let mut data = vec![1, 2, 3];
|
||||
let first = data[0]; // copy the value
|
||||
data.push(4); // OK
|
||||
println!("{}", first);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling Anti-Patterns
|
||||
|
||||
### 4. Unwrap Everywhere
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: crashes on error
|
||||
fn process_file(path: &str) {
|
||||
let content = std::fs::read_to_string(path).unwrap();
|
||||
let config: Config = toml::from_str(&content).unwrap();
|
||||
}
|
||||
|
||||
// BETTER: propagate errors
|
||||
fn process_file(path: &str) -> Result<Config, Error> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let config: Config = toml::from_str(&content)?;
|
||||
Ok(config)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Ignoring Errors
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: silent failure
|
||||
let _ = file.write_all(data);
|
||||
|
||||
// BETTER: handle or propagate
|
||||
file.write_all(data)?;
|
||||
// or at minimum, log the error
|
||||
if let Err(e) = file.write_all(data) {
|
||||
eprintln!("Warning: failed to write: {}", e);
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Panic in Library Code
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: library panics
|
||||
pub fn parse(input: &str) -> Data {
|
||||
if input.is_empty() {
|
||||
panic!("input cannot be empty");
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
// BETTER: return Result
|
||||
pub fn parse(input: &str) -> Result<Data, ParseError> {
|
||||
if input.is_empty() {
|
||||
return Err(ParseError::EmptyInput);
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## String Anti-Patterns
|
||||
|
||||
### 7. String Instead of &str
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: forces allocation
|
||||
fn greet(name: String) {
|
||||
println!("Hello, {}", name);
|
||||
}
|
||||
|
||||
greet("world".to_string()); // allocation
|
||||
|
||||
// BETTER: accept &str
|
||||
fn greet(name: &str) {
|
||||
println!("Hello, {}", name);
|
||||
}
|
||||
|
||||
greet("world"); // no allocation
|
||||
```
|
||||
|
||||
### 8. Format for Simple Concatenation
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: format overhead
|
||||
let greeting = format!("{}{}", "Hello, ", name);
|
||||
|
||||
// BETTER for simple cases: push_str
|
||||
let mut greeting = String::from("Hello, ");
|
||||
greeting.push_str(name);
|
||||
|
||||
// Or use + for String + &str
|
||||
let greeting = String::from("Hello, ") + name;
|
||||
```
|
||||
|
||||
### 9. Repeated String Operations
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: O(n²) allocations
|
||||
let mut result = String::new();
|
||||
for word in words {
|
||||
result = result + word + " ";
|
||||
}
|
||||
|
||||
// BETTER: join
|
||||
let result = words.join(" ");
|
||||
|
||||
// Or with_capacity + push_str
|
||||
let mut result = String::with_capacity(total_len);
|
||||
for word in words {
|
||||
result.push_str(word);
|
||||
result.push(' ');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Collection Anti-Patterns
|
||||
|
||||
### 10. Index Instead of Iterator
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: bounds checking overhead
|
||||
for i in 0..vec.len() {
|
||||
process(vec[i]);
|
||||
}
|
||||
|
||||
// BETTER: iterator
|
||||
for item in &vec {
|
||||
process(item);
|
||||
}
|
||||
```
|
||||
|
||||
### 11. Collect Then Iterate
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: unnecessary allocation
|
||||
let filtered: Vec<_> = items.iter().filter(|x| x.valid).collect();
|
||||
for item in filtered {
|
||||
process(item);
|
||||
}
|
||||
|
||||
// BETTER: chain iterators
|
||||
for item in items.iter().filter(|x| x.valid) {
|
||||
process(item);
|
||||
}
|
||||
```
|
||||
|
||||
### 12. Wrong Collection Type
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: Vec for frequent membership checks
|
||||
let allowed: Vec<&str> = vec!["a", "b", "c"];
|
||||
if allowed.contains(&input) { ... } // O(n)
|
||||
|
||||
// BETTER: HashSet for membership
|
||||
use std::collections::HashSet;
|
||||
let allowed: HashSet<&str> = ["a", "b", "c"].into();
|
||||
if allowed.contains(input) { ... } // O(1)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Concurrency Anti-Patterns
|
||||
|
||||
### 13. Mutex for Read-Heavy Data
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: Mutex when mostly reading
|
||||
let data = Arc::new(Mutex::new(config));
|
||||
// All readers block each other
|
||||
|
||||
// BETTER: RwLock for read-heavy workloads
|
||||
let data = Arc::new(RwLock::new(config));
|
||||
// Multiple readers can proceed in parallel
|
||||
```
|
||||
|
||||
### 14. Holding Lock Across Await
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: lock held across await
|
||||
async fn bad() {
|
||||
let guard = mutex.lock().unwrap();
|
||||
some_async_op().await; // lock held!
|
||||
use(guard);
|
||||
}
|
||||
|
||||
// BETTER: scope the lock
|
||||
async fn good() {
|
||||
let value = {
|
||||
let guard = mutex.lock().unwrap();
|
||||
guard.clone()
|
||||
}; // lock released
|
||||
some_async_op().await;
|
||||
use(value);
|
||||
}
|
||||
```
|
||||
|
||||
### 15. Blocking in Async
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: blocking call in async
|
||||
async fn bad() {
|
||||
std::thread::sleep(Duration::from_secs(1)); // blocks executor!
|
||||
}
|
||||
|
||||
// BETTER: async sleep
|
||||
async fn good() {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
// For CPU work: spawn_blocking
|
||||
async fn compute() {
|
||||
tokio::task::spawn_blocking(|| heavy_work()).await
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Type System Anti-Patterns
|
||||
|
||||
### 16. Stringly Typed
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: strings for everything
|
||||
fn connect(host: &str, port: &str, timeout: &str) { ... }
|
||||
connect("8080", "localhost", "30"); // wrong order!
|
||||
|
||||
// BETTER: strong types
|
||||
struct Host(String);
|
||||
struct Port(u16);
|
||||
struct Timeout(Duration);
|
||||
|
||||
fn connect(host: Host, port: Port, timeout: Timeout) { ... }
|
||||
```
|
||||
|
||||
### 17. Boolean Parameters
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: what does true mean?
|
||||
fn fetch(url: &str, use_cache: bool, validate_ssl: bool) { ... }
|
||||
fetch("https://...", true, false); // unclear
|
||||
|
||||
// BETTER: builder or named parameters
|
||||
struct FetchOptions {
|
||||
use_cache: bool,
|
||||
validate_ssl: bool,
|
||||
}
|
||||
|
||||
fn fetch(url: &str, options: FetchOptions) { ... }
|
||||
fetch("https://...", FetchOptions {
|
||||
use_cache: true,
|
||||
validate_ssl: false,
|
||||
});
|
||||
```
|
||||
|
||||
### 18. Option<Option<T>>
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: nested Option
|
||||
fn find(id: u32) -> Option<Option<User>> { ... }
|
||||
// What does None vs Some(None) mean?
|
||||
|
||||
// BETTER: use Result or custom enum
|
||||
enum FindResult {
|
||||
Found(User),
|
||||
NotFound,
|
||||
Error(String),
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Design Anti-Patterns
|
||||
|
||||
### 19. Taking Ownership Unnecessarily
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: takes ownership but doesn't need it
|
||||
fn validate(config: Config) -> bool {
|
||||
config.timeout > 0 && config.retries >= 0
|
||||
}
|
||||
|
||||
// BETTER: borrow
|
||||
fn validate(config: &Config) -> bool {
|
||||
config.timeout > 0 && config.retries >= 0
|
||||
}
|
||||
```
|
||||
|
||||
### 20. Returning References to Temporaries
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: impossible lifetime
|
||||
fn get_default() -> &str {
|
||||
let s = String::from("default");
|
||||
&s // ERROR: s is dropped
|
||||
}
|
||||
|
||||
// BETTER: return owned
|
||||
fn get_default() -> String {
|
||||
String::from("default")
|
||||
}
|
||||
|
||||
// Or return static
|
||||
fn get_default() -> &'static str {
|
||||
"default"
|
||||
}
|
||||
```
|
||||
|
||||
### 21. Overly Generic Functions
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: complex generics for simple function
|
||||
fn process<T, U, V>(input: T) -> V
|
||||
where
|
||||
T: Into<U>,
|
||||
U: AsRef<str> + Clone,
|
||||
V: From<String>,
|
||||
{ ... }
|
||||
|
||||
// BETTER: concrete types if generics not needed
|
||||
fn process(input: &str) -> String { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Macro Anti-Patterns
|
||||
|
||||
### 22. Macro When Function Works
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: macro for simple operation
|
||||
macro_rules! add {
|
||||
($a:expr, $b:expr) => { $a + $b };
|
||||
}
|
||||
|
||||
// BETTER: just use a function
|
||||
fn add(a: i32, b: i32) -> i32 { a + b }
|
||||
```
|
||||
|
||||
### 23. Complex Macro Without Tests
|
||||
|
||||
```rust
|
||||
// ANTI-PATTERN: complex macro with no tests
|
||||
macro_rules! define_api {
|
||||
// ... 100 lines of macro code ...
|
||||
}
|
||||
|
||||
// BETTER: test macro outputs
|
||||
#[test]
|
||||
fn test_macro_expansion() {
|
||||
// Use cargo-expand or trybuild
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Anti-Pattern | Better Alternative |
|
||||
|--------------|-------------------|
|
||||
| Clone everywhere | Borrow when possible |
|
||||
| Unwrap everywhere | Propagate with `?` |
|
||||
| `String` parameters | `&str` parameters |
|
||||
| Index loops | Iterator loops |
|
||||
| Collect then process | Chain iterators |
|
||||
| Mutex for reads | RwLock for read-heavy |
|
||||
| Lock across await | Scope the lock |
|
||||
| Blocking in async | spawn_blocking |
|
||||
| Stringly typed | Strong types |
|
||||
| Boolean params | Builders or enums |
|
||||
352
skills/meta-cognition-parallel/SKILL.md
Normal file
352
skills/meta-cognition-parallel/SKILL.md
Normal file
@@ -0,0 +1,352 @@
|
||||
---
|
||||
name: meta-cognition-parallel
|
||||
description: "EXPERIMENTAL: Three-layer parallel meta-cognition analysis. Triggers on: /meta-parallel, 三层分析, parallel analysis, 并行元认知"
|
||||
argument-hint: "<rust_question>"
|
||||
---
|
||||
|
||||
# Meta-Cognition Parallel Analysis (Experimental)
|
||||
|
||||
> **Status:** Experimental | **Version:** 0.2.0 | **Last Updated:** 2025-01-27
|
||||
>
|
||||
> This skill tests parallel three-layer cognitive analysis.
|
||||
|
||||
## Concept
|
||||
|
||||
Instead of sequential analysis, this skill launches three parallel analyzers - one for each cognitive layer - then synthesizes their results.
|
||||
|
||||
```
|
||||
User Question
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ meta-cognition-parallel │
|
||||
│ (Coordinator) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│
|
||||
├─── Layer 1 ──► Language Mechanics ──► L1 Result
|
||||
│
|
||||
├─── Layer 2 ──► Design Choices ──► L2 Result
|
||||
│ ├── Parallel (Agent Mode)
|
||||
│ │ or Sequential (Inline)
|
||||
└─── Layer 3 ──► Domain Constraints ──► L3 Result
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Cross-Layer Synthesis │
|
||||
│ (In main context with all results) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
Domain-Correct Architectural Solution
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/meta-parallel <your Rust question>
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
/meta-parallel 我的交易系统报 E0382 错误,应该用 clone 吗?
|
||||
```
|
||||
|
||||
## Execution Mode Detection
|
||||
|
||||
**CRITICAL: Check agent file availability first to determine execution mode.**
|
||||
|
||||
Try to read layer analyzer files:
|
||||
- `../../agents/layer1-analyzer.md`
|
||||
- `../../agents/layer2-analyzer.md`
|
||||
- `../../agents/layer3-analyzer.md`
|
||||
|
||||
---
|
||||
|
||||
## Agent Mode (Plugin Install) - Parallel Execution
|
||||
|
||||
**When all layer analyzer files exist at `../../agents/`:**
|
||||
|
||||
### Step 1: Parse User Query
|
||||
|
||||
Extract from `$ARGUMENTS`:
|
||||
- The original question
|
||||
- Any code snippets
|
||||
- Domain hints (trading, web, embedded, etc.)
|
||||
|
||||
### Step 2: Launch Three Parallel Agents
|
||||
|
||||
**CRITICAL: Launch all three Tasks in a SINGLE message to enable parallel execution.**
|
||||
|
||||
```
|
||||
Read agent files, then launch in parallel:
|
||||
|
||||
Task(
|
||||
subagent_type: "general-purpose",
|
||||
run_in_background: true,
|
||||
prompt: <content of ../../agents/layer1-analyzer.md>
|
||||
+ "\n\n## User Query\n" + $ARGUMENTS
|
||||
)
|
||||
|
||||
Task(
|
||||
subagent_type: "general-purpose",
|
||||
run_in_background: true,
|
||||
prompt: <content of ../../agents/layer2-analyzer.md>
|
||||
+ "\n\n## User Query\n" + $ARGUMENTS
|
||||
)
|
||||
|
||||
Task(
|
||||
subagent_type: "general-purpose",
|
||||
run_in_background: true,
|
||||
prompt: <content of ../../agents/layer3-analyzer.md>
|
||||
+ "\n\n## User Query\n" + $ARGUMENTS
|
||||
)
|
||||
```
|
||||
|
||||
### Step 3: Collect Results
|
||||
|
||||
Wait for all three agents to complete. Each returns structured analysis.
|
||||
|
||||
### Step 4: Cross-Layer Synthesis
|
||||
|
||||
With all three results, perform synthesis per template below.
|
||||
|
||||
---
|
||||
|
||||
## Inline Mode (Skills-only Install) - Sequential Execution
|
||||
|
||||
**When layer analyzer files are NOT available, execute analysis directly:**
|
||||
|
||||
### Step 1: Parse User Query
|
||||
|
||||
Same as Agent Mode - extract question, code, and domain hints from `$ARGUMENTS`.
|
||||
|
||||
### Step 2: Execute Layer 1 - Language Mechanics
|
||||
|
||||
Analyze the Rust language mechanics involved:
|
||||
|
||||
```markdown
|
||||
## Layer 1: Language Mechanics
|
||||
|
||||
**Error/Pattern Identified:**
|
||||
- Error code: E0XXX (if applicable)
|
||||
- Pattern: ownership/borrowing/lifetime/etc.
|
||||
|
||||
**Root Cause:**
|
||||
[Explain why this error occurs in terms of Rust's ownership model]
|
||||
|
||||
**Language-Level Solutions:**
|
||||
1. [Solution 1]: description
|
||||
2. [Solution 2]: description
|
||||
|
||||
**Confidence:** HIGH | MEDIUM | LOW
|
||||
**Reasoning:** [Why this confidence level]
|
||||
```
|
||||
|
||||
**Focus areas:**
|
||||
- Ownership rules (move, copy, borrow)
|
||||
- Lifetime annotations
|
||||
- Borrowing rules (shared vs mutable)
|
||||
- Error codes and their meanings
|
||||
|
||||
### Step 3: Execute Layer 2 - Design Choices
|
||||
|
||||
Analyze the design patterns and trade-offs:
|
||||
|
||||
```markdown
|
||||
## Layer 2: Design Choices
|
||||
|
||||
**Design Pattern Context:**
|
||||
- Current approach: [What pattern is being used]
|
||||
- Problem: [Why it conflicts with Rust's rules]
|
||||
|
||||
**Design Alternatives:**
|
||||
| Pattern | Pros | Cons | When to Use |
|
||||
|---------|------|------|-------------|
|
||||
| Pattern A | ... | ... | ... |
|
||||
| Pattern B | ... | ... | ... |
|
||||
|
||||
**Recommended Pattern:**
|
||||
[Which pattern fits best and why]
|
||||
|
||||
**Confidence:** HIGH | MEDIUM | LOW
|
||||
**Reasoning:** [Why this confidence level]
|
||||
```
|
||||
|
||||
**Focus areas:**
|
||||
- Smart pointer choices (Box, Rc, Arc)
|
||||
- Interior mutability patterns (Cell, RefCell, Mutex)
|
||||
- Ownership transfer vs sharing
|
||||
- Cloning vs references
|
||||
|
||||
### Step 4: Execute Layer 3 - Domain Constraints
|
||||
|
||||
Analyze domain-specific requirements:
|
||||
|
||||
```markdown
|
||||
## Layer 3: Domain Constraints
|
||||
|
||||
**Domain Identified:** [trading/fintech | web | CLI | embedded | etc.]
|
||||
|
||||
**Domain-Specific Requirements:**
|
||||
- [ ] Performance: [requirements]
|
||||
- [ ] Safety: [requirements]
|
||||
- [ ] Concurrency: [requirements]
|
||||
- [ ] Auditability: [requirements]
|
||||
|
||||
**Domain Best Practices:**
|
||||
1. [Best practice 1]
|
||||
2. [Best practice 2]
|
||||
|
||||
**Constraints on Solution:**
|
||||
- MUST: [hard requirements]
|
||||
- SHOULD: [soft requirements]
|
||||
- AVOID: [anti-patterns for this domain]
|
||||
|
||||
**Confidence:** HIGH | MEDIUM | LOW
|
||||
**Reasoning:** [Why this confidence level]
|
||||
```
|
||||
|
||||
**Focus areas:**
|
||||
- Industry requirements (FinTech regulations, web scalability, etc.)
|
||||
- Performance constraints
|
||||
- Safety and correctness requirements
|
||||
- Common patterns in the domain
|
||||
|
||||
### Step 5: Cross-Layer Synthesis
|
||||
|
||||
Combine all three layers:
|
||||
|
||||
```markdown
|
||||
## Cross-Layer Synthesis
|
||||
|
||||
### Layer Results Summary
|
||||
|
||||
| Layer | Key Finding | Confidence |
|
||||
|-------|-------------|------------|
|
||||
| L1 (Mechanics) | [Summary] | [Level] |
|
||||
| L2 (Design) | [Summary] | [Level] |
|
||||
| L3 (Domain) | [Summary] | [Level] |
|
||||
|
||||
### Cross-Layer Reasoning
|
||||
|
||||
1. **L3 → L2:** [How domain constraints affect design choice]
|
||||
2. **L2 → L1:** [How design choice determines mechanism]
|
||||
3. **L1 ← L3:** [Direct domain impact on language features]
|
||||
|
||||
### Synthesized Recommendation
|
||||
|
||||
**Problem:** [Restated with full context]
|
||||
|
||||
**Solution:** [Domain-correct architectural solution]
|
||||
|
||||
**Rationale:**
|
||||
- Domain requires: [L3 constraint]
|
||||
- Design pattern: [L2 pattern]
|
||||
- Mechanism: [L1 implementation]
|
||||
|
||||
### Confidence Assessment
|
||||
|
||||
- **Overall:** HIGH | MEDIUM | LOW
|
||||
- **Limiting Factor:** [Which layer had lowest confidence]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Output Template
|
||||
|
||||
Both modes produce the same output format:
|
||||
|
||||
```markdown
|
||||
# Three-Layer Meta-Cognition Analysis
|
||||
|
||||
> Query: [User's question]
|
||||
|
||||
---
|
||||
|
||||
## Layer 1: Language Mechanics
|
||||
[L1 analysis result]
|
||||
|
||||
---
|
||||
|
||||
## Layer 2: Design Choices
|
||||
[L2 analysis result]
|
||||
|
||||
---
|
||||
|
||||
## Layer 3: Domain Constraints
|
||||
[L3 analysis result]
|
||||
|
||||
---
|
||||
|
||||
## Cross-Layer Synthesis
|
||||
|
||||
### Reasoning Chain
|
||||
```
|
||||
L3 Domain: [Constraint]
|
||||
↓ implies
|
||||
L2 Design: [Pattern]
|
||||
↓ implemented via
|
||||
L1 Mechanism: [Feature]
|
||||
```
|
||||
|
||||
### Final Recommendation
|
||||
|
||||
**Do:** [Recommended approach]
|
||||
|
||||
**Don't:** [What to avoid]
|
||||
|
||||
**Code Pattern:**
|
||||
```rust
|
||||
// Recommended implementation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Analysis performed by meta-cognition-parallel v0.2.0 (experimental)*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### Test 1: Trading System E0382
|
||||
```
|
||||
/meta-parallel 交易系统报 E0382,trade record 被 move 了
|
||||
```
|
||||
|
||||
Expected: L3 identifies FinTech constraints → L2 suggests shared immutable → L1 recommends Arc<T>
|
||||
|
||||
### Test 2: Web API Concurrency
|
||||
```
|
||||
/meta-parallel Web API 中多个 handler 需要共享数据库连接池
|
||||
```
|
||||
|
||||
Expected: L3 identifies Web constraints → L2 suggests connection pooling → L1 recommends Arc<Pool>
|
||||
|
||||
### Test 3: CLI Tool Config
|
||||
```
|
||||
/meta-parallel CLI 工具如何处理配置文件和命令行参数的优先级
|
||||
```
|
||||
|
||||
Expected: L3 identifies CLI constraints → L2 suggests config precedence pattern → L1 recommends builder pattern
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| Agent files not found | Skills-only install | Use inline mode (sequential) |
|
||||
| Agent timeout | Complex analysis | Wait longer or use inline mode |
|
||||
| Incomplete layer result | Agent issue | Fill in with inline analysis |
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Agent Mode:** Parallel execution, faster but requires plugin install
|
||||
- **Inline Mode:** Sequential execution, slower but works everywhere
|
||||
- Cross-layer synthesis quality depends on result structure
|
||||
- May have higher latency than simple single-layer analysis
|
||||
|
||||
## Feedback
|
||||
|
||||
This is experimental. Please report issues and suggestions to improve the three-layer analysis approach.
|
||||
206
skills/rust-call-graph/SKILL.md
Normal file
206
skills/rust-call-graph/SKILL.md
Normal file
@@ -0,0 +1,206 @@
|
||||
---
|
||||
name: rust-call-graph
|
||||
description: "Visualize Rust function call graphs using LSP. Triggers on: /call-graph, call hierarchy, who calls, what calls, 调用图, 调用关系, 谁调用了, 调用了谁"
|
||||
argument-hint: "<function_name> [--depth N] [--direction in|out|both]"
|
||||
allowed-tools: ["LSP", "Read", "Glob"]
|
||||
---
|
||||
|
||||
# Rust Call Graph
|
||||
|
||||
Visualize function call relationships using LSP call hierarchy.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/rust-call-graph <function_name> [--depth N] [--direction in|out|both]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--depth N`: How many levels to traverse (default: 3)
|
||||
- `--direction`: `in` (callers), `out` (callees), `both`
|
||||
|
||||
**Examples:**
|
||||
- `/rust-call-graph process_request` - Show both callers and callees
|
||||
- `/rust-call-graph handle_error --direction in` - Show only callers
|
||||
- `/rust-call-graph main --direction out --depth 5` - Deep callee analysis
|
||||
|
||||
## LSP Operations
|
||||
|
||||
### 1. Prepare Call Hierarchy
|
||||
|
||||
Get the call hierarchy item for a function.
|
||||
|
||||
```
|
||||
LSP(
|
||||
operation: "prepareCallHierarchy",
|
||||
filePath: "src/handler.rs",
|
||||
line: 45,
|
||||
character: 8
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Incoming Calls (Who calls this?)
|
||||
|
||||
```
|
||||
LSP(
|
||||
operation: "incomingCalls",
|
||||
filePath: "src/handler.rs",
|
||||
line: 45,
|
||||
character: 8
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Outgoing Calls (What does this call?)
|
||||
|
||||
```
|
||||
LSP(
|
||||
operation: "outgoingCalls",
|
||||
filePath: "src/handler.rs",
|
||||
line: 45,
|
||||
character: 8
|
||||
)
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
User: "Show call graph for process_request"
|
||||
│
|
||||
▼
|
||||
[1] Find function location
|
||||
LSP(workspaceSymbol) or Grep
|
||||
│
|
||||
▼
|
||||
[2] Prepare call hierarchy
|
||||
LSP(prepareCallHierarchy)
|
||||
│
|
||||
▼
|
||||
[3] Get incoming calls (callers)
|
||||
LSP(incomingCalls)
|
||||
│
|
||||
▼
|
||||
[4] Get outgoing calls (callees)
|
||||
LSP(outgoingCalls)
|
||||
│
|
||||
▼
|
||||
[5] Recursively expand to depth N
|
||||
│
|
||||
▼
|
||||
[6] Generate ASCII visualization
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
### Incoming Calls (Who calls this?)
|
||||
|
||||
```
|
||||
## Callers of `process_request`
|
||||
|
||||
main
|
||||
└── run_server
|
||||
└── handle_connection
|
||||
└── process_request ◄── YOU ARE HERE
|
||||
```
|
||||
|
||||
### Outgoing Calls (What does this call?)
|
||||
|
||||
```
|
||||
## Callees of `process_request`
|
||||
|
||||
process_request ◄── YOU ARE HERE
|
||||
├── parse_headers
|
||||
│ └── validate_header
|
||||
├── authenticate
|
||||
│ ├── check_token
|
||||
│ └── load_user
|
||||
├── execute_handler
|
||||
│ └── [dynamic dispatch]
|
||||
└── send_response
|
||||
└── serialize_body
|
||||
```
|
||||
|
||||
### Bidirectional (Both)
|
||||
|
||||
```
|
||||
## Call Graph for `process_request`
|
||||
|
||||
┌─────────────────┐
|
||||
│ main │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ run_server │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│handle_connection│
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────────────────┼────────────────────┐
|
||||
│ │ │
|
||||
┌───────▼───────┐ ┌───────▼───────┐ ┌───────▼───────┐
|
||||
│ parse_headers │ │ authenticate │ │send_response │
|
||||
└───────────────┘ └───────┬───────┘ └───────────────┘
|
||||
│
|
||||
┌───────┴───────┐
|
||||
│ │
|
||||
┌──────▼──────┐ ┌──────▼──────┐
|
||||
│ check_token │ │ load_user │
|
||||
└─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
## Analysis Insights
|
||||
|
||||
After generating the call graph, provide insights:
|
||||
|
||||
```
|
||||
## Analysis
|
||||
|
||||
**Entry Points:** main, test_process_request
|
||||
**Leaf Functions:** validate_header, serialize_body
|
||||
**Hot Path:** main → run_server → handle_connection → process_request
|
||||
**Complexity:** 12 functions, 3 levels deep
|
||||
|
||||
**Potential Issues:**
|
||||
- `authenticate` has high fan-out (4 callees)
|
||||
- `process_request` is called from 3 places (consider if this is intentional)
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
| User Says | Direction | Use Case |
|
||||
|-----------|-----------|----------|
|
||||
| "Who calls X?" | incoming | Impact analysis |
|
||||
| "What does X call?" | outgoing | Understanding implementation |
|
||||
| "Show call graph" | both | Full picture |
|
||||
| "Trace from main to X" | outgoing | Execution path |
|
||||
|
||||
## Visualization Options
|
||||
|
||||
| Style | Best For |
|
||||
|-------|----------|
|
||||
| Tree (default) | Simple hierarchies |
|
||||
| Box diagram | Complex relationships |
|
||||
| Flat list | Many connections |
|
||||
| Mermaid | Export to docs |
|
||||
|
||||
### Mermaid Export
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
main --> run_server
|
||||
run_server --> handle_connection
|
||||
handle_connection --> process_request
|
||||
process_request --> parse_headers
|
||||
process_request --> authenticate
|
||||
process_request --> send_response
|
||||
```
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Find definition | rust-code-navigator |
|
||||
| Project structure | rust-symbol-analyzer |
|
||||
| Trait implementations | rust-trait-explorer |
|
||||
| Safe refactoring | rust-refactor-helper |
|
||||
159
skills/rust-code-navigator/SKILL.md
Normal file
159
skills/rust-code-navigator/SKILL.md
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
name: rust-code-navigator
|
||||
description: "Navigate Rust code using LSP. Triggers on: /navigate, go to definition, find references, where is defined, 跳转定义, 查找引用, 定义在哪, 谁用了这个"
|
||||
argument-hint: "<symbol> [in file.rs:line]"
|
||||
allowed-tools: ["LSP", "Read", "Glob"]
|
||||
---
|
||||
|
||||
# Rust Code Navigator
|
||||
|
||||
Navigate large Rust codebases efficiently using Language Server Protocol.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/rust-code-navigator <symbol> [in file.rs:line]
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
- `/rust-code-navigator parse_config` - Find definition of parse_config
|
||||
- `/rust-code-navigator MyStruct in src/lib.rs:42` - Navigate from specific location
|
||||
|
||||
## LSP Operations
|
||||
|
||||
### 1. Go to Definition
|
||||
|
||||
Find where a symbol is defined.
|
||||
|
||||
```
|
||||
LSP(
|
||||
operation: "goToDefinition",
|
||||
filePath: "src/main.rs",
|
||||
line: 25,
|
||||
character: 10
|
||||
)
|
||||
```
|
||||
|
||||
**Use when:**
|
||||
- User asks "where is X defined?"
|
||||
- User wants to understand a type/function
|
||||
- Ctrl+click equivalent
|
||||
|
||||
### 2. Find References
|
||||
|
||||
Find all usages of a symbol.
|
||||
|
||||
```
|
||||
LSP(
|
||||
operation: "findReferences",
|
||||
filePath: "src/lib.rs",
|
||||
line: 15,
|
||||
character: 8
|
||||
)
|
||||
```
|
||||
|
||||
**Use when:**
|
||||
- User asks "who uses X?"
|
||||
- Before refactoring/renaming
|
||||
- Understanding impact of changes
|
||||
|
||||
### 3. Hover Information
|
||||
|
||||
Get type and documentation for a symbol.
|
||||
|
||||
```
|
||||
LSP(
|
||||
operation: "hover",
|
||||
filePath: "src/main.rs",
|
||||
line: 30,
|
||||
character: 15
|
||||
)
|
||||
```
|
||||
|
||||
**Use when:**
|
||||
- User asks "what type is X?"
|
||||
- User wants documentation
|
||||
- Quick type checking
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
User: "Where is the Config struct defined?"
|
||||
│
|
||||
▼
|
||||
[1] Search for "Config" in workspace
|
||||
LSP(operation: "workspaceSymbol", ...)
|
||||
│
|
||||
▼
|
||||
[2] If multiple results, ask user to clarify
|
||||
│
|
||||
▼
|
||||
[3] Go to definition
|
||||
LSP(operation: "goToDefinition", ...)
|
||||
│
|
||||
▼
|
||||
[4] Show file path and context
|
||||
Read surrounding code for context
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
### Definition Found
|
||||
|
||||
```
|
||||
## Config (struct)
|
||||
|
||||
**Defined in:** `src/config.rs:15`
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub name: String,
|
||||
pub port: u16,
|
||||
pub debug: bool,
|
||||
}
|
||||
```
|
||||
|
||||
**Documentation:** Configuration for the application server.
|
||||
```
|
||||
|
||||
### References Found
|
||||
|
||||
```
|
||||
## References to `Config` (5 found)
|
||||
|
||||
| Location | Context |
|
||||
|----------|---------|
|
||||
| src/main.rs:10 | `let config = Config::load()?;` |
|
||||
| src/server.rs:25 | `fn new(config: Config) -> Self` |
|
||||
| src/server.rs:42 | `self.config.port` |
|
||||
| src/tests.rs:15 | `Config::default()` |
|
||||
| src/cli.rs:8 | `config: Option<Config>` |
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
| User Says | LSP Operation |
|
||||
|-----------|---------------|
|
||||
| "Where is X defined?" | goToDefinition |
|
||||
| "Who uses X?" | findReferences |
|
||||
| "What type is X?" | hover |
|
||||
| "Find all structs" | workspaceSymbol |
|
||||
| "What's in this file?" | documentSymbol |
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| "No LSP server" | rust-analyzer not running | Suggest: `rustup component add rust-analyzer` |
|
||||
| "Symbol not found" | Typo or not in scope | Search with workspaceSymbol first |
|
||||
| "Multiple definitions" | Generics or macros | Show all and let user choose |
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Call relationships | rust-call-graph |
|
||||
| Project structure | rust-symbol-analyzer |
|
||||
| Trait implementations | rust-trait-explorer |
|
||||
| Safe refactoring | rust-refactor-helper |
|
||||
233
skills/rust-daily/SKILL.md
Normal file
233
skills/rust-daily/SKILL.md
Normal file
@@ -0,0 +1,233 @@
|
||||
---
|
||||
name: rust-daily
|
||||
description: |
|
||||
CRITICAL: Use for Rust news and daily/weekly/monthly reports. Triggers on:
|
||||
rust news, rust daily, rust weekly, TWIR, rust blog,
|
||||
Rust 日报, Rust 周报, Rust 新闻, Rust 动态
|
||||
argument-hint: "[today|week|month]"
|
||||
context: fork
|
||||
agent: Explore
|
||||
---
|
||||
|
||||
# Rust Daily Report
|
||||
|
||||
> **Version:** 2.1.0 | **Last Updated:** 2025-01-27
|
||||
|
||||
Fetch Rust community updates, filtered by time range.
|
||||
|
||||
## Data Sources
|
||||
|
||||
| Category | Sources |
|
||||
|----------|---------|
|
||||
| Ecosystem | Reddit r/rust, This Week in Rust |
|
||||
| Official | blog.rust-lang.org, Inside Rust |
|
||||
| Foundation | rustfoundation.org (news, blog, events) |
|
||||
|
||||
## Parameters
|
||||
|
||||
- `time_range`: day | week | month (default: week)
|
||||
- `category`: all | ecosystem | official | foundation
|
||||
|
||||
## Execution Mode Detection
|
||||
|
||||
**CRITICAL: Check agent file availability first to determine execution mode.**
|
||||
|
||||
Try to read: `../../agents/rust-daily-reporter.md`
|
||||
|
||||
---
|
||||
|
||||
## Agent Mode (Plugin Install)
|
||||
|
||||
**When `../../agents/rust-daily-reporter.md` exists:**
|
||||
|
||||
### Workflow
|
||||
|
||||
```
|
||||
1. Read: ../../agents/rust-daily-reporter.md
|
||||
2. Task(subagent_type: "general-purpose", run_in_background: false, prompt: <agent content>)
|
||||
3. Wait for result
|
||||
4. Format and present to user
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Inline Mode (Skills-only Install)
|
||||
|
||||
**When agent file is NOT available, execute each source directly:**
|
||||
|
||||
### 1. Reddit r/rust
|
||||
|
||||
```bash
|
||||
# Using agent-browser CLI
|
||||
agent-browser open "https://www.reddit.com/r/rust/hot/"
|
||||
agent-browser get text ".Post" --limit 10
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
**Or with WebFetch fallback:**
|
||||
```
|
||||
WebFetch("https://www.reddit.com/r/rust/hot/", "Extract top 10 posts with scores and titles")
|
||||
```
|
||||
|
||||
**Parse output into:**
|
||||
| Score | Title | Link |
|
||||
|-------|-------|------|
|
||||
|
||||
### 2. This Week in Rust
|
||||
|
||||
```bash
|
||||
# Check actionbook first
|
||||
mcp__actionbook__search_actions("this week in rust")
|
||||
mcp__actionbook__get_action_by_id(<action_id>)
|
||||
|
||||
# Then fetch
|
||||
agent-browser open "https://this-week-in-rust.org/"
|
||||
agent-browser get text "<selector_from_actionbook>"
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
**Parse output into:**
|
||||
- Issue #{number} ({date}): highlights
|
||||
|
||||
### 3. Rust Blog (Official)
|
||||
|
||||
```bash
|
||||
agent-browser open "https://blog.rust-lang.org/"
|
||||
agent-browser get text "article" --limit 5
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
**Or with WebFetch fallback:**
|
||||
```
|
||||
WebFetch("https://blog.rust-lang.org/", "Extract latest 5 blog posts with dates and titles")
|
||||
```
|
||||
|
||||
**Parse output into:**
|
||||
| Date | Title | Summary |
|
||||
|------|-------|---------|
|
||||
|
||||
### 4. Inside Rust
|
||||
|
||||
```bash
|
||||
agent-browser open "https://blog.rust-lang.org/inside-rust/"
|
||||
agent-browser get text "article" --limit 3
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
**Or with WebFetch fallback:**
|
||||
```
|
||||
WebFetch("https://blog.rust-lang.org/inside-rust/", "Extract latest 3 posts with dates and titles")
|
||||
```
|
||||
|
||||
### 5. Rust Foundation
|
||||
|
||||
```bash
|
||||
# News
|
||||
agent-browser open "https://rustfoundation.org/media/category/news/"
|
||||
agent-browser get text "article" --limit 3
|
||||
agent-browser close
|
||||
|
||||
# Blog
|
||||
agent-browser open "https://rustfoundation.org/media/category/blog/"
|
||||
agent-browser get text "article" --limit 3
|
||||
agent-browser close
|
||||
|
||||
# Events
|
||||
agent-browser open "https://rustfoundation.org/events/"
|
||||
agent-browser get text "article" --limit 3
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
### Time Filtering
|
||||
|
||||
After fetching all sources, filter by time range:
|
||||
|
||||
| Range | Filter |
|
||||
|-------|--------|
|
||||
| day | Last 24 hours |
|
||||
| week | Last 7 days |
|
||||
| month | Last 30 days |
|
||||
|
||||
### Combining Results
|
||||
|
||||
After fetching all sources, combine into the output format below.
|
||||
|
||||
---
|
||||
|
||||
## Tool Chain Priority
|
||||
|
||||
Both modes use the same tool chain order:
|
||||
|
||||
1. **actionbook MCP** - Check for cached/pre-fetched content first
|
||||
```
|
||||
mcp__actionbook__search_actions("rust news {date}")
|
||||
mcp__actionbook__search_actions("this week in rust")
|
||||
mcp__actionbook__search_actions("rust blog")
|
||||
```
|
||||
|
||||
2. **agent-browser CLI** - For dynamic web content
|
||||
```bash
|
||||
agent-browser open "<url>"
|
||||
agent-browser get text "<selector>"
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
3. **WebFetch** - Fallback if agent-browser unavailable
|
||||
|
||||
| Source | Primary Tool | Fallback |
|
||||
|--------|--------------|----------|
|
||||
| Reddit | agent-browser | WebFetch |
|
||||
| TWIR | actionbook → agent-browser | WebFetch |
|
||||
| Rust Blog | actionbook → WebFetch | - |
|
||||
| Foundation | actionbook → WebFetch | - |
|
||||
|
||||
**DO NOT use:**
|
||||
- Chrome MCP directly
|
||||
- WebSearch for fetching news pages
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
# Rust {Weekly|Daily|Monthly} Report
|
||||
|
||||
**Time Range:** {start} - {end}
|
||||
|
||||
## Ecosystem
|
||||
|
||||
### Reddit r/rust
|
||||
| Score | Title | Link |
|
||||
|-------|-------|------|
|
||||
| {score} | {title} | [link]({url}) |
|
||||
|
||||
### This Week in Rust
|
||||
- Issue #{number} ({date}): highlights
|
||||
|
||||
## Official
|
||||
| Date | Title | Summary |
|
||||
|------|-------|---------|
|
||||
| {date} | {title} | {summary} |
|
||||
|
||||
## Foundation
|
||||
| Date | Title | Summary |
|
||||
|------|-------|---------|
|
||||
| {date} | {title} | {summary} |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation
|
||||
|
||||
- Each source should have at least 1 result, otherwise mark "No updates"
|
||||
- On fetch failure, retry with alternative tool
|
||||
- Report reason if all tools fail for a source
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| Agent file not found | Skills-only install | Use inline mode |
|
||||
| agent-browser unavailable | CLI not installed | Use WebFetch |
|
||||
| Site timeout | Network issues | Retry once, then skip source |
|
||||
| Empty results | Selector mismatch | Report and use fallback |
|
||||
114
skills/rust-deps-visualizer/SKILL.md
Normal file
114
skills/rust-deps-visualizer/SKILL.md
Normal file
@@ -0,0 +1,114 @@
|
||||
---
|
||||
name: rust-deps-visualizer
|
||||
description: "Visualize Rust project dependencies as ASCII art. Triggers on: /deps-viz, dependency graph, show dependencies, visualize deps, 依赖图, 依赖可视化, 显示依赖"
|
||||
argument-hint: "[--depth N] [--features]"
|
||||
allowed-tools: ["Bash", "Read", "Glob"]
|
||||
---
|
||||
|
||||
# Rust Dependencies Visualizer
|
||||
|
||||
Generate ASCII art visualizations of your Rust project's dependency tree.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/rust-deps-visualizer [--depth N] [--features]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--depth N`: Limit tree depth (default: 3)
|
||||
- `--features`: Show feature flags
|
||||
|
||||
## Output Format
|
||||
|
||||
### Simple Tree (Default)
|
||||
|
||||
```
|
||||
my-project v0.1.0
|
||||
├── tokio v1.49.0
|
||||
│ ├── pin-project-lite v0.2.x
|
||||
│ └── bytes v1.x
|
||||
├── serde v1.0.x
|
||||
│ └── serde_derive v1.0.x
|
||||
└── anyhow v1.x
|
||||
```
|
||||
|
||||
### Feature-Aware Tree
|
||||
|
||||
```
|
||||
my-project v0.1.0
|
||||
├── tokio v1.49.0 [rt, rt-multi-thread, macros, fs, io-util]
|
||||
│ ├── pin-project-lite v0.2.x
|
||||
│ └── bytes v1.x
|
||||
├── serde v1.0.x [derive]
|
||||
│ └── serde_derive v1.0.x (proc-macro)
|
||||
└── anyhow v1.x [std]
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
**Step 1:** Parse Cargo.toml for direct dependencies
|
||||
|
||||
```bash
|
||||
cargo metadata --format-version=1 --no-deps 2>/dev/null
|
||||
```
|
||||
|
||||
**Step 2:** Get full dependency tree
|
||||
|
||||
```bash
|
||||
cargo tree --depth=${DEPTH:-3} ${FEATURES:+--features} 2>/dev/null
|
||||
```
|
||||
|
||||
**Step 3:** Format as ASCII art tree
|
||||
|
||||
Use these box-drawing characters:
|
||||
- `├──` for middle items
|
||||
- `└──` for last items
|
||||
- `│ ` for continuation lines
|
||||
|
||||
## Visual Enhancements
|
||||
|
||||
### Dependency Categories
|
||||
|
||||
```
|
||||
my-project v0.1.0
|
||||
│
|
||||
├─[Runtime]─────────────────────
|
||||
│ ├── tokio v1.49.0
|
||||
│ └── async-trait v0.1.x
|
||||
│
|
||||
├─[Serialization]───────────────
|
||||
│ ├── serde v1.0.x
|
||||
│ └── serde_json v1.x
|
||||
│
|
||||
└─[Development]─────────────────
|
||||
├── criterion v0.5.x
|
||||
└── proptest v1.x
|
||||
```
|
||||
|
||||
### Size Visualization (Optional)
|
||||
|
||||
```
|
||||
my-project v0.1.0
|
||||
├── tokio v1.49.0 ████████████ 2.1 MB
|
||||
├── serde v1.0.x ███████ 1.2 MB
|
||||
├── regex v1.x █████ 890 KB
|
||||
└── anyhow v1.x ██ 120 KB
|
||||
─────────────────
|
||||
Total: 4.3 MB
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Check for Cargo.toml in current directory
|
||||
2. Run `cargo tree` with specified options
|
||||
3. Parse output and generate ASCII visualization
|
||||
4. Optionally categorize by purpose (runtime, dev, build)
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Crate selection advice | m11-ecosystem |
|
||||
| Workspace management | m11-ecosystem |
|
||||
| Feature flag decisions | m11-ecosystem |
|
||||
310
skills/rust-learner/SKILL.md
Normal file
310
skills/rust-learner/SKILL.md
Normal file
@@ -0,0 +1,310 @@
|
||||
---
|
||||
name: rust-learner
|
||||
description: "Use when asking about Rust versions or crate info. Keywords: latest version, what's new, changelog, Rust 1.x, Rust release, stable, nightly, crate info, crates.io, lib.rs, docs.rs, API documentation, crate features, dependencies, which crate, what version, Rust edition, edition 2021, edition 2024, cargo add, cargo update, 最新版本, 版本号, 稳定版, 最新, 哪个版本, crate 信息, 文档, 依赖, Rust 版本, 新特性, 有什么特性"
|
||||
allowed-tools: ["Task", "Read", "Glob", "mcp__actionbook__*", "Bash"]
|
||||
---
|
||||
|
||||
# Rust Learner
|
||||
|
||||
> **Version:** 2.1.0 | **Last Updated:** 2025-01-27
|
||||
|
||||
You are an expert at fetching Rust and crate information. Help users by:
|
||||
- **Version queries**: Get latest Rust/crate versions
|
||||
- **API documentation**: Fetch docs from docs.rs
|
||||
- **Changelog**: Get Rust version features from releases.rs
|
||||
|
||||
**Primary skill for fetching Rust/crate information.**
|
||||
|
||||
## Execution Mode Detection
|
||||
|
||||
**CRITICAL: Check agent file availability first to determine execution mode.**
|
||||
|
||||
Try to read the agent file for your query type. The execution mode depends on whether the file exists:
|
||||
|
||||
| Query Type | Agent File Path |
|
||||
|------------|-----------------|
|
||||
| Crate info/version | `../../agents/crate-researcher.md` |
|
||||
| Rust version features | `../../agents/rust-changelog.md` |
|
||||
| Std library docs | `../../agents/std-docs-researcher.md` |
|
||||
| Third-party crate docs | `../../agents/docs-researcher.md` |
|
||||
| Clippy lints | `../../agents/clippy-researcher.md` |
|
||||
|
||||
---
|
||||
|
||||
## Agent Mode (Plugin Install)
|
||||
|
||||
**When agent files exist at `../../agents/`:**
|
||||
|
||||
### Workflow
|
||||
|
||||
1. Read the appropriate agent file (relative to this skill)
|
||||
2. Launch Task with `run_in_background: true`
|
||||
3. Continue with other work or wait for completion
|
||||
4. Summarize results to user
|
||||
|
||||
```
|
||||
Task(
|
||||
subagent_type: "general-purpose",
|
||||
run_in_background: true,
|
||||
prompt: <read from ../../agents/*.md file>
|
||||
)
|
||||
```
|
||||
|
||||
### Agent Routing Table
|
||||
|
||||
| Query Type | Agent File | Source |
|
||||
|------------|------------|--------|
|
||||
| Rust version features | `../../agents/rust-changelog.md` | releases.rs |
|
||||
| Crate info/version | `../../agents/crate-researcher.md` | lib.rs, crates.io |
|
||||
| **Std library docs** (Send, Sync, Arc, etc.) | `../../agents/std-docs-researcher.md` | doc.rust-lang.org |
|
||||
| Third-party crate docs (tokio, serde, etc.) | `../../agents/docs-researcher.md` | docs.rs |
|
||||
| Clippy lints | `../../agents/clippy-researcher.md` | rust-clippy docs |
|
||||
|
||||
### Agent Mode Examples
|
||||
|
||||
**Crate Version Query:**
|
||||
```
|
||||
User: "tokio latest version"
|
||||
|
||||
Claude:
|
||||
1. Read ../../agents/crate-researcher.md
|
||||
2. Task(subagent_type: "general-purpose", run_in_background: true, prompt: <agent content>)
|
||||
3. Wait for agent
|
||||
4. Summarize results
|
||||
```
|
||||
|
||||
**Rust Changelog Query:**
|
||||
```
|
||||
User: "What's new in Rust 1.85?"
|
||||
|
||||
Claude:
|
||||
1. Read ../../agents/rust-changelog.md
|
||||
2. Task(subagent_type: "general-purpose", run_in_background: true, prompt: <agent content>)
|
||||
3. Wait for agent
|
||||
4. Summarize features
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Inline Mode (Skills-only Install)
|
||||
|
||||
**When agent files are NOT available, execute directly using these steps:**
|
||||
|
||||
### Crate Info Query
|
||||
|
||||
```
|
||||
1. actionbook: mcp__actionbook__search_actions("lib.rs crate info")
|
||||
2. Get action details: mcp__actionbook__get_action_by_id(<action_id>)
|
||||
3. agent-browser CLI (or WebFetch fallback):
|
||||
- open "https://lib.rs/crates/{crate_name}"
|
||||
- get text using selector from actionbook
|
||||
- close
|
||||
4. Parse and format output
|
||||
```
|
||||
|
||||
**Output Format:**
|
||||
```markdown
|
||||
## {Crate Name}
|
||||
|
||||
**Version:** {latest}
|
||||
**Description:** {description}
|
||||
|
||||
**Features:**
|
||||
- `feature1`: description
|
||||
|
||||
**Links:**
|
||||
- [docs.rs](https://docs.rs/{crate}) | [crates.io](https://crates.io/crates/{crate}) | [repo]({repo_url})
|
||||
```
|
||||
|
||||
### Rust Version Query
|
||||
|
||||
```
|
||||
1. actionbook: mcp__actionbook__search_actions("releases.rs rust changelog")
|
||||
2. Get action details for selectors
|
||||
3. agent-browser CLI (or WebFetch fallback):
|
||||
- open "https://releases.rs/docs/1.{version}.0/"
|
||||
- get text using selector from actionbook
|
||||
- close
|
||||
4. Parse and format output
|
||||
```
|
||||
|
||||
**Output Format:**
|
||||
```markdown
|
||||
## Rust 1.{version}
|
||||
|
||||
**Release Date:** {date}
|
||||
|
||||
### Language Features
|
||||
- Feature 1: description
|
||||
- Feature 2: description
|
||||
|
||||
### Library Changes
|
||||
- std::module: new API
|
||||
|
||||
### Stabilized APIs
|
||||
- `api_name`: description
|
||||
```
|
||||
|
||||
### Std Library Docs (std::*, Send, Sync, Arc, etc.)
|
||||
|
||||
```
|
||||
1. Construct URL: "https://doc.rust-lang.org/std/{path}/"
|
||||
- Traits: std/{module}/trait.{Name}.html
|
||||
- Structs: std/{module}/struct.{Name}.html
|
||||
- Modules: std/{module}/index.html
|
||||
2. agent-browser CLI (or WebFetch fallback):
|
||||
- open <url>
|
||||
- get text "main .docblock"
|
||||
- close
|
||||
3. Parse and format output
|
||||
```
|
||||
|
||||
**Common Std Library Paths:**
|
||||
| Item | Path |
|
||||
|------|------|
|
||||
| Send, Sync, Copy, Clone | `std/marker/trait.{Name}.html` |
|
||||
| Arc, Mutex, RwLock | `std/sync/struct.{Name}.html` |
|
||||
| Rc, Weak | `std/rc/struct.{Name}.html` |
|
||||
| RefCell, Cell | `std/cell/struct.{Name}.html` |
|
||||
| Box | `std/boxed/struct.Box.html` |
|
||||
| Vec | `std/vec/struct.Vec.html` |
|
||||
| String | `std/string/struct.String.html` |
|
||||
|
||||
**Output Format:**
|
||||
```markdown
|
||||
## std::{path}::{Name}
|
||||
|
||||
**Signature:**
|
||||
```rust
|
||||
{signature}
|
||||
```
|
||||
|
||||
**Description:**
|
||||
{description}
|
||||
|
||||
**Examples:**
|
||||
```rust
|
||||
{example_code}
|
||||
```
|
||||
```
|
||||
|
||||
### Third-Party Crate Docs (tokio, serde, etc.)
|
||||
|
||||
```
|
||||
1. Construct URL: "https://docs.rs/{crate}/latest/{crate}/{path}"
|
||||
2. agent-browser CLI (or WebFetch fallback):
|
||||
- open <url>
|
||||
- get text ".docblock"
|
||||
- close
|
||||
3. Parse and format output
|
||||
```
|
||||
|
||||
**Output Format:**
|
||||
```markdown
|
||||
## {crate}::{path}
|
||||
|
||||
**Signature:**
|
||||
```rust
|
||||
{signature}
|
||||
```
|
||||
|
||||
**Description:**
|
||||
{description}
|
||||
|
||||
**Examples:**
|
||||
```rust
|
||||
{example_code}
|
||||
```
|
||||
```
|
||||
|
||||
### Clippy Lints
|
||||
|
||||
```
|
||||
1. agent-browser CLI (or WebFetch fallback):
|
||||
- open "https://rust-lang.github.io/rust-clippy/stable/"
|
||||
- search for lint name in page
|
||||
- get text ".lint-doc" for matching lint
|
||||
- close
|
||||
2. Parse and format output
|
||||
```
|
||||
|
||||
**Output Format:**
|
||||
```markdown
|
||||
## Clippy Lint: {lint_name}
|
||||
|
||||
**Level:** {warn|deny|allow}
|
||||
**Category:** {category}
|
||||
|
||||
**Description:**
|
||||
{what_it_checks}
|
||||
|
||||
**Example (Bad):**
|
||||
```rust
|
||||
{bad_code}
|
||||
```
|
||||
|
||||
**Example (Good):**
|
||||
```rust
|
||||
{good_code}
|
||||
```
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool Chain Priority
|
||||
|
||||
Both modes use the same tool chain order:
|
||||
|
||||
1. **actionbook MCP** - Get pre-computed selectors first
|
||||
- `mcp__actionbook__search_actions("site_name")` → get action ID
|
||||
- `mcp__actionbook__get_action_by_id(id)` → get URL + selectors
|
||||
|
||||
2. **agent-browser CLI** - Primary execution tool
|
||||
```bash
|
||||
agent-browser open <url>
|
||||
agent-browser get text <selector_from_actionbook>
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
3. **WebFetch** - Last resort only if agent-browser unavailable
|
||||
|
||||
### Fallback Principle (CRITICAL)
|
||||
|
||||
```
|
||||
actionbook → agent-browser → WebFetch (only if agent-browser unavailable)
|
||||
```
|
||||
|
||||
**DO NOT:**
|
||||
- Skip agent-browser because it's slower
|
||||
- Use WebFetch as primary when agent-browser is available
|
||||
- Block on WebFetch without trying agent-browser first
|
||||
|
||||
---
|
||||
|
||||
## Deprecated Patterns
|
||||
|
||||
| Deprecated | Use Instead | Reason |
|
||||
|------------|-------------|--------|
|
||||
| WebSearch for crate info | Task + agent or inline mode | Structured data |
|
||||
| Direct WebFetch | actionbook + agent-browser | Pre-computed selectors |
|
||||
| Guessing version numbers | Always fetch from source | Prevents misinformation |
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| Agent file not found | Skills-only install | Use inline mode |
|
||||
| actionbook unavailable | MCP not configured | Fall back to WebFetch |
|
||||
| agent-browser not found | CLI not installed | Fall back to WebFetch |
|
||||
| Agent timeout | Site slow/down | Retry or inform user |
|
||||
| Empty results | Selector mismatch | Report and use WebFetch fallback |
|
||||
|
||||
## Proactive Triggering
|
||||
|
||||
This skill triggers AUTOMATICALLY when:
|
||||
- Any Rust crate name mentioned (tokio, serde, axum, sqlx, etc.)
|
||||
- Questions about "latest", "new", "version", "changelog"
|
||||
- API documentation requests
|
||||
- Dependency/feature questions
|
||||
|
||||
**DO NOT use WebSearch for Rust crate info. Use agents or inline mode instead.**
|
||||
273
skills/rust-refactor-helper/SKILL.md
Normal file
273
skills/rust-refactor-helper/SKILL.md
Normal file
@@ -0,0 +1,273 @@
|
||||
---
|
||||
name: rust-refactor-helper
|
||||
description: "Safe Rust refactoring with LSP analysis. Triggers on: /refactor, rename symbol, move function, extract, 重构, 重命名, 提取函数, 安全重构"
|
||||
argument-hint: "<action> <target> [--dry-run]"
|
||||
allowed-tools: ["LSP", "Read", "Glob", "Grep", "Edit"]
|
||||
---
|
||||
|
||||
# Rust Refactor Helper
|
||||
|
||||
Perform safe refactoring with comprehensive impact analysis.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/rust-refactor-helper <action> <target> [--dry-run]
|
||||
```
|
||||
|
||||
**Actions:**
|
||||
- `rename <old> <new>` - Rename symbol
|
||||
- `extract-fn <selection>` - Extract to function
|
||||
- `inline <fn>` - Inline function
|
||||
- `move <symbol> <dest>` - Move to module
|
||||
|
||||
**Examples:**
|
||||
- `/rust-refactor-helper rename parse_config load_config`
|
||||
- `/rust-refactor-helper extract-fn src/main.rs:20-35`
|
||||
- `/rust-refactor-helper move UserService src/services/`
|
||||
|
||||
## LSP Operations Used
|
||||
|
||||
### Pre-Refactor Analysis
|
||||
|
||||
```
|
||||
# Find all references before renaming
|
||||
LSP(
|
||||
operation: "findReferences",
|
||||
filePath: "src/lib.rs",
|
||||
line: 25,
|
||||
character: 8
|
||||
)
|
||||
|
||||
# Get symbol info
|
||||
LSP(
|
||||
operation: "hover",
|
||||
filePath: "src/lib.rs",
|
||||
line: 25,
|
||||
character: 8
|
||||
)
|
||||
|
||||
# Check call hierarchy for move operations
|
||||
LSP(
|
||||
operation: "incomingCalls",
|
||||
filePath: "src/lib.rs",
|
||||
line: 25,
|
||||
character: 8
|
||||
)
|
||||
```
|
||||
|
||||
## Refactoring Workflows
|
||||
|
||||
### 1. Rename Symbol
|
||||
|
||||
```
|
||||
User: "Rename parse_config to load_config"
|
||||
│
|
||||
▼
|
||||
[1] Find symbol definition
|
||||
LSP(goToDefinition)
|
||||
│
|
||||
▼
|
||||
[2] Find ALL references
|
||||
LSP(findReferences)
|
||||
│
|
||||
▼
|
||||
[3] Categorize by file
|
||||
│
|
||||
▼
|
||||
[4] Check for conflicts
|
||||
- Is 'load_config' already used?
|
||||
- Are there macro-generated uses?
|
||||
│
|
||||
▼
|
||||
[5] Show impact analysis (--dry-run)
|
||||
│
|
||||
▼
|
||||
[6] Apply changes with Edit tool
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```
|
||||
## Rename: parse_config → load_config
|
||||
|
||||
### Impact Analysis
|
||||
|
||||
**Definition:** src/config.rs:25
|
||||
**References found:** 8
|
||||
|
||||
| File | Line | Context | Change |
|
||||
|------|------|---------|--------|
|
||||
| src/config.rs | 25 | `pub fn parse_config(` | Definition |
|
||||
| src/config.rs | 45 | `parse_config(path)?` | Call |
|
||||
| src/main.rs | 12 | `config::parse_config` | Import |
|
||||
| src/main.rs | 30 | `let cfg = parse_config(` | Call |
|
||||
| src/lib.rs | 8 | `pub use config::parse_config` | Re-export |
|
||||
| tests/config_test.rs | 15 | `parse_config("test.toml")` | Test |
|
||||
| tests/config_test.rs | 25 | `parse_config("")` | Test |
|
||||
| docs/api.md | 42 | `parse_config` | Documentation |
|
||||
|
||||
### Potential Issues
|
||||
|
||||
⚠️ **Documentation reference:** docs/api.md:42 may need manual update
|
||||
⚠️ **Re-export:** src/lib.rs:8 - public API change
|
||||
|
||||
### Proceed?
|
||||
- [x] --dry-run (preview only)
|
||||
- [ ] Apply changes
|
||||
```
|
||||
|
||||
### 2. Extract Function
|
||||
|
||||
```
|
||||
User: "Extract lines 20-35 in main.rs to a function"
|
||||
│
|
||||
▼
|
||||
[1] Read the selected code block
|
||||
│
|
||||
▼
|
||||
[2] Analyze variables
|
||||
- Which are inputs? (used but not defined in block)
|
||||
- Which are outputs? (defined and used after block)
|
||||
- Which are local? (defined and used only in block)
|
||||
│
|
||||
▼
|
||||
[3] Determine function signature
|
||||
│
|
||||
▼
|
||||
[4] Check for early returns, loops, etc.
|
||||
│
|
||||
▼
|
||||
[5] Generate extracted function
|
||||
│
|
||||
▼
|
||||
[6] Replace original code with call
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```
|
||||
## Extract Function: src/main.rs:20-35
|
||||
|
||||
### Selected Code
|
||||
```rust
|
||||
let file = File::open(&path)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let config: Config = toml::from_str(&contents)?;
|
||||
validate_config(&config)?;
|
||||
```
|
||||
|
||||
### Analysis
|
||||
|
||||
**Inputs:** path: &Path
|
||||
**Outputs:** config: Config
|
||||
**Side Effects:** File I/O, may return error
|
||||
|
||||
### Extracted Function
|
||||
|
||||
```rust
|
||||
fn load_and_validate_config(path: &Path) -> Result<Config> {
|
||||
let file = File::open(path)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let config: Config = toml::from_str(&contents)?;
|
||||
validate_config(&config)?;
|
||||
Ok(config)
|
||||
}
|
||||
```
|
||||
|
||||
### Replacement
|
||||
|
||||
```rust
|
||||
let config = load_and_validate_config(&path)?;
|
||||
```
|
||||
```
|
||||
|
||||
### 3. Move Symbol
|
||||
|
||||
```
|
||||
User: "Move UserService to src/services/"
|
||||
│
|
||||
▼
|
||||
[1] Find symbol and all its dependencies
|
||||
│
|
||||
▼
|
||||
[2] Find all references (callers)
|
||||
LSP(findReferences)
|
||||
│
|
||||
▼
|
||||
[3] Analyze import changes needed
|
||||
│
|
||||
▼
|
||||
[4] Check for circular dependencies
|
||||
│
|
||||
▼
|
||||
[5] Generate move plan
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```
|
||||
## Move: UserService → src/services/user.rs
|
||||
|
||||
### Current Location
|
||||
src/handlers/auth.rs:50-120
|
||||
|
||||
### Dependencies (will be moved together)
|
||||
- struct UserService (50-80)
|
||||
- impl UserService (82-120)
|
||||
- const DEFAULT_TIMEOUT (48)
|
||||
|
||||
### Import Changes Required
|
||||
|
||||
| File | Current | New |
|
||||
|------|---------|-----|
|
||||
| src/main.rs | `use handlers::auth::UserService` | `use services::user::UserService` |
|
||||
| src/handlers/api.rs | `use super::auth::UserService` | `use crate::services::user::UserService` |
|
||||
| tests/auth_test.rs | `use crate::handlers::auth::UserService` | `use crate::services::user::UserService` |
|
||||
|
||||
### New File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── services/
|
||||
│ ├── mod.rs (NEW - add `pub mod user;`)
|
||||
│ └── user.rs (NEW - UserService moved here)
|
||||
├── handlers/
|
||||
│ └── auth.rs (UserService removed)
|
||||
```
|
||||
|
||||
### Circular Dependency Check
|
||||
✅ No circular dependencies detected
|
||||
```
|
||||
|
||||
## Safety Checks
|
||||
|
||||
| Check | Purpose |
|
||||
|-------|---------|
|
||||
| Reference completeness | Ensure all uses are found |
|
||||
| Name conflicts | Detect existing symbols with same name |
|
||||
| Visibility changes | Warn if pub/private scope changes |
|
||||
| Macro-generated code | Warn about code in macros |
|
||||
| Documentation | Flag doc comments mentioning symbol |
|
||||
| Test coverage | Show affected tests |
|
||||
|
||||
## Dry Run Mode
|
||||
|
||||
Always use `--dry-run` first to preview changes:
|
||||
|
||||
```
|
||||
/rust-refactor-helper rename old_name new_name --dry-run
|
||||
```
|
||||
|
||||
This shows all changes without applying them.
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Navigate to symbol | rust-code-navigator |
|
||||
| Understand call flow | rust-call-graph |
|
||||
| Project structure | rust-symbol-analyzer |
|
||||
| Trait implementations | rust-trait-explorer |
|
||||
239
skills/rust-router/SKILL.md
Normal file
239
skills/rust-router/SKILL.md
Normal file
@@ -0,0 +1,239 @@
|
||||
---
|
||||
name: rust-router
|
||||
description: "CRITICAL: Use for ALL Rust questions including errors, design, and coding.
|
||||
HIGHEST PRIORITY for: 比较, 对比, compare, vs, versus, 区别, difference, 最佳实践, best practice,
|
||||
tokio vs, async-std vs, 比较 tokio, 比较 async,
|
||||
Triggers on: Rust, cargo, rustc, crate, Cargo.toml,
|
||||
意图分析, 问题分析, 语义分析, analyze intent, question analysis,
|
||||
compile error, borrow error, lifetime error, ownership error, type error, trait error,
|
||||
value moved, cannot borrow, does not live long enough, mismatched types, not satisfied,
|
||||
E0382, E0597, E0277, E0308, E0499, E0502, E0596,
|
||||
async, await, Send, Sync, tokio, concurrency, error handling,
|
||||
编译错误, compile error, 所有权, ownership, 借用, borrow, 生命周期, lifetime, 类型错误, type error,
|
||||
异步, async, 并发, concurrency, 错误处理, error handling,
|
||||
问题, problem, question, 怎么用, how to use, 如何, how to, 为什么, why,
|
||||
什么是, what is, 帮我写, help me write, 实现, implement, 解释, explain"
|
||||
globs: ["**/Cargo.toml", "**/*.rs"]
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
# Rust Question Router
|
||||
|
||||
> **Version:** 2.0.0 | **Last Updated:** 2025-01-22
|
||||
>
|
||||
> **v2.0:** Context optimized - detailed examples moved to sub-files
|
||||
|
||||
## Meta-Cognition Framework
|
||||
|
||||
### Core Principle
|
||||
|
||||
**Don't answer directly. Trace through the cognitive layers first.**
|
||||
|
||||
```
|
||||
Layer 3: Domain Constraints (WHY)
|
||||
├── Business rules, regulatory requirements
|
||||
├── domain-fintech, domain-web, domain-cli, etc.
|
||||
└── "Why is it designed this way?"
|
||||
|
||||
Layer 2: Design Choices (WHAT)
|
||||
├── Architecture patterns, DDD concepts
|
||||
├── m09-m15 skills
|
||||
└── "What pattern should I use?"
|
||||
|
||||
Layer 1: Language Mechanics (HOW)
|
||||
├── Ownership, borrowing, lifetimes, traits
|
||||
├── m01-m07 skills
|
||||
└── "How do I implement this in Rust?"
|
||||
```
|
||||
|
||||
### Routing by Entry Point
|
||||
|
||||
| User Signal | Entry Layer | Direction | First Skill |
|
||||
|-------------|-------------|-----------|-------------|
|
||||
| E0xxx error | Layer 1 | Trace UP ↑ | m01-m07 |
|
||||
| Compile error | Layer 1 | Trace UP ↑ | Error table below |
|
||||
| "How to design..." | Layer 2 | Check L3, then DOWN ↓ | m09-domain |
|
||||
| "Building [domain] app" | Layer 3 | Trace DOWN ↓ | domain-* |
|
||||
| "Best practice..." | Layer 2 | Both directions | m09-m15 |
|
||||
| Performance issue | Layer 1 → 2 | UP then DOWN | m10-performance |
|
||||
|
||||
### CRITICAL: Dual-Skill Loading
|
||||
|
||||
**When domain keywords are present, you MUST load BOTH skills:**
|
||||
|
||||
| Domain Keywords | L1 Skill | L3 Skill |
|
||||
|-----------------|----------|----------|
|
||||
| Web API, HTTP, axum, handler | m07-concurrency | **domain-web** |
|
||||
| 交易, 支付, trading, payment | m01-ownership | **domain-fintech** |
|
||||
| CLI, terminal, clap | m07-concurrency | **domain-cli** |
|
||||
| kubernetes, grpc, microservice | m07-concurrency | **domain-cloud-native** |
|
||||
| embedded, no_std, MCU | m02-resource | **domain-embedded** |
|
||||
|
||||
---
|
||||
|
||||
## INSTRUCTIONS FOR CLAUDE
|
||||
|
||||
### CRITICAL: Negotiation Protocol Trigger
|
||||
|
||||
**BEFORE answering, check if negotiation is required:**
|
||||
|
||||
| Query Contains | Action |
|
||||
|----------------|--------|
|
||||
| "比较", "对比", "compare", "vs", "versus" | **MUST use negotiation** |
|
||||
| "最佳实践", "best practice" | **MUST use negotiation** |
|
||||
| Domain + error (e.g., "交易系统 E0382") | **MUST use negotiation** |
|
||||
| Ambiguous scope (e.g., "tokio 性能") | **SHOULD use negotiation** |
|
||||
|
||||
**When negotiation is required, include:**
|
||||
|
||||
```markdown
|
||||
## Negotiation Analysis
|
||||
|
||||
**Query Type:** [Comparative | Cross-domain | Synthesis | Ambiguous]
|
||||
**Negotiation:** Enabled
|
||||
|
||||
### Source: [Agent/Skill Name]
|
||||
**Confidence:** HIGH | MEDIUM | LOW | UNCERTAIN
|
||||
**Gaps:** [What's missing]
|
||||
|
||||
## Synthesized Answer
|
||||
[Answer]
|
||||
|
||||
**Overall Confidence:** [Level]
|
||||
**Disclosed Gaps:** [Gaps user should know]
|
||||
```
|
||||
|
||||
> **详细协议见:** `patterns/negotiation.md`
|
||||
|
||||
---
|
||||
|
||||
### Default Project Settings
|
||||
|
||||
When creating new Rust projects or Cargo.toml files, ALWAYS use:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
edition = "2024" # ALWAYS use latest stable edition
|
||||
rust-version = "1.85"
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
all = "warn"
|
||||
pedantic = "warn"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layer 1 Skills (Language Mechanics)
|
||||
|
||||
| Pattern | Route To |
|
||||
|---------|----------|
|
||||
| move, borrow, lifetime, E0382, E0597 | m01-ownership |
|
||||
| Box, Rc, Arc, RefCell, Cell | m02-resource |
|
||||
| mut, interior mutability, E0499, E0502, E0596 | m03-mutability |
|
||||
| generic, trait, inline, monomorphization | m04-zero-cost |
|
||||
| type state, phantom, newtype | m05-type-driven |
|
||||
| Result, Error, panic, ?, anyhow, thiserror | m06-error-handling |
|
||||
| Send, Sync, thread, async, channel | m07-concurrency |
|
||||
| unsafe, FFI, extern, raw pointer, transmute | **unsafe-checker** |
|
||||
|
||||
## Layer 2 Skills (Design Choices)
|
||||
|
||||
| Pattern | Route To |
|
||||
|---------|----------|
|
||||
| domain model, business logic | m09-domain |
|
||||
| performance, optimization, benchmark | m10-performance |
|
||||
| integration, interop, bindings | m11-ecosystem |
|
||||
| resource lifecycle, RAII, Drop | m12-lifecycle |
|
||||
| domain error, recovery strategy | m13-domain-error |
|
||||
| mental model, how to think | m14-mental-model |
|
||||
| anti-pattern, common mistake, pitfall | m15-anti-pattern |
|
||||
|
||||
## Layer 3 Skills (Domain Constraints)
|
||||
|
||||
| Domain Keywords | Route To |
|
||||
|-----------------|----------|
|
||||
| fintech, trading, decimal, currency | domain-fintech |
|
||||
| ml, tensor, model, inference | domain-ml |
|
||||
| kubernetes, docker, grpc, microservice | domain-cloud-native |
|
||||
| embedded, sensor, mqtt, iot | domain-iot |
|
||||
| web server, HTTP, REST, axum, actix | domain-web |
|
||||
| CLI, command line, clap, terminal | domain-cli |
|
||||
| no_std, microcontroller, firmware | domain-embedded |
|
||||
|
||||
---
|
||||
|
||||
## Error Code Routing
|
||||
|
||||
| Error Code | Route To | Common Cause |
|
||||
|------------|----------|--------------|
|
||||
| E0382 | m01-ownership | Use of moved value |
|
||||
| E0597 | m01-ownership | Lifetime too short |
|
||||
| E0506 | m01-ownership | Cannot assign to borrowed |
|
||||
| E0507 | m01-ownership | Cannot move out of borrowed |
|
||||
| E0515 | m01-ownership | Return local reference |
|
||||
| E0716 | m01-ownership | Temporary value dropped |
|
||||
| E0106 | m01-ownership | Missing lifetime specifier |
|
||||
| E0596 | m03-mutability | Cannot borrow as mutable |
|
||||
| E0499 | m03-mutability | Multiple mutable borrows |
|
||||
| E0502 | m03-mutability | Borrow conflict |
|
||||
| E0277 | m04/m07 | Trait bound not satisfied |
|
||||
| E0308 | m04-zero-cost | Type mismatch |
|
||||
| E0599 | m04-zero-cost | No method found |
|
||||
| E0038 | m04-zero-cost | Trait not object-safe |
|
||||
| E0433 | m11-ecosystem | Cannot find crate/module |
|
||||
|
||||
---
|
||||
|
||||
## Functional Routing Table
|
||||
|
||||
| Pattern | Route To | Action |
|
||||
|---------|----------|--------|
|
||||
| latest version, what's new | **rust-learner** | Use agents |
|
||||
| API, docs, documentation | **docs-researcher** | Use agent |
|
||||
| code style, naming, clippy | **coding-guidelines** | Read skill |
|
||||
| unsafe code, FFI | **unsafe-checker** | Read skill |
|
||||
| code review | **os-checker** | See `integrations/os-checker.md` |
|
||||
|
||||
---
|
||||
|
||||
## Priority Order
|
||||
|
||||
1. **Identify cognitive layer** (L1/L2/L3)
|
||||
2. **Load entry skill** (m0x/m1x/domain)
|
||||
3. **Trace through layers** (UP or DOWN)
|
||||
4. **Cross-reference skills** as indicated in "Trace" sections
|
||||
5. **Answer with reasoning chain**
|
||||
|
||||
### Keyword Conflict Resolution
|
||||
|
||||
| Keyword | Resolution |
|
||||
|---------|------------|
|
||||
| `unsafe` | **unsafe-checker** (more specific than m11) |
|
||||
| `error` | **m06** for general, **m13** for domain-specific |
|
||||
| `RAII` | **m12** for design, **m01** for implementation |
|
||||
| `crate` | **rust-learner** for version, **m11** for integration |
|
||||
| `tokio` | **tokio-*** for API, **m07** for concepts |
|
||||
|
||||
**Priority Hierarchy:**
|
||||
|
||||
```
|
||||
1. Error codes (E0xxx) → Direct lookup, highest priority
|
||||
2. Negotiation triggers (compare, vs, best practice) → Enable negotiation
|
||||
3. Domain keywords + error → Load BOTH domain + error skills
|
||||
4. Specific crate keywords → Route to crate-specific skill if exists
|
||||
5. General concept keywords → Route to meta-question skill
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sub-Files Reference
|
||||
|
||||
| File | Content |
|
||||
|------|---------|
|
||||
| `patterns/negotiation.md` | Negotiation protocol details |
|
||||
| `examples/workflow.md` | Workflow examples |
|
||||
| `integrations/os-checker.md` | OS-Checker integration |
|
||||
85
skills/rust-router/examples/workflow.md
Normal file
85
skills/rust-router/examples/workflow.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Workflow Examples
|
||||
|
||||
> rust-router 的工作流程示例
|
||||
|
||||
## Example 1: Error Code with Domain Context
|
||||
|
||||
```
|
||||
User: "Why am I getting E0382 in my trading system?"
|
||||
|
||||
Analysis:
|
||||
1. Entry: Layer 1 (E0382 = ownership/move error)
|
||||
2. Load: m01-ownership skill
|
||||
3. Context: "trading system" → domain-fintech
|
||||
|
||||
Trace UP ↑:
|
||||
- E0382 in trading context
|
||||
- Check domain-fintech: "immutable audit records"
|
||||
- Finding: Trading data should be shared, not moved
|
||||
|
||||
Response:
|
||||
"E0382 indicates a value was moved when still needed.
|
||||
In a trading system (domain-fintech), transaction records
|
||||
should be immutable and shareable for audit purposes.
|
||||
|
||||
Instead of cloning, consider:
|
||||
- Arc<TradeRecord> for shared immutable access
|
||||
- This aligns with financial audit requirements
|
||||
|
||||
See: m01-ownership (Trace Up section),
|
||||
domain-fintech (Audit Requirements)"
|
||||
```
|
||||
|
||||
## Example 2: Design Question
|
||||
|
||||
```
|
||||
User: "How should I handle user authentication?"
|
||||
|
||||
1. Entry: Layer 2 (design question)
|
||||
2. Trace UP to Layer 3: domain-web constraints
|
||||
3. Load: domain-web skill (security, stateless HTTP)
|
||||
4. Trace DOWN: m06-error-handling, m07-concurrency
|
||||
5. Answer: JWT with proper error types, async handlers
|
||||
```
|
||||
|
||||
## Example 3: Comparative Query
|
||||
|
||||
```
|
||||
User: "Compare tokio and async-std"
|
||||
|
||||
1. Detect: "compare" → Enable negotiation
|
||||
2. Load both runtime knowledge sources
|
||||
3. Assess confidence for each
|
||||
4. Synthesize with disclosed gaps
|
||||
5. Answer: Structured comparison table
|
||||
```
|
||||
|
||||
## Example 4: Multi-Layer Trace
|
||||
|
||||
```
|
||||
User: "My web API reports Rc cannot be sent between threads"
|
||||
|
||||
1. Entry: Layer 1 (Send/Sync error)
|
||||
2. Load: m07-concurrency
|
||||
3. Detect: "web API" → domain-web
|
||||
4. Dual-skill loading:
|
||||
- m07: Explain Send/Sync bounds
|
||||
- domain-web: Web state management patterns
|
||||
5. Answer: Use Arc instead of Rc, or move to thread-local
|
||||
```
|
||||
|
||||
## Example 5: Intent Analysis Request
|
||||
|
||||
```
|
||||
User: "Analyze this question: How do I share state in actix-web?"
|
||||
|
||||
Analysis Steps:
|
||||
1. Extract Keywords: share, state, actix-web
|
||||
2. Identify Entry Layer: Layer 1 (sharing = concurrency) + Layer 3 (actix-web = web)
|
||||
3. Map to Skills: m07-concurrency, domain-web
|
||||
4. Report:
|
||||
- Layer 1: Concurrency (state sharing mechanisms)
|
||||
- Layer 3: Web domain (HTTP handler patterns)
|
||||
- Suggested trace: L1 → L3
|
||||
5. Invoke: m07-concurrency first, then domain-web
|
||||
```
|
||||
56
skills/rust-router/integrations/os-checker.md
Normal file
56
skills/rust-router/integrations/os-checker.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# OS-Checker Integration
|
||||
|
||||
> 代码审查和安全审计工具集成
|
||||
|
||||
## Available Commands
|
||||
|
||||
| Use Case | Command | Tools |
|
||||
|----------|---------|-------|
|
||||
| Daily check | `/rust-review` | clippy |
|
||||
| Security audit | `/audit security` | cargo audit, geiger |
|
||||
| Unsafe audit | `/audit safety` | miri, rudra |
|
||||
| Concurrency audit | `/audit concurrency` | lockbud |
|
||||
| Full audit | `/audit full` | all os-checker tools |
|
||||
|
||||
## When to Suggest OS-Checker
|
||||
|
||||
| User Intent | Suggest |
|
||||
|-------------|---------|
|
||||
| Code review request | `/rust-review` |
|
||||
| Security concerns | `/audit security` |
|
||||
| Unsafe code review | `/audit safety` |
|
||||
| Deadlock/race concerns | `/audit concurrency` |
|
||||
| Pre-release check | `/audit full` |
|
||||
|
||||
## Tool Descriptions
|
||||
|
||||
### clippy
|
||||
Standard Rust linter for code style and common mistakes.
|
||||
|
||||
### cargo audit
|
||||
Security vulnerability scanner for dependencies.
|
||||
|
||||
### geiger
|
||||
Counts unsafe code usage in dependencies.
|
||||
|
||||
### miri
|
||||
Interprets MIR to detect undefined behavior.
|
||||
|
||||
### rudra
|
||||
Memory safety bug detector.
|
||||
|
||||
### lockbud
|
||||
Deadlock and concurrency bug detector.
|
||||
|
||||
## Integration Flow
|
||||
|
||||
```
|
||||
User: "Review my unsafe code"
|
||||
│
|
||||
▼
|
||||
Router detects: unsafe + review
|
||||
│
|
||||
├── Load: unsafe-checker skill (for manual review)
|
||||
│
|
||||
└── Suggest: `/audit safety` (for automated check)
|
||||
```
|
||||
154
skills/rust-router/patterns/negotiation.md
Normal file
154
skills/rust-router/patterns/negotiation.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# Negotiation Protocol
|
||||
|
||||
> 比较查询和跨领域问题的处理协议
|
||||
|
||||
## When to Enable Negotiation
|
||||
|
||||
For complex queries requiring structured agent responses, enable negotiation mode.
|
||||
|
||||
| Query Pattern | Enable Negotiation | Reason |
|
||||
|---------------|-------------------|--------|
|
||||
| Single error code lookup | No | Direct answer |
|
||||
| Single crate version | No | Direct lookup |
|
||||
| "Compare X and Y" | **Yes** | Multi-faceted |
|
||||
| Domain + error | **Yes** | Cross-layer context |
|
||||
| "Best practices for..." | **Yes** | Requires synthesis |
|
||||
| Ambiguous scope | **Yes** | Needs clarification |
|
||||
| Multi-crate question | **Yes** | Multiple sources |
|
||||
|
||||
## Negotiation Decision Flow
|
||||
|
||||
```
|
||||
Query Received
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ Is query single-lookup? │
|
||||
│ (version, error code, def) │
|
||||
└─────────────────────────────┘
|
||||
│
|
||||
├── Yes → Direct dispatch (no negotiation)
|
||||
│
|
||||
▼ No
|
||||
┌─────────────────────────────┐
|
||||
│ Does query require: │
|
||||
│ - Comparison? │
|
||||
│ - Cross-domain context? │
|
||||
│ - Synthesis/aggregation? │
|
||||
│ - Multiple sources? │
|
||||
└─────────────────────────────┘
|
||||
│
|
||||
├── Yes → Dispatch with negotiation: true
|
||||
│
|
||||
▼ No
|
||||
┌─────────────────────────────┐
|
||||
│ Is scope ambiguous? │
|
||||
└─────────────────────────────┘
|
||||
│
|
||||
├── Yes → Dispatch with negotiation: true
|
||||
│
|
||||
▼ No
|
||||
└── Direct dispatch (no negotiation)
|
||||
```
|
||||
|
||||
## Negotiation Dispatch
|
||||
|
||||
When dispatching with negotiation:
|
||||
|
||||
```
|
||||
1. Set `negotiation: true`
|
||||
2. Include original query context
|
||||
3. Expect structured response:
|
||||
- Findings
|
||||
- Confidence (HIGH/MEDIUM/LOW/UNCERTAIN)
|
||||
- Gaps identified
|
||||
- Context questions (if any)
|
||||
4. Evaluate response against original intent
|
||||
```
|
||||
|
||||
## Orchestrator Evaluation
|
||||
|
||||
After receiving negotiation response:
|
||||
|
||||
| Confidence | Intent Coverage | Action |
|
||||
|------------|-----------------|--------|
|
||||
| HIGH | Complete | Synthesize answer |
|
||||
| HIGH | Partial | May need supplementary query |
|
||||
| MEDIUM | Complete | Accept with disclosed gaps |
|
||||
| MEDIUM | Partial | Refine with context |
|
||||
| LOW | Any | Refine or try alternative |
|
||||
| UNCERTAIN | Any | Try alternative or escalate |
|
||||
|
||||
## Refinement Loop
|
||||
|
||||
If response insufficient:
|
||||
|
||||
```
|
||||
Round 1: Initial query
|
||||
│
|
||||
▼ (LOW confidence or gaps block intent)
|
||||
Round 2: Refined query with:
|
||||
- Answers to agent's context questions
|
||||
- Narrowed scope
|
||||
│
|
||||
▼ (still insufficient)
|
||||
Round 3: Final attempt with:
|
||||
- Alternative agent/source
|
||||
- Maximum context provided
|
||||
│
|
||||
▼ (still insufficient)
|
||||
Synthesize best-effort answer with disclosed gaps
|
||||
```
|
||||
|
||||
## Integration with 3-Strike Rule
|
||||
|
||||
Negotiation follows the 3-Strike escalation:
|
||||
|
||||
```
|
||||
Strike 1: Initial query returns LOW confidence
|
||||
→ Refine with more context
|
||||
|
||||
Strike 2: Refined query still LOW
|
||||
→ Try alternative agent/source
|
||||
|
||||
Strike 3: Still insufficient
|
||||
→ Synthesize best-effort answer
|
||||
→ Report gaps to user explicitly
|
||||
```
|
||||
|
||||
See `_meta/error-protocol.md` for full escalation rules.
|
||||
|
||||
## Negotiation Routing Examples
|
||||
|
||||
**Example 1: No Negotiation Needed**
|
||||
```
|
||||
Query: "What is tokio's latest version?"
|
||||
Analysis: Single lookup
|
||||
Action: Direct dispatch to crate-researcher
|
||||
```
|
||||
|
||||
**Example 2: Negotiation Required**
|
||||
```
|
||||
Query: "Compare tokio and async-std for a web server"
|
||||
Analysis: Comparative + domain context
|
||||
Action: Dispatch with negotiation: true
|
||||
Expected: Structured responses from both runtime lookups
|
||||
Evaluation: Check if web-server specific data found
|
||||
```
|
||||
|
||||
**Example 3: Cross-Domain Negotiation**
|
||||
```
|
||||
Query: "E0382 in my trading system"
|
||||
Analysis: Error code + domain context
|
||||
Action:
|
||||
- Dispatch m01-ownership (standard - error is defined)
|
||||
- Dispatch domain-fintech (negotiation: true - domain context)
|
||||
Synthesis: Combine error explanation with domain-appropriate fix
|
||||
```
|
||||
|
||||
## Related Documents
|
||||
|
||||
- `_meta/negotiation-protocol.md` - Full protocol specification
|
||||
- `_meta/negotiation-templates.md` - Response templates
|
||||
- `_meta/error-protocol.md` - 3-Strike escalation
|
||||
- `agents/_negotiation/response-format.md` - Agent response format
|
||||
265
skills/rust-skill-creator/SKILL.md
Normal file
265
skills/rust-skill-creator/SKILL.md
Normal file
@@ -0,0 +1,265 @@
|
||||
---
|
||||
name: rust-skill-creator
|
||||
description: "Use when creating skills for Rust crates or std library documentation. Keywords: create rust skill, create crate skill, create std skill, 创建 rust skill, 创建 crate skill, 创建 std skill, 动态 rust skill, 动态 crate skill, skill for tokio, skill for serde, skill for axum, generate rust skill, rust 技能, crate 技能, 从文档创建skill, from docs create skill"
|
||||
argument-hint: "<crate_name|std::module>"
|
||||
context: fork
|
||||
agent: general-purpose
|
||||
---
|
||||
|
||||
# Rust Skill Creator
|
||||
|
||||
> **Version:** 2.1.0 | **Last Updated:** 2025-01-27
|
||||
>
|
||||
> Create dynamic skills for Rust crates and std library documentation.
|
||||
|
||||
## When to Use
|
||||
|
||||
This skill handles requests to create skills for:
|
||||
- Third-party crates (tokio, serde, axum, etc.)
|
||||
- Rust standard library (std::sync, std::marker, etc.)
|
||||
- Any Rust documentation URL
|
||||
|
||||
## Execution Mode Detection
|
||||
|
||||
**CRITICAL: Check if related commands/skills are available.**
|
||||
|
||||
This skill relies on:
|
||||
- `/create-llms-for-skills` command
|
||||
- `/create-skills-via-llms` command
|
||||
|
||||
---
|
||||
|
||||
## Agent Mode (Plugin Install)
|
||||
|
||||
**When the commands above are available (full plugin installation):**
|
||||
|
||||
### Workflow
|
||||
|
||||
#### 1. Identify the Target
|
||||
|
||||
| User Request | Target Type | URL Pattern |
|
||||
|--------------|-------------|-------------|
|
||||
| "create tokio skill" | Third-party crate | `docs.rs/tokio/latest/tokio/` |
|
||||
| "create Send trait skill" | Std library | `doc.rust-lang.org/std/marker/trait.Send.html` |
|
||||
| "create skill from URL" + URL | Custom URL | User-provided URL |
|
||||
|
||||
#### 2. Execute the Command
|
||||
|
||||
Use the `/create-llms-for-skills` command:
|
||||
|
||||
```
|
||||
/create-llms-for-skills <url> [requirements]
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# For third-party crate
|
||||
/create-llms-for-skills https://docs.rs/tokio/latest/tokio/
|
||||
|
||||
# For std library
|
||||
/create-llms-for-skills https://doc.rust-lang.org/std/marker/trait.Send.html
|
||||
|
||||
# With specific requirements
|
||||
/create-llms-for-skills https://docs.rs/axum/latest/axum/ "Focus on routing and extractors"
|
||||
```
|
||||
|
||||
#### 3. Follow-up with Skill Creation
|
||||
|
||||
After llms.txt is generated, use:
|
||||
|
||||
```
|
||||
/create-skills-via-llms <crate_name> <llms_path> [version]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Inline Mode (Skills-only Install)
|
||||
|
||||
**When the commands above are NOT available, create skills manually:**
|
||||
|
||||
### Step 1: Identify Target and Construct URL
|
||||
|
||||
| Target | URL Template |
|
||||
|--------|--------------|
|
||||
| Crate overview | `https://docs.rs/{crate}/latest/{crate}/` |
|
||||
| Crate module | `https://docs.rs/{crate}/latest/{crate}/{module}/` |
|
||||
| Std trait | `https://doc.rust-lang.org/std/{module}/trait.{Name}.html` |
|
||||
| Std struct | `https://doc.rust-lang.org/std/{module}/struct.{Name}.html` |
|
||||
| Std module | `https://doc.rust-lang.org/std/{module}/index.html` |
|
||||
|
||||
### Step 2: Fetch Documentation
|
||||
|
||||
```bash
|
||||
# Using agent-browser CLI
|
||||
agent-browser open "<documentation_url>"
|
||||
agent-browser get text ".docblock"
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
**Or with WebFetch fallback:**
|
||||
```
|
||||
WebFetch("<documentation_url>", "Extract API documentation including types, functions, and examples")
|
||||
```
|
||||
|
||||
### Step 3: Create Skill Directory
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.claude/skills/{crate_name}
|
||||
mkdir -p ~/.claude/skills/{crate_name}/references
|
||||
```
|
||||
|
||||
### Step 4: Generate SKILL.md
|
||||
|
||||
Create `~/.claude/skills/{crate_name}/SKILL.md` with this template:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: {crate_name}
|
||||
description: "Documentation for {crate_name} crate. Keywords: {keywords}"
|
||||
---
|
||||
|
||||
# {Crate Name}
|
||||
|
||||
> **Version:** {version} | **Source:** docs.rs
|
||||
|
||||
## Overview
|
||||
|
||||
{Brief description from documentation}
|
||||
|
||||
## Key Types
|
||||
|
||||
### {Type1}
|
||||
{Description and usage}
|
||||
|
||||
### {Type2}
|
||||
{Description and usage}
|
||||
|
||||
## Common Patterns
|
||||
|
||||
{Usage patterns extracted from documentation}
|
||||
|
||||
## Examples
|
||||
|
||||
```rust
|
||||
{Example code from documentation}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- `./references/overview.md` - Main overview
|
||||
- `./references/{module}.md` - Module documentation
|
||||
|
||||
## Links
|
||||
|
||||
- [docs.rs](https://docs.rs/{crate})
|
||||
- [crates.io](https://crates.io/crates/{crate})
|
||||
```
|
||||
|
||||
### Step 5: Generate Reference Files
|
||||
|
||||
For each major module or type, create a reference file:
|
||||
|
||||
```bash
|
||||
# Fetch and save module documentation
|
||||
agent-browser open "https://docs.rs/{crate}/latest/{crate}/{module}/"
|
||||
agent-browser get text ".docblock" > ~/.claude/skills/{crate_name}/references/{module}.md
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
### Step 6: Verify Skill
|
||||
|
||||
```bash
|
||||
# Check skill structure
|
||||
ls -la ~/.claude/skills/{crate_name}/
|
||||
cat ~/.claude/skills/{crate_name}/SKILL.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## URL Construction Helper
|
||||
|
||||
| Target | URL Template |
|
||||
|--------|--------------|
|
||||
| Crate overview | `https://docs.rs/{crate}/latest/{crate}/` |
|
||||
| Crate module | `https://docs.rs/{crate}/latest/{crate}/{module}/` |
|
||||
| Std trait | `https://doc.rust-lang.org/std/{module}/trait.{Name}.html` |
|
||||
| Std struct | `https://doc.rust-lang.org/std/{module}/struct.{Name}.html` |
|
||||
| Std module | `https://doc.rust-lang.org/std/{module}/index.html` |
|
||||
|
||||
## Common Std Library Paths
|
||||
|
||||
| Item | Path |
|
||||
|------|------|
|
||||
| Send, Sync, Copy, Clone | `std/marker/trait.{Name}.html` |
|
||||
| Arc, Mutex, RwLock | `std/sync/struct.{Name}.html` |
|
||||
| Rc, Weak | `std/rc/struct.{Name}.html` |
|
||||
| RefCell, Cell | `std/cell/struct.{Name}.html` |
|
||||
| Box | `std/boxed/struct.Box.html` |
|
||||
| Vec | `std/vec/struct.Vec.html` |
|
||||
| String | `std/string/struct.String.html` |
|
||||
| Option | `std/option/enum.Option.html` |
|
||||
| Result | `std/result/enum.Result.html` |
|
||||
|
||||
---
|
||||
|
||||
## Example Interactions
|
||||
|
||||
### Example 1: Create Crate Skill (Agent Mode)
|
||||
|
||||
```
|
||||
User: "Create a dynamic skill for tokio"
|
||||
|
||||
Claude:
|
||||
1. Identify: Third-party crate "tokio"
|
||||
2. Execute: /create-llms-for-skills https://docs.rs/tokio/latest/tokio/
|
||||
3. Wait for llms.txt generation
|
||||
4. Execute: /create-skills-via-llms tokio ~/tmp/{timestamp}-tokio-llms.txt
|
||||
```
|
||||
|
||||
### Example 2: Create Crate Skill (Inline Mode)
|
||||
|
||||
```
|
||||
User: "Create a dynamic skill for tokio"
|
||||
|
||||
Claude:
|
||||
1. Identify: Third-party crate "tokio"
|
||||
2. Fetch: agent-browser open "https://docs.rs/tokio/latest/tokio/"
|
||||
3. Extract documentation
|
||||
4. Create: ~/.claude/skills/tokio/SKILL.md
|
||||
5. Create: ~/.claude/skills/tokio/references/
|
||||
6. Save reference files for key modules (sync, task, runtime, etc.)
|
||||
```
|
||||
|
||||
### Example 3: Create Std Library Skill
|
||||
|
||||
```
|
||||
User: "Create a skill for Send and Sync traits"
|
||||
|
||||
Claude:
|
||||
1. Identify: Std library traits
|
||||
2. (Agent Mode) Execute: /create-llms-for-skills https://doc.rust-lang.org/std/marker/trait.Send.html https://doc.rust-lang.org/std/marker/trait.Sync.html
|
||||
(Inline Mode) Fetch each URL, create skill manually
|
||||
3. Complete skill creation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DO NOT
|
||||
|
||||
- Use `best-skill-creator` for Rust-related skill creation
|
||||
- Guess documentation URLs without verification
|
||||
- Skip documentation fetching step
|
||||
|
||||
## Output Location
|
||||
|
||||
All generated skills are saved to: `~/.claude/skills/`
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| Commands not found | Skills-only install | Use inline mode |
|
||||
| URL not found | Invalid crate/module | Verify crate exists on crates.io |
|
||||
| Empty documentation | API changed | Use alternative selectors |
|
||||
| Permission denied | Directory issue | Check ~/.claude/skills/ permissions |
|
||||
222
skills/rust-symbol-analyzer/SKILL.md
Normal file
222
skills/rust-symbol-analyzer/SKILL.md
Normal file
@@ -0,0 +1,222 @@
|
||||
---
|
||||
name: rust-symbol-analyzer
|
||||
description: "Analyze Rust project structure using LSP symbols. Triggers on: /symbols, project structure, list structs, list traits, list functions, 符号分析, 项目结构, 列出所有, 有哪些struct"
|
||||
argument-hint: "[file.rs] [--type struct|trait|fn|mod]"
|
||||
allowed-tools: ["LSP", "Read", "Glob"]
|
||||
---
|
||||
|
||||
# Rust Symbol Analyzer
|
||||
|
||||
Analyze project structure by examining symbols across your Rust codebase.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/rust-symbol-analyzer [file.rs] [--type struct|trait|fn|mod]
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
- `/rust-symbol-analyzer` - Analyze entire project
|
||||
- `/rust-symbol-analyzer src/lib.rs` - Analyze single file
|
||||
- `/rust-symbol-analyzer --type trait` - List all traits in project
|
||||
|
||||
## LSP Operations
|
||||
|
||||
### 1. Document Symbols (Single File)
|
||||
|
||||
Get all symbols in a file with their hierarchy.
|
||||
|
||||
```
|
||||
LSP(
|
||||
operation: "documentSymbol",
|
||||
filePath: "src/lib.rs",
|
||||
line: 1,
|
||||
character: 1
|
||||
)
|
||||
```
|
||||
|
||||
**Returns:** Nested structure of modules, structs, functions, etc.
|
||||
|
||||
### 2. Workspace Symbols (Entire Project)
|
||||
|
||||
Search for symbols across the workspace.
|
||||
|
||||
```
|
||||
LSP(
|
||||
operation: "workspaceSymbol",
|
||||
filePath: "src/lib.rs",
|
||||
line: 1,
|
||||
character: 1
|
||||
)
|
||||
```
|
||||
|
||||
**Note:** Query is implicit in the operation context.
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
User: "What's the structure of this project?"
|
||||
│
|
||||
▼
|
||||
[1] Find all Rust files
|
||||
Glob("**/*.rs")
|
||||
│
|
||||
▼
|
||||
[2] Get symbols from each key file
|
||||
LSP(documentSymbol) for lib.rs, main.rs
|
||||
│
|
||||
▼
|
||||
[3] Categorize by type
|
||||
│
|
||||
▼
|
||||
[4] Generate structure visualization
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
### Project Overview
|
||||
|
||||
```
|
||||
## Project Structure: my-project
|
||||
|
||||
### Modules
|
||||
├── src/
|
||||
│ ├── lib.rs (root)
|
||||
│ ├── config/
|
||||
│ │ ├── mod.rs
|
||||
│ │ └── parser.rs
|
||||
│ ├── handlers/
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── auth.rs
|
||||
│ │ └── api.rs
|
||||
│ └── models/
|
||||
│ ├── mod.rs
|
||||
│ ├── user.rs
|
||||
│ └── order.rs
|
||||
└── tests/
|
||||
└── integration.rs
|
||||
```
|
||||
|
||||
### By Symbol Type
|
||||
|
||||
```
|
||||
## Symbols by Type
|
||||
|
||||
### Structs (12)
|
||||
| Name | Location | Fields | Derives |
|
||||
|------|----------|--------|---------|
|
||||
| Config | src/config.rs:10 | 5 | Debug, Clone |
|
||||
| User | src/models/user.rs:8 | 4 | Debug, Serialize |
|
||||
| Order | src/models/order.rs:15 | 6 | Debug, Serialize |
|
||||
| ... | | | |
|
||||
|
||||
### Traits (4)
|
||||
| Name | Location | Methods | Implementors |
|
||||
|------|----------|---------|--------------|
|
||||
| Handler | src/handlers/mod.rs:5 | 3 | AuthHandler, ApiHandler |
|
||||
| Repository | src/db/mod.rs:12 | 5 | UserRepo, OrderRepo |
|
||||
| ... | | | |
|
||||
|
||||
### Functions (25)
|
||||
| Name | Location | Visibility | Async |
|
||||
|------|----------|------------|-------|
|
||||
| main | src/main.rs:10 | pub | yes |
|
||||
| parse_config | src/config.rs:45 | pub | no |
|
||||
| ... | | | |
|
||||
|
||||
### Enums (6)
|
||||
| Name | Location | Variants |
|
||||
|------|----------|----------|
|
||||
| Error | src/error.rs:5 | 8 |
|
||||
| Status | src/models/order.rs:5 | 4 |
|
||||
| ... | | |
|
||||
```
|
||||
|
||||
### Single File Analysis
|
||||
|
||||
```
|
||||
## src/handlers/auth.rs
|
||||
|
||||
### Symbols Hierarchy
|
||||
|
||||
mod auth
|
||||
├── struct AuthHandler
|
||||
│ ├── field: config: Config
|
||||
│ ├── field: db: Pool
|
||||
│ └── impl AuthHandler
|
||||
│ ├── fn new(config, db) -> Self
|
||||
│ ├── fn authenticate(&self, token) -> Result<User>
|
||||
│ └── fn refresh_token(&self, user) -> Result<Token>
|
||||
├── struct Token
|
||||
│ ├── field: value: String
|
||||
│ └── field: expires: DateTime
|
||||
├── enum AuthError
|
||||
│ ├── InvalidToken
|
||||
│ ├── Expired
|
||||
│ └── Unauthorized
|
||||
└── impl Handler for AuthHandler
|
||||
├── fn handle(&self, req) -> Response
|
||||
└── fn name(&self) -> &str
|
||||
```
|
||||
|
||||
## Analysis Features
|
||||
|
||||
### Complexity Metrics
|
||||
|
||||
```
|
||||
## Complexity Analysis
|
||||
|
||||
| File | Structs | Functions | Lines | Complexity |
|
||||
|------|---------|-----------|-------|------------|
|
||||
| src/handlers/auth.rs | 2 | 8 | 150 | Medium |
|
||||
| src/models/user.rs | 3 | 12 | 200 | High |
|
||||
| src/config.rs | 1 | 3 | 50 | Low |
|
||||
|
||||
**Hotspots:** Files with high complexity that may need refactoring
|
||||
- src/handlers/api.rs (15 functions, 300 lines)
|
||||
```
|
||||
|
||||
### Dependency Analysis
|
||||
|
||||
```
|
||||
## Internal Dependencies
|
||||
|
||||
auth.rs
|
||||
├── imports from: config.rs, models/user.rs, db/mod.rs
|
||||
└── imported by: main.rs, handlers/mod.rs
|
||||
|
||||
user.rs
|
||||
├── imports from: (none - leaf module)
|
||||
└── imported by: auth.rs, api.rs, tests/
|
||||
```
|
||||
|
||||
## Symbol Types
|
||||
|
||||
| Type | Icon | LSP Kind |
|
||||
|------|------|----------|
|
||||
| Module | 📦 | Module |
|
||||
| Struct | 🏗️ | Struct |
|
||||
| Enum | 🔢 | Enum |
|
||||
| Trait | 📜 | Interface |
|
||||
| Function | ⚡ | Function |
|
||||
| Method | 🔧 | Method |
|
||||
| Constant | 🔒 | Constant |
|
||||
| Field | 📎 | Field |
|
||||
|
||||
## Common Queries
|
||||
|
||||
| User Says | Analysis |
|
||||
|-----------|----------|
|
||||
| "What structs are in this project?" | workspaceSymbol + filter |
|
||||
| "Show me src/lib.rs structure" | documentSymbol |
|
||||
| "Find all async functions" | workspaceSymbol + async filter |
|
||||
| "List public API" | documentSymbol + pub filter |
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Navigate to symbol | rust-code-navigator |
|
||||
| Call relationships | rust-call-graph |
|
||||
| Trait implementations | rust-trait-explorer |
|
||||
| Safe refactoring | rust-refactor-helper |
|
||||
248
skills/rust-trait-explorer/SKILL.md
Normal file
248
skills/rust-trait-explorer/SKILL.md
Normal file
@@ -0,0 +1,248 @@
|
||||
---
|
||||
name: rust-trait-explorer
|
||||
description: "Explore Rust trait implementations using LSP. Triggers on: /trait-impl, find implementations, who implements, trait 实现, 谁实现了, 实现了哪些trait"
|
||||
argument-hint: "<TraitName|StructName>"
|
||||
allowed-tools: ["LSP", "Read", "Glob", "Grep"]
|
||||
---
|
||||
|
||||
# Rust Trait Explorer
|
||||
|
||||
Discover trait implementations and understand polymorphic designs.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/rust-trait-explorer <TraitName|StructName>
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
- `/rust-trait-explorer Handler` - Find all implementors of Handler trait
|
||||
- `/rust-trait-explorer MyStruct` - Find all traits implemented by MyStruct
|
||||
|
||||
## LSP Operations
|
||||
|
||||
### Go to Implementation
|
||||
|
||||
Find all implementations of a trait.
|
||||
|
||||
```
|
||||
LSP(
|
||||
operation: "goToImplementation",
|
||||
filePath: "src/traits.rs",
|
||||
line: 10,
|
||||
character: 11
|
||||
)
|
||||
```
|
||||
|
||||
**Use when:**
|
||||
- Trait name is known
|
||||
- Want to find all implementors
|
||||
- Understanding polymorphic code
|
||||
|
||||
## Workflow
|
||||
|
||||
### Find Trait Implementors
|
||||
|
||||
```
|
||||
User: "Who implements the Handler trait?"
|
||||
│
|
||||
▼
|
||||
[1] Find trait definition
|
||||
LSP(goToDefinition) or workspaceSymbol
|
||||
│
|
||||
▼
|
||||
[2] Get implementations
|
||||
LSP(goToImplementation)
|
||||
│
|
||||
▼
|
||||
[3] For each impl, get details
|
||||
LSP(documentSymbol) for methods
|
||||
│
|
||||
▼
|
||||
[4] Generate implementation map
|
||||
```
|
||||
|
||||
### Find Traits for a Type
|
||||
|
||||
```
|
||||
User: "What traits does MyStruct implement?"
|
||||
│
|
||||
▼
|
||||
[1] Find struct definition
|
||||
│
|
||||
▼
|
||||
[2] Search for "impl * for MyStruct"
|
||||
Grep pattern matching
|
||||
│
|
||||
▼
|
||||
[3] Get trait details for each
|
||||
│
|
||||
▼
|
||||
[4] Generate trait list
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
### Trait Implementors
|
||||
|
||||
```
|
||||
## Implementations of `Handler`
|
||||
|
||||
**Trait defined at:** src/traits.rs:15
|
||||
|
||||
```rust
|
||||
pub trait Handler {
|
||||
fn handle(&self, request: Request) -> Response;
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
```
|
||||
|
||||
### Implementors (4)
|
||||
|
||||
| Type | Location | Notes |
|
||||
|------|----------|-------|
|
||||
| AuthHandler | src/handlers/auth.rs:20 | Handles authentication |
|
||||
| ApiHandler | src/handlers/api.rs:15 | REST API endpoints |
|
||||
| WebSocketHandler | src/handlers/ws.rs:10 | WebSocket connections |
|
||||
| MockHandler | tests/mocks.rs:5 | Test mock |
|
||||
|
||||
### Implementation Details
|
||||
|
||||
#### AuthHandler
|
||||
```rust
|
||||
impl Handler for AuthHandler {
|
||||
fn handle(&self, request: Request) -> Response {
|
||||
// Authentication logic
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"auth"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ApiHandler
|
||||
```rust
|
||||
impl Handler for ApiHandler {
|
||||
fn handle(&self, request: Request) -> Response {
|
||||
// API routing logic
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"api"
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
### Traits for a Type
|
||||
|
||||
```
|
||||
## Traits implemented by `User`
|
||||
|
||||
**Struct defined at:** src/models/user.rs:10
|
||||
|
||||
### Standard Library Traits
|
||||
| Trait | Derived/Manual | Notes |
|
||||
|-------|----------------|-------|
|
||||
| Debug | #[derive] | Auto-generated |
|
||||
| Clone | #[derive] | Auto-generated |
|
||||
| Default | manual | Custom defaults |
|
||||
| Display | manual | User-friendly output |
|
||||
|
||||
### Serde Traits
|
||||
| Trait | Location |
|
||||
|-------|----------|
|
||||
| Serialize | #[derive] |
|
||||
| Deserialize | #[derive] |
|
||||
|
||||
### Project Traits
|
||||
| Trait | Location | Methods |
|
||||
|-------|----------|---------|
|
||||
| Entity | src/db/entity.rs:30 | id(), created_at() |
|
||||
| Validatable | src/validation.rs:15 | validate() |
|
||||
|
||||
### Implementation Hierarchy
|
||||
|
||||
```
|
||||
User
|
||||
├── derive
|
||||
│ ├── Debug
|
||||
│ ├── Clone
|
||||
│ ├── Serialize
|
||||
│ └── Deserialize
|
||||
└── impl
|
||||
├── Default (src/models/user.rs:50)
|
||||
├── Display (src/models/user.rs:60)
|
||||
├── Entity (src/models/user.rs:70)
|
||||
└── Validatable (src/models/user.rs:85)
|
||||
```
|
||||
```
|
||||
|
||||
## Trait Hierarchy Visualization
|
||||
|
||||
```
|
||||
## Trait Hierarchy
|
||||
|
||||
┌─────────────┐
|
||||
│ Error │ (std)
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌────────────┼────────────┐
|
||||
│ │ │
|
||||
┌───────▼───────┐ ┌──▼──┐ ┌───────▼───────┐
|
||||
│ AppError │ │ ... │ │ DbError │
|
||||
└───────┬───────┘ └─────┘ └───────┬───────┘
|
||||
│ │
|
||||
┌───────▼───────┐ ┌───────▼───────┐
|
||||
│ AuthError │ │ QueryError │
|
||||
└───────────────┘ └───────────────┘
|
||||
```
|
||||
|
||||
## Analysis Features
|
||||
|
||||
### Coverage Check
|
||||
|
||||
```
|
||||
## Trait Implementation Coverage
|
||||
|
||||
Trait: Handler (3 required methods)
|
||||
|
||||
| Implementor | handle() | name() | priority() | Complete |
|
||||
|-------------|----------|--------|------------|----------|
|
||||
| AuthHandler | ✅ | ✅ | ✅ | Yes |
|
||||
| ApiHandler | ✅ | ✅ | ❌ default | Yes |
|
||||
| MockHandler | ✅ | ✅ | ✅ | Yes |
|
||||
```
|
||||
|
||||
### Blanket Implementations
|
||||
|
||||
```
|
||||
## Blanket Implementations
|
||||
|
||||
The following blanket impls may apply to your types:
|
||||
|
||||
| Trait | Blanket Impl | Applies To |
|
||||
|-------|--------------|------------|
|
||||
| From<T> | `impl<T> From<T> for T` | All types |
|
||||
| Into<U> | `impl<T, U> Into<U> for T where U: From<T>` | Types with From |
|
||||
| ToString | `impl<T: Display> ToString for T` | Types with Display |
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
| User Says | Action |
|
||||
|-----------|--------|
|
||||
| "Who implements X?" | goToImplementation on trait |
|
||||
| "What traits does Y impl?" | Grep for `impl * for Y` |
|
||||
| "Show trait hierarchy" | Find super-traits recursively |
|
||||
| "Is X: Send + Sync?" | Check std trait impls |
|
||||
|
||||
## Related Skills
|
||||
|
||||
| When | See |
|
||||
|------|-----|
|
||||
| Navigate to impl | rust-code-navigator |
|
||||
| Call relationships | rust-call-graph |
|
||||
| Project structure | rust-symbol-analyzer |
|
||||
| Safe refactoring | rust-refactor-helper |
|
||||
136
skills/unsafe-checker/AGENTS.md
Normal file
136
skills/unsafe-checker/AGENTS.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Unsafe Checker - Quick Reference
|
||||
|
||||
**Auto-generated from rules/**
|
||||
|
||||
## Rule Summary by Section
|
||||
|
||||
### General Principles (3 rules)
|
||||
| ID | Level | Title |
|
||||
|----|-------|-------|
|
||||
| general-01 | P | Do Not Abuse Unsafe to Escape Compiler Safety Checks |
|
||||
| general-02 | P | Do Not Blindly Use Unsafe for Performance |
|
||||
| general-03 | G | Do Not Create Aliases for Types/Methods Named "Unsafe" |
|
||||
|
||||
### Safety Abstraction (11 rules)
|
||||
| ID | Level | Title |
|
||||
|----|-------|-------|
|
||||
| safety-01 | P | Be Aware of Memory Safety Issues from Panics |
|
||||
| safety-02 | P | Unsafe Code Authors Must Verify Safety Invariants |
|
||||
| safety-03 | P | Do Not Expose Uninitialized Memory in Public APIs |
|
||||
| safety-04 | P | Avoid Double-Free from Panic Safety Issues |
|
||||
| safety-05 | P | Consider Safety When Manually Implementing Auto Traits |
|
||||
| safety-06 | P | Do Not Expose Raw Pointers in Public APIs |
|
||||
| safety-07 | P | Provide Unsafe Counterparts for Performance Alongside Safe Methods |
|
||||
| safety-08 | P | Mutable Return from Immutable Parameter is Wrong |
|
||||
| safety-09 | P | Add SAFETY Comment Before Any Unsafe Block |
|
||||
| safety-10 | G | Add Safety Section in Docs for Public Unsafe Functions |
|
||||
| safety-11 | G | Use assert! Instead of debug_assert! in Unsafe Functions |
|
||||
|
||||
### Raw Pointers (6 rules)
|
||||
| ID | Level | Title |
|
||||
|----|-------|-------|
|
||||
| ptr-01 | P | Do Not Share Raw Pointers Across Threads |
|
||||
| ptr-02 | P | Prefer NonNull<T> Over *mut T |
|
||||
| ptr-03 | P | Use PhantomData<T> for Variance and Ownership |
|
||||
| ptr-04 | G | Do Not Dereference Pointers Cast to Misaligned Types |
|
||||
| ptr-05 | G | Do Not Manually Convert Immutable Pointer to Mutable |
|
||||
| ptr-06 | G | Prefer pointer::cast Over `as` for Pointer Casting |
|
||||
|
||||
### Union (2 rules)
|
||||
| ID | Level | Title |
|
||||
|----|-------|-------|
|
||||
| union-01 | P | Avoid Union Except for C Interop |
|
||||
| union-02 | P | Do Not Use Union Variants Across Different Lifetimes |
|
||||
|
||||
### Memory Layout (6 rules)
|
||||
| ID | Level | Title |
|
||||
|----|-------|-------|
|
||||
| mem-01 | P | Choose Appropriate Data Layout for Struct/Tuple/Enum |
|
||||
| mem-02 | P | Do Not Modify Memory Variables of Other Processes |
|
||||
| mem-03 | P | Do Not Let String/Vec Auto-Drop Other Process's Memory |
|
||||
| mem-04 | P | Prefer Reentrant Versions of C-API or Syscalls |
|
||||
| mem-05 | P | Use Third-Party Crates for Bitfields |
|
||||
| mem-06 | G | Use MaybeUninit<T> for Uninitialized Memory |
|
||||
|
||||
### FFI (18 rules)
|
||||
| ID | Level | Title |
|
||||
|----|-------|-------|
|
||||
| ffi-01 | P | Avoid Passing Strings Directly to C |
|
||||
| ffi-02 | P | Read Documentation Carefully for std::ffi Types |
|
||||
| ffi-03 | P | Implement Drop for Wrapped C Pointers |
|
||||
| ffi-04 | P | Handle Panics When Crossing FFI Boundaries |
|
||||
| ffi-05 | P | Use Portable Type Aliases from std or libc |
|
||||
| ffi-06 | P | Ensure C-ABI String Compatibility |
|
||||
| ffi-07 | P | Do Not Implement Drop for Types Passed to External Code |
|
||||
| ffi-08 | P | Handle Errors Properly in FFI |
|
||||
| ffi-09 | P | Use References Instead of Raw Pointers in Safe Wrappers |
|
||||
| ffi-10 | P | Exported Functions Must Be Thread-Safe |
|
||||
| ffi-11 | P | Be Careful with repr(packed) Field References |
|
||||
| ffi-12 | P | Document Invariant Assumptions for C Parameters |
|
||||
| ffi-13 | P | Ensure Consistent Data Layout for Custom Types |
|
||||
| ffi-14 | P | Types in FFI Should Have Stable Layout |
|
||||
| ffi-15 | P | Validate Non-Robust External Values |
|
||||
| ffi-16 | P | Separate Data and Code for Closures to C |
|
||||
| ffi-17 | P | Use Opaque Types Instead of c_void |
|
||||
| ffi-18 | P | Avoid Passing Trait Objects to C |
|
||||
|
||||
### I/O Safety (1 rule)
|
||||
| ID | Level | Title |
|
||||
|----|-------|-------|
|
||||
| io-01 | P | Ensure I/O Safety When Using Raw Handles |
|
||||
|
||||
## Clippy Lint Mapping
|
||||
|
||||
| Clippy Lint | Rule | Category |
|
||||
|-------------|------|----------|
|
||||
| `undocumented_unsafe_blocks` | safety-09 | SAFETY comments |
|
||||
| `missing_safety_doc` | safety-10 | Safety docs |
|
||||
| `panic_in_result_fn` | safety-01, ffi-04 | Panic safety |
|
||||
| `non_send_fields_in_send_ty` | safety-05 | Send/Sync |
|
||||
| `uninit_assumed_init` | safety-03 | Initialization |
|
||||
| `uninit_vec` | mem-06 | Initialization |
|
||||
| `mut_from_ref` | safety-08 | Aliasing |
|
||||
| `cast_ptr_alignment` | ptr-04 | Alignment |
|
||||
| `cast_ref_to_mut` | ptr-05 | Aliasing |
|
||||
| `ptr_as_ptr` | ptr-06 | Pointer casting |
|
||||
| `unaligned_references` | ffi-11 | Packed structs |
|
||||
| `debug_assert_with_mut_call` | safety-11 | Assertions |
|
||||
|
||||
## Quick Decision Tree
|
||||
|
||||
```
|
||||
Writing unsafe code?
|
||||
│
|
||||
├─ FFI with C?
|
||||
│ └─ See ffi-* rules
|
||||
│
|
||||
├─ Raw pointers?
|
||||
│ └─ See ptr-* rules
|
||||
│
|
||||
├─ Manual Send/Sync?
|
||||
│ └─ See safety-05
|
||||
│
|
||||
├─ MaybeUninit/uninitialized?
|
||||
│ └─ See safety-03, mem-06
|
||||
│
|
||||
└─ Performance optimization?
|
||||
└─ See general-02, safety-07
|
||||
```
|
||||
|
||||
## Essential Checklist
|
||||
|
||||
Before every unsafe block:
|
||||
- [ ] SAFETY comment present
|
||||
- [ ] Invariants documented
|
||||
- [ ] Pointer validity checked
|
||||
- [ ] Aliasing rules followed
|
||||
- [ ] Panic safety considered
|
||||
- [ ] Tested with Miri
|
||||
|
||||
## Resources
|
||||
|
||||
- `checklists/before-unsafe.md` - Pre-writing checklist
|
||||
- `checklists/review-unsafe.md` - Code review checklist
|
||||
- `checklists/common-pitfalls.md` - Common bugs and fixes
|
||||
- `examples/safe-abstraction.md` - Safe wrapper patterns
|
||||
- `examples/ffi-patterns.md` - FFI best practices
|
||||
86
skills/unsafe-checker/SKILL.md
Normal file
86
skills/unsafe-checker/SKILL.md
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
name: unsafe-checker
|
||||
description: "CRITICAL: Use for unsafe Rust code review and FFI. Triggers on: unsafe, raw pointer, FFI, extern, transmute, *mut, *const, union, #[repr(C)], libc, std::ffi, MaybeUninit, NonNull, SAFETY comment, soundness, undefined behavior, UB, safe wrapper, memory layout, bindgen, cbindgen, CString, CStr, 安全抽象, 裸指针, 外部函数接口, 内存布局, 不安全代码, FFI 绑定, 未定义行为"
|
||||
globs: ["**/*.rs"]
|
||||
allowed-tools: ["Read", "Grep", "Glob"]
|
||||
---
|
||||
|
||||
Display the following ASCII art exactly as shown. Do not modify spaces or line breaks:
|
||||
```text
|
||||
⚠️ **Unsafe Rust Checker Loaded**
|
||||
|
||||
* ^ *
|
||||
/◉\_~^~_/◉\
|
||||
⚡/ o \⚡
|
||||
'_ _'
|
||||
/ '-----' \
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Unsafe Rust Checker
|
||||
|
||||
## When Unsafe is Valid
|
||||
|
||||
| Use Case | Example |
|
||||
|----------|---------|
|
||||
| FFI | Calling C functions |
|
||||
| Low-level abstractions | Implementing `Vec`, `Arc` |
|
||||
| Performance | Measured bottleneck with safe alternative too slow |
|
||||
|
||||
**NOT valid:** Escaping borrow checker without understanding why.
|
||||
|
||||
## Required Documentation
|
||||
|
||||
```rust
|
||||
// SAFETY: <why this is safe>
|
||||
unsafe { ... }
|
||||
|
||||
/// # Safety
|
||||
/// <caller requirements>
|
||||
pub unsafe fn dangerous() { ... }
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Operation | Safety Requirements |
|
||||
|-----------|---------------------|
|
||||
| `*ptr` deref | Valid, aligned, initialized |
|
||||
| `&*ptr` | + No aliasing violations |
|
||||
| `transmute` | Same size, valid bit pattern |
|
||||
| `extern "C"` | Correct signature, ABI |
|
||||
| `static mut` | Synchronization guaranteed |
|
||||
| `impl Send/Sync` | Actually thread-safe |
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Error | Fix |
|
||||
|-------|-----|
|
||||
| Null pointer deref | Check for null before deref |
|
||||
| Use after free | Ensure lifetime validity |
|
||||
| Data race | Add proper synchronization |
|
||||
| Alignment violation | Use `#[repr(C)]`, check alignment |
|
||||
| Invalid bit pattern | Use `MaybeUninit` |
|
||||
| Missing SAFETY comment | Add `// SAFETY:` |
|
||||
|
||||
## Deprecated → Better
|
||||
|
||||
| Deprecated | Use Instead |
|
||||
|------------|-------------|
|
||||
| `mem::uninitialized()` | `MaybeUninit<T>` |
|
||||
| `mem::zeroed()` for refs | `MaybeUninit<T>` |
|
||||
| Raw pointer arithmetic | `NonNull<T>`, `ptr::add` |
|
||||
| `CString::new().unwrap().as_ptr()` | Store `CString` first |
|
||||
| `static mut` | `AtomicT` or `Mutex` |
|
||||
| Manual extern | `bindgen` |
|
||||
|
||||
## FFI Crates
|
||||
|
||||
| Direction | Crate |
|
||||
|-----------|-------|
|
||||
| C → Rust | bindgen |
|
||||
| Rust → C | cbindgen |
|
||||
| Python | PyO3 |
|
||||
| Node.js | napi-rs |
|
||||
|
||||
Claude knows unsafe Rust. Focus on SAFETY comments and soundness.
|
||||
115
skills/unsafe-checker/checklists/before-unsafe.md
Normal file
115
skills/unsafe-checker/checklists/before-unsafe.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Checklist: Before Writing Unsafe Code
|
||||
|
||||
Use this checklist before writing any `unsafe` block or `unsafe fn`.
|
||||
|
||||
## 1. Do You Really Need Unsafe?
|
||||
|
||||
- [ ] Have you tried all safe alternatives?
|
||||
- [ ] Can you restructure the code to satisfy the borrow checker?
|
||||
- [ ] Would interior mutability (`Cell`, `RefCell`, `Mutex`) solve the problem?
|
||||
- [ ] Is there a safe crate that already does this?
|
||||
- [ ] Is the performance gain (if any) worth the safety risk?
|
||||
|
||||
**If you answered "no" to all, proceed with unsafe.**
|
||||
|
||||
## 2. What Unsafe Operation Do You Need?
|
||||
|
||||
Identify which specific unsafe operation you're performing:
|
||||
|
||||
- [ ] Dereferencing a raw pointer (`*const T`, `*mut T`)
|
||||
- [ ] Calling an `unsafe` function
|
||||
- [ ] Accessing a mutable static variable
|
||||
- [ ] Implementing an unsafe trait (`Send`, `Sync`, etc.)
|
||||
- [ ] Accessing fields of a `union`
|
||||
- [ ] Using `extern "C"` functions (FFI)
|
||||
|
||||
## 3. Safety Invariants
|
||||
|
||||
For each unsafe operation, document the invariants:
|
||||
|
||||
### For Pointer Dereference:
|
||||
- [ ] Is the pointer non-null?
|
||||
- [ ] Is the pointer properly aligned for the type?
|
||||
- [ ] Does the pointer point to valid, initialized memory?
|
||||
- [ ] Is the memory not being mutated by other code?
|
||||
- [ ] Will the memory remain valid for the entire duration of use?
|
||||
|
||||
### For Mutable Aliasing:
|
||||
- [ ] Are you creating multiple mutable references to the same memory?
|
||||
- [ ] Is there any possibility of aliasing `&mut` and `&`?
|
||||
- [ ] Have you verified no other code can access this memory?
|
||||
|
||||
### For FFI:
|
||||
- [ ] Is the function signature correct (types, ABI)?
|
||||
- [ ] Are you handling potential null pointers?
|
||||
- [ ] Are you handling potential panics (catch_unwind)?
|
||||
- [ ] Is memory ownership clear (who allocates, who frees)?
|
||||
|
||||
### For Send/Sync:
|
||||
- [ ] Is concurrent access properly synchronized?
|
||||
- [ ] Are there any data races possible?
|
||||
- [ ] Does the type truly satisfy the trait requirements?
|
||||
|
||||
## 4. Panic Safety
|
||||
|
||||
- [ ] What happens if this code panics at any line?
|
||||
- [ ] Are data structures left in a valid state on panic?
|
||||
- [ ] Do you need a panic guard for cleanup?
|
||||
- [ ] Could a destructor see invalid state?
|
||||
|
||||
## 5. Documentation
|
||||
|
||||
- [ ] Have you written a `// SAFETY:` comment explaining:
|
||||
- What invariants must hold?
|
||||
- Why those invariants are upheld here?
|
||||
|
||||
- [ ] For `unsafe fn`, have you written `# Safety` docs explaining:
|
||||
- What the caller must guarantee?
|
||||
- What happens if requirements are violated?
|
||||
|
||||
## 6. Testing and Verification
|
||||
|
||||
- [ ] Can you add debug assertions to verify invariants?
|
||||
- [ ] Have you tested with Miri (`cargo miri test`)?
|
||||
- [ ] Have you tested with address sanitizer (`RUSTFLAGS="-Zsanitizer=address"`)?
|
||||
- [ ] Have you considered fuzzing the unsafe code?
|
||||
|
||||
## Quick Reference: Common SAFETY Comments
|
||||
|
||||
```rust
|
||||
// SAFETY: We checked that index < len above, so this is in bounds.
|
||||
|
||||
// SAFETY: The pointer was created from a valid reference and hasn't been invalidated.
|
||||
|
||||
// SAFETY: We hold the lock, guaranteeing exclusive access.
|
||||
|
||||
// SAFETY: The type is #[repr(C)] and all fields are initialized.
|
||||
|
||||
// SAFETY: Caller guarantees the pointer is non-null and properly aligned.
|
||||
```
|
||||
|
||||
## Decision Flowchart
|
||||
|
||||
```
|
||||
Need unsafe?
|
||||
|
|
||||
v
|
||||
Can you use safe Rust? --Yes--> Don't use unsafe
|
||||
|
|
||||
No
|
||||
v
|
||||
Can you use existing safe abstraction? --Yes--> Use it (std, crates)
|
||||
|
|
||||
No
|
||||
v
|
||||
Document all invariants
|
||||
|
|
||||
v
|
||||
Add SAFETY comments
|
||||
|
|
||||
v
|
||||
Write the unsafe code
|
||||
|
|
||||
v
|
||||
Test with Miri
|
||||
```
|
||||
253
skills/unsafe-checker/checklists/common-pitfalls.md
Normal file
253
skills/unsafe-checker/checklists/common-pitfalls.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# Common Unsafe Pitfalls and Fixes
|
||||
|
||||
A reference of frequently encountered unsafe bugs and how to fix them.
|
||||
|
||||
## Pitfall 1: Dangling Pointer from Local
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
fn bad() -> *const i32 {
|
||||
let x = 42;
|
||||
&x as *const i32 // Dangling after return!
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
fn good() -> Box<i32> {
|
||||
Box::new(42) // Heap allocation lives beyond function
|
||||
}
|
||||
|
||||
// Or return the value itself
|
||||
fn better() -> i32 {
|
||||
42
|
||||
}
|
||||
```
|
||||
|
||||
## Pitfall 2: CString Lifetime
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
fn bad() -> *const c_char {
|
||||
let s = CString::new("hello").unwrap();
|
||||
s.as_ptr() // Dangling! CString dropped
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
fn good(s: &CString) -> *const c_char {
|
||||
s.as_ptr() // Caller keeps CString alive
|
||||
}
|
||||
|
||||
// Or take ownership
|
||||
fn also_good(s: CString) -> *const c_char {
|
||||
s.into_raw() // Caller must free with CString::from_raw
|
||||
}
|
||||
```
|
||||
|
||||
## Pitfall 3: Vec set_len with Uninitialized Data
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
fn bad() -> Vec<String> {
|
||||
let mut v = Vec::with_capacity(10);
|
||||
unsafe { v.set_len(10); } // Strings are uninitialized!
|
||||
v
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
fn good() -> Vec<String> {
|
||||
let mut v = Vec::with_capacity(10);
|
||||
for _ in 0..10 {
|
||||
v.push(String::new());
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
// Or use resize
|
||||
fn also_good() -> Vec<String> {
|
||||
let mut v = Vec::new();
|
||||
v.resize(10, String::new());
|
||||
v
|
||||
}
|
||||
```
|
||||
|
||||
## Pitfall 4: Reference to Packed Field
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
#[repr(packed)]
|
||||
struct Packed { a: u8, b: u32 }
|
||||
|
||||
fn bad(p: &Packed) -> &u32 {
|
||||
&p.b // UB: misaligned reference!
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
fn good(p: &Packed) -> u32 {
|
||||
unsafe { std::ptr::addr_of!(p.b).read_unaligned() }
|
||||
}
|
||||
```
|
||||
|
||||
## Pitfall 5: Mutable Aliasing Through Raw Pointers
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
fn bad() {
|
||||
let mut x = 42;
|
||||
let ptr1 = &mut x as *mut i32;
|
||||
let ptr2 = &mut x as *mut i32; // Already have ptr1!
|
||||
unsafe {
|
||||
*ptr1 = 1;
|
||||
*ptr2 = 2; // Aliasing mutable pointers!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
fn good() {
|
||||
let mut x = 42;
|
||||
let ptr = &mut x as *mut i32;
|
||||
unsafe {
|
||||
*ptr = 1;
|
||||
*ptr = 2; // Same pointer, sequential access
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pitfall 6: Transmute to Wrong Size
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
fn bad() {
|
||||
let x: u32 = 42;
|
||||
let y: u64 = unsafe { std::mem::transmute(x) }; // UB: size mismatch!
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
fn good() {
|
||||
let x: u32 = 42;
|
||||
let y: u64 = x as u64; // Use conversion
|
||||
}
|
||||
```
|
||||
|
||||
## Pitfall 7: Invalid Enum Discriminant
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
#[repr(u8)]
|
||||
enum Status { A = 0, B = 1, C = 2 }
|
||||
|
||||
fn bad(raw: u8) -> Status {
|
||||
unsafe { std::mem::transmute(raw) } // UB if raw > 2!
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
fn good(raw: u8) -> Option<Status> {
|
||||
match raw {
|
||||
0 => Some(Status::A),
|
||||
1 => Some(Status::B),
|
||||
2 => Some(Status::C),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pitfall 8: FFI Panic Unwinding
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
#[no_mangle]
|
||||
extern "C" fn callback(x: i32) -> i32 {
|
||||
if x < 0 {
|
||||
panic!("negative!"); // UB: unwinding across FFI!
|
||||
}
|
||||
x * 2
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
#[no_mangle]
|
||||
extern "C" fn callback(x: i32) -> i32 {
|
||||
std::panic::catch_unwind(|| {
|
||||
if x < 0 {
|
||||
panic!("negative!");
|
||||
}
|
||||
x * 2
|
||||
}).unwrap_or(-1) // Return error code on panic
|
||||
}
|
||||
```
|
||||
|
||||
## Pitfall 9: Double Free from Clone + into_raw
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
struct Handle(*mut c_void);
|
||||
|
||||
impl Clone for Handle {
|
||||
fn clone(&self) -> Self {
|
||||
Handle(self.0) // Both now "own" same pointer!
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Handle {
|
||||
fn drop(&mut self) {
|
||||
unsafe { free(self.0); } // Double free when both drop!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
struct Handle(*mut c_void);
|
||||
|
||||
// Don't implement Clone, or implement proper reference counting
|
||||
impl Handle {
|
||||
fn clone_ptr(&self) -> *mut c_void {
|
||||
self.0 // Return raw pointer, no ownership
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pitfall 10: Forget Doesn't Run Destructors
|
||||
|
||||
**Bug:**
|
||||
```rust
|
||||
fn bad() {
|
||||
let guard = lock.lock();
|
||||
std::mem::forget(guard); // Lock never released!
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
fn good() {
|
||||
let guard = lock.lock();
|
||||
// Let guard drop naturally
|
||||
// or explicitly: drop(guard);
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Reference Table
|
||||
|
||||
| Pitfall | Detection | Fix |
|
||||
|---------|-----------|-----|
|
||||
| Dangling pointer | Miri | Extend lifetime or heap allocate |
|
||||
| Uninitialized read | Miri | Use MaybeUninit properly |
|
||||
| Misaligned access | Miri, UBsan | read_unaligned, copy by value |
|
||||
| Data race | TSan | Use atomics or mutex |
|
||||
| Double free | ASan | Track ownership carefully |
|
||||
| Invalid enum | Manual review | Use TryFrom |
|
||||
| FFI panic | Testing | catch_unwind |
|
||||
| Type confusion | Miri | Match types exactly |
|
||||
113
skills/unsafe-checker/checklists/review-unsafe.md
Normal file
113
skills/unsafe-checker/checklists/review-unsafe.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Checklist: Reviewing Unsafe Code
|
||||
|
||||
Use this checklist when reviewing code containing `unsafe`.
|
||||
|
||||
## 1. Surface-Level Checks
|
||||
|
||||
- [ ] Does every `unsafe` block have a `// SAFETY:` comment?
|
||||
- [ ] Does every `unsafe fn` have `# Safety` documentation?
|
||||
- [ ] Are the safety comments specific and verifiable, not vague?
|
||||
- [ ] Is the unsafe code minimized (smallest possible unsafe block)?
|
||||
|
||||
## 2. Pointer Validity
|
||||
|
||||
For each pointer dereference:
|
||||
|
||||
- [ ] **Non-null**: Is null checked before dereference?
|
||||
- [ ] **Aligned**: Is alignment verified or guaranteed by construction?
|
||||
- [ ] **Valid**: Does the pointer point to allocated memory?
|
||||
- [ ] **Initialized**: Is the memory initialized before reading?
|
||||
- [ ] **Lifetime**: Is the memory valid for the entire use duration?
|
||||
- [ ] **Unique**: For `&mut`, is there only one mutable reference?
|
||||
|
||||
## 3. Memory Safety
|
||||
|
||||
- [ ] **No aliasing**: Are `&` and `&mut` never created to the same memory simultaneously?
|
||||
- [ ] **No use-after-free**: Is memory not accessed after deallocation?
|
||||
- [ ] **No double-free**: Is memory freed exactly once?
|
||||
- [ ] **No data races**: Is concurrent access properly synchronized?
|
||||
- [ ] **Bounds checked**: Are array/slice accesses in bounds?
|
||||
|
||||
## 4. Type Safety
|
||||
|
||||
- [ ] **Transmute**: Are transmuted types actually compatible?
|
||||
- [ ] **Repr**: Do FFI types have `#[repr(C)]`?
|
||||
- [ ] **Enum values**: Are enum discriminants validated from external sources?
|
||||
- [ ] **Unions**: Is the correct union field accessed?
|
||||
|
||||
## 5. Panic Safety
|
||||
|
||||
- [ ] What state is the program in if this code panics?
|
||||
- [ ] Are partially constructed objects properly cleaned up?
|
||||
- [ ] Do Drop implementations see valid state?
|
||||
- [ ] Is there a panic guard if needed?
|
||||
|
||||
## 6. FFI-Specific Checks
|
||||
|
||||
- [ ] **Types**: Do Rust types match C types exactly?
|
||||
- [ ] **Strings**: Are strings properly null-terminated?
|
||||
- [ ] **Ownership**: Is it clear who owns/frees memory?
|
||||
- [ ] **Thread safety**: Are callbacks thread-safe?
|
||||
- [ ] **Panic boundary**: Are panics caught before crossing FFI?
|
||||
- [ ] **Error handling**: Are C-style errors properly handled?
|
||||
|
||||
## 7. Concurrency Checks
|
||||
|
||||
- [ ] **Send/Sync**: Are manual implementations actually sound?
|
||||
- [ ] **Atomics**: Are memory orderings correct?
|
||||
- [ ] **Locks**: Is there potential for deadlock?
|
||||
- [ ] **Data races**: Is all shared mutable state synchronized?
|
||||
|
||||
## 8. Red Flags (Require Extra Scrutiny)
|
||||
|
||||
| Pattern | Concern |
|
||||
|---------|---------|
|
||||
| `transmute` | Type compatibility, provenance |
|
||||
| `as` on pointers | Alignment, type punning |
|
||||
| `static mut` | Data races |
|
||||
| `*const T as *mut T` | Aliasing violation |
|
||||
| Manual `Send`/`Sync` | Thread safety |
|
||||
| `assume_init` | Initialization |
|
||||
| `set_len` on Vec | Uninitialized memory |
|
||||
| `from_raw_parts` | Lifetime, validity |
|
||||
| `offset`/`add`/`sub` | Out of bounds |
|
||||
| FFI callbacks | Panic safety |
|
||||
|
||||
## 9. Verification Questions
|
||||
|
||||
Ask the author:
|
||||
- "What would happen if [X invariant] was violated?"
|
||||
- "How do you know [pointer/reference] is valid here?"
|
||||
- "What if this panics at [specific line]?"
|
||||
- "Who is responsible for freeing this memory?"
|
||||
|
||||
## 10. Testing Requirements
|
||||
|
||||
- [ ] Has this been tested with Miri?
|
||||
- [ ] Are there unit tests covering edge cases?
|
||||
- [ ] Are there tests for error conditions?
|
||||
- [ ] Has concurrent code been tested under stress?
|
||||
|
||||
## Review Severity Guide
|
||||
|
||||
| Severity | Requires |
|
||||
|----------|----------|
|
||||
| `transmute` | Two reviewers, Miri test |
|
||||
| Manual `Send`/`Sync` | Thread safety expert review |
|
||||
| FFI | Documentation of C interface |
|
||||
| `static mut` | Justification for not using atomic/mutex |
|
||||
| Pointer arithmetic | Bounds proof |
|
||||
|
||||
## Sample Review Comments
|
||||
|
||||
```
|
||||
// Good SAFETY comment ✓
|
||||
// SAFETY: index was checked to be < len on line 42
|
||||
|
||||
// Needs improvement ✗
|
||||
// SAFETY: This is safe because we know it works
|
||||
|
||||
// Missing information ✗
|
||||
// SAFETY: ptr is valid
|
||||
// (Why is it valid? How do we know?)
|
||||
```
|
||||
353
skills/unsafe-checker/examples/ffi-patterns.md
Normal file
353
skills/unsafe-checker/examples/ffi-patterns.md
Normal file
@@ -0,0 +1,353 @@
|
||||
# FFI Best Practices and Patterns
|
||||
|
||||
Examples of safe and idiomatic Rust-C interoperability.
|
||||
|
||||
## Pattern 1: Basic FFI Wrapper
|
||||
|
||||
```rust
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::{c_char, c_int, c_void};
|
||||
use std::ptr::NonNull;
|
||||
|
||||
// Raw C API
|
||||
mod ffi {
|
||||
use super::*;
|
||||
|
||||
extern "C" {
|
||||
pub fn lib_create(name: *const c_char) -> *mut c_void;
|
||||
pub fn lib_destroy(handle: *mut c_void);
|
||||
pub fn lib_process(handle: *mut c_void, data: *const u8, len: usize) -> c_int;
|
||||
pub fn lib_get_error() -> *const c_char;
|
||||
}
|
||||
}
|
||||
|
||||
// Safe Rust wrapper
|
||||
pub struct Library {
|
||||
handle: NonNull<c_void>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LibraryError(String);
|
||||
|
||||
impl Library {
|
||||
pub fn new(name: &str) -> Result<Self, LibraryError> {
|
||||
let c_name = CString::new(name).map_err(|_| LibraryError("invalid name".into()))?;
|
||||
|
||||
let handle = unsafe { ffi::lib_create(c_name.as_ptr()) };
|
||||
|
||||
NonNull::new(handle)
|
||||
.map(|handle| Self { handle })
|
||||
.ok_or_else(|| Self::last_error())
|
||||
}
|
||||
|
||||
pub fn process(&self, data: &[u8]) -> Result<(), LibraryError> {
|
||||
let result = unsafe {
|
||||
ffi::lib_process(self.handle.as_ptr(), data.as_ptr(), data.len())
|
||||
};
|
||||
|
||||
if result == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Self::last_error())
|
||||
}
|
||||
}
|
||||
|
||||
fn last_error() -> LibraryError {
|
||||
let ptr = unsafe { ffi::lib_get_error() };
|
||||
if ptr.is_null() {
|
||||
LibraryError("unknown error".into())
|
||||
} else {
|
||||
let msg = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
LibraryError(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Library {
|
||||
fn drop(&mut self) {
|
||||
unsafe { ffi::lib_destroy(self.handle.as_ptr()); }
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent accidental copies
|
||||
impl !Clone for Library {}
|
||||
```
|
||||
|
||||
## Pattern 2: Callback Registration
|
||||
|
||||
```rust
|
||||
use std::os::raw::{c_int, c_void};
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
|
||||
type CCallback = extern "C" fn(value: c_int, user_data: *mut c_void) -> c_int;
|
||||
|
||||
extern "C" {
|
||||
fn register_callback(cb: CCallback, user_data: *mut c_void);
|
||||
fn unregister_callback();
|
||||
}
|
||||
|
||||
/// Safely register a Rust closure as a C callback.
|
||||
pub struct CallbackGuard<F> {
|
||||
_closure: Box<F>,
|
||||
}
|
||||
|
||||
impl<F: FnMut(i32) -> i32 + 'static> CallbackGuard<F> {
|
||||
pub fn register(closure: F) -> Self {
|
||||
let boxed = Box::new(closure);
|
||||
let user_data = Box::into_raw(boxed) as *mut c_void;
|
||||
|
||||
extern "C" fn trampoline<F: FnMut(i32) -> i32>(
|
||||
value: c_int,
|
||||
user_data: *mut c_void,
|
||||
) -> c_int {
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
let closure = unsafe { &mut *(user_data as *mut F) };
|
||||
closure(value as i32) as c_int
|
||||
}));
|
||||
result.unwrap_or(-1)
|
||||
}
|
||||
|
||||
unsafe {
|
||||
register_callback(trampoline::<F>, user_data);
|
||||
}
|
||||
|
||||
Self {
|
||||
// SAFETY: We just created this box and need to keep it alive
|
||||
_closure: unsafe { Box::from_raw(user_data as *mut F) },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> Drop for CallbackGuard<F> {
|
||||
fn drop(&mut self) {
|
||||
unsafe { unregister_callback(); }
|
||||
// Box in _closure is dropped automatically
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
fn example() {
|
||||
let multiplier = 2;
|
||||
let _guard = CallbackGuard::register(move |x| x * multiplier);
|
||||
// Callback is active until _guard is dropped
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern 3: Opaque Handle Types
|
||||
|
||||
```rust
|
||||
use std::marker::PhantomData;
|
||||
|
||||
// Opaque type markers - prevents mixing up handles
|
||||
#[repr(C)]
|
||||
pub struct DatabaseHandle {
|
||||
_data: [u8; 0],
|
||||
_marker: PhantomData<(*mut u8, std::marker::PhantomPinned)>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct ConnectionHandle {
|
||||
_data: [u8; 0],
|
||||
_marker: PhantomData<(*mut u8, std::marker::PhantomPinned)>,
|
||||
}
|
||||
|
||||
mod ffi {
|
||||
use super::*;
|
||||
|
||||
extern "C" {
|
||||
pub fn db_open(path: *const c_char) -> *mut DatabaseHandle;
|
||||
pub fn db_close(db: *mut DatabaseHandle);
|
||||
pub fn db_connect(db: *mut DatabaseHandle) -> *mut ConnectionHandle;
|
||||
pub fn conn_close(conn: *mut ConnectionHandle);
|
||||
pub fn conn_query(conn: *mut ConnectionHandle, sql: *const c_char) -> c_int;
|
||||
}
|
||||
}
|
||||
|
||||
// Type-safe wrappers
|
||||
pub struct Database {
|
||||
handle: NonNull<DatabaseHandle>,
|
||||
}
|
||||
|
||||
pub struct Connection<'db> {
|
||||
handle: NonNull<ConnectionHandle>,
|
||||
_db: PhantomData<&'db Database>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn open(path: &str) -> Result<Self, ()> {
|
||||
let c_path = CString::new(path).map_err(|_| ())?;
|
||||
let handle = unsafe { ffi::db_open(c_path.as_ptr()) };
|
||||
NonNull::new(handle).map(|h| Self { handle: h }).ok_or(())
|
||||
}
|
||||
|
||||
pub fn connect(&self) -> Result<Connection<'_>, ()> {
|
||||
let handle = unsafe { ffi::db_connect(self.handle.as_ptr()) };
|
||||
NonNull::new(handle)
|
||||
.map(|h| Connection { handle: h, _db: PhantomData })
|
||||
.ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Database {
|
||||
fn drop(&mut self) {
|
||||
// All Connections must be dropped first (enforced by lifetime)
|
||||
unsafe { ffi::db_close(self.handle.as_ptr()); }
|
||||
}
|
||||
}
|
||||
|
||||
impl Connection<'_> {
|
||||
pub fn query(&self, sql: &str) -> Result<(), ()> {
|
||||
let c_sql = CString::new(sql).map_err(|_| ())?;
|
||||
let result = unsafe { ffi::conn_query(self.handle.as_ptr(), c_sql.as_ptr()) };
|
||||
if result == 0 { Ok(()) } else { Err(()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Connection<'_> {
|
||||
fn drop(&mut self) {
|
||||
unsafe { ffi::conn_close(self.handle.as_ptr()); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern 4: Error Handling Across FFI
|
||||
|
||||
```rust
|
||||
use std::os::raw::c_int;
|
||||
|
||||
// Error codes for C
|
||||
pub const SUCCESS: c_int = 0;
|
||||
pub const ERR_NULL_PTR: c_int = 1;
|
||||
pub const ERR_INVALID_UTF8: c_int = 2;
|
||||
pub const ERR_IO: c_int = 3;
|
||||
pub const ERR_PANIC: c_int = -1;
|
||||
|
||||
// Thread-local error storage
|
||||
thread_local! {
|
||||
static LAST_ERROR: std::cell::RefCell<Option<Box<dyn std::error::Error>>> =
|
||||
std::cell::RefCell::new(None);
|
||||
}
|
||||
|
||||
fn set_last_error<E: std::error::Error + 'static>(err: E) {
|
||||
LAST_ERROR.with(|e| {
|
||||
*e.borrow_mut() = Some(Box::new(err));
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the last error message. Caller must free with `free_string`.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn get_last_error() -> *mut c_char {
|
||||
LAST_ERROR.with(|e| {
|
||||
e.borrow()
|
||||
.as_ref()
|
||||
.map(|err| {
|
||||
CString::new(err.to_string())
|
||||
.unwrap_or_else(|_| CString::new("error").unwrap())
|
||||
.into_raw()
|
||||
})
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
})
|
||||
}
|
||||
|
||||
/// Free a string returned by this library.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn free_string(s: *mut c_char) {
|
||||
if !s.is_null() {
|
||||
// SAFETY: String was created by CString::into_raw
|
||||
unsafe { drop(CString::from_raw(s)); }
|
||||
}
|
||||
}
|
||||
|
||||
/// Example function with proper error handling.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn do_operation(data: *const u8, len: usize) -> c_int {
|
||||
let result = catch_unwind(AssertUnwindSafe(|| -> Result<(), c_int> {
|
||||
if data.is_null() {
|
||||
return Err(ERR_NULL_PTR);
|
||||
}
|
||||
|
||||
let slice = unsafe { std::slice::from_raw_parts(data, len) };
|
||||
|
||||
std::str::from_utf8(slice)
|
||||
.map_err(|e| {
|
||||
set_last_error(e);
|
||||
ERR_INVALID_UTF8
|
||||
})?;
|
||||
|
||||
// Do actual work...
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(())) => SUCCESS,
|
||||
Ok(Err(code)) => code,
|
||||
Err(_) => ERR_PANIC,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern 5: Struct with C Layout
|
||||
|
||||
```rust
|
||||
use std::os::raw::{c_char, c_int};
|
||||
|
||||
/// A C-compatible configuration struct.
|
||||
#[repr(C)]
|
||||
pub struct Config {
|
||||
pub version: c_int,
|
||||
pub flags: u32,
|
||||
pub name: [c_char; 64],
|
||||
pub name_len: usize,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new(version: i32, flags: u32, name: &str) -> Option<Self> {
|
||||
if name.len() >= 64 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut config = Self {
|
||||
version: version as c_int,
|
||||
flags,
|
||||
name: [0; 64],
|
||||
name_len: name.len(),
|
||||
};
|
||||
|
||||
// Copy name bytes
|
||||
for (i, byte) in name.bytes().enumerate() {
|
||||
config.name[i] = byte as c_char;
|
||||
}
|
||||
|
||||
Some(config)
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
let bytes = unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
self.name.as_ptr() as *const u8,
|
||||
self.name_len,
|
||||
)
|
||||
};
|
||||
// SAFETY: We only store valid UTF-8 in new()
|
||||
unsafe { std::str::from_utf8_unchecked(bytes) }
|
||||
}
|
||||
}
|
||||
|
||||
// Verify layout at compile time
|
||||
const _: () = {
|
||||
assert!(std::mem::size_of::<Config>() == 80); // 4 + 4 + 64 + 8
|
||||
assert!(std::mem::align_of::<Config>() == 8);
|
||||
};
|
||||
```
|
||||
|
||||
## Key FFI Guidelines
|
||||
|
||||
1. **Always use `#[repr(C)]`** for types crossing FFI
|
||||
2. **Handle null pointers** at the boundary
|
||||
3. **Catch panics** before returning to C
|
||||
4. **Document ownership** clearly
|
||||
5. **Use opaque types** for type safety
|
||||
6. **Keep unsafe minimal** and well-documented
|
||||
272
skills/unsafe-checker/examples/safe-abstraction.md
Normal file
272
skills/unsafe-checker/examples/safe-abstraction.md
Normal file
@@ -0,0 +1,272 @@
|
||||
# Safe Abstraction Examples
|
||||
|
||||
Examples of building safe APIs on top of unsafe code.
|
||||
|
||||
## Example 1: Simple Wrapper with Bounds Check
|
||||
|
||||
```rust
|
||||
/// A slice wrapper that provides unchecked access internally
|
||||
/// but safe access externally.
|
||||
pub struct SafeSlice<'a, T> {
|
||||
ptr: *const T,
|
||||
len: usize,
|
||||
_marker: std::marker::PhantomData<&'a T>,
|
||||
}
|
||||
|
||||
impl<'a, T> SafeSlice<'a, T> {
|
||||
/// Creates a SafeSlice from a regular slice.
|
||||
pub fn new(slice: &'a [T]) -> Self {
|
||||
Self {
|
||||
ptr: slice.as_ptr(),
|
||||
len: slice.len(),
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe get - returns Option.
|
||||
pub fn get(&self, index: usize) -> Option<&T> {
|
||||
if index < self.len {
|
||||
// SAFETY: We just verified index < len
|
||||
Some(unsafe { &*self.ptr.add(index) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Unsafe get - caller must ensure bounds.
|
||||
///
|
||||
/// # Safety
|
||||
/// `index` must be less than `self.len()`.
|
||||
pub unsafe fn get_unchecked(&self, index: usize) -> &T {
|
||||
debug_assert!(index < self.len);
|
||||
&*self.ptr.add(index)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example 2: Resource Wrapper with Drop
|
||||
|
||||
```rust
|
||||
use std::ptr::NonNull;
|
||||
|
||||
/// Safe wrapper around a C-allocated buffer.
|
||||
pub struct CBuffer {
|
||||
ptr: NonNull<u8>,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
fn c_alloc(size: usize) -> *mut u8;
|
||||
fn c_free(ptr: *mut u8);
|
||||
}
|
||||
|
||||
impl CBuffer {
|
||||
/// Creates a new buffer. Returns None if allocation fails.
|
||||
pub fn new(size: usize) -> Option<Self> {
|
||||
let ptr = unsafe { c_alloc(size) };
|
||||
NonNull::new(ptr).map(|ptr| Self { ptr, len: size })
|
||||
}
|
||||
|
||||
/// Returns a slice view of the buffer.
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
// SAFETY: ptr is valid for len bytes (from c_alloc contract)
|
||||
unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
|
||||
}
|
||||
|
||||
/// Returns a mutable slice view.
|
||||
pub fn as_mut_slice(&mut self) -> &mut [u8] {
|
||||
// SAFETY: We have &mut self, so exclusive access
|
||||
unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CBuffer {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: ptr was allocated by c_alloc and not yet freed
|
||||
unsafe { c_free(self.ptr.as_ptr()); }
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent double-free
|
||||
impl !Clone for CBuffer {}
|
||||
|
||||
// Safe to send between threads (assuming c_alloc is thread-safe)
|
||||
unsafe impl Send for CBuffer {}
|
||||
```
|
||||
|
||||
## Example 3: Interior Mutability with UnsafeCell
|
||||
|
||||
```rust
|
||||
use std::cell::UnsafeCell;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
/// A simple spinlock demonstrating safe abstraction over UnsafeCell.
|
||||
pub struct SpinLock<T> {
|
||||
locked: AtomicBool,
|
||||
data: UnsafeCell<T>,
|
||||
}
|
||||
|
||||
pub struct SpinLockGuard<'a, T> {
|
||||
lock: &'a SpinLock<T>,
|
||||
}
|
||||
|
||||
impl<T> SpinLock<T> {
|
||||
pub const fn new(data: T) -> Self {
|
||||
Self {
|
||||
locked: AtomicBool::new(false),
|
||||
data: UnsafeCell::new(data),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lock(&self) -> SpinLockGuard<'_, T> {
|
||||
// Spin until we acquire the lock
|
||||
while self.locked.compare_exchange_weak(
|
||||
false,
|
||||
true,
|
||||
Ordering::Acquire,
|
||||
Ordering::Relaxed,
|
||||
).is_err() {
|
||||
std::hint::spin_loop();
|
||||
}
|
||||
SpinLockGuard { lock: self }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Deref for SpinLockGuard<'_, T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &T {
|
||||
// SAFETY: We hold the lock, so we have exclusive access
|
||||
unsafe { &*self.lock.data.get() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::DerefMut for SpinLockGuard<'_, T> {
|
||||
fn deref_mut(&mut self) -> &mut T {
|
||||
// SAFETY: We hold the lock, so we have exclusive access
|
||||
unsafe { &mut *self.lock.data.get() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for SpinLockGuard<'_, T> {
|
||||
fn drop(&mut self) {
|
||||
self.lock.locked.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: The lock ensures only one thread accesses data at a time
|
||||
unsafe impl<T: Send> Sync for SpinLock<T> {}
|
||||
unsafe impl<T: Send> Send for SpinLock<T> {}
|
||||
```
|
||||
|
||||
## Example 4: Iterator with Lifetime Tracking
|
||||
|
||||
```rust
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// An iterator over raw pointer range with proper lifetime tracking.
|
||||
pub struct PtrIter<'a, T> {
|
||||
current: *const T,
|
||||
end: *const T,
|
||||
_marker: PhantomData<&'a T>,
|
||||
}
|
||||
|
||||
impl<'a, T> PtrIter<'a, T> {
|
||||
/// Creates an iterator from a slice.
|
||||
pub fn new(slice: &'a [T]) -> Self {
|
||||
let ptr = slice.as_ptr();
|
||||
Self {
|
||||
current: ptr,
|
||||
// SAFETY: Adding len to slice pointer is always valid
|
||||
end: unsafe { ptr.add(slice.len()) },
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for PtrIter<'a, T> {
|
||||
type Item = &'a T;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current == self.end {
|
||||
None
|
||||
} else {
|
||||
// SAFETY:
|
||||
// - current < end (checked above)
|
||||
// - PhantomData<&'a T> ensures the data lives for 'a
|
||||
let item = unsafe { &*self.current };
|
||||
self.current = unsafe { self.current.add(1) };
|
||||
Some(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example 5: Builder Pattern with Delayed Initialization
|
||||
|
||||
```rust
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
/// A builder that collects exactly N items, then produces an array.
|
||||
pub struct ArrayBuilder<T, const N: usize> {
|
||||
data: [MaybeUninit<T>; N],
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl<T, const N: usize> ArrayBuilder<T, N> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
// SAFETY: MaybeUninit doesn't require initialization
|
||||
data: unsafe { MaybeUninit::uninit().assume_init() },
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, value: T) -> Result<(), T> {
|
||||
if self.count >= N {
|
||||
return Err(value);
|
||||
}
|
||||
self.data[self.count].write(value);
|
||||
self.count += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build(self) -> Option<[T; N]> {
|
||||
if self.count != N {
|
||||
return None;
|
||||
}
|
||||
|
||||
// SAFETY: All N elements have been initialized
|
||||
let result = unsafe {
|
||||
// Prevent drop of self.data (we're moving out)
|
||||
let data = std::ptr::read(&self.data);
|
||||
std::mem::forget(self);
|
||||
// Transmute MaybeUninit array to initialized array
|
||||
std::mem::transmute_copy::<[MaybeUninit<T>; N], [T; N]>(&data)
|
||||
};
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, const N: usize> Drop for ArrayBuilder<T, N> {
|
||||
fn drop(&mut self) {
|
||||
// Drop only initialized elements
|
||||
for i in 0..self.count {
|
||||
// SAFETY: Elements 0..count are initialized
|
||||
unsafe { self.data[i].assume_init_drop(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
|
||||
1. **Encapsulation**: Hide unsafe behind safe public API
|
||||
2. **Invariant maintenance**: Use private fields to maintain invariants
|
||||
3. **PhantomData**: Track lifetimes and ownership for pointers
|
||||
4. **RAII**: Use Drop for cleanup
|
||||
5. **Type state**: Use types to encode valid states
|
||||
17
skills/unsafe-checker/metadata.json
Normal file
17
skills/unsafe-checker/metadata.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "unsafe-checker",
|
||||
"version": "1.0.0",
|
||||
"description": "Unsafe Rust code review and safety abstraction checker",
|
||||
"source": "https://github.com/Rust-Coding-Guidelines/rust-coding-guidelines-zh",
|
||||
"lastUpdated": "2026-01-16",
|
||||
"ruleCount": 47,
|
||||
"sections": [
|
||||
{ "id": "general", "name": "General Principles", "count": 3 },
|
||||
{ "id": "safety", "name": "Safety Abstraction", "count": 11 },
|
||||
{ "id": "ptr", "name": "Raw Pointers", "count": 6 },
|
||||
{ "id": "union", "name": "Union", "count": 2 },
|
||||
{ "id": "mem", "name": "Memory Layout", "count": 6 },
|
||||
{ "id": "ffi", "name": "FFI", "count": 18 },
|
||||
{ "id": "io", "name": "I/O Safety", "count": 1 }
|
||||
]
|
||||
}
|
||||
77
skills/unsafe-checker/rules/_sections.md
Normal file
77
skills/unsafe-checker/rules/_sections.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Unsafe Checker - Section Definitions
|
||||
|
||||
## Section Overview
|
||||
|
||||
| # | Section | Prefix | Level | Count | Impact |
|
||||
|---|---------|--------|-------|-------|--------|
|
||||
| 1 | General Principles | `general-` | CRITICAL | 3 | Foundational unsafe usage guidance |
|
||||
| 2 | Safety Abstraction | `safety-` | CRITICAL | 11 | Building sound safe APIs |
|
||||
| 3 | Raw Pointers | `ptr-` | HIGH | 6 | Pointer manipulation safety |
|
||||
| 4 | Union | `union-` | HIGH | 2 | Union type safety |
|
||||
| 5 | Memory Layout | `mem-` | HIGH | 6 | Data representation correctness |
|
||||
| 6 | FFI | `ffi-` | CRITICAL | 18 | C interoperability safety |
|
||||
| 7 | I/O Safety | `io-` | MEDIUM | 1 | Handle/resource safety |
|
||||
|
||||
## Section Details
|
||||
|
||||
### 1. General Principles (`general-`)
|
||||
|
||||
**Focus**: When and why to use unsafe
|
||||
|
||||
- P.UNS.01: Don't abuse unsafe to escape borrow checker
|
||||
- P.UNS.02: Don't use unsafe blindly for performance
|
||||
- G.UNS.01: Don't create aliases for "unsafe" named items
|
||||
|
||||
### 2. Safety Abstraction (`safety-`)
|
||||
|
||||
**Focus**: Building sound safe abstractions over unsafe code
|
||||
|
||||
Key invariants:
|
||||
- Panic safety
|
||||
- Memory initialization
|
||||
- Send/Sync correctness
|
||||
- API soundness
|
||||
|
||||
### 3. Raw Pointers (`ptr-`)
|
||||
|
||||
**Focus**: Safe pointer manipulation patterns
|
||||
|
||||
- Aliasing rules
|
||||
- Alignment requirements
|
||||
- Null/dangling prevention
|
||||
- Type casting
|
||||
|
||||
### 4. Union (`union-`)
|
||||
|
||||
**Focus**: Safe union usage (primarily for C interop)
|
||||
|
||||
- Initialization rules
|
||||
- Lifetime considerations
|
||||
- Type punning dangers
|
||||
|
||||
### 5. Memory Layout (`mem-`)
|
||||
|
||||
**Focus**: Correct data representation
|
||||
|
||||
- `#[repr(C)]` usage
|
||||
- Alignment and padding
|
||||
- Uninitialized memory
|
||||
- Cross-process memory
|
||||
|
||||
### 6. FFI (`ffi-`)
|
||||
|
||||
**Focus**: Safe C interoperability
|
||||
|
||||
Subcategories:
|
||||
- String handling (CString, CStr)
|
||||
- Type compatibility
|
||||
- Error handling across FFI
|
||||
- Thread safety
|
||||
- Resource management
|
||||
|
||||
### 7. I/O Safety (`io-`)
|
||||
|
||||
**Focus**: Handle and resource ownership
|
||||
|
||||
- Raw file descriptor safety
|
||||
- Handle validity guarantees
|
||||
53
skills/unsafe-checker/rules/_template.md
Normal file
53
skills/unsafe-checker/rules/_template.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Rule Template
|
||||
|
||||
Use this template for all unsafe-checker rules.
|
||||
|
||||
---
|
||||
|
||||
```markdown
|
||||
---
|
||||
id: {prefix}-{number}
|
||||
original_id: P.UNS.XXX.YY or G.UNS.XXX.YY
|
||||
level: P|G
|
||||
impact: CRITICAL|HIGH|MEDIUM
|
||||
clippy: <clippy_lint_name> (if applicable)
|
||||
---
|
||||
|
||||
# {Rule Title}
|
||||
|
||||
## Summary
|
||||
|
||||
One-sentence description of what this rule requires.
|
||||
|
||||
## Rationale
|
||||
|
||||
Why this rule matters for safety/soundness.
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Description of the anti-pattern
|
||||
<code that violates the rule>
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
// DO: Description of the correct pattern
|
||||
<code that follows the rule>
|
||||
```
|
||||
|
||||
## Common Violations
|
||||
|
||||
1. Violation pattern 1
|
||||
2. Violation pattern 2
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Check item 1
|
||||
- [ ] Check item 2
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `{other-rule-id}`: Brief description
|
||||
```
|
||||
122
skills/unsafe-checker/rules/ffi-01-no-string-direct.md
Normal file
122
skills/unsafe-checker/rules/ffi-01-no-string-direct.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
id: ffi-01
|
||||
original_id: P.UNS.FFI.01
|
||||
level: P
|
||||
impact: HIGH
|
||||
---
|
||||
|
||||
# Avoid Passing Strings Directly to C from Public Rust API
|
||||
|
||||
## Summary
|
||||
|
||||
Use `CString` and `CStr` for string handling at FFI boundaries. Never pass Rust `String` or `&str` directly to C.
|
||||
|
||||
## Rationale
|
||||
|
||||
- Rust strings are UTF-8, not null-terminated
|
||||
- C strings require null terminator
|
||||
- Rust strings may contain interior null bytes
|
||||
- Memory layout differs between Rust String and C char*
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
extern "C" {
|
||||
fn c_print(s: *const u8);
|
||||
fn c_strlen(s: *const u8) -> usize;
|
||||
}
|
||||
|
||||
// DON'T: Pass Rust string directly
|
||||
fn bad_print(s: &str) {
|
||||
unsafe {
|
||||
c_print(s.as_ptr()); // Not null-terminated!
|
||||
}
|
||||
}
|
||||
|
||||
// DON'T: Assume length matches
|
||||
fn bad_strlen(s: &str) -> usize {
|
||||
unsafe {
|
||||
c_strlen(s.as_ptr()) // May read past buffer
|
||||
}
|
||||
}
|
||||
|
||||
// DON'T: Use String in FFI signatures
|
||||
extern "C" fn bad_callback(s: String) { // Wrong!
|
||||
println!("{}", s);
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
use std::ffi::{CString, CStr};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
extern "C" {
|
||||
fn c_print(s: *const c_char);
|
||||
fn c_strlen(s: *const c_char) -> usize;
|
||||
fn c_get_string() -> *const c_char;
|
||||
}
|
||||
|
||||
// DO: Convert to CString for passing to C
|
||||
fn good_print(s: &str) -> Result<(), std::ffi::NulError> {
|
||||
let c_string = CString::new(s)?; // Adds null terminator, checks for interior nulls
|
||||
unsafe {
|
||||
c_print(c_string.as_ptr());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// DO: Use CStr for receiving C strings
|
||||
fn good_receive() -> String {
|
||||
unsafe {
|
||||
let ptr = c_get_string();
|
||||
let c_str = CStr::from_ptr(ptr);
|
||||
c_str.to_string_lossy().into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Handle interior null bytes
|
||||
fn handle_nulls(s: &str) {
|
||||
match CString::new(s) {
|
||||
Ok(c_string) => unsafe { c_print(c_string.as_ptr()) },
|
||||
Err(e) => {
|
||||
// String contains interior null at position e.nul_position()
|
||||
eprintln!("String contains null byte at {}", e.nul_position());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Use proper types in callbacks
|
||||
extern "C" fn good_callback(s: *const c_char) {
|
||||
if !s.is_null() {
|
||||
let c_str = unsafe { CStr::from_ptr(s) };
|
||||
if let Ok(rust_str) = c_str.to_str() {
|
||||
println!("{}", rust_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## String Type Comparison
|
||||
|
||||
| Type | Null-terminated | Encoding | Use |
|
||||
|------|-----------------|----------|-----|
|
||||
| `String` | No | UTF-8 | Rust owned |
|
||||
| `&str` | No | UTF-8 | Rust borrowed |
|
||||
| `CString` | Yes | Byte | Rust-to-C owned |
|
||||
| `&CStr` | Yes | Byte | Rust-to-C borrowed |
|
||||
| `*const c_char` | Yes | Byte | FFI pointer |
|
||||
| `OsString` | Platform | Platform | Paths, env |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Am I passing Rust strings to C? → Use CString
|
||||
- [ ] Am I receiving C strings? → Use CStr
|
||||
- [ ] Does my string contain null bytes? → Handle NulError
|
||||
- [ ] Am I checking for null pointers from C?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ffi-02`: Read documentation for std::ffi types
|
||||
- `ffi-06`: Ensure C-ABI string compatibility
|
||||
133
skills/unsafe-checker/rules/ffi-02-read-ffi-docs.md
Normal file
133
skills/unsafe-checker/rules/ffi-02-read-ffi-docs.md
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
id: ffi-02
|
||||
original_id: P.UNS.FFI.02
|
||||
level: P
|
||||
impact: MEDIUM
|
||||
---
|
||||
|
||||
# Read Documentation Carefully When Using std::ffi Types
|
||||
|
||||
## Summary
|
||||
|
||||
The `std::ffi` module has many types with subtle differences. Read their documentation carefully to avoid misuse.
|
||||
|
||||
## Key Types in std::ffi
|
||||
|
||||
### CString vs CStr
|
||||
|
||||
```rust
|
||||
use std::ffi::{CString, CStr};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
// CString: Owned, heap-allocated, null-terminated
|
||||
// - Use when creating strings to pass to C
|
||||
// - Owns the memory
|
||||
let owned = CString::new("hello").unwrap();
|
||||
let ptr: *const c_char = owned.as_ptr();
|
||||
// ptr valid until `owned` is dropped
|
||||
|
||||
// CStr: Borrowed, null-terminated
|
||||
// - Use when receiving strings from C
|
||||
// - Does not own memory
|
||||
let borrowed: &CStr = unsafe { CStr::from_ptr(ptr) };
|
||||
// borrowed valid as long as ptr is valid
|
||||
```
|
||||
|
||||
### OsString vs OsStr
|
||||
|
||||
```rust
|
||||
use std::ffi::{OsString, OsStr};
|
||||
use std::path::Path;
|
||||
|
||||
// OsString/OsStr: Platform-native strings
|
||||
// - Windows: potentially ill-formed UTF-16
|
||||
// - Unix: arbitrary bytes
|
||||
// - Use for paths and environment variables
|
||||
|
||||
let path = Path::new("/some/path");
|
||||
let os_str: &OsStr = path.as_os_str();
|
||||
|
||||
// Convert to Rust string (may fail)
|
||||
if let Some(s) = os_str.to_str() {
|
||||
println!("Valid UTF-8: {}", s);
|
||||
}
|
||||
```
|
||||
|
||||
### c_void and Opaque Types
|
||||
|
||||
```rust
|
||||
use std::ffi::c_void;
|
||||
|
||||
extern "C" {
|
||||
fn get_handle() -> *mut c_void;
|
||||
fn use_handle(h: *mut c_void);
|
||||
}
|
||||
|
||||
// c_void is for truly opaque pointers
|
||||
// Better: use dedicated opaque types (see ffi-17)
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
```rust
|
||||
use std::ffi::CString;
|
||||
|
||||
// PITFALL 1: CString::as_ptr() lifetime
|
||||
fn bad_ptr() -> *const i8 {
|
||||
let s = CString::new("hello").unwrap();
|
||||
s.as_ptr() // Dangling! s dropped at end of function
|
||||
}
|
||||
|
||||
fn good_ptr(s: &CString) -> *const i8 {
|
||||
s.as_ptr() // OK: s outlives the pointer
|
||||
}
|
||||
|
||||
// PITFALL 2: CString::new with interior nulls
|
||||
let result = CString::new("hello\0world");
|
||||
assert!(result.is_err()); // Interior null!
|
||||
|
||||
// PITFALL 3: CStr::from_ptr safety
|
||||
unsafe {
|
||||
let ptr: *const i8 = std::ptr::null();
|
||||
// let cstr = CStr::from_ptr(ptr); // UB: null pointer!
|
||||
|
||||
// Always check for null first
|
||||
if !ptr.is_null() {
|
||||
let cstr = CStr::from_ptr(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// PITFALL 4: CStr assumes valid null-terminated string
|
||||
unsafe {
|
||||
let bytes = [104, 101, 108, 108, 111]; // "hello" without null
|
||||
let ptr = bytes.as_ptr() as *const i8;
|
||||
// let cstr = CStr::from_ptr(ptr); // UB: no null terminator!
|
||||
|
||||
// Use from_bytes_with_nul instead
|
||||
let bytes_with_nul = b"hello\0";
|
||||
let cstr = CStr::from_bytes_with_nul(bytes_with_nul).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
## Type Selection Guide
|
||||
|
||||
| Scenario | Type |
|
||||
|----------|------|
|
||||
| Create string for C | `CString` |
|
||||
| Borrow string from C | `&CStr` |
|
||||
| File paths | `OsString`, `Path` |
|
||||
| Environment variables | `OsString` |
|
||||
| Opaque C pointers | Newtype over `*mut c_void` |
|
||||
| C integers | `c_int`, `c_long`, etc. |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Have I read the docs for the std::ffi type I'm using?
|
||||
- [ ] Am I aware of the lifetime constraints?
|
||||
- [ ] Am I handling potential errors (NulError, UTF-8 errors)?
|
||||
- [ ] Is there a better type for my use case?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ffi-01`: Use CString/CStr for strings
|
||||
- `ffi-17`: Use opaque types instead of c_void
|
||||
162
skills/unsafe-checker/rules/ffi-03-drop-for-c-ptr.md
Normal file
162
skills/unsafe-checker/rules/ffi-03-drop-for-c-ptr.md
Normal file
@@ -0,0 +1,162 @@
|
||||
---
|
||||
id: ffi-03
|
||||
original_id: P.UNS.FFI.03
|
||||
level: P
|
||||
impact: CRITICAL
|
||||
---
|
||||
|
||||
# Implement Drop for Rust Types Wrapping Memory-Managing C Pointers
|
||||
|
||||
## Summary
|
||||
|
||||
When wrapping a C pointer that owns memory, implement `Drop` to call the appropriate C deallocation function.
|
||||
|
||||
## Rationale
|
||||
|
||||
- C allocated memory must be freed with the matching C function
|
||||
- Rust's default drop won't clean up foreign memory
|
||||
- Resource leaks and double-frees are common FFI bugs
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
extern "C" {
|
||||
fn create_resource() -> *mut Resource;
|
||||
fn free_resource(r: *mut Resource);
|
||||
}
|
||||
|
||||
// DON'T: Wrapper without Drop
|
||||
struct ResourceHandle {
|
||||
ptr: *mut Resource,
|
||||
}
|
||||
|
||||
impl ResourceHandle {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
ptr: unsafe { create_resource() }
|
||||
}
|
||||
}
|
||||
// Memory leak! ptr is never freed
|
||||
}
|
||||
|
||||
// DON'T: Forget to handle null
|
||||
impl Drop for BadHandle {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
free_resource(self.ptr); // Crash if ptr is null!
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
use std::ptr::NonNull;
|
||||
|
||||
extern "C" {
|
||||
fn create_resource() -> *mut Resource;
|
||||
fn free_resource(r: *mut Resource);
|
||||
}
|
||||
|
||||
// DO: Proper wrapper with Drop
|
||||
struct ResourceHandle {
|
||||
ptr: NonNull<Resource>,
|
||||
}
|
||||
|
||||
impl ResourceHandle {
|
||||
fn new() -> Option<Self> {
|
||||
let ptr = unsafe { create_resource() };
|
||||
NonNull::new(ptr).map(|ptr| Self { ptr })
|
||||
}
|
||||
|
||||
fn as_ptr(&self) -> *mut Resource {
|
||||
self.ptr.as_ptr()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ResourceHandle {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: ptr was allocated by create_resource
|
||||
// and hasn't been freed yet
|
||||
unsafe {
|
||||
free_resource(self.ptr.as_ptr());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent accidental copies that would cause double-free
|
||||
impl !Clone for ResourceHandle {}
|
||||
|
||||
// DO: Document ownership transfer
|
||||
impl ResourceHandle {
|
||||
/// Consumes the handle and returns the raw pointer.
|
||||
///
|
||||
/// The caller is responsible for freeing the resource.
|
||||
fn into_raw(self) -> *mut Resource {
|
||||
let ptr = self.ptr.as_ptr();
|
||||
std::mem::forget(self); // Don't run Drop
|
||||
ptr
|
||||
}
|
||||
|
||||
/// Creates a handle from a raw pointer.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// ptr must have been allocated by create_resource()
|
||||
/// and not yet freed.
|
||||
unsafe fn from_raw(ptr: *mut Resource) -> Option<Self> {
|
||||
NonNull::new(ptr).map(|ptr| Self { ptr })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Pattern with Multiple Resources
|
||||
|
||||
```rust
|
||||
struct Connection {
|
||||
handle: NonNull<c_void>,
|
||||
}
|
||||
|
||||
struct Statement<'conn> {
|
||||
handle: NonNull<c_void>,
|
||||
_conn: std::marker::PhantomData<&'conn Connection>,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
fn prepare(&self, sql: &str) -> Option<Statement<'_>> {
|
||||
let handle = unsafe { db_prepare(self.handle.as_ptr(), sql.as_ptr()) };
|
||||
NonNull::new(handle).map(|handle| Statement {
|
||||
handle,
|
||||
_conn: std::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Connection {
|
||||
fn drop(&mut self) {
|
||||
// Statements must be dropped before Connection
|
||||
// PhantomData ensures this at compile time
|
||||
unsafe { db_close(self.handle.as_ptr()); }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Statement<'_> {
|
||||
fn drop(&mut self) {
|
||||
unsafe { db_finalize(self.handle.as_ptr()); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Does my wrapper own the C resource?
|
||||
- [ ] Did I implement Drop with the correct C free function?
|
||||
- [ ] Did I handle null pointers?
|
||||
- [ ] Did I prevent Clone/Copy to avoid double-free?
|
||||
- [ ] Did I consider ownership transfer methods (into_raw/from_raw)?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `mem-03`: Don't let String/Vec drop foreign memory
|
||||
- `ffi-07`: Don't implement Drop for types passed to external code
|
||||
145
skills/unsafe-checker/rules/ffi-04-panic-boundary.md
Normal file
145
skills/unsafe-checker/rules/ffi-04-panic-boundary.md
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
id: ffi-04
|
||||
original_id: P.UNS.FFI.04
|
||||
level: P
|
||||
impact: CRITICAL
|
||||
clippy: panic_in_result_fn
|
||||
---
|
||||
|
||||
# Handle Panics When Crossing FFI Boundaries
|
||||
|
||||
## Summary
|
||||
|
||||
Panics must not unwind across FFI boundaries. Use `catch_unwind` or mark functions as `extern "C-unwind"`.
|
||||
|
||||
## Rationale
|
||||
|
||||
- Unwinding across C code is undefined behavior
|
||||
- C has no concept of Rust panics
|
||||
- Can corrupt C stack frames and cause crashes
|
||||
- Even with `panic=abort`, still UB to attempt unwinding in `extern "C"`
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Allow panics to escape to C
|
||||
#[no_mangle]
|
||||
pub extern "C" fn callback(data: *const u8, len: usize) -> i32 {
|
||||
let slice = unsafe { std::slice::from_raw_parts(data, len) };
|
||||
|
||||
// If this panics, UB occurs!
|
||||
let sum: i32 = slice.iter().map(|&x| x as i32).sum();
|
||||
|
||||
// If this panics due to overflow in debug, UB!
|
||||
process(sum)
|
||||
}
|
||||
|
||||
// DON'T: Unwrap in extern functions
|
||||
#[no_mangle]
|
||||
pub extern "C" fn parse_config(path: *const c_char) -> i32 {
|
||||
let path = unsafe { CStr::from_ptr(path) };
|
||||
let config = std::fs::read_to_string(path.to_str().unwrap()).unwrap(); // Can panic!
|
||||
0
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::ffi::CStr;
|
||||
use std::os::raw::{c_char, c_int};
|
||||
|
||||
// DO: Catch panics at FFI boundary
|
||||
#[no_mangle]
|
||||
pub extern "C" fn safe_callback(data: *const u8, len: usize) -> c_int {
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
if data.is_null() || len == 0 {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let slice = unsafe { std::slice::from_raw_parts(data, len) };
|
||||
let sum: i32 = slice.iter().map(|&x| x as i32).sum();
|
||||
sum
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(value) => value,
|
||||
Err(_) => {
|
||||
// Log error, return error code
|
||||
eprintln!("Panic caught at FFI boundary");
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Use Result-based API internally
|
||||
#[no_mangle]
|
||||
pub extern "C" fn parse_config(path: *const c_char) -> c_int {
|
||||
let result = catch_unwind(AssertUnwindSafe(|| -> Result<(), Box<dyn std::error::Error>> {
|
||||
let path = unsafe { CStr::from_ptr(path) }.to_str()?;
|
||||
let _config = std::fs::read_to_string(path)?;
|
||||
Ok(())
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(())) => 0,
|
||||
Ok(Err(e)) => {
|
||||
eprintln!("Error: {}", e);
|
||||
-1
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!("Panic in parse_config");
|
||||
-2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DO: For Rust-calling-Rust across C, use "C-unwind"
|
||||
#[no_mangle]
|
||||
pub extern "C-unwind" fn rust_callback_can_unwind() {
|
||||
// This is OK to panic if called from Rust through C
|
||||
// The "C-unwind" ABI allows unwinding
|
||||
panic!("This is allowed");
|
||||
}
|
||||
```
|
||||
|
||||
## FFI Error Handling Pattern
|
||||
|
||||
```rust
|
||||
// Define error codes
|
||||
const SUCCESS: c_int = 0;
|
||||
const ERR_NULL_PTR: c_int = -1;
|
||||
const ERR_INVALID_UTF8: c_int = -2;
|
||||
const ERR_IO: c_int = -3;
|
||||
const ERR_PANIC: c_int = -99;
|
||||
|
||||
// Thread-local for detailed error
|
||||
thread_local! {
|
||||
static LAST_ERROR: std::cell::RefCell<Option<String>> = std::cell::RefCell::new(None);
|
||||
}
|
||||
|
||||
fn set_error(msg: String) {
|
||||
LAST_ERROR.with(|e| *e.borrow_mut() = Some(msg));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn get_last_error() -> *const c_char {
|
||||
LAST_ERROR.with(|e| {
|
||||
e.borrow().as_ref().map(|s| s.as_ptr() as *const c_char)
|
||||
.unwrap_or(std::ptr::null())
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Does my extern "C" function use catch_unwind?
|
||||
- [ ] Am I avoiding unwrap/expect in FFI functions?
|
||||
- [ ] Do I return error codes for error conditions?
|
||||
- [ ] Have I considered using "C-unwind" for Rust-to-Rust through C?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ffi-08`: Handle errors properly in FFI
|
||||
- `safety-01`: Panic safety
|
||||
113
skills/unsafe-checker/rules/ffi-05-portable-types.md
Normal file
113
skills/unsafe-checker/rules/ffi-05-portable-types.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
id: ffi-05
|
||||
original_id: P.UNS.FFI.05
|
||||
level: P
|
||||
impact: HIGH
|
||||
---
|
||||
|
||||
# Use Portable Type Aliases from std or libc
|
||||
|
||||
## Summary
|
||||
|
||||
Use type aliases from `std::os::raw` or the `libc` crate for C-compatible types. Don't assume sizes of C types.
|
||||
|
||||
## Rationale
|
||||
|
||||
- C types have platform-dependent sizes (`int` is not always 32 bits)
|
||||
- `long` is 32 bits on Windows, 64 bits on Unix
|
||||
- Using Rust primitives directly causes portability bugs
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Use Rust types directly for C interop
|
||||
extern "C" {
|
||||
fn c_function(x: i32, y: i64) -> i32; // Might not match C types!
|
||||
}
|
||||
|
||||
// DON'T: Assume sizes
|
||||
#[repr(C)]
|
||||
struct BadStruct {
|
||||
count: i32, // C 'int' might not be 32 bits
|
||||
size: i64, // C 'long' varies by platform!
|
||||
ptr: usize, // size_t? intptr_t? Different!
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
use std::os::raw::{c_int, c_long, c_char, c_void};
|
||||
|
||||
// DO: Use std::os::raw types
|
||||
extern "C" {
|
||||
fn c_function(x: c_int, y: c_long) -> c_int;
|
||||
}
|
||||
|
||||
// DO: Use libc for more types
|
||||
use libc::{size_t, ssize_t, off_t, pid_t, time_t};
|
||||
|
||||
extern "C" {
|
||||
fn read(fd: c_int, buf: *mut c_void, count: size_t) -> ssize_t;
|
||||
fn lseek(fd: c_int, offset: off_t, whence: c_int) -> off_t;
|
||||
fn getpid() -> pid_t;
|
||||
}
|
||||
|
||||
// DO: Match C struct layout
|
||||
#[repr(C)]
|
||||
struct GoodStruct {
|
||||
count: c_int,
|
||||
size: c_long,
|
||||
data: *mut c_void,
|
||||
}
|
||||
|
||||
// DO: Use isize/usize for pointer-sized integers
|
||||
#[repr(C)]
|
||||
struct PointerSized {
|
||||
offset: isize, // intptr_t equivalent
|
||||
size: usize, // size_t in pointer arithmetic
|
||||
}
|
||||
```
|
||||
|
||||
## Type Mapping Reference
|
||||
|
||||
| C Type | Rust Type | Notes |
|
||||
|--------|-----------|-------|
|
||||
| `char` | `c_char` | May be signed or unsigned! |
|
||||
| `signed char` | `i8` | |
|
||||
| `unsigned char` | `u8` | |
|
||||
| `short` | `c_short` | Usually i16 |
|
||||
| `int` | `c_int` | Usually i32 |
|
||||
| `long` | `c_long` | 32 or 64 bits! |
|
||||
| `long long` | `c_longlong` | Usually i64 |
|
||||
| `size_t` | `usize` or `libc::size_t` | |
|
||||
| `ssize_t` | `isize` or `libc::ssize_t` | |
|
||||
| `float` | `c_float` / `f32` | |
|
||||
| `double` | `c_double` / `f64` | |
|
||||
| `void*` | `*mut c_void` | |
|
||||
| `const void*` | `*const c_void` | |
|
||||
|
||||
## Platform Differences
|
||||
|
||||
```rust
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
type PtrDiff = i64;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
type PtrDiff = i32;
|
||||
|
||||
// Better: use isize
|
||||
let diff: isize = ptr1 as isize - ptr2 as isize;
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Am I using std::os::raw or libc types for FFI?
|
||||
- [ ] Have I avoided assuming c_long is 64 bits?
|
||||
- [ ] Am I using size_t/usize for sizes?
|
||||
- [ ] Have I tested on multiple platforms?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ffi-13`: Ensure consistent data layout
|
||||
- `ffi-14`: Types in FFI should have stable layout
|
||||
151
skills/unsafe-checker/rules/ffi-06-string-abi.md
Normal file
151
skills/unsafe-checker/rules/ffi-06-string-abi.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
id: ffi-06
|
||||
original_id: P.UNS.FFI.06
|
||||
level: P
|
||||
impact: HIGH
|
||||
---
|
||||
|
||||
# Ensure C-ABI Compatibility for Strings Between Rust and C
|
||||
|
||||
## Summary
|
||||
|
||||
When passing strings across FFI, ensure both sides agree on encoding, null-termination, and memory ownership.
|
||||
|
||||
## Rationale
|
||||
|
||||
- Rust strings are UTF-8, C strings are byte arrays
|
||||
- C expects null termination, Rust strings don't have it
|
||||
- Memory ownership must be explicit to avoid leaks/double-frees
|
||||
|
||||
## String Passing Patterns
|
||||
|
||||
### Rust to C (Caller Allocates)
|
||||
|
||||
```rust
|
||||
use std::ffi::CString;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
extern "C" {
|
||||
fn c_process_string(s: *const c_char);
|
||||
}
|
||||
|
||||
fn rust_to_c(s: &str) -> Result<(), std::ffi::NulError> {
|
||||
let c_string = CString::new(s)?;
|
||||
// c_string lives until end of scope
|
||||
unsafe {
|
||||
c_process_string(c_string.as_ptr());
|
||||
}
|
||||
// c_string dropped here, memory freed
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### C to Rust (C Allocates, Rust Borrows)
|
||||
|
||||
```rust
|
||||
use std::ffi::CStr;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
extern "C" {
|
||||
fn c_get_string() -> *const c_char;
|
||||
}
|
||||
|
||||
fn c_to_rust() -> Option<String> {
|
||||
let ptr = unsafe { c_get_string() };
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
// Borrow from C, don't take ownership
|
||||
let c_str = unsafe { CStr::from_ptr(ptr) };
|
||||
Some(c_str.to_string_lossy().into_owned())
|
||||
}
|
||||
```
|
||||
|
||||
### C to Rust (Ownership Transfer)
|
||||
|
||||
```rust
|
||||
extern "C" {
|
||||
fn c_create_string() -> *mut c_char;
|
||||
fn c_free_string(s: *mut c_char);
|
||||
}
|
||||
|
||||
struct CAllocatedString {
|
||||
ptr: *mut c_char,
|
||||
}
|
||||
|
||||
impl CAllocatedString {
|
||||
fn new() -> Option<Self> {
|
||||
let ptr = unsafe { c_create_string() };
|
||||
if ptr.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(Self { ptr })
|
||||
}
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &str {
|
||||
let c_str = unsafe { CStr::from_ptr(self.ptr) };
|
||||
c_str.to_str().unwrap_or("")
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CAllocatedString {
|
||||
fn drop(&mut self) {
|
||||
unsafe { c_free_string(self.ptr); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Rust to C (Ownership Transfer)
|
||||
|
||||
```rust
|
||||
extern "C" {
|
||||
fn c_take_ownership(s: *mut c_char); // C will free
|
||||
}
|
||||
|
||||
fn give_to_c(s: &str) -> Result<(), std::ffi::NulError> {
|
||||
let c_string = CString::new(s)?;
|
||||
let ptr = c_string.into_raw(); // Don't drop CString
|
||||
|
||||
unsafe {
|
||||
c_take_ownership(ptr);
|
||||
// C now owns this memory
|
||||
// To free it back in Rust: let _ = CString::from_raw(ptr);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Encoding Considerations
|
||||
|
||||
```rust
|
||||
// UTF-8 to platform encoding
|
||||
use std::ffi::OsString;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
fn to_platform_string(s: &str) -> CString {
|
||||
// On Unix, UTF-8 usually works
|
||||
CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn to_wide_string(s: &str) -> Vec<u16> {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
std::ffi::OsStr::new(s)
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect()
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Is the string null-terminated when passed to C?
|
||||
- [ ] Who allocates the memory? Who frees it?
|
||||
- [ ] Is the encoding (UTF-8, ASCII, platform) documented?
|
||||
- [ ] Am I handling conversion errors (interior nulls, invalid UTF-8)?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ffi-01`: Use CString/CStr at FFI boundaries
|
||||
- `ffi-02`: Read std::ffi documentation
|
||||
131
skills/unsafe-checker/rules/ffi-07-no-drop-external.md
Normal file
131
skills/unsafe-checker/rules/ffi-07-no-drop-external.md
Normal file
@@ -0,0 +1,131 @@
|
||||
---
|
||||
id: ffi-07
|
||||
original_id: P.UNS.FFI.07
|
||||
level: P
|
||||
impact: HIGH
|
||||
---
|
||||
|
||||
# Do Not Implement Drop for Types Passed to External Code
|
||||
|
||||
## Summary
|
||||
|
||||
If a type will be passed to external code that manages its lifetime, don't implement `Drop`. Otherwise, both Rust and the external code will try to free it.
|
||||
|
||||
## Rationale
|
||||
|
||||
- External code (C library) may take ownership of the data
|
||||
- If Rust also tries to drop it, you get double-free
|
||||
- Need clear ownership boundaries
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Drop on type that external code will free
|
||||
#[repr(C)]
|
||||
struct EventHandler {
|
||||
callback: extern "C" fn(i32),
|
||||
user_data: *mut c_void,
|
||||
}
|
||||
|
||||
impl Drop for EventHandler {
|
||||
fn drop(&mut self) {
|
||||
// BAD: What if the C library already freed user_data?
|
||||
unsafe { libc::free(self.user_data); }
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
// C takes ownership and frees EventHandler when done
|
||||
fn register_handler(h: *mut EventHandler);
|
||||
}
|
||||
|
||||
fn bad_register() {
|
||||
let handler = EventHandler { /* ... */ };
|
||||
let ptr = Box::into_raw(Box::new(handler));
|
||||
unsafe {
|
||||
register_handler(ptr);
|
||||
// If C code frees this, and Rust's Drop runs too = double-free
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
// DO: No Drop for types whose lifetime is managed externally
|
||||
#[repr(C)]
|
||||
struct EventHandler {
|
||||
callback: extern "C" fn(i32),
|
||||
user_data: *mut c_void,
|
||||
}
|
||||
// No Drop impl - C library manages lifetime
|
||||
|
||||
extern "C" {
|
||||
fn register_handler(h: *mut EventHandler);
|
||||
fn unregister_handler(h: *mut EventHandler);
|
||||
}
|
||||
|
||||
// DO: Wrap in a Rust type that knows when it's safe to drop
|
||||
struct RegisteredHandler {
|
||||
ptr: *mut EventHandler,
|
||||
registered: bool,
|
||||
}
|
||||
|
||||
impl RegisteredHandler {
|
||||
fn register(handler: EventHandler) -> Self {
|
||||
let ptr = Box::into_raw(Box::new(handler));
|
||||
unsafe { register_handler(ptr); }
|
||||
Self { ptr, registered: true }
|
||||
}
|
||||
|
||||
fn unregister(&mut self) {
|
||||
if self.registered {
|
||||
unsafe { unregister_handler(self.ptr); }
|
||||
self.registered = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RegisteredHandler {
|
||||
fn drop(&mut self) {
|
||||
self.unregister();
|
||||
// Only free if we still own it
|
||||
if !self.registered {
|
||||
unsafe { drop(Box::from_raw(self.ptr)); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Use ManuallyDrop for explicit control
|
||||
use std::mem::ManuallyDrop;
|
||||
|
||||
fn explicit_ownership() {
|
||||
let handler = ManuallyDrop::new(EventHandler { /* ... */ });
|
||||
let ptr = &*handler as *const EventHandler as *mut EventHandler;
|
||||
unsafe {
|
||||
register_handler(ptr);
|
||||
// C now owns handler, don't drop it in Rust
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Ownership Patterns
|
||||
|
||||
| Pattern | Who Owns | Rust Drop? |
|
||||
|---------|----------|------------|
|
||||
| Rust creates, Rust frees | Rust | Yes |
|
||||
| Rust creates, C frees | C | No |
|
||||
| C creates, C frees | C | No (use wrapper) |
|
||||
| C creates, Rust frees | Rust | Yes (in wrapper) |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Who will free this type's memory?
|
||||
- [ ] If external code frees it, am I avoiding Drop?
|
||||
- [ ] If ownership is conditional, do I track it?
|
||||
- [ ] Am I using ManuallyDrop or forget() when transferring ownership?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ffi-03`: Implement Drop for wrapped C pointers (opposite case)
|
||||
- `mem-03`: Don't let String/Vec drop foreign memory
|
||||
146
skills/unsafe-checker/rules/ffi-08-error-handling.md
Normal file
146
skills/unsafe-checker/rules/ffi-08-error-handling.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
id: ffi-08
|
||||
original_id: P.UNS.FFI.08
|
||||
level: P
|
||||
impact: HIGH
|
||||
---
|
||||
|
||||
# Handle Errors Properly in FFI
|
||||
|
||||
## Summary
|
||||
|
||||
FFI functions must use C-compatible error handling (return codes, errno, out parameters). Rust's Result/Option don't cross FFI boundaries.
|
||||
|
||||
## Rationale
|
||||
|
||||
- C doesn't have Result or Option
|
||||
- Exceptions don't exist in C
|
||||
- Must use patterns C code understands
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Return Result across FFI
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bad_open(path: *const c_char) -> Result<Handle, Error> {
|
||||
// Result is not C-compatible!
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// DON'T: Return Option across FFI
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bad_find(id: i32) -> Option<*mut Data> {
|
||||
// Option<*mut T> might work but is confusing
|
||||
unimplemented!()
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
use std::os::raw::{c_char, c_int};
|
||||
|
||||
// Error codes
|
||||
const SUCCESS: c_int = 0;
|
||||
const ERR_NULL_PTR: c_int = 1;
|
||||
const ERR_INVALID_PATH: c_int = 2;
|
||||
const ERR_FILE_NOT_FOUND: c_int = 3;
|
||||
const ERR_PERMISSION: c_int = 4;
|
||||
const ERR_UNKNOWN: c_int = -1;
|
||||
|
||||
// DO: Return error code, output via pointer
|
||||
#[no_mangle]
|
||||
pub extern "C" fn open_file(
|
||||
path: *const c_char,
|
||||
out_handle: *mut *mut Handle
|
||||
) -> c_int {
|
||||
if path.is_null() || out_handle.is_null() {
|
||||
return ERR_NULL_PTR;
|
||||
}
|
||||
|
||||
let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return ERR_INVALID_PATH,
|
||||
};
|
||||
|
||||
match File::open(path_str) {
|
||||
Ok(file) => {
|
||||
let handle = Box::into_raw(Box::new(Handle { file }));
|
||||
unsafe { *out_handle = handle; }
|
||||
SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
match e.kind() {
|
||||
std::io::ErrorKind::NotFound => ERR_FILE_NOT_FOUND,
|
||||
std::io::ErrorKind::PermissionDenied => ERR_PERMISSION,
|
||||
_ => ERR_UNKNOWN,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Use errno for POSIX-style APIs
|
||||
#[cfg(unix)]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn posix_style_read(
|
||||
fd: c_int,
|
||||
buf: *mut u8,
|
||||
count: usize
|
||||
) -> isize {
|
||||
if buf.is_null() {
|
||||
unsafe { *libc::__errno_location() = libc::EINVAL; }
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ... do read ...
|
||||
// On error:
|
||||
// unsafe { *libc::__errno_location() = error_code; }
|
||||
// return -1;
|
||||
|
||||
count as isize
|
||||
}
|
||||
|
||||
// DO: Provide error message function
|
||||
thread_local! {
|
||||
static LAST_ERROR: std::cell::RefCell<Option<String>> = std::cell::RefCell::new(None);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn get_error_message(buf: *mut c_char, len: usize) -> c_int {
|
||||
LAST_ERROR.with(|e| {
|
||||
if let Some(msg) = e.borrow().as_ref() {
|
||||
let bytes = msg.as_bytes();
|
||||
let copy_len = std::cmp::min(bytes.len(), len.saturating_sub(1));
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len);
|
||||
*buf.add(copy_len) = 0;
|
||||
}
|
||||
SUCCESS
|
||||
} else {
|
||||
ERR_UNKNOWN
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling Patterns
|
||||
|
||||
| Pattern | Usage |
|
||||
|---------|-------|
|
||||
| Return code | Simple success/failure |
|
||||
| Return code + out param | Return value on success |
|
||||
| errno | POSIX-style APIs |
|
||||
| Error message function | Detailed error info |
|
||||
| Last-error thread-local | Windows-style APIs |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Am I returning C-compatible error indicators?
|
||||
- [ ] Are output parameters used for return values?
|
||||
- [ ] Is there a way to get detailed error info?
|
||||
- [ ] Am I documenting all possible error codes?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ffi-04`: Handle panics at FFI boundary
|
||||
- `safety-10`: Document safety requirements
|
||||
136
skills/unsafe-checker/rules/ffi-09-ref-not-ptr.md
Normal file
136
skills/unsafe-checker/rules/ffi-09-ref-not-ptr.md
Normal file
@@ -0,0 +1,136 @@
|
||||
---
|
||||
id: ffi-09
|
||||
original_id: P.UNS.FFI.09
|
||||
level: P
|
||||
impact: MEDIUM
|
||||
---
|
||||
|
||||
# Use References Instead of Raw Pointers When Calling Safe C Functions
|
||||
|
||||
## Summary
|
||||
|
||||
When wrapping C functions that don't need null pointers, use Rust references in the safe wrapper to enforce non-null at compile time.
|
||||
|
||||
## Rationale
|
||||
|
||||
- References guarantee non-null
|
||||
- References have lifetime tracking
|
||||
- Raw pointers should stay in the unsafe FFI layer
|
||||
- Safe Rust API should use safe types
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
extern "C" {
|
||||
fn c_process(data: *const u8, len: usize);
|
||||
}
|
||||
|
||||
// DON'T: Expose raw pointers in safe API
|
||||
pub fn process(data: *const u8, len: usize) {
|
||||
// Caller might pass null!
|
||||
unsafe { c_process(data, len); }
|
||||
}
|
||||
|
||||
// DON'T: Unsafe function when it could be safe
|
||||
pub unsafe fn process_unsafe(data: *const u8, len: usize) {
|
||||
// Why force caller to use unsafe?
|
||||
c_process(data, len);
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
extern "C" {
|
||||
fn c_process(data: *const u8, len: usize);
|
||||
fn c_modify(data: *mut Data);
|
||||
fn c_optional(data: *const Data); // Can be null
|
||||
}
|
||||
|
||||
// DO: Use slice reference for safe API
|
||||
pub fn process(data: &[u8]) {
|
||||
// Reference guarantees non-null
|
||||
// Slice guarantees valid length
|
||||
unsafe { c_process(data.as_ptr(), data.len()); }
|
||||
}
|
||||
|
||||
// DO: Use &mut for exclusive access
|
||||
pub fn modify(data: &mut Data) {
|
||||
// Mutable reference guarantees:
|
||||
// - Non-null
|
||||
// - Exclusive access
|
||||
// - Valid for duration
|
||||
unsafe { c_modify(data as *mut Data); }
|
||||
}
|
||||
|
||||
// DO: Use Option<&T> for nullable parameters
|
||||
pub fn optional(data: Option<&Data>) {
|
||||
let ptr = data.map(|d| d as *const Data).unwrap_or(std::ptr::null());
|
||||
unsafe { c_optional(ptr); }
|
||||
}
|
||||
|
||||
// DO: Wrap FFI types in safe Rust types
|
||||
pub struct SafeHandle(*mut c_void);
|
||||
|
||||
impl SafeHandle {
|
||||
pub fn new() -> Option<Self> {
|
||||
let ptr = unsafe { create_handle() };
|
||||
if ptr.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(Self(ptr))
|
||||
}
|
||||
}
|
||||
|
||||
// Methods take &self or &mut self, not raw pointers
|
||||
pub fn do_something(&self) {
|
||||
unsafe { handle_operation(self.0); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Converting Between References and Pointers
|
||||
|
||||
```rust
|
||||
// Reference to pointer
|
||||
fn ref_to_ptr(r: &Data) -> *const Data {
|
||||
r as *const Data
|
||||
}
|
||||
|
||||
fn mut_ref_to_ptr(r: &mut Data) -> *mut Data {
|
||||
r as *mut Data
|
||||
}
|
||||
|
||||
// Slice to pointer
|
||||
fn slice_to_ptr(s: &[u8]) -> (*const u8, usize) {
|
||||
(s.as_ptr(), s.len())
|
||||
}
|
||||
|
||||
// Pointer to reference (unsafe)
|
||||
unsafe fn ptr_to_ref<'a>(p: *const Data) -> &'a Data {
|
||||
&*p
|
||||
}
|
||||
|
||||
unsafe fn ptr_to_mut<'a>(p: *mut Data) -> &'a mut Data {
|
||||
&mut *p
|
||||
}
|
||||
```
|
||||
|
||||
## When to Use Raw Pointers
|
||||
|
||||
- FFI declarations (`extern "C"`)
|
||||
- Implementing the unsafe boundary layer
|
||||
- When null is a valid value
|
||||
- When the pointee might not be valid Rust (e.g., uninitialized)
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Can this parameter be a reference instead of a pointer?
|
||||
- [ ] Am I checking for null in the unsafe layer?
|
||||
- [ ] Is the safe API free of raw pointers?
|
||||
- [ ] Do I use Option<&T> for nullable references?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `safety-06`: Don't expose raw pointers in public APIs
|
||||
- `ffi-02`: Read std::ffi documentation
|
||||
132
skills/unsafe-checker/rules/ffi-10-thread-safety.md
Normal file
132
skills/unsafe-checker/rules/ffi-10-thread-safety.md
Normal file
@@ -0,0 +1,132 @@
|
||||
---
|
||||
id: ffi-10
|
||||
original_id: P.UNS.FFI.10
|
||||
level: P
|
||||
impact: CRITICAL
|
||||
---
|
||||
|
||||
# Exported Rust Functions Must Be Designed for Thread-Safety
|
||||
|
||||
## Summary
|
||||
|
||||
Functions exported to C with `#[no_mangle] extern "C"` may be called from multiple threads. Ensure they are thread-safe.
|
||||
|
||||
## Rationale
|
||||
|
||||
- C code doesn't know about Rust's thread safety guarantees
|
||||
- C may call your function from any thread
|
||||
- Global state must be synchronized
|
||||
- Race conditions are undefined behavior
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Unsynchronized global state
|
||||
static mut COUNTER: i32 = 0;
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn increment() -> i32 {
|
||||
unsafe {
|
||||
COUNTER += 1; // Data race if called from multiple threads!
|
||||
COUNTER
|
||||
}
|
||||
}
|
||||
|
||||
// DON'T: Thread-local assuming single thread
|
||||
thread_local! {
|
||||
static CONFIG: RefCell<Config> = RefCell::new(Config::default());
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_config(value: i32) {
|
||||
// Different threads get different configs!
|
||||
// Is that what the C caller expects?
|
||||
CONFIG.with(|c| c.borrow_mut().value = value);
|
||||
}
|
||||
|
||||
// DON'T: Non-Send types in globals
|
||||
static mut HANDLE: Option<Rc<Data>> = None; // Rc is not Send!
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
// DO: Use atomics for simple counters
|
||||
static COUNTER: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn increment() -> i32 {
|
||||
COUNTER.fetch_add(1, Ordering::SeqCst) + 1
|
||||
}
|
||||
|
||||
// DO: Use Mutex for complex state
|
||||
static CONFIG: OnceLock<Mutex<Config>> = OnceLock::new();
|
||||
|
||||
fn get_config() -> &'static Mutex<Config> {
|
||||
CONFIG.get_or_init(|| Mutex::new(Config::default()))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_config_value(value: i32) -> i32 {
|
||||
match get_config().lock() {
|
||||
Ok(mut config) => {
|
||||
config.value = value;
|
||||
0 // Success
|
||||
}
|
||||
Err(_) => -1 // Lock poisoned
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Document thread safety requirements
|
||||
/// Initializes the library. NOT thread-safe.
|
||||
/// Must be called once from main thread before any other function.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn init() -> i32 {
|
||||
// One-time initialization
|
||||
0
|
||||
}
|
||||
|
||||
/// Processes data. Thread-safe.
|
||||
/// May be called from multiple threads concurrently.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn process(data: *const u8, len: usize) -> i32 {
|
||||
// Uses only local state or synchronized globals
|
||||
0
|
||||
}
|
||||
|
||||
// DO: Make non-thread-safe APIs explicit
|
||||
/// Handle for single-threaded use only.
|
||||
///
|
||||
/// # Thread Safety
|
||||
///
|
||||
/// This handle must only be used from the thread that created it.
|
||||
struct SingleThreadHandle {
|
||||
data: *mut Data,
|
||||
_not_send: std::marker::PhantomData<*const ()>, // !Send
|
||||
}
|
||||
```
|
||||
|
||||
## Synchronization Patterns
|
||||
|
||||
| Pattern | Use Case |
|
||||
|---------|----------|
|
||||
| `AtomicT` | Simple counters, flags |
|
||||
| `Mutex<T>` | Complex shared state |
|
||||
| `RwLock<T>` | Read-heavy shared state |
|
||||
| `OnceLock<T>` | Lazy one-time init |
|
||||
| `thread_local!` | Per-thread state (document!) |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Does my exported function access global state?
|
||||
- [ ] Is that state properly synchronized?
|
||||
- [ ] Have I documented thread-safety guarantees?
|
||||
- [ ] Are any types !Send/!Sync exposed across FFI?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ptr-01`: Don't share raw pointers across threads
|
||||
- `safety-05`: Send/Sync implementation safety
|
||||
142
skills/unsafe-checker/rules/ffi-11-packed-ub.md
Normal file
142
skills/unsafe-checker/rules/ffi-11-packed-ub.md
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
id: ffi-11
|
||||
original_id: P.UNS.FFI.11
|
||||
level: P
|
||||
impact: HIGH
|
||||
clippy: unaligned_references
|
||||
---
|
||||
|
||||
# Be Careful with UB When Referencing #[repr(packed)] Struct Fields
|
||||
|
||||
## Summary
|
||||
|
||||
Creating references to fields in `#[repr(packed)]` structs is undefined behavior if the field is misaligned. Use raw pointers and `read_unaligned`/`write_unaligned` instead.
|
||||
|
||||
## Rationale
|
||||
|
||||
- Packed structs have no padding, so fields may be misaligned
|
||||
- References must be aligned; misaligned references are UB
|
||||
- Even implicit references (method calls, match) can cause UB
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
#[repr(C, packed)]
|
||||
struct Packet {
|
||||
header: u8,
|
||||
value: u32, // Misaligned! At offset 1, not 4
|
||||
data: u64, // Misaligned! At offset 5, not 8
|
||||
}
|
||||
|
||||
fn bad_reference(p: &Packet) -> &u32 {
|
||||
&p.value // UB: Creates misaligned reference!
|
||||
}
|
||||
|
||||
fn bad_match(p: &Packet) {
|
||||
match p.value { // UB: Match creates a reference
|
||||
0 => {},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn bad_method(p: &Packet) {
|
||||
p.value.to_string(); // UB: Method call creates reference
|
||||
}
|
||||
|
||||
fn bad_borrow(p: &mut Packet) {
|
||||
let v = &mut p.value; // UB: Misaligned mutable reference
|
||||
*v = 42;
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
#[repr(C, packed)]
|
||||
struct Packet {
|
||||
header: u8,
|
||||
value: u32,
|
||||
data: u64,
|
||||
}
|
||||
|
||||
// DO: Copy out the value
|
||||
fn good_read(p: &Packet) -> u32 {
|
||||
p.value // Copies the value, no reference created
|
||||
}
|
||||
|
||||
// DO: Use addr_of! for raw pointer (Rust 2021+)
|
||||
fn good_ptr_read(p: &Packet) -> u32 {
|
||||
// SAFETY: read_unaligned handles misalignment
|
||||
unsafe {
|
||||
std::ptr::addr_of!(p.value).read_unaligned()
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Use addr_of_mut! for writing
|
||||
fn good_ptr_write(p: &mut Packet, value: u32) {
|
||||
// SAFETY: write_unaligned handles misalignment
|
||||
unsafe {
|
||||
std::ptr::addr_of_mut!(p.value).write_unaligned(value);
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Create accessor methods
|
||||
impl Packet {
|
||||
fn value(&self) -> u32 {
|
||||
unsafe { std::ptr::addr_of!(self.value).read_unaligned() }
|
||||
}
|
||||
|
||||
fn set_value(&mut self, value: u32) {
|
||||
unsafe { std::ptr::addr_of_mut!(self.value).write_unaligned(value); }
|
||||
}
|
||||
|
||||
fn data(&self) -> u64 {
|
||||
unsafe { std::ptr::addr_of!(self.data).read_unaligned() }
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Consider using byte arrays + from_ne_bytes
|
||||
#[repr(C, packed)]
|
||||
struct PacketBytes {
|
||||
header: u8,
|
||||
value: [u8; 4], // Store as bytes
|
||||
data: [u8; 8],
|
||||
}
|
||||
|
||||
impl PacketBytes {
|
||||
fn value(&self) -> u32 {
|
||||
u32::from_ne_bytes(self.value) // Safe, no alignment issue
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Safe Alternatives
|
||||
|
||||
```rust
|
||||
// Alternative 1: Don't use packed
|
||||
#[repr(C)]
|
||||
struct AlignedPacket {
|
||||
header: u8,
|
||||
_pad: [u8; 3],
|
||||
value: u32,
|
||||
data: u64,
|
||||
}
|
||||
|
||||
// Alternative 2: Use zerocopy crate
|
||||
// use zerocopy::{AsBytes, FromBytes};
|
||||
|
||||
// Alternative 3: Use bytemuck
|
||||
// use bytemuck::{Pod, Zeroable};
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Am I creating references to packed struct fields?
|
||||
- [ ] Am I using addr_of! / addr_of_mut! for field access?
|
||||
- [ ] Am I using read_unaligned / write_unaligned?
|
||||
- [ ] Would a byte array representation be safer?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ptr-04`: Don't dereference misaligned pointers
|
||||
- `mem-01`: Choose appropriate data layout
|
||||
164
skills/unsafe-checker/rules/ffi-12-invariant-doc.md
Normal file
164
skills/unsafe-checker/rules/ffi-12-invariant-doc.md
Normal file
@@ -0,0 +1,164 @@
|
||||
---
|
||||
id: ffi-12
|
||||
original_id: P.UNS.FFI.12
|
||||
level: P
|
||||
impact: MEDIUM
|
||||
---
|
||||
|
||||
# Document Invariant Assumptions for C-Provided Parameters
|
||||
|
||||
## Summary
|
||||
|
||||
When receiving parameters from C, document what invariants you assume (non-null, alignment, validity, lifetime) and verify them when possible.
|
||||
|
||||
## Rationale
|
||||
|
||||
- C doesn't enforce invariants at compile time
|
||||
- Rust code needs to validate or document assumptions
|
||||
- Debugging FFI bugs is hard without clear documentation
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Undocumented assumptions
|
||||
extern "C" {
|
||||
fn get_data() -> *mut Data;
|
||||
}
|
||||
|
||||
fn bad_use() -> &'static Data {
|
||||
let ptr = unsafe { get_data() };
|
||||
// Assumes:
|
||||
// - ptr is non-null (not documented)
|
||||
// - ptr is aligned (not checked)
|
||||
// - Data is valid (not verified)
|
||||
// - Lifetime is 'static (just guessing)
|
||||
unsafe { &*ptr }
|
||||
}
|
||||
|
||||
// DON'T: Silent assumptions in function signature
|
||||
#[no_mangle]
|
||||
pub extern "C" fn process(data: *const Data, len: usize) {
|
||||
// What if data is null?
|
||||
// What if len is wrong?
|
||||
// What if data contains invalid Data?
|
||||
let slice = unsafe {
|
||||
std::slice::from_raw_parts(data, len)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
/// Retrieves data from the C library.
|
||||
///
|
||||
/// # Invariants Assumed from C
|
||||
///
|
||||
/// - Returns a non-null pointer on success, null on failure
|
||||
/// - Returned pointer is valid for the lifetime of the library
|
||||
/// - Returned pointer is aligned for `Data`
|
||||
/// - The `Data` struct is fully initialized
|
||||
extern "C" {
|
||||
fn get_data() -> *mut Data;
|
||||
}
|
||||
|
||||
fn documented_use() -> Option<&'static Data> {
|
||||
let ptr = unsafe { get_data() };
|
||||
|
||||
// Verify what we can
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Document what we can't verify
|
||||
// SAFETY:
|
||||
// - Non-null: checked above
|
||||
// - Aligned: documented in C library docs
|
||||
// - Valid: C library guarantees initialized Data
|
||||
// - Lifetime: C library guarantees static lifetime
|
||||
Some(unsafe { &*ptr })
|
||||
}
|
||||
|
||||
/// Processes data provided by C caller.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// - `data`: Must be non-null, aligned for `Data`, and point to `len` valid `Data` items
|
||||
/// - `len`: Number of items. Must not exceed `isize::MAX / size_of::<Data>()`
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// - `0` on success
|
||||
/// - `-1` if `data` is null
|
||||
/// - `-2` if `len` is invalid
|
||||
///
|
||||
/// # Thread Safety
|
||||
///
|
||||
/// This function is thread-safe. The `data` array must not be mutated during the call.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn process_documented(data: *const Data, len: usize) -> i32 {
|
||||
// Verify invariants we can check
|
||||
if data.is_null() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if len > isize::MAX as usize / std::mem::size_of::<Data>() {
|
||||
return -2;
|
||||
}
|
||||
|
||||
// SAFETY:
|
||||
// - Non-null: checked above
|
||||
// - Aligned: documented requirement for caller
|
||||
// - Valid for len items: documented requirement for caller
|
||||
// - Not mutated: documented thread safety requirement
|
||||
let slice = unsafe { std::slice::from_raw_parts(data, len) };
|
||||
|
||||
for item in slice {
|
||||
// process...
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation Template
|
||||
|
||||
```rust
|
||||
/// Brief description.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// - `param`: Description, constraints (non-null, aligned, etc.)
|
||||
///
|
||||
/// # Invariants Assumed
|
||||
///
|
||||
/// The following invariants are assumed and NOT verified:
|
||||
/// - Invariant 1: explanation
|
||||
/// - Invariant 2: explanation
|
||||
///
|
||||
/// The following invariants ARE verified at runtime:
|
||||
/// - Verified 1: how it's checked
|
||||
///
|
||||
/// # Safety (for unsafe fn)
|
||||
///
|
||||
/// Caller must ensure:
|
||||
/// - Requirement 1
|
||||
/// - Requirement 2
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns error code when:
|
||||
/// - Condition 1: error code
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Have I documented all assumptions about C parameters?
|
||||
- [ ] Which invariants can I verify at runtime?
|
||||
- [ ] Which must I trust the C caller to uphold?
|
||||
- [ ] Have I documented error conditions and return values?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `safety-02`: Verify safety invariants
|
||||
- `safety-10`: Document safety requirements
|
||||
146
skills/unsafe-checker/rules/ffi-13-data-layout.md
Normal file
146
skills/unsafe-checker/rules/ffi-13-data-layout.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
id: ffi-13
|
||||
original_id: P.UNS.FFI.13
|
||||
level: P
|
||||
impact: HIGH
|
||||
---
|
||||
|
||||
# Ensure Consistent Data Layout for Custom Types
|
||||
|
||||
## Summary
|
||||
|
||||
Types shared between Rust and C must have `#[repr(C)]` to ensure the memory layout matches what C expects.
|
||||
|
||||
## Rationale
|
||||
|
||||
- Rust's default layout is unspecified and may change
|
||||
- C has specific, standardized layout rules
|
||||
- Mismatched layouts cause memory corruption
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Rust layout for FFI types
|
||||
struct BadStruct {
|
||||
a: u8,
|
||||
b: u32,
|
||||
c: u8,
|
||||
}
|
||||
// Rust may reorder to: b, a, c (for better packing)
|
||||
// C expects: a, padding, b, c, padding
|
||||
|
||||
extern "C" {
|
||||
fn use_struct(s: *const BadStruct); // Layout mismatch!
|
||||
}
|
||||
|
||||
// DON'T: Assume Rust enum layout matches C
|
||||
enum BadEnum {
|
||||
A,
|
||||
B(i32),
|
||||
C { x: u8, y: u8 },
|
||||
}
|
||||
// Rust enum layout is complex and not C-compatible
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
// DO: Use repr(C) for FFI structs
|
||||
#[repr(C)]
|
||||
struct GoodStruct {
|
||||
a: u8, // offset 0
|
||||
// 3 bytes padding
|
||||
b: u32, // offset 4
|
||||
c: u8, // offset 8
|
||||
// 3 bytes padding
|
||||
}
|
||||
// Total size: 12, align: 4
|
||||
|
||||
// DO: Use repr(C) for enums with explicit discriminant
|
||||
#[repr(C)]
|
||||
enum GoodEnum {
|
||||
A = 0,
|
||||
B = 1,
|
||||
C = 2,
|
||||
}
|
||||
// Equivalent to C: enum { A = 0, B = 1, C = 2 };
|
||||
|
||||
// DO: For complex enums, use tagged unions
|
||||
#[repr(C)]
|
||||
struct TaggedUnion {
|
||||
tag: GoodEnum,
|
||||
data: GoodUnionData,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
union GoodUnionData {
|
||||
a: (), // For GoodEnum::A
|
||||
b: i32, // For GoodEnum::B
|
||||
c: [u8; 2], // For GoodEnum::C
|
||||
}
|
||||
|
||||
// DO: Verify layout at compile time
|
||||
const _: () = {
|
||||
assert!(std::mem::size_of::<GoodStruct>() == 12);
|
||||
assert!(std::mem::align_of::<GoodStruct>() == 4);
|
||||
};
|
||||
```
|
||||
|
||||
## Layout Verification
|
||||
|
||||
```rust
|
||||
use std::mem::{size_of, align_of, offset_of};
|
||||
|
||||
#[repr(C)]
|
||||
struct Verified {
|
||||
a: u8,
|
||||
b: u32,
|
||||
c: u8,
|
||||
}
|
||||
|
||||
// Compile-time layout verification
|
||||
const _: () = {
|
||||
assert!(size_of::<Verified>() == 12);
|
||||
assert!(align_of::<Verified>() == 4);
|
||||
// offset_of! requires nightly or crate
|
||||
// assert!(offset_of!(Verified, a) == 0);
|
||||
// assert!(offset_of!(Verified, b) == 4);
|
||||
// assert!(offset_of!(Verified, c) == 8);
|
||||
};
|
||||
|
||||
// Runtime verification
|
||||
#[test]
|
||||
fn verify_layout() {
|
||||
assert_eq!(size_of::<Verified>(), 12);
|
||||
assert_eq!(align_of::<Verified>(), 4);
|
||||
|
||||
let v = Verified { a: 0, b: 0, c: 0 };
|
||||
let base = &v as *const _ as usize;
|
||||
|
||||
assert_eq!(&v.a as *const _ as usize - base, 0);
|
||||
assert_eq!(&v.b as *const _ as usize - base, 4);
|
||||
assert_eq!(&v.c as *const _ as usize - base, 8);
|
||||
}
|
||||
```
|
||||
|
||||
## repr Options
|
||||
|
||||
| Attribute | Effect |
|
||||
|-----------|--------|
|
||||
| `#[repr(C)]` | C-compatible layout |
|
||||
| `#[repr(C, packed)]` | C layout, no padding |
|
||||
| `#[repr(C, align(N))]` | C layout, minimum align N |
|
||||
| `#[repr(transparent)]` | Same layout as single field |
|
||||
| `#[repr(u8)]` etc. | Enum discriminant type |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Is every FFI struct marked `#[repr(C)]`?
|
||||
- [ ] Is every FFI enum using explicit discriminants?
|
||||
- [ ] Have I verified the layout matches the C header?
|
||||
- [ ] Have I added compile-time assertions?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `mem-01`: Choose appropriate data layout
|
||||
- `ffi-14`: Types in FFI should have stable layout
|
||||
135
skills/unsafe-checker/rules/ffi-14-stable-layout.md
Normal file
135
skills/unsafe-checker/rules/ffi-14-stable-layout.md
Normal file
@@ -0,0 +1,135 @@
|
||||
---
|
||||
id: ffi-14
|
||||
original_id: P.UNS.FFI.14
|
||||
level: P
|
||||
impact: HIGH
|
||||
---
|
||||
|
||||
# Types Used in FFI Should Have Stable Layout
|
||||
|
||||
## Summary
|
||||
|
||||
FFI types should not change layout between versions. Use `#[repr(C)]` and avoid types with unstable layout like generic `std` types.
|
||||
|
||||
## Rationale
|
||||
|
||||
- ABI compatibility requires stable layout
|
||||
- Dynamic libraries may be loaded with different compiler versions
|
||||
- Layout changes break binary compatibility
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Use Rust std types with unstable layout in FFI
|
||||
extern "C" {
|
||||
// Vec layout is not stable!
|
||||
fn bad_vec(v: Vec<i32>);
|
||||
|
||||
// String layout is not stable!
|
||||
fn bad_string(s: String);
|
||||
|
||||
// HashMap layout varies between versions
|
||||
fn bad_map(m: std::collections::HashMap<i32, i32>);
|
||||
}
|
||||
|
||||
// DON'T: Use Rust-specific types in C structs
|
||||
#[repr(C)]
|
||||
struct BadMixed {
|
||||
id: i32,
|
||||
data: Vec<u8>, // Vec is not C-compatible!
|
||||
}
|
||||
|
||||
// DON'T: Use Option with non-null optimization assumptions
|
||||
#[repr(C)]
|
||||
struct BadOption {
|
||||
value: Option<std::num::NonZeroU32>, // Layout may change!
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
use std::os::raw::{c_int, c_char, c_void};
|
||||
|
||||
// DO: Use C-compatible types
|
||||
#[repr(C)]
|
||||
struct GoodStruct {
|
||||
id: c_int,
|
||||
name: *const c_char, // C-style string
|
||||
data: *const c_void, // Generic pointer
|
||||
data_len: usize,
|
||||
}
|
||||
|
||||
// DO: Use explicit struct for what Vec would provide
|
||||
#[repr(C)]
|
||||
struct GoodBuffer {
|
||||
ptr: *mut u8,
|
||||
len: usize,
|
||||
cap: usize,
|
||||
}
|
||||
|
||||
impl GoodBuffer {
|
||||
fn from_vec(mut v: Vec<u8>) -> Self {
|
||||
let buf = Self {
|
||||
ptr: v.as_mut_ptr(),
|
||||
len: v.len(),
|
||||
cap: v.capacity(),
|
||||
};
|
||||
std::mem::forget(v);
|
||||
buf
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Must have been created by from_vec()
|
||||
unsafe fn into_vec(self) -> Vec<u8> {
|
||||
Vec::from_raw_parts(self.ptr, self.len, self.cap)
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Use fixed-size arrays for bounded data
|
||||
#[repr(C)]
|
||||
struct FixedName {
|
||||
name: [c_char; 64],
|
||||
name_len: usize,
|
||||
}
|
||||
|
||||
// DO: Define your own stable option type
|
||||
#[repr(C)]
|
||||
struct OptionalU32 {
|
||||
has_value: bool,
|
||||
value: u32,
|
||||
}
|
||||
|
||||
impl From<Option<u32>> for OptionalU32 {
|
||||
fn from(opt: Option<u32>) -> Self {
|
||||
match opt {
|
||||
Some(v) => Self { has_value: true, value: v },
|
||||
None => Self { has_value: false, value: 0 },
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Stable Types for FFI
|
||||
|
||||
| Use Instead Of | Stable Type |
|
||||
|----------------|-------------|
|
||||
| `Vec<T>` | `*mut T` + `len` + `cap` |
|
||||
| `String` | `*const c_char` or `*mut c_char` + `len` |
|
||||
| `&[T]` | `*const T` + `len` |
|
||||
| `Option<T>` | Custom tagged struct |
|
||||
| `Result<T, E>` | Error code + out parameter |
|
||||
| `Box<T>` | `*mut T` |
|
||||
| `bool` | `c_int` or explicit `u8` |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Am I using only C-compatible primitive types?
|
||||
- [ ] Am I avoiding std collection types in FFI signatures?
|
||||
- [ ] Have I created stable wrappers for Rust types?
|
||||
- [ ] Is the layout documented for other languages?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ffi-13`: Ensure consistent data layout
|
||||
- `ffi-05`: Use portable type aliases
|
||||
145
skills/unsafe-checker/rules/ffi-15-validate-external.md
Normal file
145
skills/unsafe-checker/rules/ffi-15-validate-external.md
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
id: ffi-15
|
||||
original_id: P.UNS.FFI.15
|
||||
level: P
|
||||
impact: HIGH
|
||||
---
|
||||
|
||||
# Validate Non-Robust External Values
|
||||
|
||||
## Summary
|
||||
|
||||
Data received from external sources (FFI, files, network) may be invalid. Validate before using it as Rust types with stricter invariants.
|
||||
|
||||
## Rationale
|
||||
|
||||
- External data can be malicious or corrupted
|
||||
- Rust types have invariants (e.g., valid UTF-8 for str)
|
||||
- Invalid data causes undefined behavior
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Trust external data
|
||||
extern "C" {
|
||||
fn get_status() -> u8;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Status { Active = 0, Inactive = 1, Pending = 2 }
|
||||
|
||||
fn bad_convert() -> Status {
|
||||
let raw = unsafe { get_status() };
|
||||
// BAD: Assumes C returns valid enum value
|
||||
unsafe { std::mem::transmute(raw) } // UB if raw > 2
|
||||
}
|
||||
|
||||
// DON'T: Trust strings from C
|
||||
fn bad_string(ptr: *const c_char) -> &str {
|
||||
let cstr = unsafe { CStr::from_ptr(ptr) };
|
||||
// BAD: Assumes valid UTF-8
|
||||
cstr.to_str().unwrap()
|
||||
}
|
||||
|
||||
// DON'T: Trust size values
|
||||
fn bad_size(ptr: *const u8, len: usize) -> Vec<u8> {
|
||||
// BAD: len could be huge, causing OOM
|
||||
// BAD: len could exceed actual data
|
||||
unsafe { std::slice::from_raw_parts(ptr, len) }.to_vec()
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
// DO: Validate enum values
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[repr(u8)]
|
||||
enum Status {
|
||||
Active = 0,
|
||||
Inactive = 1,
|
||||
Pending = 2,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for Status {
|
||||
type Error = InvalidStatusError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Status::Active),
|
||||
1 => Ok(Status::Inactive),
|
||||
2 => Ok(Status::Pending),
|
||||
_ => Err(InvalidStatusError(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn good_convert() -> Result<Status, InvalidStatusError> {
|
||||
let raw = unsafe { get_status() };
|
||||
Status::try_from(raw) // Returns error for invalid values
|
||||
}
|
||||
|
||||
// DO: Handle invalid UTF-8
|
||||
fn good_string(ptr: *const c_char) -> Result<String, std::str::Utf8Error> {
|
||||
if ptr.is_null() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
let cstr = unsafe { CStr::from_ptr(ptr) };
|
||||
cstr.to_str().map(|s| s.to_owned())
|
||||
}
|
||||
|
||||
fn good_string_lossy(ptr: *const c_char) -> String {
|
||||
if ptr.is_null() {
|
||||
return String::new();
|
||||
}
|
||||
let cstr = unsafe { CStr::from_ptr(ptr) };
|
||||
cstr.to_string_lossy().into_owned() // Replaces invalid UTF-8
|
||||
}
|
||||
|
||||
// DO: Validate sizes
|
||||
const MAX_REASONABLE_SIZE: usize = 100 * 1024 * 1024; // 100 MB
|
||||
|
||||
fn good_size(ptr: *const u8, len: usize) -> Result<Vec<u8>, ValidationError> {
|
||||
if ptr.is_null() {
|
||||
return Err(ValidationError::NullPointer);
|
||||
}
|
||||
if len > MAX_REASONABLE_SIZE {
|
||||
return Err(ValidationError::SizeTooLarge);
|
||||
}
|
||||
|
||||
// Still need to trust that ptr points to len valid bytes
|
||||
// Document this as a caller requirement
|
||||
let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
|
||||
Ok(slice.to_vec())
|
||||
}
|
||||
|
||||
// DO: Use num_enum for safe enum conversion
|
||||
// use num_enum::TryFromPrimitive;
|
||||
//
|
||||
// #[derive(TryFromPrimitive)]
|
||||
// #[repr(u8)]
|
||||
// enum Status { Active = 0, Inactive = 1, Pending = 2 }
|
||||
```
|
||||
|
||||
## Validation Patterns
|
||||
|
||||
| External Data | Validation |
|
||||
|---------------|------------|
|
||||
| Enum discriminant | Match against valid values |
|
||||
| String | Check UTF-8 or use lossy conversion |
|
||||
| Size/length | Check against maximum |
|
||||
| Pointer | Check for null |
|
||||
| Boolean | Explicit 0/1 check or treat any non-zero as true |
|
||||
| Float | Check for NaN, infinity if problematic |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Am I validating external enum values?
|
||||
- [ ] Am I handling potential invalid UTF-8?
|
||||
- [ ] Am I checking sizes against reasonable limits?
|
||||
- [ ] Am I using TryFrom instead of transmute?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ffi-12`: Document invariant assumptions
|
||||
- `safety-02`: Verify safety invariants
|
||||
141
skills/unsafe-checker/rules/ffi-16-closure-to-c.md
Normal file
141
skills/unsafe-checker/rules/ffi-16-closure-to-c.md
Normal file
@@ -0,0 +1,141 @@
|
||||
---
|
||||
id: ffi-16
|
||||
original_id: P.UNS.FFI.16
|
||||
level: P
|
||||
impact: HIGH
|
||||
---
|
||||
|
||||
# Separate Data and Code When Passing Rust Closures to C
|
||||
|
||||
## Summary
|
||||
|
||||
C callbacks are function pointers without captured state. To pass Rust closures to C, separate the function pointer from the closure data using a "trampoline" pattern.
|
||||
|
||||
## Rationale
|
||||
|
||||
- Rust closures can capture state (like lambdas)
|
||||
- C function pointers are just addresses, no state
|
||||
- Must pass state separately via `void*` user_data
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Try to pass closure directly
|
||||
extern "C" {
|
||||
fn set_callback(cb: fn(i32) -> i32); // Only works for non-capturing!
|
||||
}
|
||||
|
||||
fn bad_closure() {
|
||||
let multiplier = 2;
|
||||
let closure = |x| x * multiplier; // Captures multiplier
|
||||
|
||||
// This won't compile - closure is not fn pointer
|
||||
// set_callback(closure);
|
||||
}
|
||||
|
||||
// DON'T: Transmute closure to function pointer
|
||||
fn bad_transmute() {
|
||||
let closure = |x: i32| x * 2;
|
||||
let fp: fn(i32) -> i32 = unsafe { std::mem::transmute(closure) };
|
||||
// UB: Closure may have non-zero size
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
use std::os::raw::c_void;
|
||||
use std::ffi::c_int;
|
||||
|
||||
// C callback signature with user_data
|
||||
type CCallback = extern "C" fn(value: c_int, user_data: *mut c_void) -> c_int;
|
||||
|
||||
extern "C" {
|
||||
fn set_callback(cb: CCallback, user_data: *mut c_void);
|
||||
fn remove_callback();
|
||||
}
|
||||
|
||||
// DO: Use trampoline pattern
|
||||
fn good_closure<F: FnMut(i32) -> i32>(mut closure: F) {
|
||||
// Trampoline function that forwards to the closure
|
||||
extern "C" fn trampoline<F: FnMut(i32) -> i32>(
|
||||
value: c_int,
|
||||
user_data: *mut c_void,
|
||||
) -> c_int {
|
||||
let closure = unsafe { &mut *(user_data as *mut F) };
|
||||
closure(value as i32) as c_int
|
||||
}
|
||||
|
||||
let user_data = &mut closure as *mut F as *mut c_void;
|
||||
|
||||
unsafe {
|
||||
set_callback(trampoline::<F>, user_data);
|
||||
// Important: closure must live until callback is removed!
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Box the closure for 'static lifetime
|
||||
struct CallbackHandle {
|
||||
closure: Box<dyn FnMut(i32) -> i32>,
|
||||
}
|
||||
|
||||
impl CallbackHandle {
|
||||
fn new<F: FnMut(i32) -> i32 + 'static>(closure: F) -> Self {
|
||||
Self { closure: Box::new(closure) }
|
||||
}
|
||||
|
||||
fn register(&mut self) {
|
||||
extern "C" fn trampoline(value: c_int, user_data: *mut c_void) -> c_int {
|
||||
let closure = unsafe { &mut *(user_data as *mut Box<dyn FnMut(i32) -> i32>) };
|
||||
closure(value as i32) as c_int
|
||||
}
|
||||
|
||||
let user_data = &mut self.closure as *mut _ as *mut c_void;
|
||||
unsafe { set_callback(trampoline, user_data); }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CallbackHandle {
|
||||
fn drop(&mut self) {
|
||||
unsafe { remove_callback(); }
|
||||
// Now safe to drop closure
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
fn example() {
|
||||
let multiplier = 2;
|
||||
let mut handle = CallbackHandle::new(move |x| x * multiplier);
|
||||
handle.register();
|
||||
// handle must live until callback is no longer needed
|
||||
}
|
||||
```
|
||||
|
||||
## Trampoline Pattern
|
||||
|
||||
```
|
||||
Rust Closure: |x| x * captured_value
|
||||
|
|
||||
v
|
||||
+-----------------+ +-----------------+
|
||||
| trampoline fn | --> | closure data |
|
||||
| (no captures) | | (captured_value)|
|
||||
+-----------------+ +-----------------+
|
||||
| ^
|
||||
| user_data ptr |
|
||||
+-------------------------+
|
||||
|
||||
C sees: function pointer + void* user_data
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Does my closure capture any state?
|
||||
- [ ] Am I using the trampoline pattern?
|
||||
- [ ] Does the closure data live long enough?
|
||||
- [ ] Am I unregistering before dropping the closure?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ffi-03`: Implement Drop for resource wrappers
|
||||
- `ffi-10`: Thread safety for callbacks
|
||||
152
skills/unsafe-checker/rules/ffi-17-opaque-types.md
Normal file
152
skills/unsafe-checker/rules/ffi-17-opaque-types.md
Normal file
@@ -0,0 +1,152 @@
|
||||
---
|
||||
id: ffi-17
|
||||
original_id: P.UNS.FFI.17
|
||||
level: P
|
||||
impact: MEDIUM
|
||||
---
|
||||
|
||||
# Use Dedicated Opaque Type Pointers Instead of c_void for C Opaque Types
|
||||
|
||||
## Summary
|
||||
|
||||
Instead of using `*mut c_void` for opaque C handles, create dedicated marker types that provide type safety.
|
||||
|
||||
## Rationale
|
||||
|
||||
- `*mut c_void` accepts any pointer, easy to mix up handles
|
||||
- Dedicated types catch mistakes at compile time
|
||||
- Self-documenting code
|
||||
- Prevents accidental use of wrong free function
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
use std::ffi::c_void;
|
||||
|
||||
extern "C" {
|
||||
fn create_database() -> *mut c_void;
|
||||
fn create_connection() -> *mut c_void;
|
||||
fn execute(conn: *mut c_void, query: *const i8);
|
||||
fn close_database(db: *mut c_void);
|
||||
fn close_connection(conn: *mut c_void);
|
||||
}
|
||||
|
||||
fn bad_usage() {
|
||||
let db = unsafe { create_database() };
|
||||
let conn = unsafe { create_connection() };
|
||||
|
||||
// BUG: Passed db where conn was expected - compiles fine!
|
||||
unsafe { execute(db, b"SELECT 1\0".as_ptr() as *const i8) };
|
||||
|
||||
// BUG: Wrong close function - compiles fine!
|
||||
unsafe { close_connection(db) };
|
||||
unsafe { close_database(conn) };
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
use std::marker::PhantomData;
|
||||
|
||||
// DO: Define opaque marker types
|
||||
#[repr(C)]
|
||||
pub struct Database {
|
||||
_private: [u8; 0],
|
||||
_marker: PhantomData<(*mut u8, std::marker::PhantomPinned)>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct Connection {
|
||||
_private: [u8; 0],
|
||||
_marker: PhantomData<(*mut u8, std::marker::PhantomPinned)>,
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
fn create_database() -> *mut Database;
|
||||
fn create_connection(db: *mut Database) -> *mut Connection;
|
||||
fn execute(conn: *mut Connection, query: *const i8) -> i32;
|
||||
fn close_database(db: *mut Database);
|
||||
fn close_connection(conn: *mut Connection);
|
||||
}
|
||||
|
||||
fn good_usage() {
|
||||
let db = unsafe { create_database() };
|
||||
let conn = unsafe { create_connection(db) };
|
||||
|
||||
// Compile error: expected *mut Connection, found *mut Database
|
||||
// unsafe { execute(db, b"SELECT 1\0".as_ptr() as *const i8) };
|
||||
|
||||
// Correct usage
|
||||
unsafe { execute(conn, b"SELECT 1\0".as_ptr() as *const i8) };
|
||||
|
||||
unsafe { close_connection(conn) };
|
||||
unsafe { close_database(db) };
|
||||
}
|
||||
|
||||
// DO: Wrap in safe Rust types
|
||||
pub struct SafeDatabase {
|
||||
ptr: *mut Database,
|
||||
}
|
||||
|
||||
impl SafeDatabase {
|
||||
pub fn new() -> Option<Self> {
|
||||
let ptr = unsafe { create_database() };
|
||||
if ptr.is_null() { None } else { Some(Self { ptr }) }
|
||||
}
|
||||
|
||||
pub fn connect(&self) -> Option<SafeConnection<'_>> {
|
||||
let ptr = unsafe { create_connection(self.ptr) };
|
||||
if ptr.is_null() { None } else { Some(SafeConnection { ptr, _db: PhantomData }) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SafeDatabase {
|
||||
fn drop(&mut self) {
|
||||
unsafe { close_database(self.ptr); }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SafeConnection<'db> {
|
||||
ptr: *mut Connection,
|
||||
_db: PhantomData<&'db SafeDatabase>,
|
||||
}
|
||||
|
||||
impl SafeConnection<'_> {
|
||||
pub fn execute(&self, query: &str) -> Result<(), ()> {
|
||||
let query = std::ffi::CString::new(query).map_err(|_| ())?;
|
||||
let result = unsafe { execute(self.ptr, query.as_ptr()) };
|
||||
if result == 0 { Ok(()) } else { Err(()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SafeConnection<'_> {
|
||||
fn drop(&mut self) {
|
||||
unsafe { close_connection(self.ptr); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Opaque Type Pattern
|
||||
|
||||
```rust
|
||||
// The zero-sized array makes it impossible to construct
|
||||
// PhantomData ensures proper variance and !Send/!Sync if needed
|
||||
#[repr(C)]
|
||||
pub struct OpaqueHandle {
|
||||
_private: [u8; 0],
|
||||
_marker: PhantomData<(*mut u8, std::marker::PhantomPinned)>,
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Am I using `*mut c_void` for distinct handle types?
|
||||
- [ ] Would dedicated types prevent bugs?
|
||||
- [ ] Have I wrapped opaque pointers in safe Rust types?
|
||||
- [ ] Do my types enforce correct handle/function pairing?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ffi-02`: Read std::ffi documentation
|
||||
- `ffi-03`: Implement Drop for wrapped pointers
|
||||
165
skills/unsafe-checker/rules/ffi-18-no-trait-objects.md
Normal file
165
skills/unsafe-checker/rules/ffi-18-no-trait-objects.md
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
id: ffi-18
|
||||
original_id: P.UNS.FFI.18
|
||||
level: P
|
||||
impact: HIGH
|
||||
---
|
||||
|
||||
# Avoid Passing Trait Objects to C Interfaces
|
||||
|
||||
## Summary
|
||||
|
||||
Trait objects (`dyn Trait`) have Rust-specific layout (fat pointers with vtable) that is not compatible with C.
|
||||
|
||||
## Rationale
|
||||
|
||||
- Trait objects are "fat pointers": data ptr + vtable ptr
|
||||
- C expects thin pointers (single pointer)
|
||||
- Vtable layout is not stable across Rust versions
|
||||
- C cannot call Rust vtable methods
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Pass trait objects to C
|
||||
trait Handler {
|
||||
fn handle(&self, data: i32);
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
// This won't work - dyn Handler is a fat pointer!
|
||||
fn set_handler(h: *const dyn Handler);
|
||||
}
|
||||
|
||||
// DON'T: Store trait objects in FFI structs
|
||||
#[repr(C)]
|
||||
struct BadCallback {
|
||||
handler: *const dyn Handler, // Not C-compatible!
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
use std::os::raw::{c_int, c_void};
|
||||
|
||||
// DO: Use function pointers with user_data (trampoline pattern)
|
||||
type HandlerFn = extern "C" fn(data: c_int, user_data: *mut c_void);
|
||||
|
||||
extern "C" {
|
||||
fn set_handler(handler: HandlerFn, user_data: *mut c_void);
|
||||
}
|
||||
|
||||
trait Handler {
|
||||
fn handle(&self, data: i32);
|
||||
}
|
||||
|
||||
fn register_handler<H: Handler + 'static>(handler: H) {
|
||||
// Box the handler
|
||||
let boxed: Box<H> = Box::new(handler);
|
||||
let user_data = Box::into_raw(boxed) as *mut c_void;
|
||||
|
||||
extern "C" fn trampoline<H: Handler>(data: c_int, user_data: *mut c_void) {
|
||||
let handler = unsafe { &*(user_data as *const H) };
|
||||
handler.handle(data as i32);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
set_handler(trampoline::<H>, user_data);
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Use concrete types when possible
|
||||
struct ConcreteHandler {
|
||||
multiplier: i32,
|
||||
}
|
||||
|
||||
impl Handler for ConcreteHandler {
|
||||
fn handle(&self, data: i32) {
|
||||
println!("{}", data * self.multiplier);
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Create C-compatible vtable manually if needed
|
||||
#[repr(C)]
|
||||
struct HandlerVtable {
|
||||
handle: extern "C" fn(this: *const c_void, data: c_int),
|
||||
drop: extern "C" fn(this: *mut c_void),
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct CCompatibleHandler {
|
||||
data: *mut c_void,
|
||||
vtable: *const HandlerVtable,
|
||||
}
|
||||
|
||||
impl CCompatibleHandler {
|
||||
fn new<H: Handler + 'static>(handler: H) -> Self {
|
||||
extern "C" fn handle_impl<H: Handler>(this: *const c_void, data: c_int) {
|
||||
let handler = unsafe { &*(this as *const H) };
|
||||
handler.handle(data as i32);
|
||||
}
|
||||
|
||||
extern "C" fn drop_impl<H: Handler>(this: *mut c_void) {
|
||||
unsafe { drop(Box::from_raw(this as *mut H)); }
|
||||
}
|
||||
|
||||
static VTABLE: HandlerVtable = HandlerVtable {
|
||||
handle: handle_impl::<ConcreteHandler>, // Need concrete type
|
||||
drop: drop_impl::<ConcreteHandler>,
|
||||
};
|
||||
|
||||
Self {
|
||||
data: Box::into_raw(Box::new(handler)) as *mut c_void,
|
||||
vtable: &VTABLE,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle(&self, data: i32) {
|
||||
unsafe {
|
||||
((*self.vtable).handle)(self.data, data as c_int);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CCompatibleHandler {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
((*self.vtable).drop)(self.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Why Trait Objects Don't Work
|
||||
|
||||
```
|
||||
Rust trait object (*const dyn Handler):
|
||||
[data pointer][vtable pointer] <- 16 bytes on 64-bit
|
||||
|
||||
C pointer (void*):
|
||||
[pointer] <- 8 bytes on 64-bit
|
||||
|
||||
The sizes don't match!
|
||||
```
|
||||
|
||||
## Alternatives to Trait Objects
|
||||
|
||||
| Instead of | Use |
|
||||
|------------|-----|
|
||||
| `dyn Trait` | Function pointer + user_data |
|
||||
| `Box<dyn Trait>` | Boxed concrete type + trampoline |
|
||||
| `&dyn Trait` | C-compatible vtable struct |
|
||||
| `Arc<dyn Trait>` | Reference counting wrapper |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Am I passing trait objects across FFI?
|
||||
- [ ] Can I use concrete types instead?
|
||||
- [ ] Have I used the trampoline pattern for callbacks?
|
||||
- [ ] If vtable is needed, is it C-compatible?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ffi-16`: Closure to C with trampoline pattern
|
||||
- `ffi-14`: Types should have stable layout
|
||||
71
skills/unsafe-checker/rules/general-01-no-abuse.md
Normal file
71
skills/unsafe-checker/rules/general-01-no-abuse.md
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
id: general-01
|
||||
original_id: P.UNS.01
|
||||
level: P
|
||||
impact: CRITICAL
|
||||
---
|
||||
|
||||
# Do Not Abuse Unsafe to Escape Compiler Safety Checks
|
||||
|
||||
## Summary
|
||||
|
||||
Unsafe Rust should not be used as an escape hatch from the borrow checker or other compiler safety mechanisms.
|
||||
|
||||
## Rationale
|
||||
|
||||
The borrow checker exists to prevent memory safety bugs. Using `unsafe` to bypass it defeats Rust's safety guarantees and introduces potential undefined behavior.
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Using unsafe to bypass borrow checker
|
||||
fn bad_alias() {
|
||||
let mut data = vec![1, 2, 3];
|
||||
let ptr = data.as_mut_ptr();
|
||||
|
||||
// Unsafe used to create aliasing mutable references
|
||||
unsafe {
|
||||
let ref1 = &mut *ptr;
|
||||
let ref2 = &mut *ptr; // UB: Two mutable references!
|
||||
*ref1 = 10;
|
||||
*ref2 = 20;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
// DO: Work with the borrow checker, not against it
|
||||
fn good_sequential() {
|
||||
let mut data = vec![1, 2, 3];
|
||||
data[0] = 10;
|
||||
data[0] = 20; // Sequential mutations are fine
|
||||
}
|
||||
|
||||
// DO: Use interior mutability when needed
|
||||
use std::cell::RefCell;
|
||||
|
||||
fn good_interior_mut() {
|
||||
let data = RefCell::new(vec![1, 2, 3]);
|
||||
data.borrow_mut()[0] = 10;
|
||||
}
|
||||
```
|
||||
|
||||
## Legitimate Uses of Unsafe
|
||||
|
||||
1. **FFI**: Calling C functions or implementing C-compatible interfaces
|
||||
2. **Low-level abstractions**: Implementing collections, synchronization primitives
|
||||
3. **Performance**: Only after profiling shows measurable improvement, and with careful safety analysis
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Have I tried all safe alternatives first?
|
||||
- [ ] Is the borrow checker preventing a genuine design need?
|
||||
- [ ] Can I restructure the code to satisfy the borrow checker?
|
||||
- [ ] If unsafe is necessary, have I documented the safety invariants?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `general-02`: Don't blindly use unsafe for performance
|
||||
- `safety-02`: Unsafe code authors must verify safety invariants
|
||||
90
skills/unsafe-checker/rules/general-02-not-for-perf.md
Normal file
90
skills/unsafe-checker/rules/general-02-not-for-perf.md
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
id: general-02
|
||||
original_id: P.UNS.02
|
||||
level: P
|
||||
impact: CRITICAL
|
||||
---
|
||||
|
||||
# Do Not Blindly Use Unsafe for Performance
|
||||
|
||||
## Summary
|
||||
|
||||
Do not assume that using `unsafe` will automatically improve performance. Always measure first and verify the safety invariants.
|
||||
|
||||
## Rationale
|
||||
|
||||
1. Modern Rust optimizers often eliminate bounds checks when they can prove safety
|
||||
2. Unsafe code may prevent optimizations by breaking aliasing assumptions
|
||||
3. Unmeasured "optimizations" often provide no real benefit while introducing risk
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Blind unsafe for "performance"
|
||||
fn sum_bad(slice: &[i32]) -> i32 {
|
||||
let mut sum = 0;
|
||||
// Unnecessary unsafe - LLVM can optimize the safe version
|
||||
for i in 0..slice.len() {
|
||||
unsafe {
|
||||
sum += *slice.get_unchecked(i);
|
||||
}
|
||||
}
|
||||
sum
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
// DO: Use safe iteration - compiler optimizes bounds checks away
|
||||
fn sum_good(slice: &[i32]) -> i32 {
|
||||
slice.iter().sum()
|
||||
}
|
||||
|
||||
// DO: If unsafe is justified, document why
|
||||
fn sum_justified(slice: &[i32]) -> i32 {
|
||||
let mut sum = 0;
|
||||
// This is actually slower than iter().sum() in most cases
|
||||
// Only use get_unchecked when:
|
||||
// 1. Profiler shows bounds checks as bottleneck
|
||||
// 2. Iterator patterns can't be used
|
||||
// 3. Safety is proven by other means
|
||||
for i in 0..slice.len() {
|
||||
// SAFETY: i is always < slice.len() due to loop condition
|
||||
unsafe {
|
||||
sum += *slice.get_unchecked(i);
|
||||
}
|
||||
}
|
||||
sum
|
||||
}
|
||||
```
|
||||
|
||||
## When Unsafe Might Be Justified for Performance
|
||||
|
||||
1. **Hot inner loops** where profiling shows bounds checks are a bottleneck
|
||||
2. **SIMD operations** that require specific memory alignment
|
||||
3. **Lock-free data structures** with carefully verified memory orderings
|
||||
|
||||
## Measurement Workflow
|
||||
|
||||
```bash
|
||||
# 1. Benchmark the safe version first
|
||||
cargo bench --bench my_bench
|
||||
|
||||
# 2. Profile to identify actual bottlenecks
|
||||
cargo flamegraph --bench my_bench
|
||||
|
||||
# 3. Only then consider unsafe, with measurements
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Have I benchmarked the safe version?
|
||||
- [ ] Does profiling show this specific code as a bottleneck?
|
||||
- [ ] Have I measured the actual improvement from unsafe?
|
||||
- [ ] Is the performance gain worth the safety risk?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `general-01`: Don't abuse unsafe to escape safety checks
|
||||
- `safety-02`: Unsafe code authors must verify safety invariants
|
||||
74
skills/unsafe-checker/rules/general-03-no-alias.md
Normal file
74
skills/unsafe-checker/rules/general-03-no-alias.md
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
id: general-03
|
||||
original_id: G.UNS.01
|
||||
level: G
|
||||
impact: MEDIUM
|
||||
---
|
||||
|
||||
# Do Not Create Aliases for Types/Methods Named "Unsafe"
|
||||
|
||||
## Summary
|
||||
|
||||
Do not create type aliases, re-exports, or wrapper methods that hide the "unsafe" nature of operations.
|
||||
|
||||
## Rationale
|
||||
|
||||
The word "unsafe" in Rust is a signal to developers that extra scrutiny is required. Hiding this signal makes code review harder and can lead to accidental misuse.
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Hide unsafe behind an alias
|
||||
type SafePointer = *mut u8; // Still unsafe to dereference!
|
||||
|
||||
// DON'T: Wrap unsafe in a "safe-looking" name
|
||||
pub fn get_value(ptr: *const i32) -> i32 {
|
||||
unsafe { *ptr } // Caller doesn't know this is unsafe!
|
||||
}
|
||||
|
||||
// DON'T: Re-export unsafe functions with different names
|
||||
pub use std::mem::transmute as convert;
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
// DO: Keep "unsafe" visible in the API
|
||||
pub unsafe fn get_value_unchecked(ptr: *const i32) -> i32 {
|
||||
*ptr
|
||||
}
|
||||
|
||||
// DO: If providing a safe wrapper, make the safety contract clear
|
||||
/// Returns the value at the pointer.
|
||||
///
|
||||
/// # Safety
|
||||
/// This is safe because the pointer is validated internally.
|
||||
pub fn get_value_checked(ptr: *const i32) -> Option<i32> {
|
||||
if ptr.is_null() {
|
||||
None
|
||||
} else {
|
||||
// SAFETY: We checked for null above
|
||||
Some(unsafe { *ptr })
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Use clear naming for raw pointer types
|
||||
type RawHandle = *mut c_void; // "Raw" signals potential unsafety
|
||||
```
|
||||
|
||||
## Common Violations
|
||||
|
||||
1. Creating type aliases that hide pointer types
|
||||
2. Wrapping unsafe functions in safe-looking functions without proper safety analysis
|
||||
3. Re-exporting unsafe functions with "friendlier" names
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Does my API preserve visibility of unsafe operations?
|
||||
- [ ] If wrapping unsafe code in safe API, is the safety invariant enforced?
|
||||
- [ ] Are type aliases clearly named to indicate their nature?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `safety-06`: Don't expose raw pointers in public APIs
|
||||
- `safety-09`: Add SAFETY comment before any unsafe block
|
||||
151
skills/unsafe-checker/rules/io-01-raw-handle.md
Normal file
151
skills/unsafe-checker/rules/io-01-raw-handle.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
id: io-01
|
||||
original_id: P.UNS.FIO.01
|
||||
level: P
|
||||
impact: HIGH
|
||||
---
|
||||
|
||||
# Ensure I/O Safety When Using Raw Handles
|
||||
|
||||
## Summary
|
||||
|
||||
When working with raw file descriptors or handles, ensure they are valid for the duration of use and properly ownership-tracked.
|
||||
|
||||
## Rationale
|
||||
|
||||
- Raw handles can be closed by other code
|
||||
- Using a closed handle is undefined behavior
|
||||
- Handle reuse can cause data corruption
|
||||
- Rust 1.63+ provides I/O safety traits
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
#[cfg(unix)]
|
||||
mod bad_example {
|
||||
use std::os::unix::io::RawFd;
|
||||
|
||||
// DON'T: Accept raw handle without ownership
|
||||
fn bad_read(fd: RawFd) -> std::io::Result<Vec<u8>> {
|
||||
// What if fd was closed? What if it's reused?
|
||||
let mut buf = vec![0u8; 1024];
|
||||
let n = unsafe {
|
||||
libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len())
|
||||
};
|
||||
if n < 0 {
|
||||
Err(std::io::Error::last_os_error())
|
||||
} else {
|
||||
buf.truncate(n as usize);
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
// DON'T: Store raw handle without tracking ownership
|
||||
struct BadFileRef {
|
||||
fd: RawFd, // Who owns this? Who closes it?
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
#[cfg(unix)]
|
||||
mod good_example {
|
||||
use std::os::unix::io::{AsFd, BorrowedFd, OwnedFd, FromRawFd, AsRawFd};
|
||||
use std::fs::File;
|
||||
|
||||
// DO: Use BorrowedFd for borrowed access (Rust 1.63+)
|
||||
fn good_read(fd: BorrowedFd<'_>) -> std::io::Result<Vec<u8>> {
|
||||
let mut buf = vec![0u8; 1024];
|
||||
// BorrowedFd guarantees the fd is valid for this call
|
||||
let n = unsafe {
|
||||
libc::read(
|
||||
fd.as_raw_fd(),
|
||||
buf.as_mut_ptr() as *mut libc::c_void,
|
||||
buf.len()
|
||||
)
|
||||
};
|
||||
if n < 0 {
|
||||
Err(std::io::Error::last_os_error())
|
||||
} else {
|
||||
buf.truncate(n as usize);
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Use OwnedFd for owned handles
|
||||
struct GoodFileOwner {
|
||||
fd: OwnedFd, // Clearly owns the handle
|
||||
}
|
||||
|
||||
impl Drop for GoodFileOwner {
|
||||
fn drop(&mut self) {
|
||||
// OwnedFd closes automatically
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Use generic AsFd bound for flexibility
|
||||
fn generic_read<F: AsFd>(f: &F) -> std::io::Result<Vec<u8>> {
|
||||
good_read(f.as_fd())
|
||||
}
|
||||
|
||||
// Usage
|
||||
fn example() -> std::io::Result<()> {
|
||||
let file = File::open("test.txt")?;
|
||||
|
||||
// Pass as BorrowedFd
|
||||
let data = good_read(file.as_fd())?;
|
||||
|
||||
// Or use generic function
|
||||
let data = generic_read(&file)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// DO: Take ownership from raw fd
|
||||
fn from_raw(fd: i32) -> Option<GoodFileOwner> {
|
||||
if fd < 0 {
|
||||
return None;
|
||||
}
|
||||
// SAFETY: Caller guarantees fd is valid and ownership is transferred
|
||||
let owned = unsafe { OwnedFd::from_raw_fd(fd) };
|
||||
Some(GoodFileOwner { fd: owned })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## I/O Safety Types (Rust 1.63+)
|
||||
|
||||
| Type | Meaning |
|
||||
|------|---------|
|
||||
| `OwnedFd` | Owns a file descriptor, closes on drop |
|
||||
| `BorrowedFd<'a>` | Borrows a fd for lifetime 'a |
|
||||
| `RawFd` | Raw integer, no safety guarantees |
|
||||
| `AsFd` | Trait for types that have a fd |
|
||||
| `From<OwnedFd>` | Create from owned fd |
|
||||
| `Into<OwnedFd>` | Convert to owned fd |
|
||||
|
||||
## Windows Equivalents
|
||||
|
||||
```rust
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::io::{
|
||||
OwnedHandle, BorrowedHandle, RawHandle,
|
||||
AsHandle, FromRawHandle,
|
||||
OwnedSocket, BorrowedSocket, RawSocket,
|
||||
AsSocket, FromRawSocket,
|
||||
};
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Am I using BorrowedFd/OwnedFd instead of RawFd?
|
||||
- [ ] Is ownership of handles clear?
|
||||
- [ ] Am I using the AsFd trait for generic code?
|
||||
- [ ] Is the fd guaranteed valid for the duration of use?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ffi-03`: Implement Drop for resource wrappers
|
||||
- `safety-02`: Verify safety invariants
|
||||
130
skills/unsafe-checker/rules/mem-01-repr-layout.md
Normal file
130
skills/unsafe-checker/rules/mem-01-repr-layout.md
Normal file
@@ -0,0 +1,130 @@
|
||||
---
|
||||
id: mem-01
|
||||
original_id: P.UNS.MEM.01
|
||||
level: P
|
||||
impact: HIGH
|
||||
---
|
||||
|
||||
# Choose Appropriate Data Layout for Struct/Tuple/Enum
|
||||
|
||||
## Summary
|
||||
|
||||
Use `#[repr(...)]` attributes to control data layout when interfacing with C, doing memory mapping, or needing specific guarantees.
|
||||
|
||||
## Rationale
|
||||
|
||||
Rust's default layout is unspecified and may change between compiler versions. For FFI, persistence, or low-level memory operations, you need predictable layout.
|
||||
|
||||
## Repr Attributes
|
||||
|
||||
| Attribute | Use Case |
|
||||
|-----------|----------|
|
||||
| `#[repr(C)]` | C-compatible layout, stable field order |
|
||||
| `#[repr(transparent)]` | Single-field struct with same layout as field |
|
||||
| `#[repr(packed)]` | No padding (alignment = 1), careful with references! |
|
||||
| `#[repr(align(N))]` | Minimum alignment of N bytes |
|
||||
| `#[repr(u8)]`, `#[repr(i32)]`, etc. | Enum discriminant type |
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Assume Rust struct layout matches C
|
||||
struct BadFFI {
|
||||
a: u8,
|
||||
b: u32,
|
||||
c: u8,
|
||||
}
|
||||
// Rust may reorder fields or add different padding than C
|
||||
|
||||
// DON'T: Use packed without understanding the risks
|
||||
#[repr(packed)]
|
||||
struct Dangerous {
|
||||
a: u8,
|
||||
b: u32,
|
||||
}
|
||||
|
||||
fn bad_ref(d: &Dangerous) -> &u32 {
|
||||
&d.b // UB: Creates unaligned reference!
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
// DO: Use repr(C) for FFI
|
||||
#[repr(C)]
|
||||
struct GoodFFI {
|
||||
a: u8,
|
||||
b: u32,
|
||||
c: u8,
|
||||
}
|
||||
// Guaranteed: a at 0, padding 1-3, b at 4, c at 8, padding 9-11
|
||||
|
||||
// DO: Use repr(transparent) for newtypes
|
||||
#[repr(transparent)]
|
||||
struct Wrapper(u32);
|
||||
// Guaranteed same layout as u32, can be transmuted
|
||||
|
||||
// DO: Use repr(packed) carefully, access via copy
|
||||
#[repr(C, packed)]
|
||||
struct PackedData {
|
||||
header: u8,
|
||||
value: u32,
|
||||
}
|
||||
|
||||
impl PackedData {
|
||||
fn value(&self) -> u32 {
|
||||
// Copy out the value to avoid unaligned reference
|
||||
let ptr = std::ptr::addr_of!(self.value);
|
||||
// SAFETY: Reading unaligned is OK with read_unaligned
|
||||
unsafe { ptr.read_unaligned() }
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Use align for SIMD or cache line alignment
|
||||
#[repr(C, align(64))]
|
||||
struct CacheAligned {
|
||||
data: [u8; 64],
|
||||
}
|
||||
|
||||
// DO: Specify enum discriminant for FFI
|
||||
#[repr(u8)]
|
||||
enum Status {
|
||||
Ok = 0,
|
||||
Error = 1,
|
||||
Unknown = 255,
|
||||
}
|
||||
```
|
||||
|
||||
## Layout Guarantees
|
||||
|
||||
```rust
|
||||
use std::mem::{size_of, align_of};
|
||||
|
||||
#[repr(C)]
|
||||
struct Example {
|
||||
a: u8, // offset 0, size 1
|
||||
// padding: 3 bytes
|
||||
b: u32, // offset 4, size 4
|
||||
c: u8, // offset 8, size 1
|
||||
// padding: 3 bytes
|
||||
}
|
||||
|
||||
assert_eq!(size_of::<Example>(), 12);
|
||||
assert_eq!(align_of::<Example>(), 4);
|
||||
|
||||
// repr(Rust) might reorder to: b, a, c -> size 8
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Is this type used in FFI? → Use `#[repr(C)]`
|
||||
- [ ] Is this a newtype wrapper? → Consider `#[repr(transparent)]`
|
||||
- [ ] Do I need specific alignment? → Use `#[repr(align(N))]`
|
||||
- [ ] Am I using packed? → Never create references to packed fields
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ffi-13`: Ensure consistent data layout for custom types
|
||||
- `ffi-14`: Types in FFI should have stable layout
|
||||
- `ptr-04`: Alignment considerations
|
||||
113
skills/unsafe-checker/rules/mem-02-no-other-process.md
Normal file
113
skills/unsafe-checker/rules/mem-02-no-other-process.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
id: mem-02
|
||||
original_id: P.UNS.MEM.02
|
||||
level: P
|
||||
impact: CRITICAL
|
||||
---
|
||||
|
||||
# Do Not Modify Memory Variables of Other Processes or Dynamic Libraries
|
||||
|
||||
## Summary
|
||||
|
||||
Do not directly manipulate memory belonging to other processes or dynamically loaded libraries. Use proper IPC or FFI mechanisms.
|
||||
|
||||
## Rationale
|
||||
|
||||
- Other processes have separate address spaces; direct access is impossible on modern OSes
|
||||
- Shared memory requires explicit setup and synchronization
|
||||
- Dynamic library memory has ownership rules that must be respected
|
||||
- Violating these causes undefined behavior or security vulnerabilities
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Try to access another process's memory directly
|
||||
fn bad_cross_process(ptr: *mut i32) {
|
||||
// This pointer from another process is meaningless in our address space
|
||||
unsafe { *ptr = 42; } // Undefined behavior or crash
|
||||
}
|
||||
|
||||
// DON'T: Modify library internals
|
||||
extern "C" {
|
||||
static mut LIBRARY_INTERNAL: i32;
|
||||
}
|
||||
|
||||
fn bad_library_access() {
|
||||
// Modifying library internals breaks encapsulation
|
||||
unsafe { LIBRARY_INTERNAL = 100; } // May corrupt library state
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
// DO: Use proper IPC for cross-process communication
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
fn ipc_communication() -> std::io::Result<()> {
|
||||
let mut stream = UnixStream::connect("/tmp/socket")?;
|
||||
stream.write_all(b"message")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// DO: Use shared memory with proper synchronization
|
||||
#[cfg(unix)]
|
||||
fn shared_memory_example() {
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
|
||||
// Properly set up shared memory region
|
||||
// let shm = mmap shared memory...
|
||||
|
||||
// Use atomic operations for synchronization
|
||||
let shared: &AtomicI32 = /* ... */;
|
||||
shared.store(42, Ordering::Release);
|
||||
}
|
||||
|
||||
// DO: Use proper FFI for library interaction
|
||||
mod ffi {
|
||||
extern "C" {
|
||||
pub fn library_set_value(value: i32);
|
||||
pub fn library_get_value() -> i32;
|
||||
}
|
||||
}
|
||||
|
||||
fn proper_library_access() {
|
||||
unsafe {
|
||||
ffi::library_set_value(42);
|
||||
let value = ffi::library_get_value();
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Use Rust's libloading for dynamic libraries
|
||||
fn dynamic_library() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let lib = unsafe { libloading::Library::new("mylib.so")? };
|
||||
let func: libloading::Symbol<extern "C" fn(i32) -> i32> =
|
||||
unsafe { lib.get(b"my_function")? };
|
||||
let result = func(42);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Memory Ownership Rules
|
||||
|
||||
| Memory Type | Owner | Safe Access |
|
||||
|-------------|-------|-------------|
|
||||
| Stack variables | Current function | Direct |
|
||||
| Heap (Box, Vec) | Rust allocator | Through smart pointers |
|
||||
| Static | Program | With proper synchronization |
|
||||
| Shared memory | Multiple processes | Atomic ops, mutexes |
|
||||
| Library memory | Library | Through library API |
|
||||
| FFI-allocated | C allocator | Through C free functions |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Who allocated this memory?
|
||||
- [ ] Who is responsible for freeing it?
|
||||
- [ ] Is proper synchronization in place for shared access?
|
||||
- [ ] Am I using the correct API for cross-boundary access?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `mem-03`: Don't let String/Vec drop other process's memory
|
||||
- `ffi-03`: Implement Drop for wrapped C pointers
|
||||
127
skills/unsafe-checker/rules/mem-03-no-auto-drop-foreign.md
Normal file
127
skills/unsafe-checker/rules/mem-03-no-auto-drop-foreign.md
Normal file
@@ -0,0 +1,127 @@
|
||||
---
|
||||
id: mem-03
|
||||
original_id: P.UNS.MEM.03
|
||||
level: P
|
||||
impact: CRITICAL
|
||||
---
|
||||
|
||||
# Do Not Let String/Vec Auto-Drop Other Process's Memory
|
||||
|
||||
## Summary
|
||||
|
||||
Never create `String`, `Vec`, or `Box` from memory allocated outside Rust's allocator. They will try to free the memory with the wrong deallocator.
|
||||
|
||||
## Rationale
|
||||
|
||||
`String`, `Vec`, and `Box` assume memory was allocated by Rust's global allocator. When dropped, they call `dealloc`. If the memory came from C's `malloc`, a different allocator, or shared memory, this causes undefined behavior.
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Create String from C-allocated memory
|
||||
extern "C" {
|
||||
fn c_get_string() -> *mut std::os::raw::c_char;
|
||||
}
|
||||
|
||||
fn bad_string() -> String {
|
||||
unsafe {
|
||||
let ptr = c_get_string();
|
||||
// BAD: String will try to free with Rust allocator
|
||||
String::from_raw_parts(ptr as *mut u8, len, cap)
|
||||
}
|
||||
}
|
||||
|
||||
// DON'T: Create Vec from foreign memory
|
||||
fn bad_vec(ptr: *mut u8, len: usize) -> Vec<u8> {
|
||||
// BAD: Vec will free this memory incorrectly
|
||||
unsafe { Vec::from_raw_parts(ptr, len, len) }
|
||||
}
|
||||
|
||||
// DON'T: Wrap shared memory in Box
|
||||
fn bad_box(shared_ptr: *mut Data) -> Box<Data> {
|
||||
// BAD: Box will try to deallocate shared memory!
|
||||
unsafe { Box::from_raw(shared_ptr) }
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
use std::ffi::CStr;
|
||||
|
||||
extern "C" {
|
||||
fn c_get_string() -> *mut std::os::raw::c_char;
|
||||
fn c_free_string(s: *mut std::os::raw::c_char);
|
||||
}
|
||||
|
||||
// DO: Copy data into Rust-owned allocation
|
||||
fn good_string() -> String {
|
||||
unsafe {
|
||||
let ptr = c_get_string();
|
||||
let cstr = CStr::from_ptr(ptr);
|
||||
let result = cstr.to_string_lossy().into_owned();
|
||||
c_free_string(ptr); // Free with correct deallocator
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Use wrapper that calls correct deallocator
|
||||
struct CString {
|
||||
ptr: *mut std::os::raw::c_char,
|
||||
}
|
||||
|
||||
impl Drop for CString {
|
||||
fn drop(&mut self) {
|
||||
unsafe { c_free_string(self.ptr); }
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Use slice for borrowed view, don't take ownership
|
||||
fn good_slice(ptr: *const u8, len: usize) -> &'static [u8] {
|
||||
// Only borrow, don't own
|
||||
unsafe { std::slice::from_raw_parts(ptr, len) }
|
||||
}
|
||||
|
||||
// DO: For shared memory, use raw pointers or custom wrapper
|
||||
struct SharedBuffer {
|
||||
ptr: *mut u8,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl SharedBuffer {
|
||||
fn as_slice(&self) -> &[u8] {
|
||||
unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SharedBuffer {
|
||||
fn drop(&mut self) {
|
||||
// Unmap shared memory, don't deallocate
|
||||
// munmap(self.ptr, self.len);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Memory Allocation Compatibility
|
||||
|
||||
| Allocator | Can use Rust Vec/String/Box? |
|
||||
|-----------|------------------------------|
|
||||
| Rust global allocator | Yes |
|
||||
| C malloc | No - use wrapper with C free |
|
||||
| C++ new | No - use wrapper with C++ delete |
|
||||
| Custom allocator | No - use allocator_api |
|
||||
| mmap/shared memory | No - use munmap |
|
||||
| Stack/static | No - never "free" |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Who allocated this memory?
|
||||
- [ ] Is it from Rust's global allocator?
|
||||
- [ ] If not, do I have a custom Drop that frees correctly?
|
||||
- [ ] Am I copying data or taking ownership?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `mem-02`: Don't modify other process's memory
|
||||
- `ffi-03`: Implement Drop for wrapped C pointers
|
||||
- `ffi-07`: Don't implement Drop for types passed to external code
|
||||
121
skills/unsafe-checker/rules/mem-04-reentrant.md
Normal file
121
skills/unsafe-checker/rules/mem-04-reentrant.md
Normal file
@@ -0,0 +1,121 @@
|
||||
---
|
||||
id: mem-04
|
||||
original_id: P.UNS.MEM.04
|
||||
level: P
|
||||
impact: HIGH
|
||||
---
|
||||
|
||||
# Prefer Reentrant Versions of C-API or Syscalls
|
||||
|
||||
## Summary
|
||||
|
||||
When calling C functions or system calls, use reentrant (`_r`) versions to avoid data races from global state.
|
||||
|
||||
## Rationale
|
||||
|
||||
Many C library functions use static buffers or global state, making them unsafe in multithreaded programs. Reentrant versions use caller-provided buffers instead.
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
use std::ffi::CStr;
|
||||
|
||||
extern "C" {
|
||||
fn strtok(s: *mut i8, delim: *const i8) -> *mut i8;
|
||||
fn localtime(time: *const i64) -> *mut Tm;
|
||||
fn rand() -> i32;
|
||||
}
|
||||
|
||||
// DON'T: Use non-reentrant functions
|
||||
fn bad_tokenize(s: &mut [i8]) {
|
||||
unsafe {
|
||||
let delim = b" \0".as_ptr() as *const i8;
|
||||
// strtok uses static buffer - not thread-safe!
|
||||
let token = strtok(s.as_mut_ptr(), delim);
|
||||
}
|
||||
}
|
||||
|
||||
fn bad_time() {
|
||||
unsafe {
|
||||
let now: i64 = 0;
|
||||
// localtime returns pointer to static buffer
|
||||
let tm = localtime(&now); // Data race if called from multiple threads!
|
||||
}
|
||||
}
|
||||
|
||||
fn bad_random() -> i32 {
|
||||
// rand() uses global state - not thread-safe
|
||||
unsafe { rand() }
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
extern "C" {
|
||||
fn strtok_r(s: *mut i8, delim: *const i8, saveptr: *mut *mut i8) -> *mut i8;
|
||||
fn localtime_r(time: *const i64, result: *mut Tm) -> *mut Tm;
|
||||
fn rand_r(seed: *mut u32) -> i32;
|
||||
}
|
||||
|
||||
// DO: Use reentrant versions
|
||||
fn good_tokenize(s: &mut [i8]) {
|
||||
unsafe {
|
||||
let delim = b" \0".as_ptr() as *const i8;
|
||||
let mut saveptr: *mut i8 = std::ptr::null_mut();
|
||||
// strtok_r uses caller-provided saveptr
|
||||
let token = strtok_r(s.as_mut_ptr(), delim, &mut saveptr);
|
||||
}
|
||||
}
|
||||
|
||||
fn good_time() {
|
||||
unsafe {
|
||||
let now: i64 = 0;
|
||||
let mut result: Tm = std::mem::zeroed();
|
||||
// localtime_r writes to caller-provided buffer
|
||||
localtime_r(&now, &mut result);
|
||||
}
|
||||
}
|
||||
|
||||
fn good_random(seed: &mut u32) -> i32 {
|
||||
// rand_r uses caller-provided seed
|
||||
unsafe { rand_r(seed) }
|
||||
}
|
||||
|
||||
// BETTER: Use Rust standard library
|
||||
fn best_time() {
|
||||
use std::time::SystemTime;
|
||||
let now = SystemTime::now(); // Thread-safe!
|
||||
}
|
||||
|
||||
fn best_random() -> u32 {
|
||||
use rand::Rng;
|
||||
rand::thread_rng().gen() // Thread-safe!
|
||||
}
|
||||
```
|
||||
|
||||
## Common Non-Reentrant Functions
|
||||
|
||||
| Non-Reentrant | Reentrant | Rust Alternative |
|
||||
|---------------|-----------|------------------|
|
||||
| `strtok` | `strtok_r` | `str::split` |
|
||||
| `localtime` | `localtime_r` | `chrono` crate |
|
||||
| `gmtime` | `gmtime_r` | `chrono` crate |
|
||||
| `ctime` | `ctime_r` | `chrono` crate |
|
||||
| `rand` | `rand_r` | `rand` crate |
|
||||
| `strerror` | `strerror_r` | `std::io::Error` |
|
||||
| `getenv` | None (inherent race) | `std::env::var` (not atomic) |
|
||||
| `readdir` | `readdir_r` | `std::fs::read_dir` |
|
||||
| `gethostbyname` | `getaddrinfo` | `std::net::ToSocketAddrs` |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Am I calling a C function that might use global state?
|
||||
- [ ] Is there a `_r` reentrant version available?
|
||||
- [ ] Is there a Rust standard library alternative?
|
||||
- [ ] If neither, do I need synchronization?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ffi-10`: Exported functions must be thread-safe
|
||||
- `ptr-01`: Don't share raw pointers across threads
|
||||
147
skills/unsafe-checker/rules/mem-05-bitfield-crates.md
Normal file
147
skills/unsafe-checker/rules/mem-05-bitfield-crates.md
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
id: mem-05
|
||||
original_id: P.UNS.MEM.05
|
||||
level: P
|
||||
impact: MEDIUM
|
||||
---
|
||||
|
||||
# Use Third-Party Crates for Bitfields
|
||||
|
||||
## Summary
|
||||
|
||||
Use crates like `bitflags`, `bitvec`, or `modular-bitfield` instead of manual bit manipulation for complex bitfield operations.
|
||||
|
||||
## Rationale
|
||||
|
||||
- Manual bit manipulation is error-prone
|
||||
- Easy to get offsets, masks, or endianness wrong
|
||||
- Crates provide type-safe, tested abstractions
|
||||
- Proc-macro crates generate efficient code
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Manual bitfield manipulation
|
||||
struct Flags(u32);
|
||||
|
||||
impl Flags {
|
||||
const READ: u32 = 1 << 0;
|
||||
const WRITE: u32 = 1 << 1;
|
||||
const EXECUTE: u32 = 1 << 2;
|
||||
|
||||
fn has_read(&self) -> bool {
|
||||
(self.0 & Self::READ) != 0
|
||||
}
|
||||
|
||||
fn set_read(&mut self) {
|
||||
self.0 |= Self::READ;
|
||||
}
|
||||
|
||||
fn clear_read(&mut self) {
|
||||
self.0 &= !Self::READ; // Easy to forget the !
|
||||
}
|
||||
}
|
||||
|
||||
// DON'T: Manual packed bitfields for FFI
|
||||
#[repr(C)]
|
||||
struct PackedHeader {
|
||||
data: u32,
|
||||
}
|
||||
|
||||
impl PackedHeader {
|
||||
// Error-prone: wrong shift or mask values
|
||||
fn version(&self) -> u8 {
|
||||
((self.data >> 24) & 0xFF) as u8
|
||||
}
|
||||
|
||||
fn flags(&self) -> u16 {
|
||||
((self.data >> 8) & 0xFFFF) as u16
|
||||
}
|
||||
|
||||
fn tag(&self) -> u8 {
|
||||
(self.data & 0xFF) as u8
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
// DO: Use bitflags for flag sets
|
||||
use bitflags::bitflags;
|
||||
|
||||
bitflags! {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct Flags: u32 {
|
||||
const READ = 1 << 0;
|
||||
const WRITE = 1 << 1;
|
||||
const EXECUTE = 1 << 2;
|
||||
const RW = Self::READ.bits() | Self::WRITE.bits();
|
||||
}
|
||||
}
|
||||
|
||||
fn use_flags() {
|
||||
let mut flags = Flags::READ | Flags::WRITE;
|
||||
flags.insert(Flags::EXECUTE);
|
||||
flags.remove(Flags::WRITE);
|
||||
|
||||
if flags.contains(Flags::READ) {
|
||||
println!("Readable");
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Use modular-bitfield for packed structures
|
||||
use modular_bitfield::prelude::*;
|
||||
|
||||
#[bitfield]
|
||||
#[repr(C)]
|
||||
struct PackedHeader {
|
||||
tag: B8, // 8 bits
|
||||
flags: B16, // 16 bits
|
||||
version: B8, // 8 bits
|
||||
}
|
||||
|
||||
fn use_packed() {
|
||||
let header = PackedHeader::new()
|
||||
.with_version(1)
|
||||
.with_flags(0x1234)
|
||||
.with_tag(0xAB);
|
||||
|
||||
assert_eq!(header.version(), 1);
|
||||
assert_eq!(header.flags(), 0x1234);
|
||||
}
|
||||
|
||||
// DO: Use bitvec for arbitrary bit manipulation
|
||||
use bitvec::prelude::*;
|
||||
|
||||
fn use_bitvec() {
|
||||
let mut bits = bitvec![u8, Msb0; 0; 16];
|
||||
bits.set(0, true);
|
||||
bits.set(7, true);
|
||||
|
||||
let byte: u8 = bits[0..8].load_be();
|
||||
assert_eq!(byte, 0b1000_0001);
|
||||
}
|
||||
```
|
||||
|
||||
## Recommended Crates
|
||||
|
||||
| Crate | Use Case | Features |
|
||||
|-------|----------|----------|
|
||||
| `bitflags` | Flag sets (like C enums) | Type-safe, const, derives |
|
||||
| `modular-bitfield` | Packed struct fields | Proc macro, repr(C) |
|
||||
| `bitvec` | Arbitrary bit arrays | Slicing, iteration |
|
||||
| `packed_struct` | Binary protocol structs | Endianness, derive |
|
||||
| `deku` | Binary parsing | Derive, read/write |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Am I manipulating multiple bit flags? → Use `bitflags`
|
||||
- [ ] Am I packing fields into bytes? → Use `modular-bitfield` or `packed_struct`
|
||||
- [ ] Am I doing binary protocol work? → Consider `deku`
|
||||
- [ ] Is the manual approach really simpler?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `mem-01`: Choose appropriate data layout
|
||||
- `ffi-13`: Ensure consistent data layout
|
||||
146
skills/unsafe-checker/rules/mem-06-maybeuninit.md
Normal file
146
skills/unsafe-checker/rules/mem-06-maybeuninit.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
id: mem-06
|
||||
original_id: G.UNS.MEM.01
|
||||
level: G
|
||||
impact: HIGH
|
||||
clippy: uninit_assumed_init, uninit_vec
|
||||
---
|
||||
|
||||
# Use MaybeUninit<T> for Uninitialized Memory
|
||||
|
||||
## Summary
|
||||
|
||||
Use `MaybeUninit<T>` instead of `mem::uninitialized()` or `mem::zeroed()` when working with uninitialized memory.
|
||||
|
||||
## Rationale
|
||||
|
||||
- `mem::uninitialized()` is deprecated and unsound
|
||||
- `mem::zeroed()` is UB for types where zero is invalid (references, NonZero, bool)
|
||||
- `MaybeUninit<T>` clearly marks memory as potentially uninitialized
|
||||
- Compiler can optimize based on initialization state
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Use deprecated uninitialized
|
||||
fn bad_uninit<T>() -> T {
|
||||
unsafe { std::mem::uninitialized() } // Deprecated, UB
|
||||
}
|
||||
|
||||
// DON'T: Use zeroed for types where zero is invalid
|
||||
fn bad_zeroed() -> &'static str {
|
||||
unsafe { std::mem::zeroed() } // UB: null reference
|
||||
}
|
||||
|
||||
fn bad_zeroed_bool() -> bool {
|
||||
unsafe { std::mem::zeroed() } // UB: 0 might not be valid bool
|
||||
}
|
||||
|
||||
// DON'T: Transmute to "initialize"
|
||||
fn bad_transmute() -> [String; 10] {
|
||||
unsafe { std::mem::transmute([0u8; std::mem::size_of::<[String; 10]>()]) }
|
||||
}
|
||||
|
||||
// DON'T: Set Vec length without initializing
|
||||
fn bad_vec() -> Vec<String> {
|
||||
let mut v = Vec::with_capacity(10);
|
||||
unsafe { v.set_len(10); } // Elements are uninitialized!
|
||||
v
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
// DO: Use MaybeUninit for delayed initialization
|
||||
fn good_array() -> [String; 10] {
|
||||
let mut arr: [MaybeUninit<String>; 10] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
|
||||
for (i, elem) in arr.iter_mut().enumerate() {
|
||||
elem.write(format!("item {}", i));
|
||||
}
|
||||
|
||||
// SAFETY: All elements initialized above
|
||||
unsafe { std::mem::transmute::<_, [String; 10]>(arr) }
|
||||
}
|
||||
|
||||
// DO: Use MaybeUninit with arrays (cleaner with array_assume_init)
|
||||
fn good_array_nightly() -> [String; 10] {
|
||||
let mut arr: [MaybeUninit<String>; 10] =
|
||||
[const { MaybeUninit::uninit() }; 10];
|
||||
|
||||
for (i, elem) in arr.iter_mut().enumerate() {
|
||||
elem.write(format!("item {}", i));
|
||||
}
|
||||
|
||||
// On nightly: arr.map(|e| unsafe { e.assume_init() })
|
||||
unsafe { MaybeUninit::array_assume_init(arr) }
|
||||
}
|
||||
|
||||
// DO: Use zeroed only for types where it's valid
|
||||
fn good_zeroed() -> [u8; 1024] {
|
||||
// SAFETY: All-zero bytes is valid for u8
|
||||
unsafe { std::mem::zeroed() }
|
||||
}
|
||||
|
||||
// DO: Initialize buffer properly
|
||||
fn good_vec() -> Vec<u8> {
|
||||
let mut v = Vec::with_capacity(1024);
|
||||
|
||||
// Option 1: Resize with default value
|
||||
v.resize(1024, 0);
|
||||
|
||||
// Option 2: Use spare_capacity_mut
|
||||
let spare = v.spare_capacity_mut();
|
||||
for elem in spare.iter_mut().take(1024) {
|
||||
elem.write(0);
|
||||
}
|
||||
unsafe { v.set_len(1024); }
|
||||
|
||||
v
|
||||
}
|
||||
|
||||
// DO: Use MaybeUninit::uninit_array (nightly) or const array
|
||||
fn good_uninit_array<const N: usize>() -> [MaybeUninit<u8>; N] {
|
||||
// Stable: create array of uninit
|
||||
[const { MaybeUninit::uninit() }; N]
|
||||
}
|
||||
```
|
||||
|
||||
## MaybeUninit API
|
||||
|
||||
```rust
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
// Creation
|
||||
let uninit: MaybeUninit<T> = MaybeUninit::uninit();
|
||||
let zeroed: MaybeUninit<T> = MaybeUninit::zeroed();
|
||||
let init: MaybeUninit<T> = MaybeUninit::new(value);
|
||||
|
||||
// Writing
|
||||
uninit.write(value); // Returns &mut T
|
||||
|
||||
// Reading (unsafe)
|
||||
let value: T = unsafe { uninit.assume_init() };
|
||||
let ref_: &T = unsafe { uninit.assume_init_ref() };
|
||||
let mut_: &mut T = unsafe { uninit.assume_init_mut() };
|
||||
|
||||
// Pointer access
|
||||
let ptr: *const T = uninit.as_ptr();
|
||||
let mut_ptr: *mut T = uninit.as_mut_ptr();
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Am I using `mem::uninitialized()`? → Replace with `MaybeUninit`
|
||||
- [ ] Am I using `mem::zeroed()` for non-POD types? → Use `MaybeUninit`
|
||||
- [ ] Am I setting Vec length without initialization? → Use proper initialization
|
||||
- [ ] Have I initialized all MaybeUninit before assume_init?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `safety-03`: Don't expose uninitialized memory in APIs
|
||||
- `safety-01`: Panic safety with partial initialization
|
||||
113
skills/unsafe-checker/rules/ptr-01-no-thread-share.md
Normal file
113
skills/unsafe-checker/rules/ptr-01-no-thread-share.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
id: ptr-01
|
||||
original_id: P.UNS.PTR.01
|
||||
level: P
|
||||
impact: CRITICAL
|
||||
---
|
||||
|
||||
# Do Not Share Raw Pointers Across Threads
|
||||
|
||||
## Summary
|
||||
|
||||
Raw pointers (`*const T`, `*mut T`) are not `Send` or `Sync` by default. Do not share them across threads without ensuring proper synchronization.
|
||||
|
||||
## Rationale
|
||||
|
||||
Raw pointers have no synchronization guarantees. Sharing them across threads can lead to data races, which are undefined behavior.
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
use std::thread;
|
||||
|
||||
// DON'T: Share raw pointers across threads
|
||||
fn bad_sharing() {
|
||||
let mut data = 42i32;
|
||||
let ptr = &mut data as *mut i32;
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
// This is undefined behavior!
|
||||
unsafe { *ptr = 100; }
|
||||
});
|
||||
|
||||
// Main thread also accesses - data race!
|
||||
unsafe { *ptr = 200; }
|
||||
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
// DON'T: Wrap in struct and impl Send unsafely
|
||||
struct UnsafePtr(*mut i32);
|
||||
unsafe impl Send for UnsafePtr {} // Unsound without synchronization!
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
use std::sync::{Arc, Mutex, atomic::{AtomicPtr, Ordering}};
|
||||
use std::thread;
|
||||
|
||||
// DO: Use Arc<Mutex<T>> for shared mutable access
|
||||
fn good_mutex() {
|
||||
let data = Arc::new(Mutex::new(42i32));
|
||||
let data_clone = Arc::clone(&data);
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
*data_clone.lock().unwrap() = 100;
|
||||
});
|
||||
|
||||
*data.lock().unwrap() = 200;
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
// DO: Use AtomicPtr for lock-free pointer sharing
|
||||
fn good_atomic() {
|
||||
let data = Box::into_raw(Box::new(42i32));
|
||||
let atomic_ptr = Arc::new(AtomicPtr::new(data));
|
||||
let atomic_clone = Arc::clone(&atomic_ptr);
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
let ptr = atomic_clone.load(Ordering::Acquire);
|
||||
// SAFETY: We have exclusive access through atomic operations
|
||||
unsafe { println!("Value: {}", *ptr); }
|
||||
});
|
||||
|
||||
handle.join().unwrap();
|
||||
|
||||
// SAFETY: All threads done, we own the memory
|
||||
unsafe { drop(Box::from_raw(atomic_ptr.load(Ordering::Relaxed))); }
|
||||
}
|
||||
|
||||
// DO: If you must use raw pointers, ensure exclusive access
|
||||
fn good_exclusive() {
|
||||
let mut data = vec![1, 2, 3];
|
||||
|
||||
// Send data ownership to thread, not pointer
|
||||
let handle = thread::spawn(move || {
|
||||
data.push(4);
|
||||
data
|
||||
});
|
||||
|
||||
let data = handle.join().unwrap();
|
||||
println!("{:?}", data);
|
||||
}
|
||||
```
|
||||
|
||||
## When Raw Pointers Across Threads Are Valid
|
||||
|
||||
Only with proper synchronization:
|
||||
- Through `AtomicPtr` with appropriate memory orderings
|
||||
- Protected by a `Mutex` (don't share the pointer, share the Mutex)
|
||||
- Using lock-free algorithms with careful memory ordering
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Does my pointer cross thread boundaries?
|
||||
- [ ] Is there synchronization preventing concurrent access?
|
||||
- [ ] Can I use a higher-level abstraction (Arc, Mutex)?
|
||||
- [ ] If implementing Send/Sync, is thread safety proven?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `safety-05`: Consider safety when implementing Send/Sync
|
||||
- `safety-02`: Verify safety invariants
|
||||
114
skills/unsafe-checker/rules/ptr-02-prefer-nonnull.md
Normal file
114
skills/unsafe-checker/rules/ptr-02-prefer-nonnull.md
Normal file
@@ -0,0 +1,114 @@
|
||||
---
|
||||
id: ptr-02
|
||||
original_id: P.UNS.PTR.02
|
||||
level: P
|
||||
impact: MEDIUM
|
||||
---
|
||||
|
||||
# Prefer NonNull<T> Over *mut T
|
||||
|
||||
## Summary
|
||||
|
||||
Use `NonNull<T>` instead of `*mut T` when the pointer should never be null. This enables null pointer optimization and makes the intent clear.
|
||||
|
||||
## Rationale
|
||||
|
||||
- `NonNull<T>` guarantees non-null at the type level
|
||||
- Enables niche optimization: `Option<NonNull<T>>` is the same size as `*mut T`
|
||||
- Makes invariants explicit in the type system
|
||||
- Covariant over `T` (like `&T`), which is usually what you want
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Use *mut when pointer is always non-null
|
||||
struct MyBox<T> {
|
||||
ptr: *mut T, // Invariant: never null, but not enforced
|
||||
}
|
||||
|
||||
impl<T> MyBox<T> {
|
||||
pub fn new(value: T) -> Self {
|
||||
let ptr = Box::into_raw(Box::new(value));
|
||||
// ptr is guaranteed non-null, but type doesn't show it
|
||||
Self { ptr }
|
||||
}
|
||||
|
||||
pub fn get(&self) -> &T {
|
||||
// Must add null check or document the invariant
|
||||
unsafe { &*self.ptr }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
use std::ptr::NonNull;
|
||||
|
||||
// DO: Use NonNull when pointer is never null
|
||||
struct MyBox<T> {
|
||||
ptr: NonNull<T>, // Type guarantees non-null
|
||||
}
|
||||
|
||||
impl<T> MyBox<T> {
|
||||
pub fn new(value: T) -> Self {
|
||||
let ptr = Box::into_raw(Box::new(value));
|
||||
// SAFETY: Box::into_raw never returns null
|
||||
let ptr = unsafe { NonNull::new_unchecked(ptr) };
|
||||
Self { ptr }
|
||||
}
|
||||
|
||||
pub fn get(&self) -> &T {
|
||||
// SAFETY: NonNull guarantees ptr is valid
|
||||
unsafe { self.ptr.as_ref() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for MyBox<T> {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: ptr was created from Box::into_raw
|
||||
unsafe { drop(Box::from_raw(self.ptr.as_ptr())); }
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Niche optimization with Option
|
||||
struct OptionalBox<T> {
|
||||
ptr: Option<NonNull<T>>, // Same size as *mut T!
|
||||
}
|
||||
```
|
||||
|
||||
## NonNull API
|
||||
|
||||
```rust
|
||||
use std::ptr::NonNull;
|
||||
|
||||
// Creating NonNull
|
||||
let ptr: NonNull<i32> = NonNull::new(raw_ptr).expect("null pointer");
|
||||
let ptr: NonNull<i32> = unsafe { NonNull::new_unchecked(raw_ptr) };
|
||||
let ptr: NonNull<i32> = NonNull::dangling(); // For ZSTs or uninitialized
|
||||
|
||||
// Using NonNull
|
||||
let raw: *mut i32 = ptr.as_ptr();
|
||||
let reference: &i32 = unsafe { ptr.as_ref() };
|
||||
let mut_ref: &mut i32 = unsafe { ptr.as_mut() };
|
||||
|
||||
// Casting
|
||||
let ptr: NonNull<u8> = ptr.cast::<u8>();
|
||||
```
|
||||
|
||||
## When to Use *mut T Instead
|
||||
|
||||
- When null is a valid/expected value
|
||||
- FFI with C code that may return null
|
||||
- When variance matters (NonNull is covariant, sometimes you need invariance)
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Is my pointer ever null? If no, use NonNull
|
||||
- [ ] Do I need null pointer optimization?
|
||||
- [ ] Is the variance correct for my use case?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ptr-03`: Use PhantomData for variance and ownership
|
||||
- `safety-06`: Don't expose raw pointers in public APIs
|
||||
125
skills/unsafe-checker/rules/ptr-03-phantomdata.md
Normal file
125
skills/unsafe-checker/rules/ptr-03-phantomdata.md
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
id: ptr-03
|
||||
original_id: P.UNS.PTR.03
|
||||
level: P
|
||||
impact: HIGH
|
||||
---
|
||||
|
||||
# Use PhantomData<T> for Variance and Ownership with Pointer Generics
|
||||
|
||||
## Summary
|
||||
|
||||
When a struct contains raw pointers but logically owns or borrows the pointed-to data, use `PhantomData<T>` to tell the compiler about the relationship.
|
||||
|
||||
## Rationale
|
||||
|
||||
Raw pointers don't carry ownership or lifetime information. `PhantomData` lets you:
|
||||
- Indicate ownership (for `Drop` check)
|
||||
- Control variance (covariant, contravariant, invariant)
|
||||
- Participate in lifetime elision
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Raw pointer without PhantomData
|
||||
struct MyVec<T> {
|
||||
ptr: *mut T,
|
||||
len: usize,
|
||||
cap: usize,
|
||||
}
|
||||
|
||||
// Problems:
|
||||
// 1. Compiler doesn't know we "own" the T values
|
||||
// 2. T might be incorrectly determined as unused
|
||||
// 3. Drop check may allow dangling references
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
use std::marker::PhantomData;
|
||||
use std::ptr::NonNull;
|
||||
|
||||
// DO: Use PhantomData to express ownership
|
||||
struct MyVec<T> {
|
||||
ptr: NonNull<T>,
|
||||
len: usize,
|
||||
cap: usize,
|
||||
_marker: PhantomData<T>, // We own T values
|
||||
}
|
||||
|
||||
// For owned data: PhantomData<T>
|
||||
// For borrowed data: PhantomData<&'a T>
|
||||
// For mutably borrowed: PhantomData<&'a mut T>
|
||||
// For function pointers: PhantomData<fn(T)> (contravariant)
|
||||
|
||||
// DO: Express lifetime relationships
|
||||
struct Iter<'a, T> {
|
||||
ptr: *const T,
|
||||
end: *const T,
|
||||
_marker: PhantomData<&'a T>, // Borrows T for 'a
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for Iter<'a, T> {
|
||||
type Item = &'a T;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.ptr == self.end {
|
||||
None
|
||||
} else {
|
||||
// SAFETY: ptr < end, so ptr is valid
|
||||
// Lifetime is tied to 'a through PhantomData
|
||||
let current = unsafe { &*self.ptr };
|
||||
self.ptr = unsafe { self.ptr.add(1) };
|
||||
Some(current)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## PhantomData Patterns
|
||||
|
||||
| Phantom Type | Meaning | Variance |
|
||||
|--------------|---------|----------|
|
||||
| `PhantomData<T>` | Owns T | Covariant |
|
||||
| `PhantomData<&'a T>` | Borrows T for 'a | Covariant in T, covariant in 'a |
|
||||
| `PhantomData<&'a mut T>` | Mutably borrows T | Invariant in T, covariant in 'a |
|
||||
| `PhantomData<*const T>` | Just has pointer | Covariant |
|
||||
| `PhantomData<*mut T>` | Just has pointer | Invariant |
|
||||
| `PhantomData<fn(T)>` | Consumes T | Contravariant |
|
||||
| `PhantomData<fn() -> T>` | Produces T | Covariant |
|
||||
|
||||
## Drop Check
|
||||
|
||||
```rust
|
||||
use std::marker::PhantomData;
|
||||
|
||||
// This tells the compiler that dropping MyVec may drop T values
|
||||
struct MyVec<T> {
|
||||
ptr: NonNull<T>,
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> Drop for MyVec<T> {
|
||||
fn drop(&mut self) {
|
||||
// Drop all T values...
|
||||
}
|
||||
}
|
||||
|
||||
// Without PhantomData<T>, this might compile incorrectly:
|
||||
// let x = MyVec::new(&local);
|
||||
// drop(local); // Would be UB if allowed
|
||||
// drop(x); // Tries to access dropped local
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Does my pointer type logically own the pointed-to data?
|
||||
- [ ] Do I need to express a lifetime relationship?
|
||||
- [ ] What variance do I need for my generic parameter?
|
||||
- [ ] Will the type be dropped, and does it need drop check?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ptr-02`: Prefer NonNull over *mut T
|
||||
- `safety-05`: Send/Sync implementation safety
|
||||
117
skills/unsafe-checker/rules/ptr-04-alignment.md
Normal file
117
skills/unsafe-checker/rules/ptr-04-alignment.md
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
id: ptr-04
|
||||
original_id: G.UNS.PTR.01
|
||||
level: G
|
||||
impact: HIGH
|
||||
clippy: cast_ptr_alignment
|
||||
---
|
||||
|
||||
# Do Not Dereference Pointers Cast to Misaligned Types
|
||||
|
||||
## Summary
|
||||
|
||||
When casting a pointer to a different type, ensure the resulting pointer is properly aligned for the target type.
|
||||
|
||||
## Rationale
|
||||
|
||||
Misaligned pointer dereferences are undefined behavior on most architectures. Even on architectures that support unaligned access, it may cause performance penalties or subtle bugs.
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Cast without checking alignment
|
||||
fn bad_cast(bytes: &[u8]) -> u32 {
|
||||
// BAD: bytes might not be aligned for u32
|
||||
let ptr = bytes.as_ptr() as *const u32;
|
||||
unsafe { *ptr } // UB if misaligned!
|
||||
}
|
||||
|
||||
// DON'T: Assume struct layout
|
||||
#[repr(C)]
|
||||
struct Header {
|
||||
flags: u8,
|
||||
value: u32, // Aligned at offset 4 in the struct
|
||||
}
|
||||
|
||||
fn bad_field_access(bytes: &[u8]) -> u32 {
|
||||
let header = bytes.as_ptr() as *const Header;
|
||||
// Even if bytes is 4-byte aligned, this might fail
|
||||
// if Header has different alignment than expected
|
||||
unsafe { (*header).value }
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
// DO: Use read_unaligned for potentially misaligned data
|
||||
fn good_cast(bytes: &[u8]) -> u32 {
|
||||
assert!(bytes.len() >= 4);
|
||||
let ptr = bytes.as_ptr() as *const u32;
|
||||
// SAFETY: We're reading 4 bytes, alignment doesn't matter for read_unaligned
|
||||
unsafe { ptr.read_unaligned() }
|
||||
}
|
||||
|
||||
// DO: Check alignment before cast
|
||||
fn good_aligned_cast(bytes: &[u8]) -> Option<&u32> {
|
||||
if bytes.len() >= 4 && bytes.as_ptr() as usize % std::mem::align_of::<u32>() == 0 {
|
||||
// SAFETY: Checked length and alignment
|
||||
Some(unsafe { &*(bytes.as_ptr() as *const u32) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Use from_ne_bytes for portable byte conversion
|
||||
fn good_from_bytes(bytes: &[u8]) -> u32 {
|
||||
u32::from_ne_bytes(bytes[..4].try_into().unwrap())
|
||||
}
|
||||
|
||||
// DO: Use bytemuck for safe transmutation
|
||||
// use bytemuck::{Pod, Zeroable};
|
||||
// let value: u32 = bytemuck::pod_read_unaligned(bytes);
|
||||
|
||||
// DO: Use align_to for splitting at alignment boundaries
|
||||
fn process_aligned(bytes: &[u8]) {
|
||||
let (prefix, aligned, suffix) = unsafe { bytes.align_to::<u32>() };
|
||||
// prefix and suffix are unaligned portions
|
||||
// aligned is a &[u32] that's properly aligned
|
||||
}
|
||||
```
|
||||
|
||||
## Alignment Check Helpers
|
||||
|
||||
```rust
|
||||
fn is_aligned<T>(ptr: *const u8) -> bool {
|
||||
ptr as usize % std::mem::align_of::<T>() == 0
|
||||
}
|
||||
|
||||
/// Align a pointer up to the next aligned address
|
||||
fn align_up<T>(ptr: *const u8) -> *const u8 {
|
||||
let align = std::mem::align_of::<T>();
|
||||
let addr = ptr as usize;
|
||||
let aligned = (addr + align - 1) & !(align - 1);
|
||||
aligned as *const u8
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
| Arch | Misaligned Access |
|
||||
|------|-------------------|
|
||||
| x86/x64 | Works but slower |
|
||||
| ARM | UB, may trap or give wrong results |
|
||||
| RISC-V | UB, may trap |
|
||||
| WASM | UB |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Is my pointer cast changing alignment requirements?
|
||||
- [ ] Is the source pointer guaranteed to be aligned?
|
||||
- [ ] Should I use read_unaligned instead?
|
||||
- [ ] Can I use safe conversion methods (from_ne_bytes)?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `mem-01`: Choose appropriate data layout
|
||||
- `ffi-13`: Ensure consistent data layout
|
||||
120
skills/unsafe-checker/rules/ptr-05-no-const-to-mut.md
Normal file
120
skills/unsafe-checker/rules/ptr-05-no-const-to-mut.md
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
id: ptr-05
|
||||
original_id: G.UNS.PTR.02
|
||||
level: G
|
||||
impact: CRITICAL
|
||||
clippy: cast_ref_to_mut
|
||||
---
|
||||
|
||||
# Do Not Manually Convert Immutable Pointer to Mutable
|
||||
|
||||
## Summary
|
||||
|
||||
Never cast `*const T` to `*mut T` and dereference it to write. This violates aliasing rules and is undefined behavior.
|
||||
|
||||
## Rationale
|
||||
|
||||
Creating `*const T` from `&T` implies immutability. Other references might exist. Writing through a `*mut T` created from `*const T` creates mutable aliasing, which is UB.
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Cast *const to *mut
|
||||
fn bad_mutate(value: &i32) {
|
||||
let ptr = value as *const i32 as *mut i32;
|
||||
unsafe { *ptr = 42; } // UB: Mutating through &
|
||||
}
|
||||
|
||||
// DON'T: Use transmute to convert
|
||||
fn bad_transmute(value: &i32) -> &mut i32 {
|
||||
unsafe { std::mem::transmute(value) } // UB!
|
||||
}
|
||||
|
||||
// DON'T: "I know this is the only reference"
|
||||
fn bad_claim(value: &i32) {
|
||||
// Even if you "know" there's only one reference,
|
||||
// the compiler assumes & means no mutation
|
||||
let ptr = value as *const i32 as *mut i32;
|
||||
unsafe { *ptr += 1; } // Still UB - compiler may optimize incorrectly
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
// DO: Take &mut if you need to mutate
|
||||
fn good_mutate(value: &mut i32) {
|
||||
*value = 42;
|
||||
}
|
||||
|
||||
// DO: Use interior mutability
|
||||
use std::cell::{Cell, RefCell, UnsafeCell};
|
||||
|
||||
struct Mutable {
|
||||
value: Cell<i32>, // Interior mutability
|
||||
}
|
||||
|
||||
impl Mutable {
|
||||
fn modify(&self) {
|
||||
self.value.set(42); // OK: Cell provides interior mutability
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Use UnsafeCell if you need raw unsafe interior mutability
|
||||
struct RawMutable {
|
||||
value: UnsafeCell<i32>,
|
||||
}
|
||||
|
||||
impl RawMutable {
|
||||
fn modify(&self) {
|
||||
// SAFETY: We ensure exclusive access through external means
|
||||
unsafe { *self.value.get() = 42; }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## The UnsafeCell Exception
|
||||
|
||||
`UnsafeCell<T>` is the ONLY valid way to get `*mut T` from `&self`:
|
||||
|
||||
```rust
|
||||
use std::cell::UnsafeCell;
|
||||
|
||||
pub struct MyMutex<T> {
|
||||
data: UnsafeCell<T>,
|
||||
// ... lock state
|
||||
}
|
||||
|
||||
impl<T> MyMutex<T> {
|
||||
pub fn lock(&self) -> Guard<'_, T> {
|
||||
// acquire lock...
|
||||
|
||||
// SAFETY: UnsafeCell allows this, lock ensures exclusivity
|
||||
Guard { data: unsafe { &mut *self.data.get() } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Why This Is Always UB
|
||||
|
||||
The compiler assumes:
|
||||
1. `&T` means no mutation will occur
|
||||
2. Multiple `&T` can exist simultaneously
|
||||
3. Optimizations can be made based on these assumptions
|
||||
|
||||
When you mutate through cast pointer:
|
||||
1. Other `&T` references see inconsistent values
|
||||
2. Compiler may cache/eliminate reads
|
||||
3. Results are unpredictable
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Am I trying to mutate through `&`?
|
||||
- [ ] Should I use `&mut` instead?
|
||||
- [ ] Should I use `Cell`, `RefCell`, or `UnsafeCell`?
|
||||
- [ ] Is the original type designed for interior mutability?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `safety-08`: Mutable return from immutable parameter is wrong
|
||||
- `safety-02`: Verify safety invariants
|
||||
110
skills/unsafe-checker/rules/ptr-06-prefer-cast.md
Normal file
110
skills/unsafe-checker/rules/ptr-06-prefer-cast.md
Normal file
@@ -0,0 +1,110 @@
|
||||
---
|
||||
id: ptr-06
|
||||
original_id: G.UNS.PTR.03
|
||||
level: G
|
||||
impact: LOW
|
||||
clippy: ptr_as_ptr
|
||||
---
|
||||
|
||||
# Prefer pointer::cast Over `as` for Pointer Casting
|
||||
|
||||
## Summary
|
||||
|
||||
Use the `cast()` method instead of `as` for pointer type conversions. It's clearer and prevents accidental provenance loss.
|
||||
|
||||
## Rationale
|
||||
|
||||
- `cast()` only changes the pointed-to type, not pointer properties
|
||||
- `as` can accidentally convert to integer and back, losing provenance
|
||||
- `cast()` is more explicit about intent
|
||||
- Better tooling support (clippy, miri)
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Use `as` for pointer casts
|
||||
fn bad_cast(ptr: *const u8) -> *const i32 {
|
||||
ptr as *const i32 // Works, but less clear
|
||||
}
|
||||
|
||||
// DON'T: Accidental provenance loss
|
||||
fn bad_roundtrip(ptr: *const u8) -> *const u8 {
|
||||
let addr = ptr as usize; // Converts to integer
|
||||
addr as *const u8 // Loses provenance information!
|
||||
}
|
||||
|
||||
// DON'T: Multiple `as` casts in chain
|
||||
fn bad_chain(ptr: *const u8) -> *mut i32 {
|
||||
ptr as *mut u8 as *mut i32 // Hard to follow
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
// DO: Use cast() for pointer type changes
|
||||
fn good_cast(ptr: *const u8) -> *const i32 {
|
||||
ptr.cast::<i32>()
|
||||
}
|
||||
|
||||
// DO: Use cast_mut() for const-to-mut (when valid)
|
||||
fn good_cast_mut(ptr: *const u8) -> *mut u8 {
|
||||
ptr.cast_mut() // Only use when mutation is valid!
|
||||
}
|
||||
|
||||
// DO: Use cast_const() for mut-to-const
|
||||
fn good_cast_const(ptr: *mut u8) -> *const u8 {
|
||||
ptr.cast_const()
|
||||
}
|
||||
|
||||
// DO: Chain casts clearly
|
||||
fn good_chain(ptr: *const u8) -> *mut i32 {
|
||||
ptr.cast_mut().cast::<i32>()
|
||||
}
|
||||
|
||||
// DO: Use with_addr() for address manipulation (nightly)
|
||||
#[cfg(feature = "strict_provenance")]
|
||||
fn good_provenance(ptr: *const u8, new_addr: usize) -> *const u8 {
|
||||
ptr.with_addr(new_addr) // Preserves provenance
|
||||
}
|
||||
```
|
||||
|
||||
## Pointer Method Reference
|
||||
|
||||
| Method | From | To | Notes |
|
||||
|--------|------|-----|-------|
|
||||
| `.cast::<U>()` | `*T` | `*U` | Changes pointee type |
|
||||
| `.cast_mut()` | `*const T` | `*mut T` | Removes const |
|
||||
| `.cast_const()` | `*mut T` | `*const T` | Adds const |
|
||||
| `.addr()` | `*T` | `usize` | Gets address (nightly) |
|
||||
| `.with_addr(usize)` | `*T` | `*T` | Changes address, keeps provenance |
|
||||
| `.map_addr(fn)` | `*T` | `*T` | Transforms address |
|
||||
|
||||
## Provenance Considerations
|
||||
|
||||
```rust
|
||||
// Provenance = permission to access memory
|
||||
|
||||
// BAD: Loses provenance
|
||||
let ptr: *const u8 = &data as *const u8;
|
||||
let addr = ptr as usize;
|
||||
let ptr2 = addr as *const u8; // ptr2 has no provenance!
|
||||
|
||||
// GOOD: Preserves provenance (nightly strict_provenance)
|
||||
let ptr2 = ptr.with_addr(addr); // Still has permission
|
||||
|
||||
// GOOD: Use expose/from_exposed when provenance must cross integer
|
||||
let addr = ptr.expose_addr(); // "Expose" the provenance
|
||||
let ptr2 = std::ptr::from_exposed_addr(addr); // Recover it
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Am I using `as` where `cast()` would be clearer?
|
||||
- [ ] Am I accidentally converting through `usize`?
|
||||
- [ ] Do I need to preserve provenance?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `ptr-04`: Alignment considerations when casting
|
||||
- `ptr-05`: Don't convert const to mut improperly
|
||||
113
skills/unsafe-checker/rules/safety-01-panic-safety.md
Normal file
113
skills/unsafe-checker/rules/safety-01-panic-safety.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
id: safety-01
|
||||
original_id: P.UNS.SAS.01
|
||||
level: P
|
||||
impact: CRITICAL
|
||||
clippy: panic_in_result_fn
|
||||
---
|
||||
|
||||
# Be Aware of Memory Safety Issues from Panics
|
||||
|
||||
## Summary
|
||||
|
||||
Panics in unsafe code can leave data structures in an inconsistent state, leading to undefined behavior when the panic is caught.
|
||||
|
||||
## Rationale
|
||||
|
||||
When a panic occurs, Rust unwinds the stack and runs destructors. If unsafe code has partially modified data, the destructors may observe invalid state.
|
||||
|
||||
## Bad Example
|
||||
|
||||
```rust
|
||||
// DON'T: Panic can leave Vec in invalid state
|
||||
impl<T> MyVec<T> {
|
||||
pub fn push(&mut self, value: T) {
|
||||
if self.len == self.cap {
|
||||
self.grow(); // Might panic during allocation
|
||||
}
|
||||
|
||||
unsafe {
|
||||
// If Clone::clone() panics after incrementing len,
|
||||
// drop will try to drop uninitialized memory
|
||||
self.len += 1;
|
||||
ptr::write(self.ptr.add(self.len - 1), value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```rust
|
||||
// DO: Ensure panic safety by ordering operations correctly
|
||||
impl<T> MyVec<T> {
|
||||
pub fn push(&mut self, value: T) {
|
||||
if self.len == self.cap {
|
||||
self.grow();
|
||||
}
|
||||
|
||||
unsafe {
|
||||
// Write first, then increment len
|
||||
// If write somehow panics, len is still valid
|
||||
ptr::write(self.ptr.add(self.len), value);
|
||||
self.len += 1; // Only increment after successful write
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DO: Use guards for complex operations
|
||||
impl<T: Clone> MyVec<T> {
|
||||
pub fn extend_from_slice(&mut self, slice: &[T]) {
|
||||
self.reserve(slice.len());
|
||||
|
||||
let mut guard = PanicGuard {
|
||||
vec: self,
|
||||
initialized: 0,
|
||||
};
|
||||
|
||||
for item in slice {
|
||||
unsafe {
|
||||
ptr::write(guard.vec.ptr.add(guard.vec.len + guard.initialized), item.clone());
|
||||
guard.initialized += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Success - update len and forget guard
|
||||
self.len += guard.initialized;
|
||||
std::mem::forget(guard);
|
||||
}
|
||||
}
|
||||
|
||||
struct PanicGuard<'a, T> {
|
||||
vec: &'a mut MyVec<T>,
|
||||
initialized: usize,
|
||||
}
|
||||
|
||||
impl<T> Drop for PanicGuard<'_, T> {
|
||||
fn drop(&mut self) {
|
||||
// Clean up partially initialized elements on panic
|
||||
unsafe {
|
||||
for i in 0..self.initialized {
|
||||
ptr::drop_in_place(self.vec.ptr.add(self.vec.len + i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
|
||||
1. **Update bookkeeping after operations**: Increment length only after writing
|
||||
2. **Use panic guards**: RAII types that clean up on panic
|
||||
3. **Order operations carefully**: Ensure invariants hold if panic occurs at any point
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] What happens if this code panics at each line?
|
||||
- [ ] Are all invariants maintained if we unwind from here?
|
||||
- [ ] Do I need a panic guard for cleanup?
|
||||
|
||||
## Related Rules
|
||||
|
||||
- `safety-04`: Avoid double-free from panic safety issues
|
||||
- `safety-02`: Verify safety invariants
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user