Skip to content

Commit 5cb243e

Browse files
daniel-nolandclaude
andcommitted
feat(dpdk): safe rte_acl wrapper
A safe Rust wrapper around librte_acl. Independent of any pending cascade work; usable by any future DPDK ACL consumer. # What is in the wrapper dpdk/src/acl/mod.rs module overview + safe re-exports dpdk/src/acl/classify.rs ClassifyAlgorithm + discriminant conversions dpdk/src/acl/config.rs typed AclBuildConfig / AclCreateParams dpdk/src/acl/context.rs AclContext<N, State> + Configuring/Built typestate dpdk/src/acl/error.rs typed AclCreateError / AclBuildError / ... dpdk/src/acl/field.rs FieldDef / FieldSize / FieldType dpdk/src/acl/rule.rs Rule / RuleData / AclField + Priority newtype The const-generic AclContext<N, State> is the load-bearing abstraction: the field count is shared between AclContext, Rule, and AclBuildConfig, so any field-count mismatch is a compile-time error. Configuring->Built transitions enforce DPDK's "rules cannot be added after build" and "classification is &self (thread-safe)" invariants at the type level. # Soundness AclContext::classify and classify_with_algorithm are `unsafe fn` because they take `&[*const u8]` whose validity cannot be enforced by the type system -- the caller must guarantee each pointer references a buffer of at least max(offset + size) bytes per the field layout. A future safe wrapper around `&[&[u8; STRIDE]]` is deferred until a concrete consumer demonstrates the shape it wants. Everywhere else, `unsafe` is wrapped in safe abstractions (see development/code/unsafe-code.md). Union-typed accessors (AclField::value_u*/mask_range_u*) are safe -- the union holds only integer members, none of which have invalid bit patterns. # Validation A `classify_smoke` integration test builds a tiny ACL context, runs a real `rte_acl_classify` call, and verifies the match / no-match outcomes. Configured with `--no-huge --in-memory` so it needs no hugepage setup, and `--lcores 0` to skip worker-lcore spawn (which sidesteps a benign-but-flagged data race in DPDK's internal lcore readiness signalling that ThreadSanitizer reports). Hand-rolled tests are joined by bolero property tests over: - ClassifyAlgorithm::from_u32 round-trip and unknown-rejection across the full u32 domain - Priority::new range validation across the full i32 domain - AclCreateParams name validation over arbitrary strings (empty, non-ASCII, too-long, NUL-containing) - AclBuildConfig::new num_categories validation (zero, oversize, misaligned) # Build-side enablement (required by the rte_acl symbols) nix/pkgs/dpdk/default.nix Move "acl" from disabledLibs to enabledLibs so librte_acl.a is part of the sysroot build. nix/pkgs/dpdk-wrapper/src/dpdk_wrapper.h Add `#include <rte_acl.h>` so bindgen picks up the rte_acl_* symbols. dpdk-sys/build.rs Add "rte_acl" to the link list. To validate locally: just setup-roots # rebuilds DPDK + wrapper # Re-enter nix-shell to pick up the new DATAPLANE_SYSROOT. cargo nextest run -p dataplane-dpdk Signed-off-by: Daniel Noland <daniel@githedgehog.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 09accba commit 5cb243e

13 files changed

