Porting Rust standard library: Difference between revisions

Add instructions on making the PAL functional
[unchecked revision][unchecked revision]
(Update to latest rustc and add instructions about providing crt0)
(Add instructions on making the PAL functional)
Line 4:
== Guide ==
 
This guide shows how to get the standard library to compile and run for a custom target, though it won't be functional yet.
 
By the end you should be able to compile a project with
Line 11:
</source>
 
=== Get sources ===
 
<source lang="bash">
Line 17:
</source>
 
=== Configuration ===
 
<source lang="ini">
Line 32:
</source>
 
=== Adding the target ===
Adding a target to the rust compiler takes several files. rustc_target must be told about the new target with a base spec and a target spec. The bootstrap crate must be told that the new target, while not in the downloaded bootstrap compiler, is valid. Also, a test is added for the new target that checks assembly code generation. The test is not strictly necessary, but tidy, the rust compiler's style enforcer, will not pass without it, so it is good practice.
 
Line 162:
</syntaxhighlight>
 
=== Adapt library/std ===
In addition to rustc, std must also be modified to support the target. By default, std will error on build if the OS isn't explicitly supported, so we must add our OS to the list of supported OSes. In addition, we must provide a PAL (Platform Abstraction Layer) to tell std how to interact with our OS.<source lang="diff">
--- a/library/std/build.rs
Line 193:
</source>
 
=== Add toolchain ===
 
<source lang="bash">
Line 199:
</source>
 
==Making the standard library functional==
== Runtime ==
Even though both rustc and std know about the target, programs compiled using the toolchain will crash immediately with a stack overflow. This is because the standard library requires two things from the OS to be able to initialize. A memory allocator and [[Thread Local Storage|thread local storage]] (TLS).
 
===Memory Integrating a crateallocator ===
Implementing a memory allocator is done in the PAL's <code>alloc.rs</code> file and requires implementing the GlobalAlloc trait. The following bump allocator will work to let the stdlib initialize.`<syntaxhighlight lang="rust">
// library/std/src/sys/pal/myos/alloc.rs
use crate::{
alloc::{GlobalAlloc, Layout, System},
ptr,
sync::atomic::{AtomicUsize, Ordering},
};
 
#[repr(align(4096))]
struct HeapData([u8; 65536]);
 
static mut HEAP_DATA: HeapData = HeapData([0; 65536]);
static HEAP_USED: AtomicUsize = AtomicUsize::new(0);
 
#[stable(feature = "alloc_system_type", since = "1.28.0")]
unsafe impl GlobalAlloc for System {
#[inline]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
if layout.align() > 8 {
return ptr::null_mut();
}
let num_blocks = if layout.size() % 8 == 0 {
layout.size() / 8
} else {
(layout.size() / 8) + 1
};
HEAP_USED.fetch_add(num_blocks, Ordering::Relaxed);
let ptr = unsafe { ptr::addr_of_mut!(HEAP_DATA.0[HEAP_USED.load(Ordering::Relaxed) - num_blocks ]) as *mut u8 };
ptr
}
 
#[inline]
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {}
}
</syntaxhighlight>
 
===Thread local storage===
 
