本書は 1 から VPC とインスタンスを構築し、その上で Maria DB や WordPress を動かしてみるという内容になっています。章ごとにステップアップしながら成果物が完成していきます。説明も、簡潔ではあるもののポイントを抑えながら進んでいきます。とくに、新しく登場した用語について必ず何かしらの補足説明がなされるのが、丁寧で好印象でした。
#[stable(feature = "rust1", since = "1.0.0")]pubfnretain<F>(&mutself, mut f: F)
where
F: FnMut(&T) ->bool,
{
let len =self.len();
letmut del =0;
{
let v =&mut**self;
for i in0..len {
if!f(&v[i]) {
del +=1;
} elseif del >0 {
v.swap(i - del, i);
}
}
}
if del >0 {
self.truncate(len - del);
}
}
#[stable(feature = "rust1", since = "1.0.0")]#[inline]pubfnswap(&mutself, a: usize, b: usize) {
// Can't take two mutable loans from one vector, so instead just cast// them to their raw pointers to do the swap.let pa: *mut T =&mutself[a];
let pb: *mut T =&mutself[b];
// SAFETY: `pa` and `pb` have been created from safe mutable references and refer// to elements in the slice and therefore are guaranteed to be valid and aligned.// Note that accessing the elements behind `a` and `b` is checked and will// panic when out of bounds.unsafe {
ptr::swap(pa, pb);
}
}
std::ptr::swap の実装は下記のようになっています。MaybeUninit という、初期化されていないかもしれない型を使って tmp を用意した後、x, y の入れ替え処理をします。
#[inline]#[stable(feature = "rust1", since = "1.0.0")]pubunsafefnswap<T>(x: *mut T, y: *mut T) {
// Give ourselves some scratch space to work with.// We do not have to worry about drops: `MaybeUninit` does nothing when dropped.letmut tmp =MaybeUninit::<T>::uninit();
// Perform the swap// SAFETY: the caller must guarantee that `x` and `y` are// valid for writes and properly aligned. `tmp` cannot be// overlapping either `x` or `y` because `tmp` was just allocated// on the stack as a separate allocated object.unsafe {
copy_nonoverlapping(x, tmp.as_mut_ptr(), 1);
copy(y, x, 1); // `x` and `y` may overlapcopy_nonoverlapping(tmp.as_ptr(), y, 1);
}
}
#[stable(feature = "rust1", since = "1.0.0")]pubfntruncate(&mutself, len: usize) {
// This is safe because://// * the slice passed to `drop_in_place` is valid; the `len > self.len`// case avoids creating an invalid slice, and// * the `len` of the vector is shrunk before calling `drop_in_place`,// such that no value will be dropped twice in case `drop_in_place`// were to panic once (if it panics twice, the program aborts).unsafe {
if len >self.len {
return;
}
let remaining_len =self.len - len;
let s =ptr::slice_from_raw_parts_mut(self.as_mut_ptr().add(len), remaining_len);
self.len = len;
ptr::drop_in_place(s);
}
}
retain 判定の結果、不要となった要素を swap しながら truncate の範囲外に追いやるという手法ではなく、スワップはするものの不要だった要素は都度 drop しておくことにした。
swap の際には swap 関数は呼び出さず、copy_nonoverlapping を呼び出すようにした。不要な要素は drop されるので、メモリが重ならないことを保証できるためこれを使用できると思われる。
copy は全体の retain 判定がすべて終了した後、後述する構造体の drop のタイミングに一度だけ行う。
新実装を見ていく
新実装をまずは貼ります。
#[stable(feature = "rust1", since = "1.0.0")]pubfnretain<F>(&mutself, mut f: F)
where
F: FnMut(&T) ->bool,
{
let original_len =self.len();
// Avoid double drop if the drop guard is not executed,// since we may make some holes during the process.unsafe { self.set_len(0) };
// Vec: [Kept, Kept, Hole, Hole, Hole, Hole, Unchecked, Unchecked]// |<- processed len ->| ^- next to check// |<- deleted cnt ->|// |<- original_len ->|// Kept: Elements which predicate returns true on.// Hole: Moved or dropped element slot.// Unchecked: Unchecked valid elements.//// This drop guard will be invoked when predicate or `drop` of element panicked.// It shifts unchecked elements to cover holes and `set_len` to the correct length.// In cases when predicate and `drop` never panick, it will be optimized out.structBackshiftOnDrop<'a, T, A: Allocator> {
v: &'amutVec<T, A>,
processed_len: usize,
deleted_cnt: usize,
original_len: usize,
}
impl<T, A: Allocator>Dropfor BackshiftOnDrop<'_, T, A> {
fndrop(&mutself) {
ifself.deleted_cnt >0 {
// SAFETY: Trailing unchecked items must be valid since we never touch them.unsafe {
ptr::copy(
self.v.as_ptr().add(self.processed_len),
self.v.as_mut_ptr().add(self.processed_len -self.deleted_cnt),
self.original_len -self.processed_len,
);
}
}
// SAFETY: After filling holes, all items are in contiguous memory.unsafe {
self.v.set_len(self.original_len -self.deleted_cnt);
}
}
}
letmut g = BackshiftOnDrop { v: self, processed_len: 0, deleted_cnt: 0, original_len };
while g.processed_len < original_len {
// SAFETY: Unchecked element must be valid.let cur =unsafe { &mut*g.v.as_mut_ptr().add(g.processed_len) };
if!f(cur) {
// Advance early to avoid double drop if `drop_in_place` panicked.
g.processed_len +=1;
g.deleted_cnt +=1;
// SAFETY: We never touch this element again after dropped.unsafe { ptr::drop_in_place(cur) };
// We already advanced the counter.continue;
}
if g.deleted_cnt >0 {
// SAFETY: `deleted_cnt` > 0, so the hole slot must not overlap with current element.// We use copy for move, and never touch this element again.unsafe {
let hole_slot = g.v.as_mut_ptr().add(g.processed_len - g.deleted_cnt);
ptr::copy_nonoverlapping(cur, hole_slot, 1);
}
}
g.processed_len +=1;
}
// All item are processed. This can be optimized to `set_len` by LLVM.drop(g);
}
大幅に実装量は増えていますが、まず構造体が追加されたことと、その構造体に対する Drop トレイとの実装が追加されたために行数が増えています。この構造体は今回の新実装の中核を担っています。
実装の都合で State の enum に対しては現在 Clone が要求されます。現在、標準で提供しているのは BasicStateMachine という構造体です。これは State に Clone を要求しています。実装側を見ていただくとわかるのですが、どうしても clone が必要と思われる箇所が出てくるためです。
/// The basic state machine implementation./// It holds `initial_state`, `current_state`, `transition` function.pubstructBasicStateMachine<State, Input, Transition>where
Transition: Fn(&State, Input) -> State,
State: Clone,
{
/// `initial_state` is literally an initial state of the state machine./// The field isn't updated the whole life of its state machine./// That is, it always returns its initial state of its machine.
initial_state: State,
/// `current_state` is the current state of the state machine./// It transit to the next state via `transition`.
current_state: RefCell<StateWrapper<State>>,
/// `transition` is the definition of state transition./// See an example of [`StateMachine::consume()`], you can grasp how/// to define the transition.
transition: Transition,
_maker: PhantomData<Input>,
}
もし State に Clone を要求するのが、大きなパフォーマンス劣化を招く原因になりうるのだとしたら、この Clone の制約を外せるように実装を調整する必要があるのかなと思っています。が、正直これをどう上手に調整したらよいかの想像がついていません。このあたりのワークアラウンドは結構迷ってます…。
ドキュメントを書くのはかなり骨の折れる作業で、主にサンプルコードの用意の部分で結構手間取りました。ただ、サンプルコードを用意しておくと、cargo test したタイミングで doc test が走り、コメント内のコードのコンパイルが通るかや assertion が通るかを自動でチェックしてくれます。ドキュメントのメンテ漏れに気づけるようになるので、この仕組みは非常に素晴らしいです。ちなみに他の言語での経験だと Python でも同じことができますね。
Tour of Rust [一部邦訳]: これは現時点で最初の一歩におすすめできる資料かなと思います。Go 言語には A Tour of Go という、Go を始める際にうってつけな教材があります。それの Rust 版です。Rust の文法を1ページ1ページ解説しつつ、横で Playground を使ってコードを実際に実行して挙動を確かめるところまでセットでできます。まずこれを軽く通してみて、簡単に文法の概観を掴むとよいのではないかと思います。
Rust by Example: 文法の細かい解説というよりは、サンプルスクリプトを通じて Rust を学んでいける資料に仕上がっています。細かい解説はいいから、とりあえずサクサク写経しつつ概観を掴んで実際にアプリケーションを作って学んでいきたい、という方におすすめできるかなと思います。
Command Line Applications in Rust [未邦訳]: 非常に小さな機能を持つgrepの実装を通じて、Rustの文法機能の実践面での使い方も理解することまでが視野に入ったチュートリアルです。CLIツールの自作はRustで作りたいものがとくにない方にとくにおすすめです。grep以外にも、たとえばlsなどを実装してみてもおもしろいのではないかと思います。
Writing OS in Rust: Rust は OS を作るのに適した言語なのですが、Rust で OS を作るチュートリアルです。一方でただ OS を作るだけではなく、Rust の細かい機能の解説や文法の解説、ワークアラウンドの解説などが記事のなかに散りばめられています。
Rustで始める自作組込みOS入門: Rust でマイコン上で動作する組み込みの OS を作るチュートリアルです。ARMv7-M を題材に、割り込み、プロセスの切り替え、スケジューラまでを実装していきます。組み込みOSの開発に興味がある場合にはよい題材となるはずです。
Let's build a browser engine! [未邦訳]: Rust でブラウザエンジンを実装するチュートリアルです。最終的に HTML と CSS をパースし、その情報から画面に結果を描画するところまで作り上げられます。HTML パーサーなどでハードに代数データとパターンマッチを使うことになるので、その練習にいいかもしれません。
「最近話題の RISC-V などの CPU エミュレータを作ってみたいものの、いきなり作るにはハードルが高い。何か簡単なもので素振りをして CPU の動作の仕組みをまずは知りたい」という方にはかなりオススメできる教材だと思います。私自身、TD4 のエミュレータを実装してみて CPU の動作原理やコンセプトがわかり、その後製作中の RISC-V エミュレータに生きているように思います。
現在どの番地のプログラムを読み出しているかについては、プログラムカウンタ(pc)と呼ばれるフラグによって管理されます。先ほどの fetch-decode-execute サイクルが1サイクル終了するとプログラムカウンタが足され、次のプログラムを読み出し、ということを ROM の配列が終了するまで行うことになります。
まず fetch です。ここでは現状のプログラムカウンタの値を確認し、プログラムカウンタに該当するプログラムを ROM から読み取ります。
(...)
fnfetch(&self) ->u8 {
let pc =self.register.borrow().pc();
ifself.rom.borrow().size() <= pc {
return0;
}
let code =self.rom.borrow().read(pc);
code
}
(...)
(...)
let rom =Rom::new(program);
let register =Register::new();
let port =Port::new(0b0000, 0b0000);
let emulator =CpuEmulator::with(register, port, rom);
match emulator.exec() {
Ok(_) => (),
Err(err) =>panic!("{:?}", err),
}
(...)
この場合ですと、空白で split をかけると、mov, A, 0001 という文字列が取得できます。TD4 の場合、命令がとても単純なので、たとえば mov の後ろには必ず A か B という文字列が来ているはず、といった感じでシンプルに条件分岐をしながらパースしていくと、一通り命令をパースして解釈できるようになります。
(...)
pubfnparse(&mutself) ->Result<Vec<Token>, EmulatorErr> {
letmut result =Vec::new();
loop {
let op =self.source.get(self.pos);
if op.is_none() {
break;
}
let op = op.unwrap();
if op =="mov" {
self.pos +=1;
let lhs =self
.source
.get(self.pos)
.ok_or_else(||EmulatorErr::new("Failed to parse mov left hand side value"))?;
self.pos +=1;
let rhs =self
.source
.get(self.pos)
.ok_or_else(||EmulatorErr::new("Failed to parse mov right hand side value"))?;
let token =if lhs =="B"&& rhs =="A" {
Token::MovBA
} elseif lhs =="A"&& rhs =="B" {
Token::MovAB
} else {
Token::Mov(
Register::from(lhs.to_string()),
self.from_binary_to_decimal(rhs)?,
)
};
result.push(token);
}
(...)
GATs というのは先ほども軽く触れたとおり、関連型に何かしらの型パラメータ(A や T みたいなもの)をもたせることができる機能です。Rust にいわゆる高階カインド型を導入するための機能です。
Rust の場合は、他の言語でもあるような型パラメータ以外にもライフタイム注釈*7をこの位置に入れることができます。つまり、型パラメータの位置に入るのは A というような型情報と、もうひとつは 'a というライフタイム注釈が来ることになります。GATs はこれら2つを関連型で扱えるようにするための機能です。
*7:Rust にはダングリングポインタを防止するための仕組みとしてライフタイムという概念が存在します。このライフタイムというのは、ユーザーがライフタイム注釈を個別に指定してライフタイムを明示的に書くこともできるようになっています。ライフタイム注釈というのは 'a というようにシングルクオートで始まる形で、通常の型情報と同じ位置(<> 内)に入れて書きます。