Lines changed: 3456 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dpdk-sys/build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ fn main() {
9393
"rte_hash",
9494
"rte_rcu",
9595
"rte_ring",
96+
"rte_acl",
9697
"rte_eal",
9798
"rte_argparse",
9899
"rte_kvargs",

dpdk/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,6 @@ tracing = { workspace = true, features = ["attributes"] }
2121

2222
[build-dependencies]
2323
dpdk-sysroot-helper = { workspace = true }
24+
25+
[dev-dependencies]
26+
bolero = { workspace = true, default-features = false, features = ["std"] }

dpdk/src/acl/classify.rs

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Open Network Fabric Authors
3+
4+
//! ACL classification algorithm selection.
5+
//!
6+
//! DPDK provides multiple SIMD-accelerated implementations of its ACL classification engine.
7+
//! The [`ClassifyAlgorithm`] enum exposes these as a safe Rust type that can be used with
8+
//! [`AclContext::classify_with_algorithm`][super::context::AclContext] or
9+
//! [`AclContext::set_default_algorithm`][super::context::AclContext].
10+
//!
11+
//! In most cases [`ClassifyAlgorithm::Default`] is the right choice -- DPDK will automatically
12+
//! select the best implementation for the current CPU at build time. Explicit selection is useful
13+
//! for benchmarking or for targeting a specific code path.
14+
15+
use core::fmt::{self, Display, Formatter};
16+
17+
// ---------------------------------------------------------------------------
18+
// ClassifyAlgorithm
19+
// ---------------------------------------------------------------------------
20+
21+
/// SIMD implementation to use for ACL classification.
22+
///
23+
/// Maps 1:1 to the `RTE_ACL_CLASSIFY_*` constants in
24+
/// [`rte_acl_classify_alg`][mod@dpdk_sys::rte_acl_classify_alg].
25+
///
26+
/// # Platform support
27+
///
28+
/// Not every variant is available on every CPU. Requesting an unsupported algorithm will result
29+
/// in an error from [`rte_acl_classify_alg`][fn@dpdk_sys::rte_acl_classify_alg] or
30+
/// [`rte_acl_set_ctx_classify`][dpdk_sys::rte_acl_set_ctx_classify].
31+
/// [`Default`][ClassifyAlgorithm::Default] is always available and is recommended unless you have
32+
/// a specific reason to select a particular implementation.
33+
#[repr(u32)]
34+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
35+
pub enum ClassifyAlgorithm {
36+
/// Let DPDK choose the best available implementation for the current CPU.
37+
///
38+
/// This is almost always what you want.
39+
///
40+
/// Corresponds to
41+
/// [`RTE_ACL_CLASSIFY_DEFAULT`][dpdk_sys::rte_acl_classify_alg::RTE_ACL_CLASSIFY_DEFAULT].
42+
#[default]
43+
Default = dpdk_sys::rte_acl_classify_alg::RTE_ACL_CLASSIFY_DEFAULT,
44+
45+
/// Portable scalar (non-SIMD) implementation.
46+
///
47+
/// Available on all platforms. Useful as a baseline for benchmarks.
48+
///
49+
/// Corresponds to
50+
/// [`RTE_ACL_CLASSIFY_SCALAR`][dpdk_sys::rte_acl_classify_alg::RTE_ACL_CLASSIFY_SCALAR].
51+
Scalar = dpdk_sys::rte_acl_classify_alg::RTE_ACL_CLASSIFY_SCALAR,
52+
53+
/// SSE 4.1 vectorized implementation.
54+
///
55+
/// Requires x86-64 SSE 4.1 support.
56+
///
57+
/// Corresponds to
58+
/// [`RTE_ACL_CLASSIFY_SSE`][dpdk_sys::rte_acl_classify_alg::RTE_ACL_CLASSIFY_SSE].
59+
Sse = dpdk_sys::rte_acl_classify_alg::RTE_ACL_CLASSIFY_SSE,
60+
61+
/// AVX2 vectorized implementation.
62+
///
63+
/// Requires x86-64 AVX2 support.
64+
///
65+
/// Corresponds to
66+
/// [`RTE_ACL_CLASSIFY_AVX2`][dpdk_sys::rte_acl_classify_alg::RTE_ACL_CLASSIFY_AVX2].
67+
Avx2 = dpdk_sys::rte_acl_classify_alg::RTE_ACL_CLASSIFY_AVX2,
68+
69+
/// ARM NEON vectorized implementation.
70+
///
71+
/// Requires AArch64 NEON support.
72+
///
73+
/// Corresponds to
74+
/// [`RTE_ACL_CLASSIFY_NEON`][dpdk_sys::rte_acl_classify_alg::RTE_ACL_CLASSIFY_NEON].
75+
Neon = dpdk_sys::rte_acl_classify_alg::RTE_ACL_CLASSIFY_NEON,
76+
77+
/// PowerPC AltiVec vectorized implementation.
78+
///
79+
/// Requires PowerPC AltiVec / VMX support.
80+
///
81+
/// Corresponds to
82+
/// [`RTE_ACL_CLASSIFY_ALTIVEC`][dpdk_sys::rte_acl_classify_alg::RTE_ACL_CLASSIFY_ALTIVEC].
83+
Altivec = dpdk_sys::rte_acl_classify_alg::RTE_ACL_CLASSIFY_ALTIVEC,
84+
85+
/// AVX-512 vectorized implementation processing 16 flows in parallel.
86+
///
87+
/// Requires x86-64 AVX-512 support (specifically AVX-512BW).
88+
///
89+
/// Corresponds to
90+
/// [`RTE_ACL_CLASSIFY_AVX512X16`][dpdk_sys::rte_acl_classify_alg::RTE_ACL_CLASSIFY_AVX512X16].
91+
Avx512x16 = dpdk_sys::rte_acl_classify_alg::RTE_ACL_CLASSIFY_AVX512X16,
92+
93+
/// AVX-512 vectorized implementation processing 32 flows in parallel.
94+
///
95+
/// Requires x86-64 AVX-512 support (specifically AVX-512BW).
96+
///
97+
/// Corresponds to
98+
/// [`RTE_ACL_CLASSIFY_AVX512X32`][dpdk_sys::rte_acl_classify_alg::RTE_ACL_CLASSIFY_AVX512X32].
99+
Avx512x32 = dpdk_sys::rte_acl_classify_alg::RTE_ACL_CLASSIFY_AVX512X32,
100+
}
101+
102+
impl ClassifyAlgorithm {
103+
/// Convert to the raw `u32` discriminant value expected by the DPDK C API.
104+
#[must_use]
105+
#[inline]
106+
pub const fn as_u32(self) -> u32 {
107+
self as u32
108+
}
109+
110+
/// Attempt to parse a raw `u32` into a [`ClassifyAlgorithm`].
111+
///
112+
/// Returns `None` if the value does not correspond to a known algorithm.
113+
/// See also the [`TryFrom<u32>`] impl, which is the same operation framed as the
114+
/// idiomatic conversion trait.
115+
#[must_use]
116+
pub const fn from_u32(value: u32) -> Option<Self> {
117+
match value {
118+
x if x == Self::Default as u32 => Some(Self::Default),
119+
x if x == Self::Scalar as u32 => Some(Self::Scalar),
120+
x if x == Self::Sse as u32 => Some(Self::Sse),
121+
x if x == Self::Avx2 as u32 => Some(Self::Avx2),
122+
x if x == Self::Neon as u32 => Some(Self::Neon),
123+
x if x == Self::Altivec as u32 => Some(Self::Altivec),
124+
x if x == Self::Avx512x16 as u32 => Some(Self::Avx512x16),
125+
x if x == Self::Avx512x32 as u32 => Some(Self::Avx512x32),
126+
_ => None,
127+
}
128+
}
129+
130+
/// Returns `true` if this is an x86-64 specific algorithm variant.
131+
#[must_use]
132+
pub const fn is_x86_64(&self) -> bool {
133+
matches!(
134+
self,
135+
Self::Sse | Self::Avx2 | Self::Avx512x16 | Self::Avx512x32
136+
)
137+
}
138+
139+
/// Returns `true` if this is an ARM specific algorithm variant.
140+
#[must_use]
141+
pub const fn is_aarch64(&self) -> bool {
142+
matches!(self, Self::Neon)
143+
}
144+
145+
/// Returns `true` if this is a PowerPC specific algorithm variant.
146+
#[must_use]
147+
pub const fn is_powerpc(&self) -> bool {
148+
matches!(self, Self::Altivec)
149+
}
150+
151+
/// Returns `true` if this is a platform-independent variant.
152+
#[must_use]
153+
pub const fn is_portable(&self) -> bool {
154+
matches!(self, Self::Default | Self::Scalar)
155+
}
156+
}
157+
158+
impl Display for ClassifyAlgorithm {
159+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
160+
match self {
161+
Self::Default => write!(f, "Default"),
162+
Self::Scalar => write!(f, "Scalar"),
163+
Self::Sse => write!(f, "SSE"),
164+
Self::Avx2 => write!(f, "AVX2"),
165+
Self::Neon => write!(f, "NEON"),
166+
Self::Altivec => write!(f, "AltiVec"),
167+
Self::Avx512x16 => write!(f, "AVX-512 (x16)"),
168+
Self::Avx512x32 => write!(f, "AVX-512 (x32)"),
169+
}
170+
}
171+
}
172+
173+
impl From<ClassifyAlgorithm> for dpdk_sys::rte_acl_classify_alg::Type {
174+
#[inline]
175+
fn from(alg: ClassifyAlgorithm) -> Self {
176+
alg.as_u32()
177+
}
178+
}
179+
180+
/// Unknown algorithm discriminant returned by [`ClassifyAlgorithm::try_from`].
181+
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
182+
#[error("unknown rte_acl_classify_alg discriminant {0}")]
183+
pub struct UnknownClassifyAlgorithm(pub u32);
184+
185+
impl TryFrom<u32> for ClassifyAlgorithm {
186+
type Error = UnknownClassifyAlgorithm;
187+
fn try_from(value: u32) -> Result<Self, Self::Error> {
188+
Self::from_u32(value).ok_or(UnknownClassifyAlgorithm(value))
189+
}
190+
}
191+
192+
// ---------------------------------------------------------------------------
193+
// Compile-time assertions
194+
// ---------------------------------------------------------------------------
195+
196+
/// Verify that our enum discriminants match the DPDK constants exactly.
197+
const _: () = {
198+
use dpdk_sys::rte_acl_classify_alg::*;
199+
200+
assert!(ClassifyAlgorithm::Default as u32 == RTE_ACL_CLASSIFY_DEFAULT);
201+
assert!(ClassifyAlgorithm::Scalar as u32 == RTE_ACL_CLASSIFY_SCALAR);
202+
assert!(ClassifyAlgorithm::Sse as u32 == RTE_ACL_CLASSIFY_SSE);
203+
assert!(ClassifyAlgorithm::Avx2 as u32 == RTE_ACL_CLASSIFY_AVX2);
204+
assert!(ClassifyAlgorithm::Neon as u32 == RTE_ACL_CLASSIFY_NEON);
205+
assert!(ClassifyAlgorithm::Altivec as u32 == RTE_ACL_CLASSIFY_ALTIVEC);
206+
assert!(ClassifyAlgorithm::Avx512x16 as u32 == RTE_ACL_CLASSIFY_AVX512X16);
207+
assert!(ClassifyAlgorithm::Avx512x32 as u32 == RTE_ACL_CLASSIFY_AVX512X32);
208+
};
209+
210+
// ---------------------------------------------------------------------------
211+
// Tests
212+
// ---------------------------------------------------------------------------
213+
214+
#[cfg(test)]
215+
mod tests {
216+
use super::*;
217+
218+
#[test]
219+
fn default_is_zero() {
220+
assert_eq!(ClassifyAlgorithm::Default.as_u32(), 0);
221+
assert_eq!(ClassifyAlgorithm::default(), ClassifyAlgorithm::Default);
222+
}
223+
224+
#[test]
225+
fn round_trip_all_variants() {
226+
let variants = [
227+
ClassifyAlgorithm::Default,
228+
ClassifyAlgorithm::Scalar,
229+
ClassifyAlgorithm::Sse,
230+
ClassifyAlgorithm::Avx2,
231+
ClassifyAlgorithm::Neon,
232+
ClassifyAlgorithm::Altivec,
233+
ClassifyAlgorithm::Avx512x16,
234+
ClassifyAlgorithm::Avx512x32,
235+
];
236+
for variant in variants {
237+
let raw = variant.as_u32();
238+
let parsed = ClassifyAlgorithm::from_u32(raw);
239+
assert_eq!(parsed, Some(variant), "round-trip failed for {variant}");
240+
}
241+
}
242+
243+
#[test]
244+
fn from_u32_rejects_unknown() {
245+
assert_eq!(ClassifyAlgorithm::from_u32(99), None);
246+
assert_eq!(ClassifyAlgorithm::from_u32(u32::MAX), None);
247+
}
248+
249+
#[test]
250+
fn display_all_variants() {
251+
let display_strings = [
252+
(ClassifyAlgorithm::Default, "Default"),
253+
(ClassifyAlgorithm::Scalar, "Scalar"),
254+
(ClassifyAlgorithm::Sse, "SSE"),
255+
(ClassifyAlgorithm::Avx2, "AVX2"),
256+
(ClassifyAlgorithm::Neon, "NEON"),
257+
(ClassifyAlgorithm::Altivec, "AltiVec"),
258+
(ClassifyAlgorithm::Avx512x16, "AVX-512 (x16)"),
259+
(ClassifyAlgorithm::Avx512x32, "AVX-512 (x32)"),
260+
];
261+
for (variant, expected) in display_strings {
262+
assert_eq!(format!("{variant}"), expected);
263+
}
264+
}
265+
266+
#[test]
267+
fn platform_classification() {
268+
assert!(ClassifyAlgorithm::Default.is_portable());
269+
assert!(ClassifyAlgorithm::Scalar.is_portable());
270+
271+
assert!(ClassifyAlgorithm::Sse.is_x86_64());
272+
assert!(ClassifyAlgorithm::Avx2.is_x86_64());
273+
assert!(ClassifyAlgorithm::Avx512x16.is_x86_64());
274+
assert!(ClassifyAlgorithm::Avx512x32.is_x86_64());
275+
276+
assert!(ClassifyAlgorithm::Neon.is_aarch64());
277+
assert!(ClassifyAlgorithm::Altivec.is_powerpc());
278+
279+
// Cross-checks: portable variants should not be platform-specific.
280+
assert!(!ClassifyAlgorithm::Default.is_x86_64());
281+
assert!(!ClassifyAlgorithm::Default.is_aarch64());
282+
assert!(!ClassifyAlgorithm::Default.is_powerpc());
283+
284+
// Platform-specific variants should not be portable.
285+
assert!(!ClassifyAlgorithm::Sse.is_portable());
286+
assert!(!ClassifyAlgorithm::Neon.is_portable());
287+
assert!(!ClassifyAlgorithm::Altivec.is_portable());
288+
}
289+
290+
#[test]
291+
fn into_dpdk_type() {
292+
let alg = ClassifyAlgorithm::Avx2;
293+
let raw: dpdk_sys::rte_acl_classify_alg::Type = alg.into();
294+
assert_eq!(raw, dpdk_sys::rte_acl_classify_alg::RTE_ACL_CLASSIFY_AVX2);
295+
}
296+
297+
/// All known discriminants -- the universe `from_u32` must accept and the
298+
/// universe `as_u32` round-trips through.
299+
const KNOWN: &[ClassifyAlgorithm] = &[
300+
ClassifyAlgorithm::Default,
301+
ClassifyAlgorithm::Scalar,
302+
ClassifyAlgorithm::Sse,
303+
ClassifyAlgorithm::Avx2,
304+
ClassifyAlgorithm::Neon,
305+
ClassifyAlgorithm::Altivec,
306+
ClassifyAlgorithm::Avx512x16,
307+
ClassifyAlgorithm::Avx512x32,
308+
];
309+
310+
/// Property: for every `u32`, `from_u32` either round-trips through `as_u32`
311+
/// (when the value is a known discriminant) or rejects with `None` (when it
312+
/// is not). Generalises the hand-rolled `round_trip_all_variants` test over
313+
/// the entire `u32` domain.
314+
#[test]
315+
fn from_u32_round_trip_property() {
316+
bolero::check!().with_type::<u32>().for_each(
317+
|value: &u32| match ClassifyAlgorithm::from_u32(*value) {
318+
Some(alg) => assert_eq!(
319+
alg.as_u32(),
320+
*value,
321+
"from_u32({value}) -> {alg:?} but {alg:?}.as_u32() = {}",
322+
alg.as_u32()
323+
),
324+
None => {
325+
for variant in KNOWN {
326+
assert_ne!(
327+
variant.as_u32(),
328+
*value,
329+
"from_u32({value}) returned None but {variant:?} has that discriminant"
330+
);
331+
}
332+
}
333+
},
334+
);
335+
}
336+
337+
/// Property: `TryFrom<u32>` matches `from_u32` exactly.
338+
#[test]
339+
fn try_from_matches_from_u32() {
340+
bolero::check!().with_type::<u32>().for_each(|value: &u32| {
341+
let opt = ClassifyAlgorithm::from_u32(*value);
342+
let res = ClassifyAlgorithm::try_from(*value).ok();
343+
assert_eq!(opt, res);
344+
});
345+
}
346+
}

0 commit comments

Comments
 (0)