Author: Yu Hang/Post editor: Zhang Handong

This article focuses on the Rust Runtime and ABI. Because both concepts are low-level and generic and do not involve implementation details of specific standard library application layer types such as STD ::Vec, they are put together as the main content of this article.

This article mainly introduces the Runtime and ABI of Rust (version 1.52.0). Because both concepts are low-level and generic and do not involve the implementation details of specific standard library application layer concepts such as STD ::Vec, they are put together as the main content of this article.

Whether you’re doing front-end, back-end, or mobile, you’ve probably heard the term runtime. Runtime translates to “Runtime,” and there is a definition of Runtime available on Wikipedia:

In computer science, runtime, run time, or execution time is the final phase of a computer program’s life cycle, in which the code is being executed on the computer’s central processing unit (CPU) as machine code. In other words, “runtime” is the running phase of a program.

In this definition, we can simply read “Runtime” as “specifically the time during which the program code is executed by the CPU.” This is the most literal interpretation of the term runtime, meaning that runtime refers to the most important phase of a program’s life cycle. For example, a common runtime error is the “division by zero exception”, where “runtime” refers to the running phase of the program.

Each programming language has its own “Execution Model.” For example, in THE CASE of C, the C standard specifies the order in which C statements should be executed (as shown in the following reference), as well as what the Execution environment should do when C programs are started and terminated: For example, when the program starts, it should call a function called main. Depending on the signature of the function, the corresponding argc and argv arguments should be passed selectively; The exit system call needs to be called selectively before the program finishes, and so on. For another example, WebAssembly also defines in its standard that when an “abstract machine” executes a piece of Wasm code, the machine can be abstracted from its constituent parts, For example, the stack structure conforms to the Wasm computing model, and the Store structure contains all the Wasm global instances (func \ table \ memory \ global \ Element \ data, etc.). It is important to note, however, that the definition of an abstract machine is not exactly the same as a real virtual machine implementation, as long as the virtual machine implementation ensures that Wasm code execution at the abstraction level behaves exactly as the abstract machine does.

A statement specifies an action to be performed. Except as indicated, statements are executed in sequence.

On the other hand, the execution model itself is “detached” from the specific syntax and semantics of the language, meaning that the source code itself does not intuitively reflect the full details of its execution. In general, the execution model of a programming language can be implemented through either a compiler or an interpreter (corresponding to two different modes of execution). For the compiler, it can convert high-level upper-level code into lower-level intermediate (IR) or assembly code, where the execution model implied by the upper-level code has been “deconstructed” into the execution semantics of the lower-level code. For the interpreter, the execution semantics implied by the upper-layer language need to be analyzed in a structured way, and then processed accordingly according to the specific token category. In general, we refer to “Runtime systems” as “all the processing and implementation that support the execution model and make the program work that is not directly visible in the program source code.”

The runtime system of a programming language that provides an environment in which programs written in that language can run. This environment involves many important aspects of a program’s ability to run correctly, from the management of the application’s memory to providing an interactive interface to the operating system. Small enough to properly set up prologue, epilogue, and so on during function calls. Taking C as an example, when we run a C application that requires dynamic linking, I personally think that the behavior of the dynamic linker also belongs to the category of the runtime system. For example, when we run this application on a Unix-like system, the operating system will use the dynamic linker as the entry point of execution. The dynamic linker first completes its own symbol relocation, and then carries out a series of work for the C application to be run, such as loading the address space and symbol relocation depending on the shared library. Finally, the execution process (PC) is handed over to the application itself. The purpose of this series of work of dynamic linker is to correctly execute our target C application, but this part of the process is not limited to THE C language itself, belongs to the runtime system composition independent of the specific language.

As for C language itself, it also has its own Runtime system, which is generally called CRT (C-Runtime). On Unix-like systems, CRT is typically supplied as several different object files (crt1.o \ crti.o \ crtn.o). Crt1. o contains the program’s actual entry function (_start). In this part of assembly implementation, the run-time system usually sets the parameters of argc and argv correctly, and finally calls the main function defined in C source code. After the main function returns, it also calls the exit system call to exit the program correctly. Other object files crti. O and crtn.o provide some implementation components for assisting global construction and destruction-related functions, which are not detailed here.

In summary, there is no detailed, clear distinction (for now) between the identification boundaries of “run-time systems.” Depending on the programming language, technology system, sometimes you need to use a different perspective to judge. In the following presentation of the Rust Runtime System, we will focus on the parts of the Runtime System that are relevant to the Rust language itself, but not the language-independent parts (such as the dynamic linker provided above).

