Sorry, I wrote the post at a "conceptual" level trying not to delve too much into the details. The idea was to see a bunch of TryFrom
implementations thanks to "macros" (in a wave-handed fashion), while also showcasing a potential syntactic improvement that has been on my mind for a while.
match_!
can be written and implemented as:
macro_rules! match_ {(
( $($input:tt)* ) $rules:tt
) => (
macro_rules! recurse $rules
recurse! { $($input)* }
)}
and is basically a potential shorthand for doing things like:
macro_rules! try_from_impls {( $($T:tt),* $(,)? ) => ($(
impl TryFrom...
)*)}
try_from_impls! { i8, i16, ..., u64 }
rationale
I've never been super fond of ordering the items like that (macro def before macro call), since macros are generally hard to read, and when defined in this order, the inputs to the macro are only known later on; which is information that would help the readability of the macro definition.
I thus prefer doing:
try_from_impls! { i8, i16, ..., u64 }
// where
macro_rules! try_from_impls {( $($T:tt),* $(,)?? => (
// ^^^^^^^^^^
// we know here that `$T` stands for `i8`, `i16`, etc.
...
)}
use try_from_impls;
but then we have a try_from_impls
identifier being repeated three times for just one invocation of the macro. Hence the match_!
shorthand.
aside
And if that were blessed to be a language construct (match!
syntax), then it could also let itself to chaning macros / eager expansion, in a rather controlled fashion:
match! concat_idents!(foo, bar) {( $foobar:ident ) => (
fn $foobar() { ... }
)}
// or
match! include_str!("...") {( $contents:tt ) => (
my_proc_macro! { $contents } /* problem of proc-macro accessing the fs solved. */
)}
Yes.
It is intended to shadow that type w.r.t. bindgen
's generated definitions:
- Here is the relevant part of
::bindgen
's API that allows to do that.
Basically you have, in C, an int
type, and the question that you and/or bindgen asks is which type, in Rust, should represent it. And it turns out we are free to choose whichever we want, provided it have the same ABI as a C integer: same layout, and same calling convention.
By construction, ::std::os::raw::c_int
or ::libc::c_int
are both types which satisfy these conditions, but they have the huge caveat of being defined as type aliases w.r.t. one of the Rust primitive integer types (e.g., i32
), which makes the conversion between such a type and these aliases too lenient / unchecked on most platforms, and fail to compile on the platforms where the alias points to something else.
-
That is, these are the same problems that we'd have if usize
had been defined as:
#[cfg(target_pointer_width = "32")]
type usize = u32;
#[cfg(target_pointer_width = "64")]
type usize = u64;
Luckily usize
is a type that is always distinct from both u32
and u64
, which makes code have to explicitly perform some kind of transformation between these (in this instance, often as overused as
casts, or as preferred .try_into()
checked casts, or, for more fine-grained control over the cast's behavior, third-party crates out there feature quite nifty APIs, such as ::az
.
So the question then becomes: could we have a type
-
with the same ABI as C's int
type (i.e., same ABI as ::std::os::raw::c_int
),
-
but distinct from the primitive integer types?
Achieving the latter is trivial: just define a new type (e.g., a struct
). And for the former, we just mark that newtype as being #[repr(transparent)]
over a type with the desired ABI. Hence:
#[repr(transparent)]
pub struct strict_c_int(::std::os::raw::c_int);
- Aside: feel free to slap
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
onto it
At that point, thanks to having an actually distinct type, we can use privacy to encapsulate its construction i.e., to only offer explicitly checked casts!
And finally, to reduce the boilerplate with cbindgen
, as well as avoid the "redundancy" of strict::strict_c_int;
, I chose to rename it the same as c_int
.
-
But if you find yourself in a codebase already using the "fuzzy" c_int
from stdlib, and this new one, then shadowing may not be that well suited (although you could always "unshadow" as needed: use strict::c_int as strict_c_int;
).
-
I personally, on the contrary, love shadowing the ill-designed types with these more robust ones as often as possible, to reduce the chances of accidentally using a "lenient" c_int
, and so on.
Yes, the missing part is the case of constants (or at least literals). This a typical problem in the Rust language, since we'd like the same for, for instance, &'static CStr
(we'd like b"Hello, World!\0"
to be somehow convertible to a &'static CStr
without runtime errors, and b"Ill F\0rmed"
to trigger a compilation error). Similar to NonZeroU64
and other similar types featuring a const fn
construction, etc. etc.
There is currently no auto-generated tool for these things, although const fn
can get you quite far.
Let's start with NonZeroI32::new()
, for instance:
macro_rules! nz_i32 {( $N:expr $(,)? ) => (
{
const __N: ::core::primitive::i32 = $N;
{
use ::core::{prelude::v1::*, num::NonZeroI32};
const NZ: NonZeroI32 = match NonZeroI32::new(__N) {
| Some(nz) => nz,
| None => {
#[forbid(const_err)]
let _panic = 0 / __N;
[][0]
},
};
NZ
}
}
)} pub(in crate) use nz_i32;
-
Playground
-
The key idea is that through helper const
blocks, we can ensure come const fn
fallible computation (such as NonZero::new()
) is computed at compile-time, so as to make the failures become always-caught compile-time errors rather than runtime failures that may dodge unit tests.
Since a const
block is cumbersome to write (until we get inline const { ... }
blocks), and "compile-time panics" require getting creative (although we'll soon have proper compile-time panics!), the current solution to alleviate this pattern / feature a lightweight construction is to use macros.
Another options is to use a procedural macro (or a build.rs
script, but that's awfully cumbersome to write!) to inspect the contents of a given literal (≠ a const
), and only emitted an unchecked construction if no invalid input is detected.
- But that is brittle (what if the given
const
is not a literal, but the name / path to another const
ant (e.g., N
); what if the given const
is the output of some other macro?), so it should be avoided when a const fn
-based implementation can be featured.
A final solution, which does have the advantage of not requiring to think too hard about const fn
s nor proc-macros, is, for the case of literals, again, to generate an anonymous test (thus a doctest) that will eagerly check that the input does not cause failures. It does have the issue that the input will still be checked at runtime each time, but does have the advantage of guaranteed a lack of runtime panics should the doctests have been run before releasing the code.
macro_rules! doctested_nz_i32 {( $N:literal $(,)? ) => ({
/// ```rust
#[doc = stringify! {
let _ =
::core::num::NonZeroI32::new($N)
.unwrap_or_else(|| panic!(
"`doctested_nz_i32!({:?})` failed", $N,
))
;
}]
/// ```
extern {}
::core::num::NonZeroI32::new($N).unwrap()
})} use doctested_nz_i32;
In the case of CStr
ings, there are several crates out there which feature this compiletime-checked & runtime-infallible CStr
constants. I'll mention the case of
since it features a "modern" const
-friendly implementation (feature-gated for MSRV reasons), as well as a historical proc-macro implementation
From all these examples it shouldn't be that difficult to feature similar macros for runtime-unfallible construction of strict::c_int
s out of numeric const
ants (of any type, for what is worth):
#[macro_export]
macro_rules! const_c_int {( $N:expr $(,)? ) => ({
#[forbid(const_err)]
const __C_INT: $crate::strict::c_int = {
const MIN: ::core::primitive::i128 = ::std::os::raw::c_int::MIN as _;
const MAX: ::core::primitive::i128 = ::std::os::raw::c_int::MAX as _;
match $N as i128 {
ok @ MIN ..= MAX => unsafe { ::core::mem::transmute(ok as ::std::os::raw::c_int) },
_ => [][0],
}
};
__C_INT
})}
but then such a construction would feature the same portability issues as we initially had: a const_c_int!(42)
passing is not that different from a let it: c_int = 42;
: for a big enough constant, insome platforms both forms may pass compilation but on others it may fail.
So the right thing to do here would be to either remain open to the possibility that even literal numbers can have a fallible conversion (since, even if the input number is constant, the target size is not, portability-wise!), or to choose to support integers only up to a specific integer size which you know shall fit the choice of ::std::os::raw::c_int
for all platforms (is that i16
?).
The macro above can then be amended to use such lower bounds for MIN
and MAX
(rather than those of c_int
). Assuming i16
is OK:
- const MIN: ::core::primitive::i128 = ::std::os::raw::c_int::MIN as _;
+ const MIN: ::core::primitive::i128 = ::core::primitive::i16::MIN as _;
// etc.