aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorUlf Lilleengen <[email protected]>2025-08-01 06:42:55 +0000
committerGitHub <[email protected]>2025-08-01 06:42:55 +0000
commitf3cb0f3c3062cd1b863414186c29b604fab305fa (patch)
treebe38ef8879e91d3a265f43fcfcf89fd83869a2a4
parent6409931ef972f6493e7b4d9a93c2cb7bb048d2af (diff)
parent54d9a7fed3ab211b1049aae0af0bc49f912c9df4 (diff)
Merge pull request #4443 from Brezak/task-unsafe
executor: mark unsafe tasks as unsafe
-rw-r--r--embassy-executor-macros/src/macros/task.rs82
-rw-r--r--embassy-executor/CHANGELOG.md1
-rw-r--r--embassy-executor/src/lib.rs8
-rw-r--r--embassy-executor/tests/test.rs11
-rw-r--r--embassy-executor/tests/ui.rs3
-rw-r--r--embassy-executor/tests/ui/bad_return_impl_future_nightly.stderr2
-rw-r--r--embassy-executor/tests/ui/nonstatic_struct_elided.stderr14
-rw-r--r--embassy-executor/tests/ui/task_safety_attribute.rs25
-rw-r--r--embassy-executor/tests/ui/unsafe_op_in_unsafe_task.rs10
-rw-r--r--embassy-executor/tests/ui/unsafe_op_in_unsafe_task.stderr18
10 files changed, 153 insertions, 21 deletions
diff --git a/embassy-executor-macros/src/macros/task.rs b/embassy-executor-macros/src/macros/task.rs
index 1c5e3571d..fc8673743 100644
--- a/embassy-executor-macros/src/macros/task.rs
+++ b/embassy-executor-macros/src/macros/task.rs
@@ -5,7 +5,7 @@ use darling::FromMeta;
5use proc_macro2::{Span, TokenStream}; 5use proc_macro2::{Span, TokenStream};
6use quote::{format_ident, quote}; 6use quote::{format_ident, quote};
7use syn::visit::{self, Visit}; 7use syn::visit::{self, Visit};
8use syn::{Expr, ExprLit, Lit, LitInt, ReturnType, Type}; 8use syn::{Expr, ExprLit, Lit, LitInt, ReturnType, Type, Visibility};
9 9
10use crate::util::*; 10use crate::util::*;
11 11
@@ -112,13 +112,11 @@ pub fn run(args: TokenStream, item: TokenStream) -> TokenStream {
112 } 112 }
113 } 113 }
114 114
115 let task_ident = f.sig.ident.clone(); 115 // Copy the generics + where clause to avoid more spurious errors.
116 let task_inner_ident = format_ident!("__{}_task", task_ident); 116 let generics = &f.sig.generics;
117 117 let where_clause = &f.sig.generics.where_clause;
118 let mut task_inner = f.clone(); 118 let unsafety = &f.sig.unsafety;
119 let visibility = task_inner.vis.clone(); 119 let visibility = &f.vis;
120 task_inner.vis = syn::Visibility::Inherited;
121 task_inner.sig.ident = task_inner_ident.clone();
122 120
123 // assemble the original input arguments, 121 // assemble the original input arguments,
124 // including any attributes that may have 122 // including any attributes that may have
@@ -131,6 +129,64 @@ pub fn run(args: TokenStream, item: TokenStream) -> TokenStream {
131 )); 129 ));
132 } 130 }
133 131
132 let task_ident = f.sig.ident.clone();
133 let task_inner_ident = format_ident!("__{}_task", task_ident);
134
135 let task_inner_future_output = match &f.sig.output {
136 ReturnType::Default => quote! {-> impl ::core::future::Future<Output = ()>},
137 // Special case the never type since we can't stuff it into a `impl Future<Output = !>`
138 ReturnType::Type(arrow, maybe_never)
139 if f.sig.asyncness.is_some() && matches!(**maybe_never, Type::Never(_)) =>
140 {
141 quote! {
142 #arrow impl ::core::future::Future<Output=#embassy_executor::_export::Never>
143 }
144 }
145 ReturnType::Type(arrow, maybe_never) if matches!(**maybe_never, Type::Never(_)) => quote! {
146 #arrow #maybe_never
147 },
148 // Grab the arrow span, why not
149 ReturnType::Type(arrow, typ) if f.sig.asyncness.is_some() => quote! {
150 #arrow impl ::core::future::Future<Output = #typ>
151 },
152 // We assume that if `f` isn't async, it must return `-> impl Future<...>`
153 // This is checked using traits later
154 ReturnType::Type(arrow, typ) => quote! {
155 #arrow #typ
156 },
157 };
158
159 // We have to rename the function since it might be recursive;
160 let mut task_inner_function = f.clone();
161 let task_inner_function_ident = format_ident!("__{}_task_inner_function", task_ident);
162 task_inner_function.sig.ident = task_inner_function_ident.clone();
163 task_inner_function.vis = Visibility::Inherited;
164
165 let task_inner_body = if errors.is_empty() {
166 quote! {
167 #task_inner_function
168
169 // SAFETY: All the preconditions to `#task_ident` apply to
170 // all contexts `#task_inner_ident` is called in
171 #unsafety {
172 #task_inner_function_ident(#(#full_args,)*)
173 }
174 }
175 } else {
176 quote! {
177 async {::core::todo!()}
178 }
179 };
180
181 let task_inner = quote! {
182 #visibility fn #task_inner_ident #generics (#fargs)
183 #task_inner_future_output
184 #where_clause
185 {
186 #task_inner_body
187 }
188 };
189
134 let spawn = if returns_impl_trait { 190 let spawn = if returns_impl_trait {
135 quote!(spawn) 191 quote!(spawn)
136 } else { 192 } else {
@@ -173,7 +229,7 @@ pub fn run(args: TokenStream, item: TokenStream) -> TokenStream {
173 unsafe { __task_pool_get(#task_inner_ident).#spawn(move || #task_inner_ident(#(#full_args,)*)) } 229 unsafe { __task_pool_get(#task_inner_ident).#spawn(move || #task_inner_ident(#(#full_args,)*)) }
174 }; 230 };
175 231
176 let task_outer_attrs = task_inner.attrs.clone(); 232 let task_outer_attrs = &f.attrs;
177 233
178 if !errors.is_empty() { 234 if !errors.is_empty() {
179 task_outer_body = quote! { 235 task_outer_body = quote! {
@@ -183,10 +239,6 @@ pub fn run(args: TokenStream, item: TokenStream) -> TokenStream {
183 }; 239 };
184 } 240 }
185 241
186 // Copy the generics + where clause to avoid more spurious errors.
187 let generics = &f.sig.generics;
188 let where_clause = &f.sig.generics.where_clause;
189
190 let result = quote! { 242 let result = quote! {
191 // This is the user's task function, renamed. 243 // This is the user's task function, renamed.
192 // We put it outside the #task_ident fn below, because otherwise 244 // We put it outside the #task_ident fn below, because otherwise
@@ -196,7 +248,7 @@ pub fn run(args: TokenStream, item: TokenStream) -> TokenStream {
196 #task_inner 248 #task_inner
197 249
198 #(#task_outer_attrs)* 250 #(#task_outer_attrs)*
199 #visibility fn #task_ident #generics (#fargs) -> #embassy_executor::SpawnToken<impl Sized> #where_clause{ 251 #visibility #unsafety fn #task_ident #generics (#fargs) -> #embassy_executor::SpawnToken<impl Sized> #where_clause{
200 #task_outer_body 252 #task_outer_body
201 } 253 }
202 254
@@ -213,7 +265,7 @@ fn check_arg_ty(errors: &mut TokenStream, ty: &Type) {
213 265
214 impl<'a, 'ast> Visit<'ast> for Visitor<'a> { 266 impl<'a, 'ast> Visit<'ast> for Visitor<'a> {
215 fn visit_type_reference(&mut self, i: &'ast syn::TypeReference) { 267 fn visit_type_reference(&mut self, i: &'ast syn::TypeReference) {
216 // only check for elided lifetime here. If not elided, it's checked by `visit_lifetime`. 268 // Only check for elided lifetime here. If not elided, it's checked by `visit_lifetime`.
217 if i.lifetime.is_none() { 269 if i.lifetime.is_none() {
218 error( 270 error(
219 self.errors, 271 self.errors,
diff --git a/embassy-executor/CHANGELOG.md b/embassy-executor/CHANGELOG.md
index 914863a83..7404961f3 100644
--- a/embassy-executor/CHANGELOG.md
+++ b/embassy-executor/CHANGELOG.md
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
21- Added support for `-> impl Future<Output = ()>` in `#[task]` 21- Added support for `-> impl Future<Output = ()>` in `#[task]`
22- Fixed `Send` unsoundness with `-> impl Future` tasks 22- Fixed `Send` unsoundness with `-> impl Future` tasks
23- Marked `Spawner::for_current_executor` as `unsafe` 23- Marked `Spawner::for_current_executor` as `unsafe`
24- `#[task]` now properly marks the generated function as unsafe if the task is marked unsafe
24 25
25## 0.7.0 - 2025-01-02 26## 0.7.0 - 2025-01-02
26 27
diff --git a/embassy-executor/src/lib.rs b/embassy-executor/src/lib.rs
index e174a0594..0747db032 100644
--- a/embassy-executor/src/lib.rs
+++ b/embassy-executor/src/lib.rs
@@ -216,7 +216,7 @@ pub mod _export {
216 ); 216 );
217 217
218 #[allow(dead_code)] 218 #[allow(dead_code)]
219 trait HasOutput { 219 pub trait HasOutput {
220 type Output; 220 type Output;
221 } 221 }
222 222
@@ -225,7 +225,7 @@ pub mod _export {
225 } 225 }
226 226
227 #[allow(dead_code)] 227 #[allow(dead_code)]
228 type Never = <fn() -> ! as HasOutput>::Output; 228 pub type Never = <fn() -> ! as HasOutput>::Output;
229} 229}
230 230
231/// Implementation details for embassy macros. 231/// Implementation details for embassy macros.
@@ -242,7 +242,7 @@ pub mod _export {
242 impl TaskReturnValue for Never {} 242 impl TaskReturnValue for Never {}
243 243
244 #[allow(dead_code)] 244 #[allow(dead_code)]
245 trait HasOutput { 245 pub trait HasOutput {
246 type Output; 246 type Output;
247 } 247 }
248 248
@@ -251,5 +251,5 @@ pub mod _export {
251 } 251 }
252 252
253 #[allow(dead_code)] 253 #[allow(dead_code)]
254 type Never = <fn() -> ! as HasOutput>::Output; 254 pub type Never = <fn() -> ! as HasOutput>::Output;
255} 255}
diff --git a/embassy-executor/tests/test.rs b/embassy-executor/tests/test.rs
index c1e7ec5d7..b84d3785a 100644
--- a/embassy-executor/tests/test.rs
+++ b/embassy-executor/tests/test.rs
@@ -7,7 +7,7 @@ use std::sync::{Arc, Mutex};
7use std::task::Poll; 7use std::task::Poll;
8 8
9use embassy_executor::raw::Executor; 9use embassy_executor::raw::Executor;
10use embassy_executor::task; 10use embassy_executor::{task, Spawner};
11 11
12#[export_name = "__pender"] 12#[export_name = "__pender"]
13fn __pender(context: *mut ()) { 13fn __pender(context: *mut ()) {
@@ -317,3 +317,12 @@ fn executor_task_cfg_args() {
317 let (_, _, _) = (a, b, c); 317 let (_, _, _) = (a, b, c);
318 } 318 }
319} 319}
320
321#[test]
322fn recursive_task() {
323 #[embassy_executor::task(pool_size = 2)]
324 async fn task1() {
325 let spawner = unsafe { Spawner::for_current_executor().await };
326 spawner.spawn(task1());
327 }
328}
diff --git a/embassy-executor/tests/ui.rs b/embassy-executor/tests/ui.rs
index 7757775ee..5486a0624 100644
--- a/embassy-executor/tests/ui.rs
+++ b/embassy-executor/tests/ui.rs
@@ -32,4 +32,7 @@ fn ui() {
32 t.compile_fail("tests/ui/self.rs"); 32 t.compile_fail("tests/ui/self.rs");
33 t.compile_fail("tests/ui/type_error.rs"); 33 t.compile_fail("tests/ui/type_error.rs");
34 t.compile_fail("tests/ui/where_clause.rs"); 34 t.compile_fail("tests/ui/where_clause.rs");
35 t.compile_fail("tests/ui/unsafe_op_in_unsafe_task.rs");
36
37 t.pass("tests/ui/task_safety_attribute.rs");
35} 38}
diff --git a/embassy-executor/tests/ui/bad_return_impl_future_nightly.stderr b/embassy-executor/tests/ui/bad_return_impl_future_nightly.stderr
index 73ceb989d..3c3c9503b 100644
--- a/embassy-executor/tests/ui/bad_return_impl_future_nightly.stderr
+++ b/embassy-executor/tests/ui/bad_return_impl_future_nightly.stderr
@@ -7,4 +7,4 @@ error[E0277]: task futures must resolve to `()` or `!`
7 = note: use `async fn` or change the return type to `impl Future<Output = ()>` 7 = note: use `async fn` or change the return type to `impl Future<Output = ()>`
8 = help: the following other types implement trait `TaskReturnValue`: 8 = help: the following other types implement trait `TaskReturnValue`:
9 () 9 ()
10 <fn() -> ! as _export::HasOutput>::Output 10 <fn() -> ! as HasOutput>::Output
diff --git a/embassy-executor/tests/ui/nonstatic_struct_elided.stderr b/embassy-executor/tests/ui/nonstatic_struct_elided.stderr
index 099ef8b4e..0ee1bfe0c 100644
--- a/embassy-executor/tests/ui/nonstatic_struct_elided.stderr
+++ b/embassy-executor/tests/ui/nonstatic_struct_elided.stderr
@@ -8,3 +8,17 @@ help: indicate the anonymous lifetime
8 | 8 |
96 | async fn task(_x: Foo<'_>) {} 96 | async fn task(_x: Foo<'_>) {}
10 | ++++ 10 | ++++
11
12error[E0700]: hidden type for `impl Sized` captures lifetime that does not appear in bounds
13 --> tests/ui/nonstatic_struct_elided.rs:5:1
14 |
155 | #[embassy_executor::task]
16 | ^^^^^^^^^^^^^^^^^^^^^^^^^ opaque type defined here
176 | async fn task(_x: Foo) {}
18 | --- hidden type `impl Sized` captures the anonymous lifetime defined here
19 |
20 = note: this error originates in the attribute macro `embassy_executor::task` (in Nightly builds, run with -Z macro-backtrace for more info)
21help: add a `use<...>` bound to explicitly capture `'_`
22 |
235 | #[embassy_executor::task] + use<'_>
24 | +++++++++
diff --git a/embassy-executor/tests/ui/task_safety_attribute.rs b/embassy-executor/tests/ui/task_safety_attribute.rs
new file mode 100644
index 000000000..ab5a2f99f
--- /dev/null
+++ b/embassy-executor/tests/ui/task_safety_attribute.rs
@@ -0,0 +1,25 @@
1#![cfg_attr(feature = "nightly", feature(impl_trait_in_assoc_type))]
2#![deny(unused_unsafe)]
3
4use std::mem;
5
6#[embassy_executor::task]
7async fn safe() {}
8
9#[embassy_executor::task]
10async unsafe fn not_safe() {}
11
12#[export_name = "__pender"]
13fn pender(_: *mut ()) {
14 // The test doesn't link if we don't include this.
15 // We never call this anyway.
16}
17
18fn main() {
19 let _forget_me = safe();
20 // SAFETY: not_safe has not safety preconditions
21 let _forget_me2 = unsafe { not_safe() };
22
23 mem::forget(_forget_me);
24 mem::forget(_forget_me2);
25}
diff --git a/embassy-executor/tests/ui/unsafe_op_in_unsafe_task.rs b/embassy-executor/tests/ui/unsafe_op_in_unsafe_task.rs
new file mode 100644
index 000000000..ee7924838
--- /dev/null
+++ b/embassy-executor/tests/ui/unsafe_op_in_unsafe_task.rs
@@ -0,0 +1,10 @@
1#![cfg_attr(feature = "nightly", feature(impl_trait_in_assoc_type))]
2#![deny(unsafe_op_in_unsafe_fn)]
3
4#[embassy_executor::task]
5async unsafe fn task() {
6 let x = 5;
7 (&x as *const i32).read();
8}
9
10fn main() {}
diff --git a/embassy-executor/tests/ui/unsafe_op_in_unsafe_task.stderr b/embassy-executor/tests/ui/unsafe_op_in_unsafe_task.stderr
new file mode 100644
index 000000000..d987a4b95
--- /dev/null
+++ b/embassy-executor/tests/ui/unsafe_op_in_unsafe_task.stderr
@@ -0,0 +1,18 @@
1error[E0133]: call to unsafe function `std::ptr::const_ptr::<impl *const T>::read` is unsafe and requires unsafe block
2 --> tests/ui/unsafe_op_in_unsafe_task.rs:7:5
3 |
47 | (&x as *const i32).read();
5 | ^^^^^^^^^^^^^^^^^^^^^^^^^ call to unsafe function
6 |
7 = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/unsafe-op-in-unsafe-fn.html>
8 = note: consult the function's documentation for information on how to avoid undefined behavior
9note: an unsafe function restricts its caller, but its body is safe by default
10 --> tests/ui/unsafe_op_in_unsafe_task.rs:5:1
11 |
125 | async unsafe fn task() {
13 | ^^^^^^^^^^^^^^^^^^^^^^
14note: the lint level is defined here
15 --> tests/ui/unsafe_op_in_unsafe_task.rs:2:9
16 |
172 | #![deny(unsafe_op_in_unsafe_fn)]
18 | ^^^^^^^^^^^^^^^^^^^^^^