✨ feat(native-planning): 添加 Rust ICSP 原生规划核心及 C# 互操作集成
* 新增 Rust 原生 ICSP 规划库 (Cargo 项目, CubicSpline 算法, FFI 接口) * 新增 Flyshot.Native.Planning C# 互操作项目 (P/Invoke 封装) * 更新 ControllerClientCompat 适配新的规划接口 * 添加构建脚本 (Copy-NativePlanningArtifacts.ps1) * 新增互操作测试及一致性对比测试 * 更新解决方案文件和设计文档
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
# Rust ICSP Native Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 为现有 `ICspPlanner` 增加一套基于 Rust `cdylib` 的原生 ICSP 规划实现,并通过 C ABI + C# P/Invoke 完成可验证的对拍接入,同时不改变当前默认生产路径。
|
||||
|
||||
**Architecture:** 新增 `native/flyshot-native-planning` Rust 原生库承载 clamped-zero 三次样条与 ICSP 迭代核心;新增 `src/Flyshot.Native.Planning` 托管互操作项目负责库加载、数组编排、错误转换和结果复制。现有 `Flyshot.Core.Planning.ICspPlanner` 保持不变,`RustIcspPlanner` 仅作为显式新入口并在测试中与托管实现逐项对拍。
|
||||
|
||||
**Tech Stack:** Rust `cdylib`、C ABI、.NET 8、`LibraryImport`、`SafeHandle`、xUnit、PowerShell、Cargo
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 建立原生与托管骨架
|
||||
|
||||
**Files:**
|
||||
- Create: `native/flyshot-native-planning/Cargo.toml`
|
||||
- Create: `native/flyshot-native-planning/src/lib.rs`
|
||||
- Create: `native/flyshot-native-planning/src/error.rs`
|
||||
- Create: `native/flyshot-native-planning/src/ffi.rs`
|
||||
- Create: `native/flyshot-native-planning/src/icsp.rs`
|
||||
- Create: `native/flyshot-native-planning/src/spline.rs`
|
||||
- Create: `src/Flyshot.Native.Planning/Flyshot.Native.Planning.csproj`
|
||||
- Create: `src/Flyshot.Native.Planning/NativeIcspMethods.cs`
|
||||
- Create: `src/Flyshot.Native.Planning/NativeIcspSafeHandle.cs`
|
||||
- Create: `src/Flyshot.Native.Planning/NativePlanningException.cs`
|
||||
- Create: `src/Flyshot.Native.Planning/NativePlanningLibraryResolver.cs`
|
||||
- Modify: `FlyshotReplacement.sln`
|
||||
- Modify: `tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj`
|
||||
|
||||
- [x] **Step 1: 先写托管侧失败测试,锁定缺少原生库时的报错边界**
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void RustIcspPlanner_ThrowsClearError_WhenNativeLibraryIsMissing()
|
||||
{
|
||||
var planner = new RustIcspPlanner(
|
||||
options: NativeIcspPlannerOptions.Default,
|
||||
libraryResolver: new NativePlanningLibraryResolver(_ => "missing-library"));
|
||||
|
||||
var exception = Assert.Throws<NativePlanningException>(() => planner.Plan(CreateSimpleRequest()));
|
||||
Assert.Contains("missing-library", exception.Message);
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 2: 运行单测确认当前失败**
|
||||
|
||||
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --filter RustIcspPlanner_ThrowsClearError_WhenNativeLibraryIsMissing -v minimal`
|
||||
Expected: FAIL,提示 `RustIcspPlanner` 或互操作类型不存在
|
||||
|
||||
- [x] **Step 3: 创建 Rust `cdylib` 和托管互操作项目骨架**
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "flyshot-native-planning"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = "symbols"
|
||||
panic = "abort"
|
||||
```
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Flyshot.Core.Domain\Flyshot.Core.Domain.csproj" />
|
||||
<ProjectReference Include="..\Flyshot.Core.Planning\Flyshot.Core.Planning.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
- [x] **Step 4: 添加最小库解析与异常包装实现**
|
||||
|
||||
```csharp
|
||||
public sealed class NativePlanningException : Exception
|
||||
{
|
||||
public NativePlanningException(string message, int errorCode, Exception? innerException = null)
|
||||
: base(message, innerException)
|
||||
{
|
||||
ErrorCodeValue = errorCode;
|
||||
}
|
||||
|
||||
public int ErrorCodeValue { get; }
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
public sealed class NativePlanningLibraryResolver
|
||||
{
|
||||
private readonly Func<RuntimeInformation, string> _resolveLibraryPath;
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 5: 重新运行单测,确认错误已变为清晰的原生库缺失异常**
|
||||
|
||||
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --filter RustIcspPlanner_ThrowsClearError_WhenNativeLibraryIsMissing -v minimal`
|
||||
Expected: PASS
|
||||
|
||||
### Task 2: 先做样条基础能力
|
||||
|
||||
**Files:**
|
||||
- Modify: `native/flyshot-native-planning/src/spline.rs`
|
||||
- Modify: `native/flyshot-native-planning/src/lib.rs`
|
||||
- Create: `native/flyshot-native-planning/tests/spline_tests.rs`
|
||||
|
||||
- [x] **Step 1: 先写 Rust 失败测试,锁定 clamped-zero 样条边界和值/导数接口**
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn clamped_zero_spline_hits_waypoints_and_zero_boundary_velocity() {
|
||||
let times = [0.0, 1.0, 2.0, 3.0];
|
||||
let values = [0.0, 1.0, 0.5, 2.0];
|
||||
let spline = CubicSpline1D::new_clamped_zero(×, &values).unwrap();
|
||||
|
||||
assert_abs_diff_eq!(spline.position(0.0), 0.0, epsilon = 1e-12);
|
||||
assert_abs_diff_eq!(spline.position(3.0), 2.0, epsilon = 1e-12);
|
||||
assert_abs_diff_eq!(spline.velocity(0.0), 0.0, epsilon = 1e-12);
|
||||
assert_abs_diff_eq!(spline.velocity(3.0), 0.0, epsilon = 1e-12);
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 2: 运行 Rust 测试确认失败**
|
||||
|
||||
Run: `cargo test --manifest-path native/flyshot-native-planning/Cargo.toml spline_tests::clamped_zero_spline_hits_waypoints_and_zero_boundary_velocity -- --exact`
|
||||
Expected: FAIL,提示样条类型或构造函数不存在
|
||||
|
||||
- [x] **Step 3: 实现三对角求解、样条系数和每段导数峰值接口**
|
||||
|
||||
```rust
|
||||
pub struct CubicSpline1D {
|
||||
knots: Vec<f64>,
|
||||
coefficients: Vec<[f64; 4]>,
|
||||
}
|
||||
|
||||
impl CubicSpline1D {
|
||||
pub fn new_clamped_zero(times: &[f64], values: &[f64]) -> Result<Self, NativePlanningError> { /* ... */ }
|
||||
pub fn position(&self, time: f64) -> f64 { /* ... */ }
|
||||
pub fn velocity(&self, time: f64) -> f64 { /* ... */ }
|
||||
pub fn acceleration(&self, time: f64) -> f64 { /* ... */ }
|
||||
pub fn jerk(&self, time: f64) -> f64 { /* ... */ }
|
||||
pub fn segment_peak_derivatives(&self, segment_index: usize) -> [f64; 3] { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 4: 重新运行 Rust 样条测试**
|
||||
|
||||
Run: `cargo test --manifest-path native/flyshot-native-planning/Cargo.toml spline_tests::clamped_zero_spline_hits_waypoints_and_zero_boundary_velocity -- --exact`
|
||||
Expected: PASS
|
||||
|
||||
### Task 3: 实现纯 Rust ICSP 核心
|
||||
|
||||
**Files:**
|
||||
- Modify: `native/flyshot-native-planning/src/icsp.rs`
|
||||
- Create: `native/flyshot-native-planning/tests/icsp_tests.rs`
|
||||
|
||||
- [x] **Step 1: 先写 Rust 失败测试,锁定初始段时长、scale 计算和未收敛路径**
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn icsp_returns_same_iteration_count_as_managed_baseline_fixture() {
|
||||
let input = test_fixture_three_segment_case();
|
||||
let result = plan_icsp(&input).unwrap();
|
||||
|
||||
assert_eq!(result.iterations, 2);
|
||||
assert_abs_diff_eq!(result.threshold, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 2: 运行 Rust ICSP 测试确认失败**
|
||||
|
||||
Run: `cargo test --manifest-path native/flyshot-native-planning/Cargo.toml icsp_tests::icsp_returns_same_iteration_count_as_managed_baseline_fixture -- --exact`
|
||||
Expected: FAIL,提示 `plan_icsp` 或结果结构不存在
|
||||
|
||||
- [x] **Step 3: 实现最小 ICSP 纯算法结构,逐项复刻 C# 收敛语义**
|
||||
|
||||
```rust
|
||||
pub struct IcspInput<'a> {
|
||||
pub waypoints: &'a [f64],
|
||||
pub waypoint_count: usize,
|
||||
pub dof: usize,
|
||||
pub velocity_limits: &'a [f64],
|
||||
pub acceleration_limits: &'a [f64],
|
||||
pub jerk_limits: &'a [f64],
|
||||
pub options: IcspOptions,
|
||||
}
|
||||
|
||||
pub fn plan_icsp(input: &IcspInput<'_>) -> Result<IcspPlanResult, NativePlanningError> { /* ... */ }
|
||||
```
|
||||
|
||||
- [x] **Step 4: 重新运行 Rust ICSP 测试**
|
||||
|
||||
Run: `cargo test --manifest-path native/flyshot-native-planning/Cargo.toml icsp_tests::icsp_returns_same_iteration_count_as_managed_baseline_fixture -- --exact`
|
||||
Expected: PASS
|
||||
|
||||
### Task 4: 接通 C ABI 和托管结果复制
|
||||
|
||||
**Files:**
|
||||
- Modify: `native/flyshot-native-planning/src/ffi.rs`
|
||||
- Modify: `native/flyshot-native-planning/src/error.rs`
|
||||
- Modify: `src/Flyshot.Native.Planning/NativeIcspMethods.cs`
|
||||
- Modify: `src/Flyshot.Native.Planning/NativeIcspSafeHandle.cs`
|
||||
- Create: `src/Flyshot.Native.Planning/NativeIcspInteropModels.cs`
|
||||
- Create: `src/Flyshot.Native.Planning/RustIcspPlanner.cs`
|
||||
- Create: `tests/Flyshot.Core.Tests/RustIcspPlannerInteropTests.cs`
|
||||
|
||||
- [x] **Step 1: 先写托管失败测试,锁定数组布局、结果复制和释放路径**
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void RustIcspPlanner_ReturnsManagedPlannedTrajectory_FromNativeResult()
|
||||
{
|
||||
var planner = CreateRustPlannerAgainstBuiltNativeLibrary();
|
||||
var result = planner.Plan(CreateSimpleRequest());
|
||||
|
||||
Assert.Equal(PlanningMethod.Icsp, result.Method);
|
||||
Assert.Equal(result.PlannedWaypointCount, result.WaypointTimes.Count);
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 2: 运行单测确认失败**
|
||||
|
||||
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --filter RustIcspPlanner_ReturnsManagedPlannedTrajectory_FromNativeResult -v minimal`
|
||||
Expected: FAIL,提示 P/Invoke 签名、结果结构或释放逻辑未实现
|
||||
|
||||
- [x] **Step 3: 实现 C ABI 入口、线程局部错误文本和 `SafeHandle` 释放**
|
||||
|
||||
```rust
|
||||
#[no_mangle]
|
||||
pub extern "C" fn flyshot_icsp_plan(/* ... */) -> i32 { /* ... */ }
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn flyshot_icsp_result_free(result: *mut FlyshotIcspResult) { /* ... */ }
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn flyshot_last_error_message() -> *const c_char { /* ... */ }
|
||||
```
|
||||
|
||||
```csharp
|
||||
[LibraryImport(NativeLibraryName, EntryPoint = "flyshot_icsp_plan")]
|
||||
internal static unsafe partial int FlyshotIcspPlan(/* ... */);
|
||||
```
|
||||
|
||||
- [x] **Step 4: 重新运行托管互操作测试**
|
||||
|
||||
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --filter RustIcspPlanner_ReturnsManagedPlannedTrajectory_FromNativeResult -v minimal`
|
||||
Expected: PASS
|
||||
|
||||
### Task 5: 增加 C#/Rust 对拍测试和构建复制脚本
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/Flyshot.Core.Tests/PlanningCompatibilityTests.cs`
|
||||
- Create: `tests/Flyshot.Core.Tests/RustIcspPlannerParityTests.cs`
|
||||
- Create: `build/Copy-NativePlanningArtifacts.ps1`
|
||||
- Modify: `README.md`
|
||||
|
||||
- [x] **Step 1: 先写失败测试,锁定 C# 与 Rust 在基础样本上的逐项一致**
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void RustIcspPlanner_MatchesManagedIcspPlanner_ForCompatibilityFixture()
|
||||
{
|
||||
var request = CreateCompatibilityRequest();
|
||||
var managed = new ICspPlanner().Plan(request);
|
||||
var native = CreateRustPlannerAgainstBuiltNativeLibrary().Plan(request);
|
||||
|
||||
AssertTrajectoryMatches(managed, native, tolerance: 1e-9);
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 2: 运行对拍测试确认失败**
|
||||
|
||||
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --filter RustIcspPlanner_MatchesManagedIcspPlanner_ForCompatibilityFixture -v minimal`
|
||||
Expected: FAIL,提示数值未对齐或原生库复制失败
|
||||
|
||||
- [x] **Step 3: 补齐原生库复制脚本、测试输出复制规则和对拍辅助断言**
|
||||
|
||||
```powershell
|
||||
param(
|
||||
[string]$Configuration = "Release"
|
||||
)
|
||||
|
||||
cargo build --manifest-path native/flyshot-native-planning/Cargo.toml --release
|
||||
Copy-Item native/flyshot-native-planning/target/release/flyshot_native_planning.dll tests/Flyshot.Core.Tests/bin/$Configuration/net8.0/ -Force
|
||||
```
|
||||
|
||||
- [x] **Step 4: 重新运行对拍测试**
|
||||
|
||||
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --filter RustIcspPlanner_MatchesManagedIcspPlanner_ForCompatibilityFixture -v minimal`
|
||||
Expected: PASS
|
||||
|
||||
### Task 6: 解决方案级验证
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
- Modify: `docs/superpowers/specs/2026-06-22-rust-icsp-native-design.md`
|
||||
|
||||
- [x] **Step 1: 运行 Rust 全量测试**
|
||||
|
||||
Run: `cargo test --manifest-path native/flyshot-native-planning/Cargo.toml`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 2: 运行 Core 测试**
|
||||
|
||||
Run: `dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj -v minimal`
|
||||
Expected: PASS
|
||||
|
||||
当前验证:新增 Rust 相关测试全部通过;Core 全套仍保留实施前已存在的两项
|
||||
`FlyshotExecutionSendSequenceBuilder` speed-ratio 失败,本计划未修改该行为。
|
||||
|
||||
- [x] **Step 3: 运行解决方案构建**
|
||||
|
||||
Run: `dotnet build FlyshotReplacement.sln -v minimal`
|
||||
Expected: PASS
|
||||
|
||||
- [x] **Step 4: 更新文档,记录本地构建、库复制和当前上线边界**
|
||||
|
||||
```markdown
|
||||
- Rust 规划器当前仅显式使用,不替换默认 `ICspPlanner`
|
||||
- Windows 需要 `flyshot_native_planning.dll`
|
||||
- Linux 需要 `libflyshot_native_planning.so`
|
||||
```
|
||||
@@ -249,3 +249,23 @@ Planning:Backend = Managed | Rust
|
||||
- Linux x64 实际加载 `.so` 并完成规划。
|
||||
- 缺少原生库时能得到明确且可诊断的错误。
|
||||
- 现有 C# `ICspPlanner` 和上层运行链路行为不变。
|
||||
|
||||
## 11. 第一阶段落地状态(2026-06-23)
|
||||
|
||||
当前已落地:
|
||||
|
||||
- `native/flyshot-native-planning` Rust `cdylib`,包含 clamped-zero 样条、
|
||||
一至三阶导数解析峰值、ICSP 迭代和稳定 C ABI。
|
||||
- `src/Flyshot.Native.Planning` 托管互操作项目,包含显式平台解析、
|
||||
`LibraryImport`、`SafeHandle`、错误码转换和结果复制。
|
||||
- Rust 样条、ICSP、FFI 测试,以及 C#/Rust `1e-9` 自动对拍测试。
|
||||
- `build/Copy-NativePlanningArtifacts.ps1`,用于初始化 Windows MSVC 环境、
|
||||
构建原生库并复制到测试或发布目录。
|
||||
- Windows x64 已实际生成并加载 `flyshot_native_planning.dll` 完成规划。
|
||||
|
||||
当前上线边界保持不变:
|
||||
|
||||
- 默认生产路径继续使用 C# `ICspPlanner`。
|
||||
- Rust 规划器只允许显式选择,库缺失、路径不一致或架构不支持时返回明确错误。
|
||||
- Linux x64 的源码、库名和构建脚本边界已落地,但 `.so` 的实际加载验证必须在
|
||||
Linux x64 构建环境执行后才能标记完成。
|
||||
|
||||
Reference in New Issue
Block a user