====Global statics (easiest, single-threaded only) ====
{{Warning | This WILL not work on multi-threaded systems.}}
If your OS is single-threaded or you don't want to implement full TLS before adding multithreading to your PAL, Rust can implement TLS via global statics. <syntaxhighlight lang="diff">
--- a/library/std/src/sys/thread_local/mod.rs
+++ b/library/std/src/sys/thread_local/mod.rs
@@ -7,7 +7,7 @@
// "static" is for single-threaded platforms where a global static is sufficient.
cfg_if::cfg_if! {
- if #[cfg(any(all(target_family = "wasm", not(target_feature = "atomics")), target_os = "uefi"))] {
+ if #[cfg(any(all(target_family = "wasm", not(target_feature = "atomics")), target_os = "uefi", target_os = "myos"))] {
#[doc(hidden)]
mod static_local;
#[doc(hidden)]
</syntaxhighlight>
 
====OS APIs (slow, easier than ELF native)====
This option is slower than the other multi-threaded compatible version, but is easier to implement, requiring only to implement the functions in the PAL's <code>thread_local_key.rs</code> file. The key is a value unique to a TLS variable, though shared amongst threads. This is rustc's default method of doing TLS.
 
====Native ELF TLS (fastest)====
This option is the fastest way of doing TLS, but requires more complex suport from the OS. To implement it, see the linked wiki article on TLS. Enabling it in rustc is done by adding a target option for your OS.<syntaxhighlight lang="diff">
--- a/compiler/rustc_target/src/spec/base/myos.rs
+++ b/compiler/rustc_target/src/spec/base/myos.rs
@@ -10,6 +10,7 @@ pub fn opts() -> TargetOptions {
relocation_model: RelocModel::Static,
pre_link_objects: crt_objects::pre_myos(),
post_link_objects: crt_objects::post_myos(),
+ has_thread_local: true,
..Default::default()
}
}
</syntaxhighlight>
 
=== Basic printing ===
With memory allocation and TLS in place, the Rust stdlib is now functional enough to not crash. However, it's fairly useless as no IO functions work. Adding print output is a natural next step, and will also allow easier debugging of panics.
 
Adding print support is fairly simple given the architecture of the PAL, and merly requires filling out the Write implementations of stdout/err in <code>stdio.rs</code>. In addition, the panic_output function shoudl be set to the desired output stream for panic messages, usually stderr.
 
Example code is provided below. The given code assumes syscalls are done with interrupt 0x80 with the number in rax and the first parameter in rcx, and syscall 0 takes a byte to write to some text output device. You will probably need to change the syscall number and parameters for your OS, but this should be a basic starting point.<syntaxhighlight lang="diff">
--- a/library/std/src/sys/pal/myos/stdio.rs
+++ b/library/std/src/sys/pal/myos/stdio.rs
@@ -1,3 +1,4 @@
+use crate::arch::asm;
use crate::io;
pub struct Stdin;
@@ -24,6 +25,11 @@ pub const fn new() -> Stdout {
impl io::Write for Stdout {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ for byte in buf {
+ unsafe {
+ asm!("int 0x80", in("rax") 0, in ("rcx") *byte as u64);
+ };
+ }
Ok(buf.len())
}
@@ -40,6 +46,11 @@ pub const fn new() -> Stderr {
impl io::Write for Stderr {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ for byte in buf {
+ unsafe {
+ asm!("int 0x80", in("rax") 0, in ("rcx") *byte as u64);
+ };
+ }
Ok(buf.len())
}
@@ -54,6 +65,6 @@ pub fn is_ebadf(_err: &io::Error) -> bool {
true
}
-pub fn panic_output() -> Option<Vec<u8>> {
- None
+pub fn panic_output() -> Option<impl io::Write> {
+ Some(Stderr::new())
}
</syntaxhighlight>Now you should be able to compile and run the default Rust hello world program for your target and see it print to screen.
 
==Runtime==
 
===Integrating a crate===
 
If you use a crate for the runtime (e.g. <code>myos_rt</code>), you can add it as a dependency to the standard library:
Line 230 ⟶ 353:
Do keep in mind that the same crate with different feature flags are seen as [https://github.com/rust-lang/cargo/issues/2363 <strong>different crates</strong> by the compiler]. This means that if you any globals in the runtime crate and have a project that uses both stdlib and your runtime crate there will be two separate sets of those globals. One way to work around this is by giving these globals an explicit name with <code>#[export_name = "__rt_whatever"]</code> and weakly linking them with <code>#[linkage = "weak"]</code>.
 
== Troubleshooting ==
 
=== error[E0463]: can't find crate for `compiler_builtins` ===
 
Add <code>compiler_builtins</code> as a dependency for the crates you use in stdlib, e.g.:
Anonymous user