For the Rust ABI section, we won’t go into every single detail of the ABI. But Rust ABI is actually very similar to C/C++, for example: The size and storage of built-in types in memory (byte order), the storage and memory distribution of composite types, the calling convention, register usage conventions, the content and layout of virtual function tables, and so on. In this article, we’ll give you an overview of the Rust ABI at this stage to give you a sense of what it looks like.

Rust Runtime System

In order to ensure smooth reading, we will refer to Runtime System as Runtime. As you can see from the official Rust FAQ documentation, the Rust language has very few run-time systems (see the following reference). This makes it easier to integrate with other languages with GC.

By avoiding GC, Rust can offer numerous benefits: predictable cleanup of resources, lower overhead for memory management, and essentially no runtime system.

However, “hardly any” is not a complete failure, and a portion of Rust’s library implementations can be identified as functional areas of Rust runtime systems, providing specific implementations related to concepts such as Panic, backtrace, Stack Unwinding, and Stack protection. Moreover, like C, Rust also has some run-time system functionality that provides preparation for actual main function calls.

We can from the Rust project source code position * Rust/library/STD/SRC/rt. Rs * see Rust complete implementation of the Runtime. This code implementation is very short, the complete code reference is as follows:

/ /! Runtime services //! / /! The `rt` module provides a narrow set of runtime services, //! including the global heap (exported in `heap`) and unwinding and //! backtrace support. The APIs in this module are highly unstable, //! and should be considered as private implementation details for the //! time being. #! [unstable( feature = "rt", reason = "this public module should not exist and is highly likely \ to disappear", issue = "none" )] #! [doc(hidden)] // Re-export some of our utilities which are expected by other crates. pub use crate::panicking::{begin_panic, begin_panic_fmt, panic_count}; // To reduce the generated code of the new `lang_start`, this function is doing // the real work. #[cfg(not(test))] fn lang_start_internal( main: &(dyn Fn() -> i32 + Sync + crate::panic::RefUnwindSafe), argc: isize, argv: *const *const u8, ) -> isize { use crate::panic; use crate::sys_common; // SAFETY: Only called once during runtime initialization. unsafe { sys_common::rt::init(argc, argv) }; let exit_code = panic::catch_unwind(main); sys_common::rt::cleanup(); exit_code.unwrap_or(101) as isize } #[cfg(not(test))] #[lang = "start"] fn lang_start<T: crate::process::Termination + 'static>( main: fn() -> T, argc: isize, argv: *const *const u8, ) -> isize { lang_start_internal( &move || crate::sys_common::backtrace::__rust_begin_short_backtrace(main).report(), argc, argv, ) }Copy the code

Exported Panicking internal interface

Looking at the code from the top, you can first see that there are some reexports associated with Panic! Macro-related internal functions, let’s look at them one by one.

// Re-export some of our utilities which are expected by other crates.
pub use crate::panicking::{begin_panic, begin_panic_fmt, panic_count};
Copy the code

Among them, the function called begin_panic is the panic! And assert! The actual entry function for the variant, which can accept any (STD ::any:: any) parameter type as Panic Payload. Internally, the rust_panic_with_hook function performs some of the processing for triggering Panic, including: Handle recursive Panic (such as triggering Panic in Panic Hook), execute Panic Hook (custom or default), output Panic information, and finally call the __rust_start_panic function provided by Panic Runtime, Rust can optionally implement the final Panic side effect in two different ways: panic_abort or panic_unwind (the default). The former calls abort functions such as those in the C library directly to terminate the current process. The latter, in turn, “deconstructs” the exception-related call Stack frame by calling the platform-specific Stack Unwinding logic until the control logic, if any, can be transferred to the outermost STD :: Panic ::catch_unwind function Stack frame. A simple example of setting up Panic Hook and Panic capture can be found below:

