From 5e661c8b04f4b9b08f8ce2774a768cb46f96e892 Mon Sep 17 00:00:00 2001 From: widberg Date: Thu, 19 Feb 2026 16:52:05 -0500 Subject: [PATCH 1/3] Fix new warning --- examples/zz_experimental_problems.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/zz_experimental_problems.rs b/examples/zz_experimental_problems.rs index c0f488b..56929a7 100644 --- a/examples/zz_experimental_problems.rs +++ b/examples/zz_experimental_problems.rs @@ -17,9 +17,9 @@ enum Unfilled { fn main() { // This file mostly shows one flaw to still be solved or at least to be made configurable: // The inner value of a bitfield, which holds invariants, can currently still be changed. - let mut a = CanBeChanged::new(Unfilled::A); + let mut _a = CanBeChanged::new(Unfilled::A); // There is no enum value for `3` or `0b11`, but we can set it anyways: - a.value = u4::new(3); + _a.value = u4::new(3); // This panics internally: // a.val_0(); @@ -53,8 +53,8 @@ mod somebits { #[allow(dead_code)] fn modify_inner() { let a = 0b10101010; - let mut b = SomeBits::new(true, true, true); - b.value = u3::new(a); + let mut _b = SomeBits::new(true, true, true); + _b.value = u3::new(a); } } use somebits::*; From 14b9f8ecdd4333d2744ee1514d9f722e3ad8d4a4 Mon Sep 17 00:00:00 2001 From: widberg Date: Thu, 19 Feb 2026 17:38:27 -0500 Subject: [PATCH 2/3] Update UI tests --- tests/ui/default-should-be-used.stderr | 9 ++++++++- tests/ui/fallback/more.stderr | 2 +- tests/ui/vis-privacy-is-respected.stderr | 8 ++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/ui/default-should-be-used.stderr b/tests/ui/default-should-be-used.stderr index 6385609..89a7d9c 100644 --- a/tests/ui/default-should-be-used.stderr +++ b/tests/ui/default-should-be-used.stderr @@ -22,4 +22,11 @@ error[E0277]: the trait bound `Inner: Default` is not satisfied --> tests/ui/default-should-be-used.rs:14:8 | 14 | b: Inner, - | ^^^^^ the trait `Default` is not implemented for `Inner` + | ^^^^^ unsatisfied trait bound + | +help: the trait `Default` is not implemented for `Inner` + --> tests/ui/default-should-be-used.rs:17:1 + | +17 | #[bitsize(2)] + | ^^^^^^^^^^^^^ + = note: this error originates in the attribute macro `::bilge::bitsize_internal` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/fallback/more.stderr b/tests/ui/fallback/more.stderr index 1615002..399f52c 100644 --- a/tests/ui/fallback/more.stderr +++ b/tests/ui/fallback/more.stderr @@ -4,7 +4,7 @@ error: `#[fallback]` does not support variants with named fields --> tests/ui/fallback/more.rs:9:5 | -9 | / #[fallback] + 9 | / #[fallback] 10 | | Dee { fallback: u15 }, | |_________________________^ diff --git a/tests/ui/vis-privacy-is-respected.stderr b/tests/ui/vis-privacy-is-respected.stderr index ea52bce..8d2020a 100644 --- a/tests/ui/vis-privacy-is-respected.stderr +++ b/tests/ui/vis-privacy-is-respected.stderr @@ -1,7 +1,7 @@ error[E0624]: method `val_0` is private --> tests/ui/vis-privacy-is-respected.rs:29:11 | -5 | #[bitsize(96)] + 5 | #[bitsize(96)] | -------------- private method defined here ... 29 | diary.val_0(); @@ -10,7 +10,7 @@ error[E0624]: method `val_0` is private error[E0624]: method `set_val_0` is private --> tests/ui/vis-privacy-is-respected.rs:31:11 | -5 | #[bitsize(96)] + 5 | #[bitsize(96)] | -------------- private method defined here ... 31 | diary.set_val_0(rusti); @@ -19,7 +19,7 @@ error[E0624]: method `set_val_0` is private error[E0624]: method `val_1_at` is private --> tests/ui/vis-privacy-is-respected.rs:37:7 | -9 | #[bitsize(8)] + 9 | #[bitsize(8)] | ------------- private method defined here ... 37 | a.val_1_at(1); @@ -28,7 +28,7 @@ error[E0624]: method `val_1_at` is private error[E0624]: method `set_val_1_at` is private --> tests/ui/vis-privacy-is-respected.rs:38:7 | -9 | #[bitsize(8)] + 9 | #[bitsize(8)] | ------------- private method defined here ... 38 | a.set_val_1_at(1, u2::new(0)); From f0c43f988f902600d2bc1db4bb9ee240b5610c5c Mon Sep 17 00:00:00 2001 From: widberg Date: Sat, 6 Sep 2025 10:07:43 -0400 Subject: [PATCH 3/3] Add schemars support --- Cargo.toml | 2 + bilge-impl/Cargo.toml | 1 + bilge-impl/src/lib.rs | 13 +++++ bilge-impl/src/schemars_bits.rs | 85 +++++++++++++++++++++++++++++++++ src/lib.rs | 4 ++ tests/schemars.rs | 56 ++++++++++++++++++++++ 6 files changed, 161 insertions(+) create mode 100644 bilge-impl/src/schemars_bits.rs create mode 100644 tests/schemars.rs diff --git a/Cargo.toml b/Cargo.toml index 7dd1cef..d5b4bff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ rust-version = "1.83" default = [] # Enables constness on nightly; FIXME: re-enable when const convert and const trait impl are back on nightly # nightly = ["arbitrary-int/const_convert_and_const_trait_impl", "bilge-impl/nightly"] +schemars = ["bilge-impl/schemars", "arbitrary-int/schemars"] serde = ["bilge-impl/serde", "arbitrary-int/serde"] [dependencies] @@ -53,6 +54,7 @@ rustversion = "1.0" trybuild = "1.0" custom_bits = { path = "tests/custom_bits" } assert_matches = "1.5.0" +schemars = "0.8" serde = "1.0" serde_test = "1.0" diff --git a/bilge-impl/Cargo.toml b/bilge-impl/Cargo.toml index ab4596f..8bd44ba 100644 --- a/bilge-impl/Cargo.toml +++ b/bilge-impl/Cargo.toml @@ -17,6 +17,7 @@ proc-macro = true default = [] # Enables constness, see README.md for the specific nightly version nightly = [] +schemars = [] serde = [] [dependencies] diff --git a/bilge-impl/src/lib.rs b/bilge-impl/src/lib.rs index 56b5b0f..bf7646f 100644 --- a/bilge-impl/src/lib.rs +++ b/bilge-impl/src/lib.rs @@ -7,6 +7,9 @@ mod debug_bits; mod default_bits; mod fmt_bits; mod from_bits; +#[cfg(feature = "schemars")] +#[cfg_attr(docsrs, doc(cfg(feature = "schemars")))] +mod schemars_bits; #[cfg(feature = "serde")] #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] mod serde_bits; @@ -78,6 +81,16 @@ pub fn derive_default_bits(item: TokenStream) -> TokenStream { default_bits::default_bits(item.into()).into() } +/// Generate an `impl schemars::JsonSchema` for bitfield structs. +/// +/// Please use normal #[derive(JsonSchema)] for enums. +#[cfg(feature = "schemars")] +#[proc_macro_error] +#[proc_macro_derive(JsonSchemaBits, attributes(bitsize_internal))] +pub fn json_schema_bits(item: TokenStream) -> TokenStream { + schemars_bits::json_schema_bits(item.into()).into() +} + /// Generate an `impl serde::Serialize` for bitfield structs. /// /// Please use normal #[derive(Serialize)] for enums. diff --git a/bilge-impl/src/schemars_bits.rs b/bilge-impl/src/schemars_bits.rs new file mode 100644 index 0000000..4be5bcf --- /dev/null +++ b/bilge-impl/src/schemars_bits.rs @@ -0,0 +1,85 @@ +use proc_macro2::TokenStream; +use proc_macro_error2::abort_call_site; +use quote::quote; +use syn::{Data, Field, Fields}; + +use crate::shared::{self, unreachable}; + +fn filter_not_reserved_or_padding(field: &&Field) -> bool { + let field_name_string = field.ident.as_ref().unwrap().to_string(); + !field_name_string.starts_with("reserved_") && !field_name_string.starts_with("padding_") +} + +pub(super) fn json_schema_bits(item: TokenStream) -> TokenStream { + let derive_input = shared::parse_derive(item); + let name = &derive_input.ident; + let name_str = name.to_string(); + let struct_data = match derive_input.data { + Data::Struct(s) => s, + Data::Enum(_) => abort_call_site!("use derive(JsonSchema) for enums"), + Data::Union(_) => unreachable(()), + }; + + let json_schema_impl = match struct_data.fields { + Fields::Named(fields) => { + let calls = fields.named.iter().filter(filter_not_reserved_or_padding).map(|f| { + // We can unwrap since this is a named field + let field_name = f.ident.as_ref().unwrap().to_string(); + let ty = &f.ty; + quote! { + object_validation + .properties + .insert(::to_owned(#field_name), generator.subschema_for::<#ty>()); + object_validation.required.insert(::to_owned(#field_name)); + } + }); + quote! { + let mut schema_object = ::schemars::schema::SchemaObject { + instance_type: ::core::option::Option::Some(::schemars::schema::SingleOrVec::Single(::std::boxed::Box::new(::schemars::schema::InstanceType::Object))), + ..::core::default::Default::default() + }; + let object_validation = schema_object.object(); + object_validation.additional_properties = + ::core::option::Option::Some(::std::boxed::Box::new(::schemars::schema::Schema::Bool(false))); + #(#calls)* + ::schemars::schema::Schema::Object(schema_object) + } + } + Fields::Unnamed(fields) => { + let len = fields.unnamed.len() as u32; + let calls = fields.unnamed.iter().map(|f| { + let ty = &f.ty; + quote!(generator.subschema_for::<#ty>()) + }); + quote! { + ::schemars::schema::Schema::Object(::schemars::schema::SchemaObject { + instance_type: ::core::option::Option::Some(::schemars::schema::SingleOrVec::Single(::std::boxed::Box::new(::schemars::schema::InstanceType::Array))), + array: ::core::option::Option::Some(::std::boxed::Box::new(::schemars::schema::ArrayValidation { + items: ::core::option::Option::Some(::schemars::schema::SingleOrVec::Vec(::std::vec![#(#calls),*])), + max_items: ::core::option::Option::Some(#len), + min_items: ::core::option::Option::Some(#len), + ..::core::default::Default::default() + })), + ..::core::default::Default::default() + }) + } + } + Fields::Unit => todo!("this is a unit struct, which is not supported right now"), + }; + + quote! { + impl ::schemars::JsonSchema for #name { + fn schema_name() -> ::std::string::String { + ::to_owned(#name_str) + } + + fn schema_id() -> ::std::borrow::Cow<'static, str> { + ::std::borrow::Cow::Borrowed(concat!(module_path!(), "::", #name_str)) + } + + fn json_schema(generator: &mut ::schemars::r#gen::SchemaGenerator) -> ::schemars::schema::Schema { + #json_schema_impl + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index bcefb7a..d8cf472 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,8 @@ use core::fmt; #[doc(no_inline)] pub use arbitrary_int; +#[cfg(feature = "schemars")] +pub use bilge_impl::JsonSchemaBits; pub use bilge_impl::{bitsize, bitsize_internal, BinaryBits, DebugBits, DefaultBits, FromBits, TryFromBits}; #[cfg(feature = "serde")] pub use bilge_impl::{DeserializeBits, SerializeBits}; @@ -19,6 +21,8 @@ pub mod prelude { // we control the version, so this should not be a problem arbitrary_int::prelude::*, }; + #[cfg(feature = "schemars")] + pub use super::JsonSchemaBits; #[cfg(feature = "serde")] pub use super::{DeserializeBits, SerializeBits}; } diff --git a/tests/schemars.rs b/tests/schemars.rs new file mode 100644 index 0000000..ab19445 --- /dev/null +++ b/tests/schemars.rs @@ -0,0 +1,56 @@ +#![cfg(feature = "schemars")] + +use bilge::prelude::*; +use schemars::{ + schema::{InstanceType, SingleOrVec}, + schema_for, +}; + +#[bitsize(17)] +#[derive(JsonSchemaBits)] +struct BitsStruct { + padding: u1, + reserved: u1, + field1: u8, + padding: u1, + field2: u5, + reserved: u1, +} + +#[test] +fn schemars_struct() { + let schema = schema_for!(BitsStruct); + let object = schema.schema.object.expect("named bitfield should generate object schema"); + assert_eq!(schema.schema.instance_type, Some(InstanceType::Object.into())); + + assert_eq!(object.properties.len(), 2); + assert!(object.properties.contains_key("field1")); + assert!(object.properties.contains_key("field2")); + assert!(!object.properties.contains_key("padding_i")); + assert!(!object.properties.contains_key("reserved_i")); + + assert_eq!(object.required.len(), 2); + assert!(object.required.contains("field1")); + assert!(object.required.contains("field2")); + assert_eq!(object.additional_properties, Some(Box::new(false.into()))); +} + +#[bitsize(13)] +#[derive(JsonSchemaBits)] +struct BitsTupleStruct(u8, u5); + +#[test] +fn schemars_tuple_struct() { + let schema = schema_for!(BitsTupleStruct); + let array = schema.schema.array.expect("tuple bitfield should generate array schema"); + assert_eq!(schema.schema.instance_type, Some(InstanceType::Array.into())); + + assert_eq!(array.min_items, Some(2)); + assert_eq!(array.max_items, Some(2)); + + let items = array.items.expect("tuple bitfield should define tuple items"); + match items { + SingleOrVec::Single(_) => panic!("tuple bitfield should have one schema per element"), + SingleOrVec::Vec(items) => assert_eq!(items.len(), 2), + } +}