1. Symptoms
The Rust compiler emits error E0667 when attempting to declare an async function within an extern block or alongside the #[no_mangle] attribute for foreign function interface definitions. This error manifests during compilation and prevents the binary from being built.
When triggered, the compiler produces output similar to the following:
error[E0667]: `async` is not allowed in extern declarations
--> src/main.rs:3:1
|
3 | extern "C" {
| ^^^^^^^^^^^^^^ invalid decoration
4 | pub async fn foreign_async_function();
| ^^^^^ invalid here
The error message specifically highlights the async keyword as invalid, pointing to its location within the extern block. In some variations of the error, the compiler may also indicate that this restriction applies to both extern blocks and FFI function declarations with #[no_mangle].
Common scenarios where developers encounter this error include:
- Defining async foreign functions for integration with async runtime bindings from other languages
- Attempting to expose Rust async functions to C/C++ code through FFI
- Marking callback functions passed to foreign libraries as async
- Using
#[no_mangle]with async functions intended for external linking
The error occurs at the declaration site and prevents compilation from proceeding past the problematic code section.
2. Root Cause
The Rust language specification explicitly prohibits the use of the async keyword within extern blocks because of fundamental incompatibilities between async functions and the C ABI (Application Binary Interface) that extern declarations typically target.
Async functions in Rust are built on top of generator-based state machines. When you declare a function as async fn, the compiler transforms it into a state machine that implements the Future trait. This transformation introduces internal bookkeeping, allocates heap memory in some configurations, and fundamentally changes the function’s calling convention compared to synchronous functions.
The C ABI, which is the predominant ABI used in extern blocks (specified via "C" or other ABIs), has no concept of async functions. There is no standardized way to pass an async state machine across the C ABI boundary, nor is there a defined calling convention for resumable functions. The C language and most systems programming languages that Rust interops with do not have native support for async/await syntax or futures.
Furthermore, extern blocks in Rust are designed to declare symbols that exist in external libraries or the operating system. These symbols must have a known, stable representation that both sides of the boundary understand. Since async functions have compiler-generated implementations that vary based on optimization level and target platform, they cannot be reliably represented in extern declarations.
The restriction exists because allowing async in extern blocks would create a false impression that async functions could be called from C code directly, which is not possible without additional runtime support that Rust’s FFI system does not provide.
3. Step-by-Step Fix
To resolve E0667, you must restructure your code to separate the async Rust logic from the FFI boundary. The standard approach involves using synchronous wrapper functions at the FFI boundary and handling async execution entirely within Rust’s type system.
Before:
// This code does not compile
extern "C" {
pub async fn process_async_data(data: *const u8, len: usize) -> i32;
}
#[no_mangle]
pub async extern "C" fn rust_async_exposed_to_c(data: i32) -> i32 {
// This cannot work - async and extern are mutually exclusive
let result = some_async_operation(data).await;
result
}
After:
use std::future::Future;
use std::pin::Pin;
// Define the async logic as a regular async function
async fn process_async_data_impl(data: *const u8, len: usize) -> i32 {
// Your async business logic here
let data_slice = unsafe { std::slice::from_raw_parts(data, len) };
// Process the data asynchronously
process_data_async(data_slice).await
}
// Create a synchronous FFI wrapper that handles the async runtime
#[no_mangle]
pub extern "C" fn process_async_data_wrapper(data: *const u8, len: usize) -> i32 {
// This synchronous function can be called from C
// It initiates the async operation within Rust's runtime
let future = process_async_data_impl(data, len);
// Blocking wait - only appropriate in specific contexts
// For better results, use a proper async runtime integration
futures::executor::block_on(future)
}
// Alternative: Return a handle for non-blocking integration
#[repr(C)]
pub struct AsyncHandle {
// Opaque handle that C code can poll
state: u64,
}
#[no_mangle]
pub extern "C" fn init_async_operation() -> AsyncHandle {
// Return a handle that C code can use with other polling functions
AsyncHandle { state: 1 }
}
For more sophisticated async-FFI integration, consider implementing a proper async runtime adapter that allows C code to poll the future from the foreign side:
use std::task::{Context, Poll, RawWaker, RawTaskVTable, Task};
use std::mem::ManuallyDrop;
// Create a polling interface for foreign code
#[repr(C)]
pub struct FuturePollResult {
is_ready: bool,
output: u64,
}
#[no_mangle]
pub extern "C" fn poll_async_future(
future_ptr: *mut dyn Future<Output = u64>,
) -> FuturePollResult {
// Safety: Caller guarantees future_ptr is valid and alive
let future = unsafe { &mut *future_ptr };
let waker = create_noop_waker();
let mut cx = Context::from_waker(&waker);
match future.poll(&mut cx) {
Poll::Ready(val) => FuturePollResult { is_ready: true, output: val },
Poll::Pending => FuturePollResult { is_ready: false, output: 0 },
}
}
4. Verification
After applying the fix, verify that the code compiles successfully by running the Rust compiler:
cargo build
The build should complete without any E0667 errors. If the project includes C or C++ integration, ensure that the header files generated from the Rust code reflect the synchronous signatures.
For FFI projects, create a test C file that includes the generated headers and calls the wrapper functions:
// test_ffi.c
#include <stdint.h>
#include "rust_bindings.h"
int main() {
uint8_t data[] = {1, 2, 3, 4, 5};
int32_t result = process_async_data_wrapper(data, 5);
return result == 0 ? 0 : 1;
}
Compile and run the test to ensure the FFI boundary works correctly:
gcc test_ffi.c -L./target/debug -lrustlib -o test_ffi
./test_ffi
echo "Exit code: $?"
Additional verification steps include running the test suite if available, checking that async operations within Rust still behave correctly, and confirming that memory safety is maintained across the FFI boundary using tools like miri or sanitizers.
5. Common Pitfalls
Several common mistakes lead to continued E0667 errors or introduce subtle bugs after attempting to fix the issue.
Removing extern without providing a working FFI interface is a frequent error. Some developers simply delete the extern block without replacing it with a proper synchronous wrapper, breaking the integration with foreign code that depends on those symbols.
Using blocking operations in async contexts within the FFI wrapper can cause deadlocks in single-threaded async runtimes. The block_on approach shown earlier only works in contexts where blocking the current thread is acceptable, such as in tests or in a dedicated runtime thread.
Assuming async functions can be made extern by removing async is another pitfall. Simply removing the async keyword without adjusting the function body can cause compilation errors if the function body contains .await expressions that require the async context.
Ignoring the output type compatibility between C and Rust types causes ABI mismatches. Ensure that all types passed across the FFI boundary have explicit #[repr(C)] annotations and match the expected memory layout.
Memory management across the boundary requires careful attention. If async operations hold references to data passed from C, those references must not outlive the data’s validity period. Use Pin correctly when wrapping futures that hold references.
Forgetting to handle the future’s Send/Sync requirements can cause compilation errors when moving futures across thread boundaries. If your async operation needs to run on a different thread from where it was created, ensure all captured data implements the appropriate trait.
6. Related Errors
E0566 (conflicting recommendations) relates to async in trait definitions, where the compiler detects incompatibilities between async functions and certain trait bounds or object safety rules. Unlike E0667 which focuses on extern blocks, E0566 addresses async function declarations within trait contexts that violate the language’s object safety constraints.
E0733 (async closures are not yet supported) demonstrates another context where async is restricted. The Rust compiler historically did not support async closures, similar to how it prohibits async in extern blocks, because the underlying implementation mechanisms were not standardized.
E0603 (unnecessary struct qualification in extern block) occasionally appears alongside E0667 when developers attempt to use qualified struct names within extern blocks that contain invalid async declarations, indicating a pattern of confusion about what syntax is valid in extern contexts.