fn main() { // setup a panic hook, will be called once a panic occurs. std::panic::set_hook(Box::new(|panic_info| { if let Some(s) = panic_info.payload().downcast_ref::<&str>() { println! ("Panic occurred: {:? }", s); }})); // catch occurred panics. let result = std::panic::catch_unwind(|| { (|| panic! ("A panic occurs!" ())); }); /** * Output: Err( Any { .. }, ) */ println! (" {: #? }", &result); }Copy the code

The begin_panic_fmt function is similar to begin_panic, except that it is mainly used to handle receiving formats like format! Macro format parameter panic! Call, this function will also indirectly call rust_panIC_WITH_hook function in the execution process, and its subsequent processing flow is consistent with the above.

The resulting exported panic_count module contains mainly internal interfaces related to “Panic counting,” which counts the number of Panic occurrences independent of process and thread (via TLS) and performs different processing. The basic logic is: when Panic occurs, the counter increases by one; In contrast, when Panic is caught and processed (such as via catch_unwind), the counter is reduced by one. Under normal circumstances, a Panic will directly terminate the Rust program.

Runtime entry function

For the sake of code generation size, the actual Runtime entry function is split into two parts, corresponding to lang_start and lang_start_internal, where the former is the function that is called first (marked by attribute #[lang = “start”]), The function calls the latter directly within itself. The way to call is as follows:

// ...
#[cfg(not(test))]
#[lang = "start"]
fn lang_start<T: crate::process::Termination + 'static>(
    main: fn() -> T,
    argc: isize,
    argv: *const *const u8,
) -> isize {
    lang_start_internal(
        &move || crate::sys_common::backtrace::__rust_begin_short_backtrace(main).report(),
        argc,
        argv,
    )
}
Copy the code

The first argument passed in is a Closure. Inside the closure, a pointer to main is passed as an argument to the __rust_begin_short_backtrace function. Rust since version 1.47.0, by default, an application can output a more concise stack backtrace when a Panic occurs. __rust_begin_short_backtrace works with __rust_end_short_backtrace to make this change.

These two functions need to be used in pairs; they don’t have any special logic inside them, but simply call the function passed in and return the result. From the call stack of Rust applications, function calls between __rust_begin_short_backtrace and __rust_end_short_backtrace can be considered complete user code calls. When Rust encounters a Panic and needs to print the current backtrace, it actually traverses each stack frame and uses the “symbol name” attached to the stack frame to distinguish whether the current stack frame is one of the above two functions. Rust, in turn, can distinguish between which stack frames belong to user code and which belong to the runtime system.

Returning to the previous closure, when the function __rust_begin_short_backtrace calls main internally and returns, Rust calls a function named report based on this return value. As you can see from the implementation code of the Runtime entry function lang_start given above, The main function of the type of fn () – > T T actually with its return value, a crate: : process: : trait of Termination bound, and the trait and provides a method called the report. This method, when called, returns an i32 integer representing the status information, which is then passed to the operating system as the status information. As the figure below shows, Rust actually has an effect on items such as () \! \ Result<(), E> and other common main functions return values that implement this trait by default. Typically, Rust uses the value (integer 0) of the macro libc::EXIT_SUCCESS from liBC to indicate success status; Instead, use the value of the macro libc::EXIT_FAILURE (integer 1) to indicate failure status. You can see the use of these values in the implementation of this trait in the common return value types of the main function.

Next, the lang_start_internal function is called with the last two arguments argc and argv, which you are probably familiar with. They have exactly the same meanings and numeric types as the two arguments received by the main function in C/C++ programs. The process continues inside the lang_start_internal function. Within this function, Rust first calls a “runtime initialization” function called sys_common:: RT ::init, which does the following:

  • Initialization is used for storageargcargvGlobal static variable of;
  • For the currentmainThe thread setting name of the function (” main “);
  • For the currentmainThe Thread on which the function is located sets Thread Guard to prevent (pass)bounds checking) stack buffer overflow.

Its internal call looks like this:

// One-time runtime initialization. // Runs before `main`. // SAFETY: must be called only once during runtime initialization. // NOTE: this is not guaranteed to run, for example when Rust code is called externally. #[cfg_attr(test, allow(dead_code))] pub unsafe fn init(argc: isize, argv: *const *const u8) { unsafe { sys::init(argc, argv); let main_guard = sys::thread::guard::init(); // Next, set up the current Thread with the guard information we just // created. Note that this isn't necessary in general for new threads, // but we just do this to name the main thread and to give it correct // info about the stack bounds. let thread = Thread::new(Some("main".to_owned())); thread_info::set(main_guard, thread); }}Copy the code

After the initialization function is called, the main function we defined in the Rust source code is actually called. Here the main function is actually placed in the catch_unwind call to check whether Panic occurred during the actual call to the main function, thereby setting a different return value. Immediately after main completes and returns, Rust cleans up the runtime context by calling the sys_common:: RT ::cleanup function. This cleanup includes:

  • Output and disablestdoutThe buffer;
  • Cleanup was previously used for savingargcargvStatic variable of.

