Skip to content

Commit

Permalink
Auto merge of #1536 - Aaron1011:feature/const-fn, r=gnzlbg
Browse files Browse the repository at this point in the history
Add support for making functions `const`

PR rust-lang/rust#64906 adds the ability to write `const extern fn` and `const unsafe extern fn`, which will allow manys functions in `libc` to become `const`.

This is particuarly useful for functions which correspond to C macros (e.g. `CMSG_SPACE`). In C, these macros are constant expressions, allowing them to be used when declaring arrays. However, since the corresponding `libc` functions are not `const`, writing equivalent Rust code is impossible. Users must either perform an unecessary heap allocation, or pull in `bindgen` to evaluate the macro for specific values (e.g. `CMSG_SPACE(1)`).

However, the syntax `const extern fn` is not currently parsed by rust. To allow libc to use this without breaking backwards compatibility (i.e. bumping the minimum Rust version), I've taken the following approach:

1. A new off-by-default feature `extern-const-fn` is added to `libc`.
2. The internal `f!` macro has two versions, selected at compile-time by a `cfg_if`. When `extern-const-fn` is enabled, the declared `f!` macro passes through the `const` keyword from the macro user to the final definition (`pub const unsafe extern fn foo`. When  `extern-const-fn` is disabled, the `const` keyword passed by the macro user is discarded, resulting in a plain `pub extern const fn` being declared.

Unfortunately, I couldn't manage to get `macro_rules` to accept a normal `const` token in the proper place (after `pub`). I had to resort to placing it in curly brackets:

```rust
pub {const} fn foo(val: u8) -> i8 {
}
```

The `f!` macro then translates this to a function definition with `const` in the proper position.

I'd appreciate it if someone who's more familiar with `macro_rules!` could see if I missed a way to get the desired syntax.
  • Loading branch information
bors committed Nov 18, 2019
2 parents 5e5c1ee + ca2d53e commit d742eed
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 16 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ std = []
align = []
rustc-dep-of-std = ['align', 'rustc-std-workspace-core']
extra_traits = []
const-extern-fn = []
# use_std is deprecated, use `std` instead
use_std = [ 'std' ]

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ libc = "0.2"
* `extra_traits`: all `struct`s implemented in `libc` are `Copy` and `Clone`.
This feature derives `Debug`, `Eq`, `Hash`, and `PartialEq`.

* `const-extern-fn`: Changes some `extern fn`s into `const extern fn`s.
This features requires a nightly rustc

* **deprecated**: `use_std` is deprecated, and is equivalent to `std`.

## Rust version support
Expand Down
23 changes: 19 additions & 4 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ use std::process::Command;
use std::str;

fn main() {
let rustc_minor_ver =
rustc_minor_version().expect("Failed to get rustc version");
let (rustc_minor_ver, is_nightly) =
rustc_minor_nightly().expect("Failed to get rustc version");
let rustc_dep_of_std = env::var("CARGO_FEATURE_RUSTC_DEP_OF_STD").is_ok();
let align_cargo_feature = env::var("CARGO_FEATURE_ALIGN").is_ok();
let const_extern_fn_cargo_feature =
env::var("CARGO_FEATURE_CONST_EXTERN_FN").is_ok();
let libc_ci = env::var("LIBC_CI").is_ok();

if env::var("CARGO_FEATURE_USE_STD").is_ok() {
Expand Down Expand Up @@ -72,9 +74,16 @@ fn main() {
if rustc_dep_of_std {
println!("cargo:rustc-cfg=libc_thread_local");
}

if const_extern_fn_cargo_feature {
if !is_nightly || rustc_minor_ver < 40 {
panic!("const-extern-fn requires a nightly compiler >= 1.40")
}
println!("cargo:rustc-cfg=libc_const_extern_fn");
}
}

fn rustc_minor_version() -> Option<u32> {
fn rustc_minor_nightly() -> Option<(u32, bool)> {
macro_rules! otry {
($e:expr) => {
match $e {
Expand All @@ -93,7 +102,13 @@ fn rustc_minor_version() -> Option<u32> {
return None;
}

otry!(pieces.next()).parse().ok()
let minor = pieces.next();
let nightly_raw = otry!(otry!(pieces.next()).split('-').nth(1));
let nightly =
nightly_raw.starts_with("dev") || nightly_raw.starts_with("nightly");
let minor = otry!(otry!(minor).parse().ok());

Some((minor, nightly))
}

fn which_freebsd() -> Option<i32> {
Expand Down
7 changes: 7 additions & 0 deletions ci/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ test_target() {
cargo "+${RUST}" "${BUILD_CMD}" -vv $opt --no-default-features --target "${TARGET}" \
--features extra_traits

# Test the 'const-extern-fn' feature on nightly
if [ "${RUST}" = "nightly" ]; then
cargo "+${RUST}" "${BUILD_CMD}" -vv $opt --no-default-features --target "${TARGET}" \
--features const-extern-fn
fi


# Also test that it builds with `extra_traits` and default features:
if [ "$NO_STD" != "1" ]; then
cargo "+${RUST}" "${BUILD_CMD}" -vv $opt --target "${TARGET}" \
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#![no_std]
#![cfg_attr(feature = "rustc-dep-of-std", no_core)]
#![cfg_attr(target_os = "redox", feature(static_nobundle))]
#![cfg_attr(libc_const_extern_fn, feature(const_extern_fn))]

#[macro_use]
mod macros;
Expand Down
98 changes: 89 additions & 9 deletions src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,16 +121,96 @@ macro_rules! s_no_extra_traits {
);
}

#[allow(unused_macros)]
macro_rules! f {
($(pub fn $i:ident($($arg:ident: $argty:ty),*) -> $ret:ty {
$($body:stmt);*
})*) => ($(
#[inline]
pub unsafe extern fn $i($($arg: $argty),*) -> $ret {
$($body);*
// This is a pretty horrible hack to allow us to conditionally mark
// some functions as 'const', without requiring users of this macro
// to care about the "const-extern-fn" feature.
//
// When 'const-extern-fn' is enabled, we emit the captured 'const' keyword
// in the expanded function.
//
// When 'const-extern-fn' is disabled, we always emit a plain 'pub unsafe extern fn'.
// Note that the expression matched by the macro is exactly the same - this allows
// users of this macro to work whether or not 'const-extern-fn' is enabled
//
// Unfortunately, we need to duplicate most of this macro between the 'cfg_if' blocks.
// This is because 'const unsafe extern fn' won't even parse on older compilers,
// so we need to avoid emitting it at all of 'const-extern-fn'.
//
// Specifically, moving the 'cfg_if' into the macro body will *not* work.
// Doing so would cause the '#[cfg(feature = "const-extern-fn")]' to be emiited
// into user code. The 'cfg' gate will not stop Rust from trying to parse the
// 'pub const unsafe extern fn', so users would get a compiler error even when
// the 'const-extern-fn' feature is disabled
//
// Note that users of this macro need to place 'const' in a weird position
// (after the closing ')' for the arguments, but before the return type).
// This was the only way I could satisfy the following two requirements:
// 1. Avoid ambuguity errors from 'macro_rules!' (which happen when writing '$foo:ident fn'
// 2. Allow users of this macro to mix 'pub fn foo' and 'pub const fn bar' within the same
// 'f!' block
cfg_if! {
if #[cfg(libc_const_extern_fn)] {
#[allow(unused_macros)]
macro_rules! f {
($(pub $({$constness:ident})* fn $i:ident(
$($arg:ident: $argty:ty),*
) -> $ret:ty {
$($body:stmt);*
})*) => ($(
#[inline]
pub $($constness)* unsafe extern fn $i($($arg: $argty),*
) -> $ret {
$($body);*
}
)*)
}
)*)

#[allow(unused_macros)]
macro_rules! const_fn {
($($({$constness:ident})* fn $i:ident(
$($arg:ident: $argty:ty),*
) -> $ret:ty {
$($body:stmt);*
})*) => ($(
#[inline]
$($constness)* fn $i($($arg: $argty),*
) -> $ret {
$($body);*
}
)*)
}

} else {
#[allow(unused_macros)]
macro_rules! f {
($(pub $({$constness:ident})* fn $i:ident(
$($arg:ident: $argty:ty),*
) -> $ret:ty {
$($body:stmt);*
})*) => ($(
#[inline]
pub unsafe extern fn $i($($arg: $argty),*
) -> $ret {
$($body);*
}
)*)
}

#[allow(unused_macros)]
macro_rules! const_fn {
($($({$constness:ident})* fn $i:ident(
$($arg:ident: $argty:ty),*
) -> $ret:ty {
$($body:stmt);*
})*) => ($(
#[inline]
fn $i($($arg: $argty),*
) -> $ret {
$($body);*
}
)*)
}
}
}

#[allow(unused_macros)]
Expand Down
8 changes: 5 additions & 3 deletions src/unix/linux_like/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1165,8 +1165,10 @@ pub const ARPHRD_IEEE802154: u16 = 804;
pub const ARPHRD_VOID: u16 = 0xFFFF;
pub const ARPHRD_NONE: u16 = 0xFFFE;

fn CMSG_ALIGN(len: usize) -> usize {
len + ::mem::size_of::<usize>() - 1 & !(::mem::size_of::<usize>() - 1)
const_fn! {
{const} fn CMSG_ALIGN(len: usize) -> usize {
len + ::mem::size_of::<usize>() - 1 & !(::mem::size_of::<usize>() - 1)
}
}

f! {
Expand All @@ -1182,7 +1184,7 @@ f! {
cmsg.offset(1) as *mut ::c_uchar
}

pub fn CMSG_SPACE(length: ::c_uint) -> ::c_uint {
pub {const} fn CMSG_SPACE(length: ::c_uint) -> ::c_uint {
(CMSG_ALIGN(length as usize) + CMSG_ALIGN(::mem::size_of::<cmsghdr>()))
as ::c_uint
}
Expand Down
5 changes: 5 additions & 0 deletions tests/const_fn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#![cfg(libc_const_extern_fn)] // If this does not hold, the file is empty

#[cfg(target_os = "linux")]
const _FOO: libc::c_uint = unsafe { libc::CMSG_SPACE(1) };
//^ if CMSG_SPACE is not const, this will fail to compile

0 comments on commit d742eed

Please sign in to comment.