Skip to content
This repository was archived by the owner on Jan 22, 2025. It is now read-only.

Commit d1c0f4b

Browse files
mergify[bot]Aaron1011jackcmaymvines
authored
Fix hygiene issues in declare_program! and declare_loader! (bp #10905) (#11073)
* Fix hygiene issues in `declare_program!` and `declare_loader!` The `declare_program!` and `declare_loader!` macros both expand to new macro definitions (based on the `$name` argument). These 'inner' macros make use of the special `$crate` metavariable to access items in the crate where the 'inner' macros is defined. However, this only works due to a bug in rustc. When a macro is expanded, all `$crate` tokens in its output are 'marked' as being resolved in the defining crate of that macro. An inner macro (including the body of its arms) is 'just' another set of tokens that appears in the body of the outer macro, so any `$crate` identifiers used there are resolved relative to the 'outer' macro. For example, consider the following code: ```rust macro_rules! outer { () => { macro_rules! inner { () => { $crate::Foo } } } } ``` The path `$crate::Foo` will be resolved relative to the crate that defines `outer`, **not** the crate which defines `inner`. However, rustc currently loses this extra resolution information (referred to as 'hygiene' information) when a crate is serialized. In the above example, this means that the macro `inner` (which gets defined in whatever crate invokes `outer!`) will behave differently depending on which crate it is invoked from: When `inner` is invoked from the same crate in which it is defined, the hygiene information will still be available, which will cause `$crate::Foo` to be resolved in the crate which defines 'outer'. When `inner` is invoked from a different crate, it will be loaded from the metadata of the crate which defines 'inner'. Since the hygiene information is currently lost, rust will 'forget' that `$crate::Foo` is supposed to be resolved in the context of 'outer'. Instead, it will be resolved relative to the crate which defines 'inner', which can cause incorrect code to compile. This bug will soon be fixed in rust (see rust-lang/rust#72121), which will break `declare_program!` and `declare_loader!`. Fortunately, it's possible to obtain the desired behavior (`$crate` resolving in the context of the 'inner' macro) by use of a procedural macro. This commit adds a `respan!` proc-macro to the `sdk/macro` crate. Using the newly-stabilized (on Nightly) `Span::resolved_at` method, the `$crate` identifier can be made to be resolved in the context of the proper crate. Since `Span::resolved_at` is only stable on the latest nightly, referencing it on an earlier version of Rust will cause a compilation error. This requires the `rustversion` crate to be used, which allows conditionally compiling code epending on the Rust compiler version in use. Since this method is already stabilized in the latest nightly, there will never be a situation where the hygiene bug is fixed (e.g. rust-lang/rust#72121) is merged but we are unable to call `Span::resolved_at`. (cherry picked from commit 05445c7) # Conflicts: # Cargo.lock # sdk/Cargo.toml * Replace FIXME with an issue link (cherry picked from commit b0cb2b0) * Update lock files (cherry picked from commit 42f8848) # Conflicts: # programs/bpf/Cargo.lock # programs/librapay/Cargo.lock # programs/move_loader/Cargo.lock * Split comment over multiple lines Due to rust-lang/rustfmt#4325, leaving this as one line causes rustfmt to add extra indentation to the surrounding code. (cherry picked from commit fed69e9) * Fix clippy lints (cherry picked from commit e7387f6) * Apply #![feature(proc_macro_hygiene)] when needed This allows the rust-bpf-builder toolchain to build the sdk (cherry picked from commit 95490ff) # Conflicts: # sdk/build.rs # sdk/src/lib.rs * Update Cargo.toml * Update lib.rs * Add rustc_version * lock file updates Co-authored-by: Aaron Hill <[email protected]> Co-authored-by: Jack May <[email protected]> Co-authored-by: Michael Vines <[email protected]>
1 parent b72b837 commit d1c0f4b

File tree

12 files changed

+233
-15
lines changed

12 files changed

+233
-15
lines changed

Cargo.lock

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

programs/bpf/Cargo.lock

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

programs/bpf_loader/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ thiserror = "1.0"
2323

2424
[dev-dependencies]
2525
rand = "0.7.3"
26+
rustversion = "1.0.3"
2627

2728
[lib]
2829
crate-type = ["lib", "cdylib"]

programs/bpf_loader/src/lib.rs

+8
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,14 @@ mod tests {
297297
}
298298
}
299299

300+
#[rustversion::since(1.46.0)]
301+
#[test]
302+
fn test_bpf_loader_same_crate() {
303+
// Ensure that we can invoke this macro from the same crate
304+
// where it is defined.
305+
solana_bpf_loader_program!();
306+
}
307+
300308
#[test]
301309
#[should_panic(expected = "ExceededMaxInstructions(10)")]
302310
fn test_bpf_loader_non_terminating_program() {

programs/librapay/Cargo.lock

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

programs/move_loader/Cargo.lock

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,13 @@ ed25519-dalek = { version = "=1.0.0-pre.3", optional = true }
5454
solana-crate-features = { path = "../crate-features", version = "1.2.11", optional = true }
5555
solana-logger = { path = "../logger", version = "1.2.11", optional = true }
5656
solana-sdk-macro = { path = "macro", version = "1.2.11" }
57+
rustversion = "1.0.3"
5758

5859
[dev-dependencies]
5960
tiny-bip39 = "0.7.0"
6061

6162
[package.metadata.docs.rs]
6263
targets = ["x86_64-unknown-linux-gnu"]
64+
65+
[build-dependencies]
66+
rustc_version = "0.2"

sdk/build.rs

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
extern crate rustc_version;
2+
use rustc_version::{version_meta, Channel};
3+
4+
fn main() {
5+
// Copied and adapted from
6+
// https://github.com/Kimundi/rustc-version-rs/blob/1d692a965f4e48a8cb72e82cda953107c0d22f47/README.md#example
7+
// Licensed under Apache-2.0 + MIT
8+
match version_meta().unwrap().channel {
9+
Channel::Stable => {
10+
println!("cargo:rustc-cfg=RUSTC_WITHOUT_SPECIALIZATION");
11+
}
12+
Channel::Beta => {
13+
println!("cargo:rustc-cfg=RUSTC_WITHOUT_SPECIALIZATION");
14+
}
15+
Channel::Nightly => {
16+
println!("cargo:rustc-cfg=RUSTC_WITH_SPECIALIZATION");
17+
}
18+
Channel::Dev => {
19+
println!("cargo:rustc-cfg=RUSTC_WITH_SPECIALIZATION");
20+
// See https://github.com/solana-labs/solana/issues/11055
21+
// We may be running the custom `rust-bpf-builder` toolchain,
22+
// which currently needs `#![feature(proc_macro_hygiene)]` to
23+
// be applied.
24+
println!("cargo:rustc-cfg=RUSTC_NEEDS_PROC_MACRO_HYGIENE");
25+
}
26+
}
27+
}

sdk/macro/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ bs58 = "0.3.0"
1616
proc-macro2 = "1.0"
1717
quote = "1.0"
1818
syn = { version = "1.0", features = ["full", "extra-traits"] }
19+
rustversion = "1.0.3"
1920

2021
[package.metadata.docs.rs]
2122
targets = ["x86_64-unknown-linux-gnu"]

sdk/macro/src/lib.rs

+71-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
extern crate proc_macro;
66

77
use proc_macro::TokenStream;
8-
use proc_macro2::Span;
8+
use proc_macro2::{Delimiter, Span, TokenTree};
99
use quote::{quote, ToTokens};
1010
use std::convert::TryFrom;
1111
use syn::{
@@ -14,7 +14,7 @@ use syn::{
1414
parse_macro_input,
1515
punctuated::Punctuated,
1616
token::Bracket,
17-
Expr, Ident, LitByte, LitStr, Token,
17+
Expr, Ident, LitByte, LitStr, Path, Token,
1818
};
1919

2020
struct Id(proc_macro2::TokenStream);
@@ -63,6 +63,75 @@ impl ToTokens for Id {
6363
}
6464
}
6565

66+
#[allow(dead_code)] // `respan` may be compiled out
67+
struct RespanInput {
68+
to_respan: Path,
69+
respan_using: Span,
70+
}
71+
72+
impl Parse for RespanInput {
73+
fn parse(input: ParseStream) -> Result<Self> {
74+
let to_respan: Path = input.parse()?;
75+
let _comma: Token![,] = input.parse()?;
76+
let respan_tree: TokenTree = input.parse()?;
77+
match respan_tree {
78+
TokenTree::Group(g) if g.delimiter() == Delimiter::None => {
79+
let ident: Ident = syn::parse2(g.stream())?;
80+
Ok(RespanInput {
81+
to_respan,
82+
respan_using: ident.span(),
83+
})
84+
}
85+
val => Err(syn::Error::new_spanned(
86+
val,
87+
"expected None-delimited group",
88+
)),
89+
}
90+
}
91+
}
92+
93+
/// A proc-macro which respans the tokens in its first argument (a `Path`)
94+
/// to be resolved at the tokens of its second argument.
95+
/// For internal use only.
96+
///
97+
/// There must be exactly one comma in the input,
98+
/// which is used to separate the two arguments.
99+
/// The second argument should be exactly one token.
100+
///
101+
/// For example, `respan!($crate::foo, with_span)`
102+
/// produces the tokens `$crate::foo`, but resolved
103+
/// at the span of `with_span`.
104+
///
105+
/// The input to this function should be very short -
106+
/// its only purpose is to override the span of a token
107+
/// sequence containing `$crate`. For all other purposes,
108+
/// a more general proc-macro should be used.
109+
#[rustversion::since(1.46.0)] // `Span::resolved_at` is stable in 1.46.0 and above
110+
#[proc_macro]
111+
pub fn respan(input: TokenStream) -> TokenStream {
112+
// Obtain the `Path` we are going to respan, and the ident
113+
// whose span we will be using.
114+
let RespanInput {
115+
to_respan,
116+
respan_using,
117+
} = parse_macro_input!(input as RespanInput);
118+
// Respan all of the tokens in the `Path`
119+
let to_respan: proc_macro2::TokenStream = to_respan
120+
.into_token_stream()
121+
.into_iter()
122+
.map(|mut t| {
123+
// Combine the location of the token with the resolution behavior of `respan_using`
124+
// Note: `proc_macro2::Span::resolved_at` is currently gated with cfg(procmacro2_semver_exempt)
125+
// Once this gate is removed, we will no longer need to use 'unwrap()' to call
126+
// the underling `proc_macro::Span::resolved_at` method.
127+
let new_span: Span = t.span().unwrap().resolved_at(respan_using.unwrap()).into();
128+
t.set_span(new_span);
129+
t
130+
})
131+
.collect();
132+
TokenStream::from(to_respan)
133+
}
134+
66135
#[proc_macro]
67136
pub fn declare_id(input: TokenStream) -> TokenStream {
68137
let id = parse_macro_input!(input as Id);

0 commit comments

Comments
 (0)