Finally, Rust returns a user-specified or default (101 – error occurred) exit status code to the operating system, and the application ends.

It all looks simple enough up to this point, and Rust Runtime doesn’t offer much functionality as we observe it, but there’s actually one problem that hasn’t been solved. As we mentioned at the beginning of this article, the function named lang_start is called first by Rust, followed by the main function we define in Rust source code. But how exactly is the lang_start function called? How do we get the argc and argv arguments passed to a function when it is called?

Entry Point

In fact, the function **lang_start** we described above is only an entry point to Rust Runtime, not an execution entry point for the entire program. When we compile Rust source code through RUSTC, the Rust compiler helps us dynamically generate a function called “main” whose signature is basically the same as the main function we defined in C/C++. The main function can then be called correctly by Rust (typically specified by assembly in _start) with the help of a liBC linked during compilation. The *lang_start* function we introduced earlier is called by the ruSTC auto-generated main function. Rustc_codegen_ssa/SRC /base.rs create_entry_fn

// location: compiler/rustc_codegen_ssa/src/base.rs.
fn create_entry_fn<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>>(
    cx: &'a Bx::CodegenCx,
    rust_main: Bx::Value,
    rust_main_def_id: DefId,
    use_start_lang_item: bool,
) -> Bx::Function {}
Copy the code

Of course, there are ways to make RUSTC not use the default lang_start function marked by the lang item in some special cases. For example, with the #[start] attribute, we can tell RUSTC to use the entry function we specify directly at compile time. A simple example is shown below.

#! [feature(start)] #[start] fn my_custom_start(argc: isize, argv: *const *const u8) -> isize { println! ("{}", argc); unsafe { use std::ffi::{CStr, OsString}; use std::os::unix::ffi::OsStringExt; use std::os::raw::c_char; let v: Vec<OsString> = (0.. argc).map(|i| { let cstr = CStr::from_ptr(*argv.offset(i) as *const c_char); OsStringExt::from_vec(cstr.to_bytes().to_vec()) }).collect(); println! (" {:? }", v); // print out the argc and argv. } 0 }Copy the code

But here’s the problem with that: Because RUSTC uses our specified entry function directly (which is still called by the compiler’s auto-generated main function) rather than performing Rust’s default Runtime initialization function, It will not be enforced in this case. Whether this has an impact on the actual performance of the application depends on the circumstances.

Input parameter (argc/argv)

For argc and argv arguments entered at runtime, Rust essentially retrieves them in two ways:

  • Runtime startup throughmainFunctions are passed in directly and stored in static variables;
  • FFI is obtained through linked external system libraries.

In the first case, when linking with liBC, the assembly code at the _start tag might handle argc and argv. When liBC actually calls main (generated dynamically in Rust by RUSTC), these two parameters are placed on the stack and passed in directly as parameters to main. Depending on the type of link, glibc, for example, may use an “init_array extension” to retrieve the actual values of argc and argv.

A good example of the second approach is the _NSGetArgc and _NSGetArgv methods on macOS. Both methods are provided by the operating system and can be used directly to obtain argc and argv parameter information passed to the currently running process. So, when we try to get the input parameters of the current process in Rust via STD ::env::args, as shown in the following code, on macOS Rust will call both functions directly via FFI to get this information.

// location: library/std/src/sys/unix/args.rs.
#[cfg(target_os = "macos")]
pub fn args() -> Args {
    use crate::os::unix::prelude::*;
    extern "C" {
        // These functions are in crt_externs.h.
        fn _NSGetArgc() -> *mut libc::c_int;
        fn _NSGetArgv() -> *mut *mut *mut libc::c_char;
    }
  // ...
}
Copy the code

Rust ABI

Likewise, as you can see from Rust’s official FAQ, Rust does not currently have a stable ABI. So I’m just going to pick out a few of them briefly.

Memory Layout

The Rust ABI guarantees compatibility with C/C++ for basic types such as i32 \ f32 \ &T \ *const T and the compound array.

For some compound types, we know from Rustonomicon that by default the Rust compiler does not fix the memory arrangement of the struct’s internal fields. In some cases, for optimization purposes, the actual field memory order may not be the same as the “visible order” as defined. To keep the memory layout stable, we can add additional attributes such as #[repr(C)] to specify which ABI memory arrangement the marked structure should use. As shown below, we specify that the structure Foo should follow the C ABI for the actual memory layout. The same can be applied to enum types, but it is important to note that the nature of the enum in Rust is different from that in C.

#[repr(C)]struct Foo {    a: bool,    b: f64,    c: bool,    d: i32,}
Copy the code

In addition, for some special types (such as Option

\ enum), the Rust compiler may use “null-pointer Optimization” to optimize the memory layout for these types. At this point, the memory layout of Option

will be the same as that of T (T must be non-null).

Alignment

All types in Rust have alignment requirements in bytes. Primitive types (integers, floating-point, Booleans, and character values) are usually aligned to the size of their type itself (subject to platform constraints, of course). By default, the overall size of a compound type (such as struct) needs to be an integer multiple of the maximum aligned size of its internal fields. Also, the starting offset of each internal field needs to be an integer multiple of the byte size of that field (same as in C/C++). In some cases, Rust automatically inserts “aligned bytes” to meet the above requirements.

The size and alignment of dynamic size types (DSTs) may not be known at statically compile time. Operations on zero-size types (ZSTs) are typically optimized by the compiler for “no-op”, and references to ZSTs must be non-null and properly aligned. Dereferencing a null or unaligned ZST pointer is undefined behavior (UB).

Calling Convention

No specific invocation specification is currently documented for rust-to-rust invocation. For c-to-rust calls, we can choose to make the function call follow the current platform’s default C ABI with the extern keyword. The sample code is shown below.

#[no_mangle] pub extern "C" fn call_from_c() { println! ("Just called a Rust function from C!" ); }Copy the code

Also, we can explicitly specify the other ABI we want to use, such as _cdecl:

#[no_mangle] pub extern "cdecl" fn call_from_c() { println! ("Just called a Rust function from C!" ); }Copy the code

The resources

  1. https://en.wikipedia.org/wiki/Runtime_system.
  2. https://en.wikipedia.org/wiki/Execution_model.
  3. En.wikipedia.org/wiki/Runtim….
  4. https://whatis.techtarget.com/definition/runtime-system.
  5. https://www.techopedia.com/definition/24023/runtime-system.
  6. https://edge.seas.harvard.edu/runtime-systems.
  7. https://stackoverflow.com/questions/42728239/runtime-system-in-c.
  8. https://www.quora.com/Is-there-an-execution-model-for-every-programming-language-I-cannot-find-any-for-C++.
  9. http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf.
  10. https://webassembly.github.io/spec/core/exec/index.html.
  11. https://prev.rust-lang.org/en-US/faq.html.
  12. https://ferrous-systems.github.io/rust-three-days-course/presentation/index.html?chapter=libcore-and-libstd&locale=en-US.
  13. https://blog.mgattozzi.dev/rusts-runtime.
  14. https://en.wikipedia.org/wiki/Stack_trace.
  15. https://rustc-dev-guide.rust-lang.org/panic-implementation.html.
  16. https://en.wikipedia.org/wiki/Call_stack#Unwinding.
  17. https://doc.rust-lang.org/beta/std/panic/fn.catch_unwind.html.
  18. https://mashplant.online/2020/09/06/panic-in-wasm/.
  19. https://blog.rust-lang.org/2020/10/08/Rust-1.47.html#shorter-backtraces.
  20. https://en.wikipedia.org/wiki/Buffer_overflow_protection.
  21. https://www.gnu.org/software/libc/manual/html_node/Exit-Status.html.
  22. https://users.rust-lang.org/t/who-calls-lang-start/51446/2.
  23. https://stackoverflow.com/questions/67444319/how-does-rust-begin-short-backtrace-work-in-rust.
  24. https://stackoverflow.com/questions/67445967/how-does-rust-retrieve-the-input-argc-and-argv-values-from-a-running-progra m.
  25. https://gankra.github.io/blah/rust-layouts-and-abis/.
  26. https://people.gnome.org/~federico/blog/rust-stable-abi.html.
  27. https://users.rust-lang.org/t/rust-function-calling-conventions/13499.
  28. https://doc.rust-lang.org/nomicon/ffi.html.
  29. https://www.reddit.com/r/rust/comments/50qk14/rust_abi/.
  30. https://github.com/rust-lang/rust/issues/29633.
  31. https://doc.rust-lang.org/unstable-book/language-features/lang-items.html.
  32. www.dpldocs.info/experimenta….