<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>lhz07's Blog</title>
    <link>https://lhz07.com/</link>
    <description>lhz07's Blog</description>
    <language>zh-cn</language>
    <lastBuildDate>Tue, 21 Jul 2026 00:38:06 +0800</lastBuildDate>
    
    <item>
      <title>在 Rust 中实现 defer</title>
      <link>https://lhz07.com/posts/implement-defer-in-rust</link>
      <guid>https://lhz07.com/posts/implement-defer-in-rust</guid>
      <pubDate>Tue, 21 Jul 2026 00:38:06 +0800</pubDate>
      <description
      ><![CDATA[<img src=static/img/posts/implement-defer-in-rust/cover.webp alt="Image" />
      <p><em>介绍 DropGuard 与 macro_rules，并实现 defer! 宏</em></p>
      <br>
      <p>RAII 和 defer 是两种常见的资源管理方式，理论上来说，它们是完全可以互相替代的，但实际上它们更像是互补的，各有各的优势。当对象拥有资源时，比如一个 <code>File</code> 对象，这时候适合用 RAII，在离开作用域时自动关闭文件；而需要在作用域结束时执行特定的清理逻辑，这就更适合用 defer<br />
比如在写包管理器的时候，使用数据库来管理安装的文件，数据库通过事务操作，可以确保数据具有原子性，要么提交，要么回滚，不存在中间状态。但是一般的文件系统不具备这样的功能，这时候可以用 defer 来实现，如果出错了，提前返回，通过 defer 自动回滚文件；如果成功了，就取消 defer，相当于是成功提交了事务。<br />
下面这段代码就通过 defer 实现了这样的逻辑：</p>
<pre><code class="language-rust">// runnable
use std::{
    mem::ManuallyDrop,
    ops::{Deref, DerefMut},
};

pub struct DropGuard&lt;F: FnOnce(T), T&gt; {
    f: ManuallyDrop&lt;F&gt;,
    inner: ManuallyDrop&lt;T&gt;,
}

impl&lt;F: FnOnce(T), T&gt; DropGuard&lt;F, T&gt; {
    pub fn new(inner: T, f: F) -&gt; Self {
        Self {
            f: ManuallyDrop::new(f),
            inner: ManuallyDrop::new(inner),
        }
    }

    /// Consumes the `DropGuard` without invoking the drop function
    pub fn into_inner(self) -&gt; T {
        let mut new_guard = ManuallyDrop::new(self);
        let value = unsafe { ManuallyDrop::take(&amp;mut new_guard.inner) };
        unsafe { ManuallyDrop::drop(&amp;mut new_guard.f) };
        value
    }
}

impl&lt;F: FnOnce(T), T&gt; Drop for DropGuard&lt;F, T&gt; {
    fn drop(&amp;mut self) {
        let value = unsafe { ManuallyDrop::take(&amp;mut self.inner) };
        let f = unsafe { ManuallyDrop::take(&amp;mut self.f) };
        f(value);
    }
}

impl&lt;F: FnOnce(T), T&gt; Deref for DropGuard&lt;F, T&gt; {
    type Target = T;

    fn deref(&amp;self) -&gt; &amp;Self::Target {
        &amp;self.inner
    }
}

impl&lt;F: FnOnce(T), T&gt; DerefMut for DropGuard&lt;F, T&gt; {
    fn deref_mut(&amp;mut self) -&gt; &amp;mut Self::Target {
        &amp;mut self.inner
    }
}

#[macro_export]
macro_rules! defer {
    ($($move:ident)? |$($tokens:tt)*) =&gt; {
        $crate::defer_impl!(__guard $($move)? [] [] [] [,$($tokens)*]);
    };

    ($guard:ident, $($move:ident)? |$($tokens:tt)*) =&gt; {
        $crate::defer_impl!($guard $($move)? [] [] [] [,$($tokens)*] );
    };

    ($guard:ident, $body:expr) =&gt; {
        let $guard = DropGuard::new(
            (),
            |_| $body(),
        );
    };

    ($body:expr) =&gt; {
        let mut __defer_guard = DropGuard::new(
            (),
            |_| $body(),
        );
    };

}

#[macro_export]
macro_rules! defer_impl {
    ($guard:ident $($move:ident)? [$($caps:ident)*] [$($pats:tt)*]
     [$($comma:tt)?]
     [,mut $var:ident $($rest:tt)*]
    ) =&gt; {
        $crate::defer_impl!(
            $guard
            $($move)?
            [$($caps)* $var]
            [$($pats)* $($comma)? mut $var]
            [,]
            [$($rest)*]
        );
    };

    // ident, rest
    ($guard:ident $($move:ident)? [$($caps:ident)*] [$($pats:tt)*]
     [$($comma:tt)?]
     [, $bind:ident: &amp;$var:ident $($rest:tt)*]
    ) =&gt; {
        let $bind = &amp;mut $var;
        $crate::defer_impl!(
            $guard
            $($move)?
            [$($caps)* $bind]
            [$($pats)* $($comma)? $bind]
            [,]
            [$($rest)*]
        );
        let $bind = &amp;mut **$bind;
    };

    ($guard:ident $($move:ident)? [$($caps:ident)*] [$($pats:tt)*]
     [$($comma:tt)?]
     [, &amp;$var:ident $($rest:tt)*]
    ) =&gt; {
        let $var = &amp;mut $var;
        $crate::defer_impl!(
            $guard
            $($move)?
            [$($caps)* $var]
            [$($pats)* $($comma)? $var]
            [,]
            [$($rest)*]
        );
        let $var = &amp;mut **$var;
    };

    // ident, rest
    ($guard:ident $($move:ident)? [$($caps:ident)*] [$($pats:tt)*]
     [$($comma:tt)?]
     [, $var:ident $($rest:tt)*]
    ) =&gt; {
        $crate::defer_impl!(
            $guard
            $($move)?
            [$($caps)* $var]
            [$($pats)* $($comma)? $var]
            [,]
            [$($rest)*]
        );
    };

    // END
    ($guard:ident $($move:ident)? [$($caps:ident)*] [$($pats:tt)*] [,] [$(,)? | $body:block]) =&gt; {
        #[allow(unused_parens)]
        let mut $guard = DropGuard::new(
            ($($caps),*),
            $($move)? |($($pats)*)| $body,
        );

        #[allow(unused_parens)]
        let ($($caps),*) = &amp;mut *$guard;
    };

}

// ANCHOR
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

fn copy_files() -&gt; Result&lt;(), io::Error&gt; {
    let mut installed_files: Vec&lt;PathBuf&gt; = Vec::new();
    defer!(guard, |&amp;installed_files| {
        for p in installed_files {
            let res = fs::remove_file(&amp;p);
            println!(&quot;delete the file: {}, {res:?}&quot;, p.display());
        }
    });

    let files = fs::read_dir(&quot;.&quot;)?;
    for entry in files {
        let entry = entry?;
        let file = entry.path();
        let copy_to = Path::new(&quot;target&quot;).join(entry.file_name());
        installed_files.push(copy_to);
        println!(
            &quot;copy file: {}, type: {:?}&quot;,
            file.display(),
            entry.file_type()?
        );
        fs::copy(file, installed_files.last().unwrap())?;
    }

    guard.into_inner();
    Ok(())
}

fn main() {
    let res = copy_files();
    println!(&quot;result: {res:?}&quot;);
}
// ANCHOR_END
</code></pre>
<p>这里先用 defer 定义了出错时需要执行哪些逻辑，再开始复制文件，出错时会通过问号提前返回，defer 在退出作用域时自动执行，整个流程非常自然，不妨点击右上角的运行按钮看看结果如何。<br />
关于 Rust 中的 defer，社区中也有不少讨论<sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup>，可见需求不小，但官方依旧不太想做这个功能，社区里比较有名的实现是 <a href="https://crates.io/crates/scopeguard">scopeguard</a> ，但是它提供的 defer 宏并不好用，由于借用检查器的限制，完全无法实现上面那样的代码，所以我尝试自己实现了一个比较好用的 defer 宏，效果如上，用起来很灵活，不会因为借用检查器而受到很大的限制。下面介绍一下如何实现这个宏。</p>
<h2>实现 DropGuard</h2>
<h3>尝试简单实现</h3>
<p>defer 宏的作用是让代码写起来更简单，但要实现 defer，首先需要实现一个 DropGuard，以上面的例子来说，除去取消功能，它还至少需要两个功能：</p>
<ol>
<li>可以存储一个实现了 FnOnce 的闭包<sup class="footnote-ref"><a href="#fn-2" id="fnref-2" data-footnote-ref>2</a></sup>，并在 drop 时调用。</li>
<li>可以单独存储数据，虽然闭包也能捕获数据，但这样只能在 drop 的时候访问，而我们上面的代码，是需要在定义 defer 后，往<code>installed_files</code> 里添加文件路径的，所以数据必须单独存储，便于外部访问。</li>
</ol>
<p>那么最简单的实现大概长这样</p>
<pre><code class="language-Rust">// runnable
// ANCHOR
struct DropGuard&lt;T, F: FnOnce(T)&gt; {
    inner: T,
    f: F,
}

impl&lt;T, F: FnOnce(T)&gt; Drop for DropGuard&lt;T, F&gt; {
    fn drop(&amp;mut self) {
        (self.f)(self.inner);
    }
}
// ANCHOR_END

fn main() {}
</code></pre>
<p>但很可惜，这段代码没法编译，原因是 FnOnce 被调用时会被消耗，存储的值 T 也需要传给 FnOnce 使用，但 drop 方法的参数是 <code>&amp;mut self</code>，这意味着只能更改 Self 里的值，但不能消耗掉。</p>
<h4>为什么这么设计？</h4>
<p>乍一看很奇怪，结构体被 drop 之后就不能再使用了，为什么不能消耗所有权？<br />
一方面，drop 的本质是在作用域结束时，会自动帮你调用 drop 方法，像这样：</p>
<pre><code class="language-Rust">fn main() {
    {
        let s1 = &quot;hello&quot;.to_string();
        let s2 = &quot;hello&quot;.to_string();
        // 作用域结束，自动调用 drop 方法
        s2.drop();
        s1.drop();
    }
}
</code></pre>
<p>而对于方法，如果方法参数是 <code>self</code>，自然也需要调用 drop 方法。那对于 drop 方法本身来说就会发生无限的递归了。</p>
<pre><code class="language-Rust">fn drop(self) {
    self.drop();
}
</code></pre>
<p>想解决这个问题，必须要对 drop 方法的实现做一些特殊的调整，但这样就会使语言的行为更加不一致了。</p>
<p>另一方面，对于 unsized type，它们可能也需要 drop 方法，如果方法参数是引用，引用的大小是可以在编译时确定的，但如果是 <code>self</code> ，就没法确定大小了，无法编译。</p>
<pre><code class="language-Rust">// runnable
// ANCHOR
struct UnsizedType&lt;T: ?Sized&gt;(T);

impl&lt;T: ?Sized&gt; UnsizedType&lt;T&gt; {
    fn drop(self) {}
}
// ANCHOR_END

fn main() {}
</code></pre>
<h3>进一步探索实现</h3>
<p>既然 drop 方法的签名没法改，就只能想办法从可变引用中取出值了，最简单的想法是用 <code>Option</code>，可以取出里面的值，留下 <code>None</code> 在里面，但是它有两个问题：</p>
<ol>
<li>每次都要判断里面是 <code>Some</code> 还是 <code>None</code> ，虽然开销很小，但没法忽略</li>
<li><code>Option</code> 的本质是 tagged union，需要额外占用 1 字节的空间存储当前的状态，而且由于结构体需要内存对齐，为了存储状态而多占用的空间可能不止 1 字节。</li>
</ol>
<!-- end list -->
<pre><code class="language-Rust">// runnable
struct DropGuard&lt;T, F: FnOnce(T)&gt; {
    inner: Option&lt;T&gt;,
    f: Option&lt;F&gt;,
}

impl&lt;T, F: FnOnce(T)&gt; Drop for DropGuard&lt;T, F&gt; {
    fn drop(&amp;mut self) {
        unsafe {
            let f = self.f.take().unwrap();
            let inner = self.inner.take().unwrap();
            (f)(inner);
        }
    }
}

fn main() {
    println!(&quot;u64 size: {}&quot;, size_of::&lt;u64&gt;());
    println!(&quot;u64 with Option size: {}&quot;, size_of::&lt;Option&lt;u64&gt;&gt;());
}
</code></pre>
<p>其实我们根本不需要那个多余的 tag，直接用 union 不就行了，像这样</p>
<pre><code class="language-Rust">// runnable
// ANCHOR
use std::ptr;

union Opt&lt;T&gt; {
    val: T,
    _nothing: (),
}

struct DropGuard&lt;T, F: FnOnce(T)&gt; {
    inner: Opt&lt;T&gt;,
    f: Opt&lt;F&gt;,
}

impl&lt;T, F: FnOnce(T)&gt; Drop for DropGuard&lt;T, F&gt; {
    fn drop(&amp;mut self) {
        unsafe {
            let f = ptr::read(&amp;self.f.val);
            let inner = ptr::read(&amp;self.inner.val);
            (f)(inner);
        }
    }
}
// ANCHOR_END

fn main() {}
</code></pre>
<p>Oops，被编译器阻止了，为什么之前用 <code>Option</code> 没问题，但现在用 <code>union</code> 就不行了呢？<br />
主要问题是，对于 <code>Option</code>，它被 drop 的时候，因为有 tag 记录了当前状态，可以判断当前是否存储了值，如果有值就 drop 掉，如果没有值就什么都不做。<br />
但是对于 <code>union</code>，无法得知它的状态，保守的做法应该是什么也不干，但是这样就内存泄漏了，所以这里编译器要求 <code>union</code> 里存储的类型要么是 <code>Copy</code> 的，无需 drop，要么是 <code>ManuallyDrop&lt;T&gt;</code>，也不需要 drop<br />
顾名思义，被 <code>ManuallyDrop</code> 包裹的值，将不会被 drop，需要手动进行，这似乎也正好是我们需要的。</p>
<p>我们刚刚还没有尝试，如果直接读出值会怎么样，请运行下面的例子看看：</p>
<pre><code class="language-Rust">// runnable
use std::ptr;

struct DropGuard&lt;T, F: FnOnce(T)&gt; {
    inner: T,
    f: F,
}

impl&lt;T, F: FnOnce(T)&gt; Drop for DropGuard&lt;T, F&gt; {
    fn drop(&amp;mut self) {
        unsafe {
            let f = ptr::read(&amp;self.f);
            let inner = ptr::read(&amp;self.inner);
            (f)(inner);
        }
    }
}

fn main() {
    let a = Box::new(3);
    let _guard = DropGuard {
        inner: a,
        f: |_| {
            // do nothing
        },
    };
}
</code></pre>
<p>我们成功触发了 double free，这是因为 <code>drop</code> 方法只用来处理当前结构体的析构过程，而结构体内部的各个字段，则由它们自己的 <code>drop</code> 方法来处理。<br />
在上面的代码中，当 <code>DropGuard</code> 离开作用域时，先运行了它自己的 <code>drop</code> 方法，读出了 <code>a</code>，并传入闭包，闭包结束时，<code>a</code> 自然被 drop，<code>Box</code><sup class="footnote-ref"><a href="#fn-3" id="fnref-3" data-footnote-ref>3</a></sup> 的 <code>drop</code> 方法里运行了一次 <code>free</code>，之后则开始析构各个字段，析构 <code>inner</code> 字段的时候，又 drop 了一次 <code>a</code>，<code>Box</code> 的 <code>drop</code> 方法里又运行了一次 <code>free</code>，于是就触发 double free 了。</p>
<p>显而易见，<code>ManuallyDrop</code> 在这里非常有用，它可以阻止第二次 drop，从而让这段代码正常运行。</p>
<h3>最终实现</h3>
<pre><code class="language-rust">// runnable
use std::mem::ManuallyDrop;

pub struct DropGuard&lt;F: FnOnce(T), T&gt; {
    f: ManuallyDrop&lt;F&gt;,
    inner: ManuallyDrop&lt;T&gt;,
}

impl&lt;F: FnOnce(T), T&gt; DropGuard&lt;F, T&gt; {
    pub fn new(inner: T, f: F) -&gt; Self {
        Self {
            f: ManuallyDrop::new(f),
            inner: ManuallyDrop::new(inner),
        }
    }
}

impl&lt;F: FnOnce(T), T&gt; Drop for DropGuard&lt;F, T&gt; {
    fn drop(&amp;mut self) {
        let value = unsafe { ManuallyDrop::take(&amp;mut self.inner) };
        let f = unsafe { ManuallyDrop::take(&amp;mut self.f) };
        f(value);
    }
}

fn main() {
    {
        let a = Box::new(3);
        let _guard = DropGuard::new(a, |_| {
            println!(&quot;drop `a`&quot;);
        });
    }
    println!(&quot;dropped `a` safely&quot;);
}
</code></pre>
<p>这个实现就比较完善了，也没有内存错误，但作为 <code>DropGuard</code>，还需要实现两个功能</p>
<h4>Deref Trait</h4>
<p>当把值交给 <code>DropGuard</code> 后，可能还需要修改值，每次都调用 <code>guard.inner</code> 非常麻烦，可以通过实现 Deref trait 来改善这一点。<br />
顾名思义，<code>Deref</code> trait 就是定义当解引用这个结构体的引用时，应该返回什么，因为 Rust 区分可变性，所以 <code>Deref</code> 和 <code>DerefMut</code> 分别对应着不可变引用和可变引用两种情况。</p>
<pre><code class="language-rust">// runnable
use std::mem::ManuallyDrop;

pub struct DropGuard&lt;F: FnOnce(T), T&gt; {
    f: ManuallyDrop&lt;F&gt;,
    inner: ManuallyDrop&lt;T&gt;,
}

impl&lt;F: FnOnce(T), T&gt; DropGuard&lt;F, T&gt; {
    pub fn new(inner: T, f: F) -&gt; Self {
        Self {
            f: ManuallyDrop::new(f),
            inner: ManuallyDrop::new(inner),
        }
    }
}

impl&lt;F: FnOnce(T), T&gt; Drop for DropGuard&lt;F, T&gt; {
    fn drop(&amp;mut self) {
        let value = unsafe { ManuallyDrop::take(&amp;mut self.inner) };
        let f = unsafe { ManuallyDrop::take(&amp;mut self.f) };
        f(value);
    }
}

// ANCHOR
use std::ops::{Deref, DerefMut};

impl&lt;F: FnOnce(T), T&gt; Deref for DropGuard&lt;F, T&gt; {
    type Target = T;

    fn deref(&amp;self) -&gt; &amp;Self::Target {
        &amp;self.inner
    }
}

impl&lt;F: FnOnce(T), T&gt; DerefMut for DropGuard&lt;F, T&gt; {
    fn deref_mut(&amp;mut self) -&gt; &amp;mut Self::Target {
        &amp;mut self.inner
    }
}

fn main() {
    let a = vec![1, 2];
    let mut guarded_a = DropGuard::new(a, |a| {
        println!(&quot;drop `a`&quot;);
        println!(&quot;{:?}&quot;, a);
    });
    println!(&quot;modify a&quot;);
    // 这里调用 `push` 方法时，因为 `DropGuard` 没有这个方法，
    // 编译器会加上足够多的 `*` 号进行解引用，直到找到对应的方法为止。
    guarded_a.push(3);
    // 显式地写出来就是这样
    let v: &amp;mut Vec&lt;i32&gt; = &amp;mut *guarded_a;
    v.push(4);
}
// ANCHOR_END
</code></pre>
<h4>取消 guard</h4>
<p>文章开头，我举了一个包管理器的例子，安装文件时，如果出错了，就通过 <code>DropGuard</code> 自动回滚，如果成功了，则需要取消它，如何实现呢？<br />
显然，我们没法直接取消 <code>drop</code> 方法，但我们可以把 <code>DropGuard</code> 本身也塞进 <code>ManuallyDrop</code> 里，这样就可以避免它的 <code>drop</code> 方法被执行了。</p>
<pre><code class="language-rust">// runnable
use std::mem::ManuallyDrop;
use std::ops::{Deref, DerefMut};

impl&lt;F: FnOnce(T), T&gt; Drop for DropGuard&lt;F, T&gt; {
    fn drop(&amp;mut self) {
        let value = unsafe { ManuallyDrop::take(&amp;mut self.inner) };
        let f = unsafe { ManuallyDrop::take(&amp;mut self.f) };
        f(value);
    }
}

impl&lt;F: FnOnce(T), T&gt; Deref for DropGuard&lt;F, T&gt; {
    type Target = T;

    fn deref(&amp;self) -&gt; &amp;Self::Target {
        &amp;self.inner
    }
}

impl&lt;F: FnOnce(T), T&gt; DerefMut for DropGuard&lt;F, T&gt; {
    fn deref_mut(&amp;mut self) -&gt; &amp;mut Self::Target {
        &amp;mut self.inner
    }
}

// ANCHOR
pub struct DropGuard&lt;F: FnOnce(T), T&gt; {
    f: ManuallyDrop&lt;F&gt;,
    inner: ManuallyDrop&lt;T&gt;,
}

impl&lt;F: FnOnce(T), T&gt; DropGuard&lt;F, T&gt; {
    // ANCHOR_END
    pub fn new(inner: T, f: F) -&gt; Self {
        Self {
            f: ManuallyDrop::new(f),
            inner: ManuallyDrop::new(inner),
        }
    }
    // ANCHOR
    pub fn into_inner(self) -&gt; T {
        // 塞进 ManuallyDrop
        let mut new_guard = ManuallyDrop::new(self);
        // 取出存储的值
        let value = unsafe { ManuallyDrop::take(&amp;mut new_guard.inner) };
        // 不需要运行闭包了，直接 drop 掉
        unsafe { ManuallyDrop::drop(&amp;mut new_guard.f) };
        // 返回值
        value
    }
}

fn main() {
    let a = vec![1, 2];
    let mut guarded_a = DropGuard::new(a, |a| {
        println!(&quot;drop `a`&quot;);
        println!(&quot;{:?}&quot;, a);
    });
    println!(&quot;modify a&quot;);
    guarded_a.push(3);
    // 取消 guard
    let a = guarded_a.into_inner();
}
// ANCHOR_END
</code></pre>
<h3>灵活使用</h3>
<p>这个 <code>DropGuard</code> 看起来简单，但是因为使用了泛型，用起来十分灵活。<br />
比如同时处理多个值，可以把值打包成元组塞进去，重新借用后再解构元组<br />
也可以只传引用进去，不把整个值 move 进去，这样之后还能继续用。</p>
<pre><code class="language-Rust">// runnable
use std::mem::ManuallyDrop;
use std::ops::{Deref, DerefMut};

impl&lt;F: FnOnce(T), T&gt; Drop for DropGuard&lt;F, T&gt; {
    fn drop(&amp;mut self) {
        let value = unsafe { ManuallyDrop::take(&amp;mut self.inner) };
        let f = unsafe { ManuallyDrop::take(&amp;mut self.f) };
        f(value);
    }
}

impl&lt;F: FnOnce(T), T&gt; Deref for DropGuard&lt;F, T&gt; {
    type Target = T;

    fn deref(&amp;self) -&gt; &amp;Self::Target {
        &amp;self.inner
    }
}

impl&lt;F: FnOnce(T), T&gt; DerefMut for DropGuard&lt;F, T&gt; {
    fn deref_mut(&amp;mut self) -&gt; &amp;mut Self::Target {
        &amp;mut self.inner
    }
}

pub struct DropGuard&lt;F: FnOnce(T), T&gt; {
    f: ManuallyDrop&lt;F&gt;,
    inner: ManuallyDrop&lt;T&gt;,
}

impl&lt;F: FnOnce(T), T&gt; DropGuard&lt;F, T&gt; {
    pub fn new(inner: T, f: F) -&gt; Self {
        Self {
            f: ManuallyDrop::new(f),
            inner: ManuallyDrop::new(inner),
        }
    }

    pub fn into_inner(self) -&gt; T {
        // 塞进 ManuallyDrop
        let mut new_guard = ManuallyDrop::new(self);
        // 取出存储的值
        let value = unsafe { ManuallyDrop::take(&amp;mut new_guard.inner) };
        // 不需要运行闭包了，直接 drop 掉
        unsafe { ManuallyDrop::drop(&amp;mut new_guard.f) };
        // 返回值
        value
    }
}

// ANCHOR
fn handle_multiple_values() {
    let a = vec![1, 2];
    let b = &quot;hello&quot;.to_string();
    // 把 a, b 打包成元组放进去，创建 guard 之后，使用模式匹配解构元组
    let (a, b) = &amp;mut *DropGuard::new((a, b), |(a, b)| {
        println!(&quot;drop `a`&quot;);
        println!(&quot;{:?}&quot;, a);
        println!(&quot;drop b: {b}&quot;);
    });
    println!(&quot;modify a&quot;);
    a.push(3);
    println!(&quot;print b: {b}&quot;);
}

fn return_value() -&gt; Vec&lt;i32&gt; {
    let mut a = vec![1, 2];
    let b = &quot;hello&quot;.to_string();
    // 为 a 创建可变引用，之后只把引用放进 guard，外面的 a 不受影响
    let a_ref = &amp;mut a;
    let mut guard = DropGuard::new((a_ref, b), |(a_ref, b)| {
        a_ref.push(4);
        println!(&quot;drop b: {b}&quot;);
    });
    let (a_ref, b) = &amp;mut *guard;
    println!(&quot;modify a&quot;);
    a_ref.push(3);
    println!(&quot;print b: {b}&quot;);
    // guard 持有对 a 的引用，必须先 drop 掉 guard，才能返回 a
    drop(guard);
    a
}

fn main() {
    println!(&quot;----------------- handle multiple values -----------------&quot;);
    handle_multiple_values();
    println!(&quot;----------------- return value -----------------&quot;);
    let a = return_value();
    println!(&quot;returned a: {a:?}&quot;);
}
// ANCHOR_END
</code></pre>
<p>其实就功能性来说，目前这个 <code>DropGuard</code> 已经完全可以实现开头提到的包管理器的例子了，但显然的问题是，写起来太繁琐了，要处理 a, b 两个变量，需要写整整 3 次 <code>(a, b)</code>，如果变量名再长一点，写起来更麻烦；如果希望只放引用进去，还要再多写一行 <code>let a_ref = &amp;mut a;</code>，实在是繁琐得不能再繁琐了。对于这种重复的工作，在 Rust 中可以用宏来解决。</p>
<h2>宏与 <code>macro_rules!</code></h2>
<p>Rust 中有两种宏：声明式宏和过程宏，就像 C 里的 <code>define</code> 一样，可以在编译之前，先处理一遍代码。<br />
其中过程宏最强大，它几乎可以处理任意输入，感觉上像是编译器的插件，你可以自由地定义一套语法，只需要在过程宏里写 Rust 代码解析，并输出为合法的语法树就行，比如你可以在里面写 html，然后在过程宏里解析并生成合法的 Rust 语法树。而本质上它是个单独的程序，运行 <code>cargo build</code> 时，会先编译它，之后 <code>rustc</code> 会在需要的地方运行它来处理代码，再进入正常的编译流程。显然，这意味着过程宏会大幅拖慢编译速度，不仅需要多编译一个程序，语法树需要在编译器和过程宏的动态库之间传递，而且这一操作是阻塞的，后续的类型检查、借用检查等都要等过程宏的代码生成结束才能运行。</p>
<p>而声明式宏则是内置在 <code>rustc</code> 里的一套语法规则，和普通代码的不同之处是，它处理的是输入的 tokens，并输出为合法的代码。它的规则相对有限，且只能通过模式匹配和递归来处理，无法像过程宏那样拥有判断和循环功能。但它最大的优点就是快，因为内置在编译器里，对编译速度的影响很小。</p>
<h3>简单的 <code>macro_rules</code></h3>
<p>声明式宏本质上就是按照分支顺序挨个进行模式匹配，比如一个简单的宏：</p>
<pre><code class="language-Rust">// runnable
macro_rules! string {
    ($s:literal) =&gt; {{ String::from($s) }};
}

fn main() {
    let s = string!(&quot;hello&quot;);
    println!(&quot;type of s: {}&quot;, std::any::type_name_of_val(&amp;s));
}
</code></pre>
<p>这里使用美元符号来声明参数，紧跟着的是参数名，冒号后面是参数类型<br />
本文用到的参数类型<sup class="footnote-ref"><a href="#fn-4" id="fnref-4" data-footnote-ref>4</a></sup>有这几种：</p>
<ul>
<li><code>literal</code>: 表示字面量</li>
<li><code>block</code>: 表示代码块，也就是用大括号包裹的一系列语句，值得注意的是，<code>block</code> 本身就是一个表达式</li>
<li><code>expr</code>: 表示表达式，它也包括 <code>block</code></li>
<li><code>ident</code>: 表示 identifier，基本上就是匹配一个单词，但是要遵循变量的命名规则，不能以数字开头，中间的符号也有限制，不过比变量的限制更少，这里可以匹配保留的关键字</li>
<li><code>ty</code>: 表示类型，可以匹配 <code>String</code>, <code>&amp;mut Vec&lt;i32&gt;</code> 之类的各种类型</li>
<li><code>tt</code>: 最通用的匹配，表示一个 token，而 Rust 代码就是由一堆 tokens 组成的</li>
</ul>
<p><code>=&gt;</code> 符号后面的内容会直接在原地展开，如果多加一个大括号，则展开的内容都被大括号包裹起来了，整个分支的内容作为表达式返回；接单大括号，则直接展开这个分支的所有内容。</p>
<p>宏也可以直接匹配字面 token，比如这样：</p>
<pre><code class="language-Rust">// runnable
macro_rules! string {
    // 这个分支返回表达式
    (@expression $s:literal) =&gt; {{ String::from($s) }};
    // 这个分支是语句
    (@statement $var:ident, $s:literal) =&gt; {
        // 就像 shell 语法一样，使用美元符号来引用匹配到的 token
        let $var = String::from($s);
    };
}

fn main() {
    let s1 = string!(@expression &quot;hello&quot;);
    println!(&quot;type of s1: {}&quot;, std::any::type_name_of_val(&amp;s1));
    string!(@statement s2, &quot;hello&quot;);
    println!(&quot;type of s2: {}&quot;, std::any::type_name_of_val(&amp;s2));
}
</code></pre>
<p>原地展开的好处是，可以传入变量名，并在宏里面定义变量，以及宏里面定义的变量和外部是同一作用域<br />
比如这个例子，可以给外部的变量绑定宏内部的变量引用。</p>
<pre><code class="language-Rust">// runnable
macro_rules! string {
    ($var:ident, $s:literal) =&gt; {
        let temp = String::from($s);
        let $var = &amp;temp;
    };
}

fn main() {
    string!(s, &quot;hello&quot;);
    println!(&quot;type of s: {}&quot;, std::any::type_name_of_val(&amp;s));
}
</code></pre>
<p>使用表达式就做不到这点，因为宏内部是单独的块作用域，返回的时候内部的变量就被销毁了。</p>
<pre><code class="language-Rust">// runnable
macro_rules! string {
    ($s:literal) =&gt; {{
        let temp = String::from($s);
        &amp;temp
    }};
}

fn main() {
    let s = string!(&quot;hello&quot;);
    println!(&quot;type of s: {}&quot;, std::any::type_name_of_val(&amp;s));
}
</code></pre>
<p>Rust 的宏具有卫生性<sup class="footnote-ref"><a href="#fn-5" id="fnref-5" data-footnote-ref>5</a></sup> (hygiene)，所以即使是原地展开，和外部共享作用域，也不会污染命名空间。</p>
<pre><code class="language-Rust">// runnable
macro_rules! string {
    ($var:ident, $s:literal) =&gt; {
        let temp = String::from($s);
        let $var = &amp;temp;
    };
}

fn main() {
    string!(s, &quot;hello&quot;);
    println!(&quot;type of s: {}&quot;, std::any::type_name_of_val(&amp;s));
    println!(&quot;{}&quot;, temp);
}
</code></pre>
<h3>重复匹配</h3>
<p>可以重复匹配 token，比如我想要打印东西，可以写这样的宏</p>
<pre><code class="language-Rust">// runnable
macro_rules! myprint {
    ($s:literal, $($args:expr),*) =&gt; {
        print!($s, $($args),*);
    };
}

fn main() {
    let s1 = &quot;hello&quot;;
    let s2 = &quot;world&quot;.to_string();
    myprint!(&quot;{} {}\n&quot;, s1, s2);
    myprint!(&quot;\n&quot;,);
}
</code></pre>
<p><code>$($args:expr),*</code> 可以重复匹配一堆表达式，它们以逗号分隔</p>
<ul>
<li>小括号里是要匹配的模式，可以是任何合法的其他模式，比如字面量 tokens，使用美元符号捕获参数，甚至是另一个重复匹配。在这里是匹配一个表达式</li>
<li>小括号后面紧跟的是分隔符，可以省略，表示没有分隔符，也就是默认的按空格分隔</li>
<li>最后面的 <code>*</code> 表示匹配 0 次或多次，换成 <code>+</code> 则表示匹配 1 次或多次，换成 <code>?</code> 则表示匹配 0 次或 1 次</li>
</ul>
<p>匹配之后使用相同的语法来展开，但不需要写类型<br />
可以写成 <code>$($args),*</code></p>
<ul>
<li>小括号里是要重复输出的内容，此处可以输出字面量，也可以使用小括号内部匹配到的变量，也可以使用外部匹配的变量，但不能使用其他小括号匹配到的变量</li>
<li>小括号后面紧跟的是分隔符，可以省略，表示没有分隔符，也就是默认的按空格分隔，这里的分隔符不必跟匹配时用的一样</li>
<li>最后面的 <code>*</code> 表示重复次数，可以跟之前匹配时使用的不相同，但仍然遵循相同的规则，也就是说，如果前面用 <code>*</code> 匹配到了 1 次，后面用 <code>+</code> 来重复是没问题的；但如果前面用 <code>*</code> 匹配到了 0 次，那就会在后面用 <code>+</code> 的时候报错。</li>
</ul>
<p>我们刚刚的 <code>myprint!</code> 宏有点小问题，就是在没有参数时，没法省略逗号，有多个参数时，尾部也不能多一个逗号。</p>
<pre><code class="language-Rust">// runnable
macro_rules! myprint {
    ($s:literal , $($args:expr),*) =&gt; {
        print!($s, $($args),*);
    };
}

fn main() {
    let s1 = &quot;hello&quot;;
    let s2 = &quot;world&quot;.to_string();
    myprint!(&quot;{} {}\n&quot;, s1, s2,);
    myprint!(&quot;hi\n&quot;);
}
</code></pre>
<p>使用问号来改进一下</p>
<pre><code class="language-Rust">// runnable
macro_rules! myprint {
    ($s:literal $(,$args:expr)* $(,)?) =&gt; {
        print!($s, $($args),*);
    };
}

fn main() {
    let s1 = &quot;hello&quot;;
    let s2 = &quot;world&quot;.to_string();
    myprint!(&quot;{} {}\n&quot;, s1, s2);
    myprint!(&quot;{} {}\n&quot;, s1, s2,);
    myprint!(&quot;hi\n&quot;);
    myprint!(&quot;hi\n&quot;,);
}
</code></pre>
<p>我们刚刚提到了 <code>tt</code>，重复匹配 <code>tt</code>，则相当于是匹配任意的 Rust token tree，这是个非常强力的匹配模式<br />
所以可以把所有传进来的 tokens 直接传给其他的宏</p>
<pre><code class="language-Rust">// runnable
macro_rules! myprint {
    ($($token:tt)*) =&gt; {
        print!($($token)*);
    };
}

fn main() {
    let s1 = &quot;hello&quot;;
    let s2 = &quot;world&quot;.to_string();
    myprint!(&quot;{} {}\n&quot;, s1, s2);
    myprint!(&quot;{} {}\n&quot;, s1, s2,);
    myprint!(&quot;hi\n&quot;);
    myprint!(&quot;hi\n&quot;,);
}
</code></pre>
<p>利用展开时的语法，还可以做些有趣的事，比如重复匹配了一堆参数，不知道有多少个，可以将每个参数展开为 <code>+1</code>，然后在最前面补个 0，就组合成了一个表达式</p>
<pre><code class="language-Rust">// runnable
// 对于多个分支的宏，需要注意顺序，将条件更精确的写在前面
macro_rules! count {
    (@consume $token:tt) =&gt; {
        1
    };
    ($($token:tt)*) =&gt; {
        0 $(+ count!(@consume $token))*
    };
}

macro_rules! myprint {
    ($($token:tt)*) =&gt; {
        let count = count!($($token)*);
        println!(&quot;tokens count: {count}&quot;);
        print!($($token)*);
    };
}

fn main() {
    let s1 = &quot;hello&quot;;
    let s2 = &quot;world&quot;.to_string();
    myprint!(&quot;{} {}\n&quot;, s1, s2);
}
</code></pre>
<p>使用 <code>cargo expand</code><sup class="footnote-ref"><a href="#fn-6" id="fnref-6" data-footnote-ref>6</a></sup> 可以看到宏被展开成了这样：</p>
<pre><code class="language-Rust">fn main() {
    let s1 = &quot;hello&quot;;
    let s2 = &quot;world&quot;.to_string();
    let count = 0 + 1 + 1 + 1 + 1 + 1;
    {
        ::std::io::_print(format_args!(&quot;tokens count: {0}\n&quot;, count));
    };
    {
        ::std::io::_print(format_args!(&quot;{0} {1}\n&quot;, s1, s2));
    };
}
</code></pre>
<p>当然也有其他写法，比如把每个 token 展开为 <code>()</code>，即 unit type，然后放到数组里，并统计这个数组的长度。<br />
unit type 是 ZST (zero-sized type) 类型，不占用任何空间，所以这里无论有多少参数都不会导致栈溢出，这一写法和刚刚的加法表达式在编译后的机器码应该是等价的。</p>
<pre><code class="language-Rust">// runnable
macro_rules! count {
    (@consume $token:tt) =&gt; {
        ()
    };
    ($($token:tt)*) =&gt; {
        [$(count!(@consume $token)),*].len()
    };
}

macro_rules! myprint {
    ($($token:tt)*) =&gt; {
        let count = count!($($token)*);
        println!(&quot;tokens count: {count}&quot;);
        print!($($token)*);
    };
}

fn main() {
    let s1 = &quot;hello&quot;;
    let s2 = &quot;world&quot;.to_string();
    myprint!(&quot;{} {}\n&quot;, s1, s2);
}
</code></pre>
<p>展开后是这样：</p>
<pre><code class="language-Rust">fn main() {
    let s1 = &quot;hello&quot;;
    let s2 = &quot;world&quot;.to_string();
    let count = [(), (), (), (), ()].len();
    {
        ::std::io::_print(format_args!(&quot;tokens count: {0}\n&quot;, count));
    };
    {
        ::std::io::_print(format_args!(&quot;{0} {1}\n&quot;, s1, s2));
    };
}
</code></pre>
<h3>Debug</h3>
<p>我们的宏越来越复杂了，很多时候没法一次写对，好在编译器提供了一些实用的调试工具</p>
<ul>
<li><code>trace_macros!</code><br />
在启用后，会输出宏每次的展开过程<br />
需要启用 <code>trace_macros</code> feature，并使用 nightly 版本的 Rust 编译<br />
这里的在线运行功能好像在编译成功时就不会输出宏的展开过程了，所以我仍然选择使用 stable 版本进行编译</li>
</ul>
<!-- end list -->
<pre><code class="language-Rust">// runnable
#![feature(trace_macros)]

macro_rules! count {
    (@consume $token:tt) =&gt; {
        ()
    };
    ($($token:tt)*) =&gt; {
        [$(count!(@consume $token)),*].len()
    };
}

macro_rules! myprint {
    ($($token:tt)*) =&gt; {
        let count = count!($($token)*);
        println!(&quot;tokens count: {count}&quot;);
        print!($($token)*);
    };
}

fn main() {
    let s1 = &quot;hello&quot;;
    let s2 = &quot;world&quot;.to_string();
    trace_macros!(true);
    myprint!(&quot;{} {}\n&quot;, s1, s2);
    trace_macros!(false);
}
</code></pre>
<ul>
<li><code>log_syntax!($tt)</code><br />
需要启用 <code>log_syntax</code> feature，并使用 nightly 版本的 Rust 编译<br />
它会把输入的 tokens 全部打印出来，比如把 hello world 程序传进去，它会输出所有 tokens</li>
</ul>
<!-- end list -->
<pre><code class="language-Rust">// runnable nightly
#![feature(log_syntax)]

fn main() {
    log_syntax!(
        fn main() {
            println!(&quot;hello world&quot;);
        }
    );
}
</code></pre>
<h3>递归</h3>
<p>结合递归与重复匹配，可以写出来的东西就更有趣了，比如著名的 tt muncher（Token 吞噬者），即递归下降解析 token tree</p>
<pre><code class="language-Rust">// runnable nightly
#![feature(log_syntax)]

macro_rules! tt_muncher{
    // 没有 token 的时候终止
    () =&gt; {};
    // 每次匹配一个 token，剩下的继续传递
    ($token:tt $($rest:tt)*) =&gt; {
        log_syntax!($token);
        tt_muncher!($($rest)*);
    }
}

fn main() {
    tt_muncher!(
        fn main() {
            println!(&quot;hello world&quot;);
        }
    );
}
</code></pre>
<p>tt 会将大/中/小括号作为一个整体，所以上面的代码会把 <code>main</code> 函数的函数体作为整个 token 打印出来<br />
我们可以让它匹配得更细一些</p>
<pre><code class="language-Rust">// runnable
macro_rules! tt_muncher{
    // 终止分支
    () =&gt; {};
    // 匹配大括号里面的内容
    ({$($inner:tt)*} $($rest:tt)*) =&gt; {
        println!(&quot;{{&quot;);
        tt_muncher!($($inner)*);
        println!(&quot;}}&quot;);
        tt_muncher!($($rest)*);
    };
    // 匹配小括号里面的内容
    (($($inner:tt)*) $($rest:tt)*) =&gt; {
        println!(&quot;(&quot;);
        tt_muncher!($($inner)*);
        println!(&quot;)&quot;);
        tt_muncher!($($rest)*);
    };
    // 匹配单个 token
    ($token:tt $($rest:tt)*) =&gt; {
        // stringify 可以将输入的 token 转换为 &amp;'static str
        println!(&quot;{}&quot;, stringify!($token));
        tt_muncher!($($rest)*);
    };
}

fn main() {
    tt_muncher!(
        fn main() {
            println!(&quot;hello world&quot;);
        }
    );
}
</code></pre>
<p>tt muncher 每次吞噬一个 token，并传递剩下的 token，到最后一个分支的时候就没有任何 token 了<br />
如果希望能保留一些 token 呢，注意我们处理的是 tokens，并且是在编译时最早期的阶段处理，所以没法用 Vec 之类的存起来，而 <code>macro_rules</code> 也不支持把 tokens 作为返回值传回去，所以如果希望保留一些中间值，则需要作为参数继续传递下去，每次调用都带上之前的参数，并加上新的内容，我们称之为 Push-down Accumulator （下推累加器）<br />
我们之前提到过，只有大/中/小括号在解析时，会连带内部的内容视为一整个 token，这其实意味着，只有它们可以终止 $($token:tt)* 的匹配，而不会出现歧义。所以在使用累加器的时候，如果搭配 tt 来存储内容，则一般都会用括号把中间的内容包起来</p>
<p>举个解析函数签名的例子：<br />
乍一看像是一堆乱码，其实只是在 tt muncher 的基础上加了累加器</p>
<pre><code class="language-Rust">// runnable
macro_rules! parse_fn{
    // 入口分支，提取出函数名及小括号内部的参数列表
    (fn $name:ident ($($inner:tt)*);) =&gt; {
        // 把函数名继续传下去，`[]` 是存放解析结果的，目前为空，最后把参数列表整个传进去
        // 注意这里在最后加了一个逗号，这是为了方便处理逗号匹配，保证最后一个参数后面也有逗号
        // 这样每次都匹配参数和逗号就行了
        // 如果参数列表尾部已经有逗号了，那多余的逗号会在结果分支被处理
        parse_fn!($name [] $($inner)*,);
    };
    // 传递函数名和解析结果的列表，这里不关心列表内部的内容，直接用 tt 匹配
    // 在列表后面，匹配形如“参数名: 参数类型,” 这样的参数，注意这里会匹配一个逗号
    // 最后传递剩下的 tokens
    ($name:ident [$($tokens:tt)*] $var:ident : $type:ty, $($rest:tt)*) =&gt; {
        // 原样展开匹配到的结果列表，并把参数存储为 (, 名字, 类型) 的形式
        parse_fn!($name [$($tokens)* (, $var, $type)] $($rest)*);
    };
    // 这里负责匹配参数名前面带 mut 的情况
    ($name:ident [$($tokens:tt)*] mut $var:ident : $type:ty, $($rest:tt)*) =&gt; {
        // 把参数存储为 (mut, 名字, 类型) 的形式
        parse_fn!($name [$($tokens)* (mut, $var, $type)] $($rest)*);
    };
    // 结果分支
    // 接收函数名和结果列表
    // 我们之前在列表里存的要么是(, name, type)，要么是(mut, name, type)
    // 所以这里可选匹配第一个逗号前面的内容
    // 最后可选匹配一个逗号，处理参数列表尾部有逗号的情况
    ($name:ident [$(($($mut:ident)?, $v:ident, $t:ty))*] $(,)?) =&gt; {
        println!(&quot;function name: {}&quot;, stringify!($name));
        println!(&quot;parameters:&quot;);
        $(
            print!(&quot;\tname: {}, type: {}&quot;, stringify!($v), stringify!($t));
            let mutable = stringify!($($mut)?);
            if !mutable.is_empty(){
                println!(&quot;, is_mutable: {}&quot;, mutable);
            }else{
                println!();
            }
        )*
    };
}

fn main() {
    parse_fn!(
        fn run(title: String, mut tags: Vec&lt;String&gt;);
    );
}
</code></pre>
<p>使用 <code>trace_macros!</code> 可以看到这样的展开过程：</p>
<pre><code class="language-Rust">// DONT_FORMAT
= note: expanding `parse_fn! { fn run(title: String, mut tags: Vec&lt;String&gt;); }`
= note: to `parse_fn! (run [] title: String, mut tags: Vec&lt;String&gt;,);`
= note: expanding `parse_fn! { run [] title: String, mut tags: Vec&lt;String&gt;, }`
= note: to `parse_fn! (run [(, title, String)] mut tags: Vec&lt;String&gt;,);`
= note: expanding `parse_fn! { run [(, title, String)] mut tags: Vec&lt;String&gt;, }`
= note: to `parse_fn! (run [(, title, String)(mut, tags, Vec&lt;String&gt;)]);`
</code></pre>
<p>通过递归下降和累加器来计算，这让人想起了 Haskell 和函数式编程，值得一提的是，<code>macro_rules!</code> 是图灵完备的，玩法很多，比如已经有人用它写出了 <a href="https://gist.github.com/judofyr/7ed4b52af2107119d4cc1f989ca63201">brainfxxk 的编译器</a>。</p>
<p>当然也可以用它来计算斐波那契数列，不过算不了多少项，n 大了之后会很慢<br />
毕竟 <code>macro_rules</code> 是用来处理 tokens 的，数字也只能表示成 tokens 的形式，编译期计算还是应该用 <code>const fn</code></p>
<pre><code class="language-Rust">// runnable
macro_rules! fib {
    () =&gt; {
        0
    };
    (a) =&gt; {
        1
    };
    (a a $($n:ident)*) =&gt; {
        fib!(a $($n)*) + fib!($($n)*)
    };
}

fn main() {
    println!(&quot;fib 18: {}&quot;, fib!(a a a a a a a a a a a a a a a a a a));
}
</code></pre>
<h2>实现 <code>defer!</code> 宏</h2>
<p>刚刚介绍了一大堆 <code>macro_rules!</code> 的内容，让我们回忆一下 <code>defer!</code> 宏要简化的东西：</p>
<pre><code class="language-Rust">// DONT_FORMAT
let (a, b) = &amp;mut *DropGuard::new((a, b), |(a, b)| {
    println!(&quot;drop `a`&quot;);
    println!(&quot;{:?}&quot;, a);
    println!(&quot;drop b: {b}&quot;);
});
</code></pre>
<p>显然，我们希望通过宏实现少写几次 <code>(a, b)</code></p>
<h3>简单实现</h3>
<pre><code class="language-Rust">// runnable
use std::mem::ManuallyDrop;
use std::ops::{Deref, DerefMut};

impl&lt;F: FnOnce(T), T&gt; Drop for DropGuard&lt;F, T&gt; {
    fn drop(&amp;mut self) {
        let value = unsafe { ManuallyDrop::take(&amp;mut self.inner) };
        let f = unsafe { ManuallyDrop::take(&amp;mut self.f) };
        f(value);
    }
}

impl&lt;F: FnOnce(T), T&gt; Deref for DropGuard&lt;F, T&gt; {
    type Target = T;

    fn deref(&amp;self) -&gt; &amp;Self::Target {
        &amp;self.inner
    }
}

impl&lt;F: FnOnce(T), T&gt; DerefMut for DropGuard&lt;F, T&gt; {
    fn deref_mut(&amp;mut self) -&gt; &amp;mut Self::Target {
        &amp;mut self.inner
    }
}

pub struct DropGuard&lt;F: FnOnce(T), T&gt; {
    f: ManuallyDrop&lt;F&gt;,
    inner: ManuallyDrop&lt;T&gt;,
}

impl&lt;F: FnOnce(T), T&gt; DropGuard&lt;F, T&gt; {
    pub fn new(inner: T, f: F) -&gt; Self {
        Self {
            f: ManuallyDrop::new(f),
            inner: ManuallyDrop::new(inner),
        }
    }
    pub fn into_inner(self) -&gt; T {
        let mut new_guard = ManuallyDrop::new(self);
        let value = unsafe { ManuallyDrop::take(&amp;mut new_guard.inner) };
        unsafe { ManuallyDrop::drop(&amp;mut new_guard.f) };
        value
    }
}

// ANCHOR
macro_rules! defer{
    ([$($vars:tt)*] $body:block) =&gt; {
        #[allow(unused_parens)]
        let mut __guard = $crate::DropGuard::new(
            ($($vars)*),
            |($($vars)*)| $body,
        );

        #[allow(unused_parens)]
        let ($($vars)*) = &amp;mut *__guard;
    };
}

fn main() {
    let a = vec![1, 2];
    let b = &quot;hello&quot;.to_string();
    defer!([a, b] {
        println!(&quot;drop `a`&quot;);
        println!(&quot;{:?}&quot;, a);
        println!(&quot;drop b: {b}&quot;);
    });
    println!(&quot;modify a&quot;);
    a.push(3);
    println!(&quot;print b: {b}&quot;);
}
// ANCHOR_END
</code></pre>
<p>看起来不错，但是有个问题，我们设计成了类似 C++ 的 lambda 表达式的语法，而 rustfmt 根本不认，它会直接放弃格式化宏内部的所有内容。<br />
想让 rustfmt 能够格式化，我们必须得写成合法的 Rust 语法，这里最适合的是伪装成闭包</p>
<h3>伪装成闭包的宏</h3>
<pre><code class="language-Rust">// runnable
macro_rules! defer{
    (|$($vars:tt)*| $body:block) =&gt; {
        #[allow(unused_parens)]
        let mut __guard = $crate::DropGuard::new(
            ($($vars)*),
            |($($vars)*)| $body,
        );

        #[allow(unused_parens)]
        let ($($vars)*) = &amp;mut *__guard;
    };
}

fn main() {
    let a = vec![1, 2];
    let b = &quot;hello&quot;.to_string();
    defer!(|a, b| {
        println!(&quot;drop `a`&quot;);
        println!(&quot;{:?}&quot;, a);
        println!(&quot;drop b: {b}&quot;);
    });
    println!(&quot;modify a&quot;);
    a.push(3);
    println!(&quot;print b: {b}&quot;);
}
// ANCHOR_END
</code></pre>
<p>竟然没法编译！原因是这里产生了歧义，之前我们说过，<code>$($vars:tt)*</code> 是一个非常强力的匹配模式，它会吞噬掉后面的一切 token，直到遇到括号，这也是为什么之前可以编译，因为之前用的是中括号。现在的竖线则会被吞噬进去，所以编译器无法决定这个竖线应该算成重复匹配 tt，还是匹配字面上的竖线。</p>
<p>这就使我们的宏稍微复杂了一些，需要先匹配左竖线，再匹配右竖线。<br />
不妨分成 <code>defer</code> 和 <code>defer_impl</code> 两个宏</p>
<pre><code class="language-Rust">// runnable
use std::mem::ManuallyDrop;
use std::ops::{Deref, DerefMut};

impl&lt;F: FnOnce(T), T&gt; Drop for DropGuard&lt;F, T&gt; {
    fn drop(&amp;mut self) {
        let value = unsafe { ManuallyDrop::take(&amp;mut self.inner) };
        let f = unsafe { ManuallyDrop::take(&amp;mut self.f) };
        f(value);
    }
}

impl&lt;F: FnOnce(T), T&gt; Deref for DropGuard&lt;F, T&gt; {
    type Target = T;

    fn deref(&amp;self) -&gt; &amp;Self::Target {
        &amp;self.inner
    }
}

impl&lt;F: FnOnce(T), T&gt; DerefMut for DropGuard&lt;F, T&gt; {
    fn deref_mut(&amp;mut self) -&gt; &amp;mut Self::Target {
        &amp;mut self.inner
    }
}

pub struct DropGuard&lt;F: FnOnce(T), T&gt; {
    f: ManuallyDrop&lt;F&gt;,
    inner: ManuallyDrop&lt;T&gt;,
}

impl&lt;F: FnOnce(T), T&gt; DropGuard&lt;F, T&gt; {
    pub fn new(inner: T, f: F) -&gt; Self {
        Self {
            f: ManuallyDrop::new(f),
            inner: ManuallyDrop::new(inner),
        }
    }
    pub fn into_inner(self) -&gt; T {
        let mut new_guard = ManuallyDrop::new(self);
        let value = unsafe { ManuallyDrop::take(&amp;mut new_guard.inner) };
        unsafe { ManuallyDrop::drop(&amp;mut new_guard.f) };
        value
    }
}

// ANCHOR
macro_rules! defer_impl{
    ([$($vars:tt)*] $(,)? | $body:block) =&gt; {
        #[allow(unused_parens)]
        let mut __guard = $crate::DropGuard::new(
            ($($vars)*),
            |($($vars)*)| $body,
        );

        #[allow(unused_parens)]
        let ($($vars)*) = &amp;mut *__guard;
    };
    ([$($vars:tt)*] ,$v:tt $($rest:tt)*) =&gt; {
        defer_impl!([$($vars)* $v,] $($rest)*);
    };
}

macro_rules! defer{
    (|$($vars:tt)*) =&gt; {
        defer_impl!([] ,$($vars)*);
    };
}

fn main() {
    let a = vec![1, 2];
    let b = &quot;hello&quot;.to_string();
    defer!(|a, b| {
        println!(&quot;drop `a`&quot;);
        println!(&quot;{:?}&quot;, a);
        println!(&quot;drop b: {b}&quot;);
    });
    println!(&quot;modify a&quot;);
    a.push(3);
    println!(&quot;print b: {b}&quot;);
}
// ANCHOR_END
</code></pre>
<p>值得一提的是这里匹配逗号的方式，每次匹配形如 <code>, tt</code> 这样的内容，这样只需要一开始在最前面加一个逗号，匹配完所有输入后，要么剩下的是一个逗号，要么什么都不剩，结果分支那里不需要额外调整。</p>
<p>如果每次匹配 <code>tt ,</code> 这样的内容，为了处理末尾没有逗号的情况，要么在输入内容的最后面加一个逗号，但这里做不到，一开始输入进来的内容包括了右竖线和 block；要么在结果分支处理最后一次匹配，会更麻烦。</p>
<h3>支持取消功能</h3>
<p>需要给 <code>defer!</code> 宏增加一个分支，并给 <code>defer_impl!</code> 宏的每个分支加上 <code>guard</code> ，以便传递到结果分支</p>
<pre><code class="language-Rust">// runnable
use std::mem::ManuallyDrop;
use std::ops::{Deref, DerefMut};

impl&lt;F: FnOnce(T), T&gt; Drop for DropGuard&lt;F, T&gt; {
    fn drop(&amp;mut self) {
        let value = unsafe { ManuallyDrop::take(&amp;mut self.inner) };
        let f = unsafe { ManuallyDrop::take(&amp;mut self.f) };
        f(value);
    }
}

impl&lt;F: FnOnce(T), T&gt; Deref for DropGuard&lt;F, T&gt; {
    type Target = T;

    fn deref(&amp;self) -&gt; &amp;Self::Target {
        &amp;self.inner
    }
}

impl&lt;F: FnOnce(T), T&gt; DerefMut for DropGuard&lt;F, T&gt; {
    fn deref_mut(&amp;mut self) -&gt; &amp;mut Self::Target {
        &amp;mut self.inner
    }
}

pub struct DropGuard&lt;F: FnOnce(T), T&gt; {
    f: ManuallyDrop&lt;F&gt;,
    inner: ManuallyDrop&lt;T&gt;,
}

impl&lt;F: FnOnce(T), T&gt; DropGuard&lt;F, T&gt; {
    pub fn new(inner: T, f: F) -&gt; Self {
        Self {
            f: ManuallyDrop::new(f),
            inner: ManuallyDrop::new(inner),
        }
    }
    pub fn into_inner(self) -&gt; T {
        let mut new_guard = ManuallyDrop::new(self);
        let value = unsafe { ManuallyDrop::take(&amp;mut new_guard.inner) };
        unsafe { ManuallyDrop::drop(&amp;mut new_guard.f) };
        value
    }
}

// ANCHOR
macro_rules! defer_impl{
    ($guard:ident [$($vars:tt)*] $(,)? | $body:block) =&gt; {
        #[allow(unused_parens)]
        let mut $guard = $crate::DropGuard::new(
            ($($vars)*),
            |($($vars)*)| $body,
        );

        #[allow(unused_parens)]
        let ($($vars)*) = &amp;mut *$guard;
    };
    ($guard:ident [$($vars:tt)*] ,$v:tt $($rest:tt)*) =&gt; {
        defer_impl!($guard [$($vars)* $v,] $($rest)*);
    };
}

macro_rules! defer{
    (|$($vars:tt)*) =&gt; {
        defer_impl!(__guard [] ,$($vars)*);
    };
    ($guard:ident, |$($vars:tt)*) =&gt; {
        defer_impl!($guard [] ,$($vars)*);
    };
}

fn main() {
    let a = vec![1, 2];
    defer!(guard, |a| {
        println!(&quot;drop `a`&quot;);
        println!(&quot;{:?}&quot;, a);
    });
    println!(&quot;modify a&quot;);
    a.push(3);
    guard.into_inner();
}
// ANCHOR_END
</code></pre>
<h3>支持修饰符</h3>
<p>对于上面的例子，无法实现在 <code>defer!</code> 的闭包里修改 <code>a</code>，虽然大部分情况用不上，而且通过重新绑定也能实现，但是既然都伪装成闭包的形状了，干脆支持 <code>mut</code> 关键字吧。<br />
这里宏又变得更复杂了，之前我们只是提取竖线内部的内容，并按原样使用，但现在不一样了，声明闭包的时候需要 <code>mut</code> 关键字，但构造元组和解构元组的时候并不需要，所以我们得存储更多的信息。<br />
这里使用了我们之前解析函数签名时的技巧，把 <code>mut</code> 存成可选的，并用逗号分隔。</p>
<p>顺带一提，之前的实现有个缺点，会把单变量存储为 <code>(var,)</code>（一个单变量元组）， 而不是 <code>(var)</code>（单独的变量，带有多余的括号），虽然对 <code>defer!</code> 宏没什么影响，但是调用 guard 的 <code>into_inner</code> 方法时就很反直觉了，明明只存了一个变量，怎么变成了一个单变量元组呢？<br />
而这个缺点在下面的实现里被解决了，因为逗号不再被存储到列表里，而是在展开的时候作为分隔符加入。</p>
<p>这是我写到这里才想到的，之前我设计成了存储成两个列表，一个用于元组，另一个用于闭包，但处理逗号时很麻烦。如果对那个方案感兴趣，可以看看本文最开始的代码块，点击右上角的第一个按钮就会显示出完整的代码。</p>
<pre><code class="language-Rust">// runnable
use std::mem::ManuallyDrop;
use std::ops::{Deref, DerefMut};

impl&lt;F: FnOnce(T), T&gt; Drop for DropGuard&lt;F, T&gt; {
    fn drop(&amp;mut self) {
        let value = unsafe { ManuallyDrop::take(&amp;mut self.inner) };
        let f = unsafe { ManuallyDrop::take(&amp;mut self.f) };
        f(value);
    }
}

impl&lt;F: FnOnce(T), T&gt; Deref for DropGuard&lt;F, T&gt; {
    type Target = T;

    fn deref(&amp;self) -&gt; &amp;Self::Target {
        &amp;self.inner
    }
}

impl&lt;F: FnOnce(T), T&gt; DerefMut for DropGuard&lt;F, T&gt; {
    fn deref_mut(&amp;mut self) -&gt; &amp;mut Self::Target {
        &amp;mut self.inner
    }
}

pub struct DropGuard&lt;F: FnOnce(T), T&gt; {
    f: ManuallyDrop&lt;F&gt;,
    inner: ManuallyDrop&lt;T&gt;,
}

impl&lt;F: FnOnce(T), T&gt; DropGuard&lt;F, T&gt; {
    pub fn new(inner: T, f: F) -&gt; Self {
        Self {
            f: ManuallyDrop::new(f),
            inner: ManuallyDrop::new(inner),
        }
    }
    pub fn into_inner(self) -&gt; T {
        let mut new_guard = ManuallyDrop::new(self);
        let value = unsafe { ManuallyDrop::take(&amp;mut new_guard.inner) };
        unsafe { ManuallyDrop::drop(&amp;mut new_guard.f) };
        value
    }
}

// ANCHOR
macro_rules! defer_impl{
    ($guard:ident [$($($mut:ident)?, $vars:ident)*] $(,)? | $body:block) =&gt; {
        // 在这里展开时，逗号作为分隔符被加入
        // 所以对于单个变量，会变成 `(var)`，这里的括号是多余的
        // 可以使用下面这个属性宏来避免警告
        #[allow(unused_parens)]
        let mut $guard = $crate::DropGuard::new(
            ($($vars),*),
            |($($($mut)? $vars),*)| $body,
        );

        #[allow(unused_parens)]
        let ($($vars),*) = &amp;mut *$guard;
    };
    // 增加一个匹配 mut 的分支，因为逗号都在这些中间的分支被处理了，所以结果完全一样，
    // 仍然只需要一个结果分支
    ($guard:ident [$($($mut:ident)?, $vars:ident)*] , mut $v:ident $($rest:tt)*) =&gt; {
        defer_impl!($guard [$($vars)* mut, $v] $($rest)*);
    };
    ($guard:ident [$($($mut:ident)?, $vars:ident)*] ,$v:ident $($rest:tt)*) =&gt; {
        defer_impl!($guard [$($vars)* , $v] $($rest)*);
    };
}

macro_rules! defer{
    (|$($vars:tt)*) =&gt; {
        defer_impl!(__guard [] ,$($vars)*);
    };
    ($guard:ident, |$($vars:tt)*) =&gt; {
        defer_impl!($guard [] ,$($vars)*);
    };
}

fn main() {
    let a = vec![1, 2];
    defer!(|mut a| {
        a.push(4);
        println!(&quot;drop `a`&quot;);
        println!(&quot;{:?}&quot;, a);
    });
    println!(&quot;modify a&quot;);
    a.push(3);
}
// ANCHOR_END
</code></pre>
<h3>支持捕获引用</h3>
<p>之前的实现都是捕获变量本身，把所有权捕获到 <code>DropGuard</code> 结构体里。但有时可能不希望移动所有权，只想捕获引用。<br />
目前想要捕获引用，只能自己手动创建</p>
<pre><code class="language-Rust">// runnable
use std::mem::ManuallyDrop;
use std::ops::{Deref, DerefMut};

impl&lt;F: FnOnce(T), T&gt; Drop for DropGuard&lt;F, T&gt; {
    fn drop(&amp;mut self) {
        let value = unsafe { ManuallyDrop::take(&amp;mut self.inner) };
        let f = unsafe { ManuallyDrop::take(&amp;mut self.f) };
        f(value);
    }
}

impl&lt;F: FnOnce(T), T&gt; Deref for DropGuard&lt;F, T&gt; {
    type Target = T;

    fn deref(&amp;self) -&gt; &amp;Self::Target {
        &amp;self.inner
    }
}

impl&lt;F: FnOnce(T), T&gt; DerefMut for DropGuard&lt;F, T&gt; {
    fn deref_mut(&amp;mut self) -&gt; &amp;mut Self::Target {
        &amp;mut self.inner
    }
}

pub struct DropGuard&lt;F: FnOnce(T), T&gt; {
    f: ManuallyDrop&lt;F&gt;,
    inner: ManuallyDrop&lt;T&gt;,
}

impl&lt;F: FnOnce(T), T&gt; DropGuard&lt;F, T&gt; {
    pub fn new(inner: T, f: F) -&gt; Self {
        Self {
            f: ManuallyDrop::new(f),
            inner: ManuallyDrop::new(inner),
        }
    }
    pub fn into_inner(self) -&gt; T {
        let mut new_guard = ManuallyDrop::new(self);
        let value = unsafe { ManuallyDrop::take(&amp;mut new_guard.inner) };
        unsafe { ManuallyDrop::drop(&amp;mut new_guard.f) };
        value
    }
}

macro_rules! defer_impl{
    ($guard:ident [$($($mut:ident)?, $vars:ident)*] $(,)? | $body:block) =&gt; {
        #[allow(unused_parens)]
        let mut $guard = $crate::DropGuard::new(
            ($($vars),*),
            |($($($mut)? $vars),*)| $body,
        );

        #[allow(unused_parens)]
        let ($($vars),*) = &amp;mut *$guard;
    };
    ($guard:ident [$($($mut:ident)?, $vars:ident)*] , mut $v:ident $($rest:tt)*) =&gt; {
        defer_impl!($guard [$($vars)* mut, $v] $($rest)*);
    };
    ($guard:ident [$($($mut:ident)?, $vars:ident)*] ,$v:ident $($rest:tt)*) =&gt; {
        defer_impl!($guard [$($vars)* , $v] $($rest)*);
    };
}

macro_rules! defer{
    (|$($vars:tt)*) =&gt; {
        defer_impl!(__guard [] ,$($vars)*);
    };
    ($guard:ident, |$($vars:tt)*) =&gt; {
        defer_impl!($guard [] ,$($vars)*);
    };
}

// ANCHOR
fn main() {
    let mut a = vec![1, 2];
    let a = &amp;mut a;
    defer!(|a| {
        a.push(4);
        println!(&quot;drop `a`&quot;);
        println!(&quot;{:?}&quot;, a);
    });
    println!(&quot;modify a&quot;);
    a.push(3);
    println!(&quot;{}&quot;, std::any::type_name_of_val(&amp;a));
}
// ANCHOR_END
</code></pre>
<p>这个写法有点麻烦，而且内部重新借用后的 <code>a</code> 的类型变成了 <code>&amp;mut &amp;mut Vec&lt;i32&gt;</code>，这个双重引用完全没必要。<br />
模仿 C++ 的语法，可以给宏加入捕获引用的功能</p>
<pre><code class="language-Rust">// runnable
use std::mem::ManuallyDrop;
use std::ops::{Deref, DerefMut};

impl&lt;F: FnOnce(T), T&gt; Drop for DropGuard&lt;F, T&gt; {
    fn drop(&amp;mut self) {
        let value = unsafe { ManuallyDrop::take(&amp;mut self.inner) };
        let f = unsafe { ManuallyDrop::take(&amp;mut self.f) };
        f(value);
    }
}

impl&lt;F: FnOnce(T), T&gt; Deref for DropGuard&lt;F, T&gt; {
    type Target = T;

    fn deref(&amp;self) -&gt; &amp;Self::Target {
        &amp;self.inner
    }
}

impl&lt;F: FnOnce(T), T&gt; DerefMut for DropGuard&lt;F, T&gt; {
    fn deref_mut(&amp;mut self) -&gt; &amp;mut Self::Target {
        &amp;mut self.inner
    }
}

pub struct DropGuard&lt;F: FnOnce(T), T&gt; {
    f: ManuallyDrop&lt;F&gt;,
    inner: ManuallyDrop&lt;T&gt;,
}

impl&lt;F: FnOnce(T), T&gt; DropGuard&lt;F, T&gt; {
    pub fn new(inner: T, f: F) -&gt; Self {
        Self {
            f: ManuallyDrop::new(f),
            inner: ManuallyDrop::new(inner),
        }
    }
    pub fn into_inner(self) -&gt; T {
        let mut new_guard = ManuallyDrop::new(self);
        let value = unsafe { ManuallyDrop::take(&amp;mut new_guard.inner) };
        unsafe { ManuallyDrop::drop(&amp;mut new_guard.f) };
        value
    }
}

// ANCHOR
macro_rules! defer_impl{
    ($guard:ident [$($($mut:ident)?, $vars:ident)*] $(,)? | $body:block) =&gt; {
        #[allow(unused_parens)]
        let mut $guard = $crate::DropGuard::new(
            ($($vars),*),
            |($($($mut)? $vars),*)| $body,
        );

        #[allow(unused_parens)]
        let ($($vars),*) = &amp;mut *$guard;
    };
    ($guard:ident [$($($mut:ident)?, $vars:ident)*] , mut $v:ident $($rest:tt)*) =&gt; {
        defer_impl!($guard [$($vars)* mut, $v] $($rest)*);
    };
    // 只需要多加一个分支，匹配带 `&amp;` 符号的变量
    ($guard:ident [$($($mut:ident)?, $vars:ident)*] , &amp;$v:ident $($rest:tt)*) =&gt; {
        // 先创建可变引用
        let $v = &amp;mut $v;
        // 把可变引用的所有权交给 guard
        defer_impl!($guard [$($vars)* , $v] $($rest)*);
        // 之后重新借用回来，并且多解引用一次，避免双重引用
        let $v = &amp;mut **$v;
    };
    ($guard:ident [$($($mut:ident)?, $vars:ident)*] ,$v:ident $($rest:tt)*) =&gt; {
        defer_impl!($guard [$($vars)* , $v] $($rest)*);
    };
}

macro_rules! defer{
    (|$($vars:tt)*) =&gt; {
        defer_impl!(__guard [] ,$($vars)*);
    };
    ($guard:ident, |$($vars:tt)*) =&gt; {
        defer_impl!($guard [] ,$($vars)*);
    };
}

fn main() {
    let mut a = vec![1, 2];
    defer!(|&amp;a| {
        a.push(4);
        println!(&quot;drop `a`&quot;);
        println!(&quot;{:?}&quot;, a);
    });
    println!(&quot;modify a&quot;);
    a.push(3);
    println!(&quot;{}&quot;, std::any::type_name_of_val(&amp;a));
}
// ANCHOR_END
</code></pre>
<p>但是这样一来少了给引用命名的功能，多数情况下，捕获引用而非所有权，是为了后续能在 drop 之后返回变量<br />
所以再加一个分支</p>
<pre><code class="language-Rust">// runnable
use std::mem::ManuallyDrop;
use std::ops::{Deref, DerefMut};

impl&lt;F: FnOnce(T), T&gt; Drop for DropGuard&lt;F, T&gt; {
    fn drop(&amp;mut self) {
        let value = unsafe { ManuallyDrop::take(&amp;mut self.inner) };
        let f = unsafe { ManuallyDrop::take(&amp;mut self.f) };
        f(value);
    }
}

impl&lt;F: FnOnce(T), T&gt; Deref for DropGuard&lt;F, T&gt; {
    type Target = T;

    fn deref(&amp;self) -&gt; &amp;Self::Target {
        &amp;self.inner
    }
}

impl&lt;F: FnOnce(T), T&gt; DerefMut for DropGuard&lt;F, T&gt; {
    fn deref_mut(&amp;mut self) -&gt; &amp;mut Self::Target {
        &amp;mut self.inner
    }
}

pub struct DropGuard&lt;F: FnOnce(T), T&gt; {
    f: ManuallyDrop&lt;F&gt;,
    inner: ManuallyDrop&lt;T&gt;,
}

impl&lt;F: FnOnce(T), T&gt; DropGuard&lt;F, T&gt; {
    pub fn new(inner: T, f: F) -&gt; Self {
        Self {
            f: ManuallyDrop::new(f),
            inner: ManuallyDrop::new(inner),
        }
    }
    pub fn into_inner(self) -&gt; T {
        let mut new_guard = ManuallyDrop::new(self);
        let value = unsafe { ManuallyDrop::take(&amp;mut new_guard.inner) };
        unsafe { ManuallyDrop::drop(&amp;mut new_guard.f) };
        value
    }
}

// ANCHOR
macro_rules! defer_impl{
    ($guard:ident [$($($mut:ident)?, $vars:ident)*] $(,)? | $body:block) =&gt; {
        #[allow(unused_parens)]
        let mut $guard = $crate::DropGuard::new(
            ($($vars),*),
            |($($($mut)? $vars),*)| $body,
        );

        #[allow(unused_parens)]
        let ($($vars),*) = &amp;mut *$guard;
    };
    ($guard:ident [$($($mut:ident)?, $vars:ident)*] , mut $v:ident $($rest:tt)*) =&gt; {
        defer_impl!($guard [$($vars)* mut, $v] $($rest)*);
    };
    // 使用 `bind: &amp;var`，类似构建结构体时的语法
    ($guard:ident [$($($mut:ident)?, $vars:ident)*] , $bind:ident: &amp;$var:ident $($rest:tt)*) =&gt; {
        let $bind = &amp;mut $var;
        defer_impl!($guard [$($vars)* , $bind] $($rest)*);
        let $bind = &amp;mut **$bind;
    };
    ($guard:ident [$($($mut:ident)?, $vars:ident)*] , &amp;$v:ident $($rest:tt)*) =&gt; {
        let $v = &amp;mut $v;
        defer_impl!($guard [$($vars)* , $v] $($rest)*);
        let $v = &amp;mut **$v;
    };
    ($guard:ident [$($($mut:ident)?, $vars:ident)*] ,$v:ident $($rest:tt)*) =&gt; {
        defer_impl!($guard [$($vars)* , $v] $($rest)*);
    };
}

macro_rules! defer{
    (|$($vars:tt)*) =&gt; {
        defer_impl!(__guard [] ,$($vars)*);
    };
    ($guard:ident, |$($vars:tt)*) =&gt; {
        defer_impl!($guard [] ,$($vars)*);
    };
}

fn return_owned() -&gt; Vec&lt;i32&gt; {
    let mut a = vec![1, 2];
    defer!(guard, |a_ref: &amp;a| {
        a_ref.push(4);
        println!(&quot;drop `a_ref`&quot;);
        println!(&quot;{:?}&quot;, a_ref);
    });
    println!(&quot;modify a&quot;);
    a_ref.push(3);
    println!(&quot;a_ref: {}&quot;, std::any::type_name_of_val(&amp;a_ref));
    drop(guard);
    a
}

fn main() {
    let a = return_owned();
    println!(&quot;return a: {}&quot;, std::any::type_name_of_val(&amp;a));
}
// ANCHOR_END
</code></pre>
<h3>再加点小功能</h3>
<p>闭包支持使用 <code>move</code> 来表示内部使用的变量都需要被移动进去，虽然我觉得在这里不太用得到，但是这个功能很好加。<br />
另外就是加上不指定变量的两种情况。</p>
<pre><code class="language-Rust">// runnable
use std::mem::ManuallyDrop;
use std::ops::{Deref, DerefMut};

impl&lt;F: FnOnce(T), T&gt; Drop for DropGuard&lt;F, T&gt; {
    fn drop(&amp;mut self) {
        let value = unsafe { ManuallyDrop::take(&amp;mut self.inner) };
        let f = unsafe { ManuallyDrop::take(&amp;mut self.f) };
        f(value);
    }
}

impl&lt;F: FnOnce(T), T&gt; Deref for DropGuard&lt;F, T&gt; {
    type Target = T;

    fn deref(&amp;self) -&gt; &amp;Self::Target {
        &amp;self.inner
    }
}

impl&lt;F: FnOnce(T), T&gt; DerefMut for DropGuard&lt;F, T&gt; {
    fn deref_mut(&amp;mut self) -&gt; &amp;mut Self::Target {
        &amp;mut self.inner
    }
}

pub struct DropGuard&lt;F: FnOnce(T), T&gt; {
    f: ManuallyDrop&lt;F&gt;,
    inner: ManuallyDrop&lt;T&gt;,
}

impl&lt;F: FnOnce(T), T&gt; DropGuard&lt;F, T&gt; {
    pub fn new(inner: T, f: F) -&gt; Self {
        Self {
            f: ManuallyDrop::new(f),
            inner: ManuallyDrop::new(inner),
        }
    }
    pub fn into_inner(self) -&gt; T {
        let mut new_guard = ManuallyDrop::new(self);
        let value = unsafe { ManuallyDrop::take(&amp;mut new_guard.inner) };
        unsafe { ManuallyDrop::drop(&amp;mut new_guard.f) };
        value
    }
}

// ANCHOR
macro_rules! defer_impl{
    // 只需将 move 作为可选参数捕获并传递即可，最后在闭包前面展开
    ($guard:ident $($move:ident)? [$($($mut:ident)?, $vars:ident)*] $(,)? | $body:block) =&gt; {
        #[allow(unused_parens)]
        let mut $guard = $crate::DropGuard::new(
            ($($vars),*),
            $($move)? |($($($mut)? $vars),*)| $body,
        );

        #[allow(unused_parens)]
        let ($($vars),*) = &amp;mut *$guard;
    };
    ($guard:ident $($move:ident)? [$($($mut:ident)?, $vars:ident)*] , mut $v:ident $($rest:tt)*) =&gt; {
        defer_impl!($guard [$($vars)* mut, $v] $($rest)*);
    };
    ($guard:ident $($move:ident)? [$($($mut:ident)?, $vars:ident)*] , $bind:ident: &amp;$var:ident $($rest:tt)*) =&gt; {
        let $bind = &amp;mut $var;
        defer_impl!($guard [$($vars)* , $bind] $($rest)*);
        let $bind = &amp;mut **$bind;
    };
    ($guard:ident $($move:ident)? [$($($mut:ident)?, $vars:ident)*] , &amp;$v:ident $($rest:tt)*) =&gt; {
        let $v = &amp;mut $v;
        defer_impl!($guard [$($vars)* , $v] $($rest)*);
        let $v = &amp;mut **$v;
    };
    ($guard:ident $($move:ident)? [$($($mut:ident)?, $vars:ident)*] ,$v:ident $($rest:tt)*) =&gt; {
        defer_impl!($guard [$($vars)* , $v] $($rest)*);
    };
}

macro_rules! defer{
    // 指定捕获变量，使用内部 guard，不可取消
    (|$($vars:tt)*) =&gt; {
        defer_impl!(__guard [] ,$($vars)*);
    };
    // 指定捕获变量，使用指定的 guard，可取消
    ($guard:ident, $($move:ident)? |$($vars:tt)*) =&gt; {
        defer_impl!($guard $($move:ident)? [] ,$($vars)*);
    };
    // 不指定捕获变量，使用指定的 guard，可取消
    // 这里直接捕获表达式，可以接收闭包，move 等功能都由闭包原生支持
    ($guard:ident, $body:expr) =&gt; {
        let $guard = $crate::DropGuard::new(
            (),
            |_| $body(),
        );
    };
    // 不指定捕获变量，使用内部 guard，不可取消
    // scopeguard 的 defer 宏就只支持这一种情况
    ($body:expr) =&gt; {
        let __guard = $crate::DropGuard::new(
            (),
            |_| $body(),
        );
    };
}

fn main() {
    let mut a = vec![1, 2];
    defer!(guard, move || {
        a.push(3);
        println!(&quot;drop `a`&quot;);
        println!(&quot;{:?}&quot;, a);
        println!(&quot;type of a: {}&quot;, std::any::type_name_of_val(&amp;a));
    });
}
// ANCHOR_END
</code></pre>
<h3>关于可变性的设计</h3>
<p>Rust 里采取的是默认不可变，需要时使用 <code>mut</code> 声明可变这一策略，这个宏也在尽量遵循这个设计。</p>
<p>对于可变性的需求，可以分为在宏内部和在宏之后<br />
这里的可变性，是指对被捕获值的可变性，而不是变量本身的可变性，如捕获 <code>String</code> 类型为可变引用，宏内部使用的是 <code>s: &amp;mut String</code> ，这里可以通过 <code>s</code> 去修改字符串，就称为可变，但 <code>s</code> 这个变量本身不可变（而且似乎也没有可变的需求）</p>
<table>
<thead>
<tr>
<th>方式</th>
<th>在宏内部</th>
<th>在宏之后</th>
</tr>
</thead>
<tbody>
<tr>
<td>捕获为可变引用</td>
<td>可变</td>
<td>可变</td>
</tr>
<tr>
<td>捕获时移动变量所有权</td>
<td>不可变（默认行为）</td>
<td>可变</td>
</tr>
<tr>
<td>捕获时移动变量所有权</td>
<td>可变（通过 <code>mut</code> 来声明）</td>
<td>可变</td>
</tr>
<tr>
<td>使用闭包自带的捕获功能捕获为不可变引用</td>
<td>不可变</td>
<td>不可变</td>
</tr>
<tr>
<td>不支持</td>
<td>可变</td>
<td>不可变</td>
</tr>
</tbody>
</table>
<p>对于捕获引用的情况来说，宏内部和宏之后使用的变量名和类型是一样的，可变性最好保持一样；<br />
对于捕获所有权的情况来说，捕获所有权之后，内部当然可以选择是否需要可变性，但是宏之后的变量是重新绑定的，两者的类型不一致，所以可变性不必相同<br />
理论上来说可以支持最后一种情况，但是相当奇怪，且语法不好设计，就没实现。如果确实有需求，完全可以通过手动的 <code>let</code> 重绑定来实现。</p>
<h2>局限性</h2>
<p>我们在 Rust 中实现了 <code>defer!</code> 宏，简化了语法，覆盖了基础功能，但使用 <code>macro_rules!</code> 能做到的功能有限，比如它需要手动指定需要的变量，不能完全自动捕获；它不像 Go 的 defer，可以直接 return 函数返回值；也不像 Zig 中的 defer，可以在根据出错与否自动取消或执行。使用过程宏或许能实现更多功能，但更复杂的功能终究还是需要编译器来实现，那样会更简单也更高效，希望 defer 能早日进入 Rust 的 RFC</p>
<h3>defer 中的陷阱</h3>
<p>defer 与 RAII 的不同之处在于，它更灵活自由，但仍然遵循着先定义的后释放的原则，所以使用时需要特别小心释放顺序<br />
如果是手动管理 C 里的资源，最好像 RAII 一样，获取后立即声明 defer</p>
<pre><code class="language-Rust">// runnable
use std::mem::ManuallyDrop;
use std::ops::{Deref, DerefMut};

impl&lt;F: FnOnce(T), T&gt; Drop for DropGuard&lt;F, T&gt; {
    fn drop(&amp;mut self) {
        let value = unsafe { ManuallyDrop::take(&amp;mut self.inner) };
        let f = unsafe { ManuallyDrop::take(&amp;mut self.f) };
        f(value);
    }
}

impl&lt;F: FnOnce(T), T&gt; Deref for DropGuard&lt;F, T&gt; {
    type Target = T;

    fn deref(&amp;self) -&gt; &amp;Self::Target {
        &amp;self.inner
    }
}

impl&lt;F: FnOnce(T), T&gt; DerefMut for DropGuard&lt;F, T&gt; {
    fn deref_mut(&amp;mut self) -&gt; &amp;mut Self::Target {
        &amp;mut self.inner
    }
}

pub struct DropGuard&lt;F: FnOnce(T), T&gt; {
    f: ManuallyDrop&lt;F&gt;,
    inner: ManuallyDrop&lt;T&gt;,
}

impl&lt;F: FnOnce(T), T&gt; DropGuard&lt;F, T&gt; {
    pub fn new(inner: T, f: F) -&gt; Self {
        Self {
            f: ManuallyDrop::new(f),
            inner: ManuallyDrop::new(inner),
        }
    }
    pub fn into_inner(self) -&gt; T {
        let mut new_guard = ManuallyDrop::new(self);
        let value = unsafe { ManuallyDrop::take(&amp;mut new_guard.inner) };
        unsafe { ManuallyDrop::drop(&amp;mut new_guard.f) };
        value
    }
}

macro_rules! defer_impl{
    ($guard:ident $($move:ident)? [$($($mut:ident)?, $vars:ident)*] $(,)? | $body:block) =&gt; {
        #[allow(unused_parens)]
        let mut $guard = $crate::DropGuard::new(
            ($($vars),*),
            $($move)? |($($($mut)? $vars),*)| $body,
        );

        #[allow(unused_parens)]
        let ($($vars),*) = &amp;mut *$guard;
    };
    ($guard:ident $($move:ident)? [$($($mut:ident)?, $vars:ident)*] , mut $v:ident $($rest:tt)*) =&gt; {
        defer_impl!($guard [$($vars)* mut, $v] $($rest)*);
    };
    ($guard:ident $($move:ident)? [$($($mut:ident)?, $vars:ident)*] , $bind:ident: &amp;$var:ident $($rest:tt)*) =&gt; {
        let $bind = &amp;mut $var;
        defer_impl!($guard [$($vars)* , $bind] $($rest)*);
        let $bind = &amp;mut **$bind;
    };
    ($guard:ident $($move:ident)? [$($($mut:ident)?, $vars:ident)*] , &amp;$v:ident $($rest:tt)*) =&gt; {
        let $v = &amp;mut $v;
        defer_impl!($guard [$($vars)* , $v] $($rest)*);
        let $v = &amp;mut **$v;
    };
    ($guard:ident $($move:ident)? [$($($mut:ident)?, $vars:ident)*] ,$v:ident $($rest:tt)*) =&gt; {
        defer_impl!($guard [$($vars)* , $v] $($rest)*);
    };
}

macro_rules! defer{
    (|$($vars:tt)*) =&gt; {
        defer_impl!(__guard [] ,$($vars)*);
    };
    ($guard:ident, $($move:ident)? |$($vars:tt)*) =&gt; {
        defer_impl!($guard $($move:ident)? [] ,$($vars)*);
    };
    ($guard:ident, $body:expr) =&gt; {
        let $guard = $crate::DropGuard::new(
            (),
            |_| $body(),
        );
    };
    ($body:expr) =&gt; {
        let __guard = $crate::DropGuard::new(
            (),
            |_| $body(),
        );
    };
}

// ANCHOR
fn correct() {
    let a: *mut i32 = Box::leak(Box::new(1));
    defer!(|| {
        unsafe {
            println!(&quot;drop a: {}&quot;, *a);
            drop(Box::from_raw(a));
        }
    });
    let b = Box::leak(Box::new(a));
    defer!(|| {
        unsafe {
            println!(&quot;drop b: {}&quot;, **b);
            drop(Box::from_raw(b));
        }
    });
}

fn wrong() {
    let a: *mut i32 = Box::leak(Box::new(1));
    let b = Box::leak(Box::new(a));
    defer!(|| {
        unsafe {
            println!(&quot;drop b: {}&quot;, **b);
            drop(Box::from_raw(b));
        }
    });
    defer!(|| {
        unsafe {
            println!(&quot;drop a: {}&quot;, *a);
            drop(Box::from_raw(a));
        }
    });
}

fn main() {
    println!(&quot;correct:&quot;);
    correct();
    println!(&quot;wrong:&quot;);
    wrong();
    println!(&quot;finish&quot;);
}
// ANCHOR_END
</code></pre>
<p>运行程序会发现 <code>wrong</code> 函数里 drop b 的时候发生了 use after free 的经典问题，原因就是先释放了 a，后释放了 b</p>
<h2>最后</h2>
<p>本来还想再多说说各种语言里的 defer，但就这样吧，这篇文章已经很长很长了……</p>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-1">
<p><a href="https://gaultier.github.io/blog/perhaps_rust_needs_defer.html">Perhaps Rust needs &quot;defer&quot;</a><br />
<a href="https://internals.rust-lang.org/t/pre-rfc-defer-statement/16644">Pre-RFC: defer statement</a><br />
<a href="https://internals.rust-lang.org/t/a-defer-discussion/20387">A defer discussion</a><br />
<a href="https://internals.rust-lang.org/t/pre-pre-pre-rfc-implicit-code-control-and-defer-statements/20071">Pre-pre-pre-RFC: implicit code control and defer statements</a> <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
<li id="fn-2">
<p>Rust 的闭包有 3 种类型，<code>Fn</code>, <code>FnMut</code>，<code>FnOnce</code>，这里使用 <code>FnOnce</code>，即只能执行一次，会消耗所有权的闭包，另外两种闭包都可以协变为 <code>FnOnce</code>，所以用在这里通用性最好。 <a href="#fnref-2" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="2" aria-label="Back to reference 2">↩</a></p>
</li>
<li id="fn-3">
<p><code>Box</code> 是 Rust 中的智能指针，相当于 C++ 里的 <code>unique_ptr</code> <a href="#fnref-3" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="3" aria-label="Back to reference 3">↩</a></p>
</li>
<li id="fn-4">
<p>更多类型请参考<a href="https://danielkeep.github.io/tlborm/book/mbe-macro-rules.html#captures">Captures</a> <a href="#fnref-4" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="4" aria-label="Back to reference 4">↩</a></p>
</li>
<li id="fn-5">
<p>可以参考<a href="https://danielkeep.github.io/tlborm/book/mbe-min-hygiene.html">Hygiene</a>这一章中的解释 <a href="#fnref-5" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="5" aria-label="Back to reference 5">↩</a></p>
</li>
<li id="fn-6">
<p>可以通过 crates.io 安装 <a href="https://crates.io/crates/cargo-expand">cargo-expand</a> 包 <a href="#fnref-6" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="6" aria-label="Back to reference 6">↩</a></p>
</li>
</ol>
</section>
]]>
      </description>
      </item>
    
    <item>
      <title>写一个类似doas的程序（一）</title>
      <link>https://lhz07.com/posts/build-my-doas-1</link>
      <guid>https://lhz07.com/posts/build-my-doas-1</guid>
      <pubDate>Thu, 7 May 2026 23:00:06 +0800</pubDate>
      <description
      ><![CDATA[<img src=/static/img/posts/build-my-doas-1/cover.webp alt="Image" />
      <p><em>用 Rust 写一个类似 doas 的程序，并增加更多功能</em></p>
      <br>
      <h2>前言</h2>
<p>在 macOS 上，我一开始用的是系统自带的 sudo，主要是方便，不用自己装。但是 Apple 似乎觉得 insults<sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup> 不文明，不符合他们高端的品牌调性，所以在编译的时候使用 <code>--with-insults=no</code> 选项，把我最喜欢的 insults 功能给去掉了。<br />
为了用上 insults 功能，我只好从 sudo 的仓库自己编译，这下没问题了，用起来很舒适。但是用久了之后，我不想每次都输密码，想用 MacBook 自带的 Touch ID 来认证。如果通过 pam 来验证，系统自带的<code>pam_tid.so</code> 支持这个功能，但可惜的是，Apple 似乎为了安全，做了很严格的限制，只有系统自带的 sudo 能调用 <code>pam_tid.so</code> 进行验证，我自己编译的 sudo 用不了。<br />
所以看起来只能 insults 和 Touch ID 二选一？我不想妥协，于是摆在面前的有两种方案：</p>
<ol>
<li>fork sudo 仓库，在里面调用 Objective-C 的接口，使用 LocalAuthentication 框架进行指纹认证，但后续需要跟随上游更新。</li>
<li>写一个简单的类似 sudo 的程序，可以加入 insults 功能，也可以使用 LocalAuthentication 框架进行指纹认证，后续完全自己维护。</li>
</ol>
<p>sudo 的十几万行代码还是令我望而却步了，所以我决定自己写一个类似 sudo 的程序，然后加入一切我想要的功能。而且，我并不需要完全从头开始，应对各种意想不到的安全漏洞，我可以把 doas 移植过来，然后加点功能就行了。<br />
说实话，我并没有兴趣，也没有精力，rewrite everything in Rust，但我还是决定用 Rust 写一个完全兼容的类似 doas 的程序，而不是在 doas 的基础上修改。原因有两点：</p>
<ol>
<li>opendoas 虽然可以直接在 macOS 上运行，但是不支持 timestamp 功能，也就是说，每次运行 doas 都需要输密码，即使几秒前我才运行过一次，这显然无法接受。</li>
<li>我对 C 没有那么熟悉，看看代码还行，实现几百行的 timestamp 功能，以及后期的维护，我感觉还是用 Rust 更简单。</li>
</ol>
<h2>实现</h2>
<p>虽然 doas 非常简单，但功能也不算少，这里我们只看两个比较有意思的功能，timestamp 和配置文件解析。限于篇幅，timestamp 功能就留给下一篇吧。</p>
<h3>解析配置文件</h3>
<p>不同于 sudo，doas 的配置文件非常简单，每一行都是一条独立的规则，执行命令时，doas 会从上到下尝试匹配所有规则，以匹配到的最后一条规则为准，判断是否允许执行这条命令。如果你还不熟悉 doas 的规则，可以查看官方的 <a href="https://man.openbsd.org/doas.conf.5">man page </a> 学习一下。<br />
在 OpenBSD 的原版 doas，和跨平台的 OpenDoas 里面，配置文件的解析都是由 <code>parse.y</code><sup class="footnote-ref"><a href="#fn-2" id="fnref-2" data-footnote-ref>2</a></sup> 这个文件实现的，它是 yacc 语法文件，可以使用比较简洁的语法描述解析规则，在编译的时候，会通过 yacc 自动生成对应的 C 代码，运行的时候，解析规则的就是这些自动生成的 C 代码，没有额外的依赖。<br />
这对于 C 语言实现的 doas 来说，确实很方便，但是在 Rust 里就很麻烦了，即使在 Rust 里面通过 FFI 调用这些 C 代码，解析出来的也是 C 里的结构，Rust 代码还无法直接用。而且，对于 doas 这种配置文件，Rust 的表达力比 C 强多了。比如在 doas 里面，一条配置，解析出来的结构体是这样的：</p>
<pre><code class="language-C">struct rule {
	int action;
	int options;
	const char *ident;
	const char *target;
	const char *cmd;
	const char **cmdargs;
	const char **envlist;
};
</code></pre>
<p>我个人感觉其实是不太完美的，做事只做了一半，比如环境变量列表，doas 的配置文件里，有多种对环境变量的操作：保留/清除某个环境变量，设置某个环境变量的值，设置某个环境变量的值为另一个环境变量。这在 C 里面表示起来并不简单，而且很容易出错，所以 doas 的做法是，直接把配置文件里的环境变量列表解析为字符串数组，之后运行时匹配规则的时候，再动态解析这些操作。<br />
而在 Rust 里，我们可以这样表示环境变量列表：</p>
<pre><code class="language-Rust">#[derive(Debug)]
pub struct Config {
    action: Action,
    pub options: Options,
    identity: Identity,
    target: Option&lt;String&gt;,
    cmd: Option&lt;Cmd&gt;,
}

#[derive(Debug)]
pub enum Env {
    Keep(String),
    Remove(String),
    Set { key: String, val: Val },
}

#[derive(Debug)]
pub enum Val {
    New(String),
    FromEnv(String),
}

#[derive(Debug, Default)]
pub struct Options {
    pub nopass: bool,
    pub insult: bool,
    pub nolog: bool,
    pub persist: Option&lt;Duration&gt;,
    pub keepenv: bool,
    pub envs: Vec&lt;Env&gt;,
}
</code></pre>
<p>环境变量列表是 Options 的一部分，每个环境变量都是一个 enum，可以清晰地表达“保留/移除/设置”这三种状态。而设置环境变量，值又可以是单纯的字符串，或是来自于某个环境变量。</p>
<h4>简单的实现</h4>
<p>定义好了配置文件的结构，接下来就是解析配置文件，分为两步，词法分析和语法分析，语法分析听起来复杂，其实按部就班挨个解析就行，此处略过不表，我们来仔细看看词法分析。<br />
在这里为了简单起见，我们不处理引号和转译符 (\)，那么主要任务大概是这几点：</p>
<ol>
<li>跳过所有空行</li>
<li>按空格分割单词</li>
<li>遇到 &quot;#&quot; 时，将当前行之后的内容视为注释，全部跳过</li>
<li>遇到大括号时，始终视为一个 token，即使它和后面或前面的内容间没有空格，也不受影响。</li>
<li>记录当前是配置文件的第几行，方便之后语法解析报错的时候能够带上行号。</li>
</ol>
<p>于是我写出来了第一个版本，一次性解析完整个文件，整个文件是一个数组，数组的每个元素也是一个数组，代表着一条配置，里面有多个 Tokens</p>
<blockquote>
<p>我上个月给网站加入了在线运行 Rust 代码的功能，点击代码块右上角的运行按钮试试吧！</p>
</blockquote>
<pre><code class="language-Rust">// runnable
use std::mem;

// ANCHOR
const CONTENT: &amp;str = r#&quot;# set nopass for tedu
permit nopass keepenv tedu as root cmd /usr/sbin/procmap
permit persist lhz# set persist

permit persist setenv {PKG_CACHE PKG_PATH } aja cmd pkg_add&quot;#;

// 这里每个 Token 都记录着自己的行号，因为之后加上转译符后，一条配置可以跨越多行，所以即使在同一条配置里，
// 每个 Token 的物理行号都可能是不同的。
#[derive(Debug)]
pub struct Token(String, usize);

fn tokenizer(content: &amp;str) -&gt; Vec&lt;Vec&lt;Token&gt;&gt; {
    // 暂时存放当前 token
    let mut token = String::new();
    // 暂时存放当前行
    let mut tokens = Vec::new();
    // 最终的结果数组
    let mut lines = Vec::new();
    let mut skipping_comment = false;
    let mut line_count = 1;
    for ch in content.chars() {
        // 跳过后面的所有内容，直到下一行
        if skipping_comment {
            if ch != '\n' {
                continue;
            } else {
                // ch == '\n'
                skipping_comment = false;
            }
        }
        match ch {
            '\n' =&gt; {
                // 遇到了换行符，表示下面就是新的一行了。
                // 比如最简单的规则，permit alice
                // 如果 alice 的后面还有个空格，那这个 token 应该已经被推进去了，当前 token 是空的，则什么都不做。
                // 否则，上一个字符应该是 'e'，当前的 Token 是“alice”，此时 '\n' 起到了分隔的作用，
                // 我们需要把当前的 token 推到当前行的末尾。
                if !token.is_empty() {
                    tokens.push(Token(mem::take(&amp;mut token), line_count));
                }
                // 如果当前行已经有内容了，那就推入 lines。
                if !tokens.is_empty() {
                    lines.push(mem::take(&amp;mut tokens));
                }
                // 因为遇到了换行符，所以物理行数加一
                line_count += 1;
            }
            _ if ch.is_ascii_whitespace() =&gt; {
                // 遇到了空格，如果当前的 token 不为空，那就推到 tokens 里去。
                if !token.is_empty() {
                    tokens.push(Token(mem::take(&amp;mut token), line_count));
                }
                continue;
            }
            '{' | '}' =&gt; {
                // 遇到了大括号，大括号应该视为单独的 token，所以如果当前已经有 token 了，那就把它交给 tokens
                if !token.is_empty() {
                    tokens.push(Token(mem::take(&amp;mut token), line_count));
                }
                // 把大括号本身也作为一个单独的 token 放进 tokens 里面
                token.push(ch);
                tokens.push(Token(mem::take(&amp;mut token), line_count));
                continue;
            }
            // skip comment
            '#' =&gt; {
                // 注释从这里开始，即使当前的 token 或 tokens 不为空，也没关系，它们要么会在遇到下一个换行符的时候被处理，
                // 要么这是个不规范的文件，最后一行没有以换行符结尾，这也没关系，我们在遍历完所有字符之后，会额外检查一次当前是否
                // 有未处理的 token 或 tokens
                skipping_comment = true;
            }
            _ =&gt; {
                token.push(ch);
            }
        }
    }
    // 最后额外检查当前是否有未处理的 token 或 tokens，避免漏掉规则
    if !token.is_empty() {
        tokens.push(Token(mem::take(&amp;mut token), line_count));
    }
    if !tokens.is_empty() {
        lines.push(mem::take(&amp;mut tokens));
    }
    lines
}

fn main() {
    let tokens = tokenizer(CONTENT);
    for token in tokens {
        println!(&quot;{:?}&quot;, token);
    }
}
// ANCHOR_END
</code></pre>
<p>目前来看，这个实现很不错，至少按照 man page 的定义来说，应该能解析任何不带引号和转译符的 doas 配置文件了。但是从效率上来看，能不能更高效一点呢？目前需要一个大的数组来存储这些 tokens ，能不能做成流式解析，把内存空间压缩到单个 token 呢？</p>
<h4>流式解析器</h4>
<p>当然可以，使用生成器<sup class="footnote-ref"><a href="#fn-3" id="fnref-3" data-footnote-ref>3</a></sup>就可以做到这一点。<br />
于是可以得到第二个版本，和第一个版本的差别很小。主要差别就两点：</p>
<ol>
<li>推入 <code>Vec</code> 的操作变成了 <code>yield</code>，为了区分是当前这条规则的 token，还是要换到下一条，我加入了 <code>State</code>，每次切换到下一条规则的时候，就返回一次 <code>NewLine</code>，表示当前规则结束了。</li>
<li>虽然我们不再使用 <code>Vec</code> 了，但仍然需要记录当前这一行是否有内容，这样才能决定是否要返回 <code>NewLine</code> 表示当前这条规则结束。空白行或者注释行应该直接跳过，不应该返回 <code>NewLine</code> 。所以使用 <code>token_empty</code> 变量来记录当前行是否有内容，它的作用相当于之前的 <code>tokens.is_empty()</code></li>
</ol>
<p>但是从性能上来说，差距就大多了，这个版本完全不需要额外的数组空间，编译器会把它编译成无栈协程，开销就跟手写迭代器差不多，甚至由于是编译器自己生成的状态机，优化更好，可能速度更快。</p>
<pre><code class="language-Rust">// runnable nightly
#![feature(gen_blocks)]
use std::mem;

const CONTENT: &amp;str = r#&quot;# set nopass for tedu
permit nopass keepenv tedu as root cmd /usr/sbin/procmap
permit persist lhz# set persist

permit persist setenv {PKG_CACHE PKG_PATH } aja cmd pkg_add&quot;#;

// ANCHOR
#[derive(Debug)]
pub enum State {
    Token(String, usize),
    NewLine(usize),
}

gen fn tokenizer(content: &amp;str) -&gt; State {
    let mut token = String::new();
    let mut skipping_comment = false;
    let mut token_empty = true;
    let mut line_count = 1;
    for ch in content.chars() {
        if skipping_comment {
            if ch != '\n' {
                continue;
            } else {
                // ch == '\n'
                skipping_comment = false;
            }
        }
        match ch {
            '\n' =&gt; {
                if !token.is_empty() {
                    token_empty = false;
                    yield State::Token(mem::take(&amp;mut token), line_count);
                }
                if !token_empty {
                    token_empty = true;
                    yield State::NewLine(line_count);
                }
                line_count += 1;
            }
            _ if ch.is_ascii_whitespace() =&gt; {
                if !token.is_empty() {
                    token_empty = false;
                    yield State::Token(mem::take(&amp;mut token), line_count);
                }
                continue;
            }
            '{' | '}' =&gt; {
                if !token.is_empty() {
                    token_empty = false;
                    yield State::Token(mem::take(&amp;mut token), line_count);
                }
                token.push(ch);
                yield State::Token(mem::take(&amp;mut token), line_count);
                continue;
            }
            // skip comment
            '#' =&gt; {
                skipping_comment = true;
            }
            _ =&gt; {
                token.push(ch);
            }
        }
    }
    if !token.is_empty() {
        token_empty = false;
        yield State::Token(mem::take(&amp;mut token), line_count);
    }
    if !token_empty {
        yield State::NewLine(line_count);
    }
}
// ANCHOR_END

fn main() {
    let tokens = tokenizer(CONTENT);
    for token in tokens {
        println!(&quot;{:?}&quot;, token);
    }
}
</code></pre>
<p>但是 gen blocks 目前仍然是 Rust 的不稳定特性，能不能在 stable 版本下实现类似的功能呢？</p>
<h4>手写生成器</h4>
<p>完全可以，因为我们的逻辑比较简单，只需要实现一个简单的状态机就行了。</p>
<pre><code class="language-Rust">// runnable
use std::mem;
use std::str::Chars;

const CONTENT: &amp;str = r#&quot;# set nopass for tedu
permit nopass keepenv tedu as root cmd /usr/sbin/procmap
permit persist lhz# set persist

permit persist setenv {PKG_CACHE PKG_PATH } aja cmd pkg_add&quot;#;

#[derive(Debug)]
pub enum State {
    Token(String, usize),
    NewLine(usize),
}

// ANCHOR
pub struct Tokenizer&lt;'a&gt; {
    content: Chars&lt;'a&gt;,
    token: String,
    skipping_comment: bool,
    token_empty: bool,
    line_count: usize,
    location: Option&lt;Location&gt;,
}

enum Location {
    TokenEmptyLineCount,
    LineCount,
    ReturnBrace(u8),
}

impl Iterator for Tokenizer&lt;'_&gt; {
    type Item = State;
    fn next(&amp;mut self) -&gt; Option&lt;Self::Item&gt; {
        self.next_impl()
    }
}

impl&lt;'a&gt; Tokenizer&lt;'a&gt; {
    pub fn new(content: &amp;'a str) -&gt; Self {
        Self {
            content: content.chars(),
            token: String::new(),
            skipping_comment: false,
            token_empty: true,
            line_count: 1,
            location: None,
        }
    }

    fn next_impl(&amp;mut self) -&gt; Option&lt;State&gt; {
        // 如果上次调用的时候，记录了这次要做什么，那就先把要做的事做了，之后再进入循环。
        if let Some(location) = self.location.take() {
            match location {
                Location::TokenEmptyLineCount =&gt; {
                    if !self.token_empty {
                        self.token_empty = true;
                        self.location = Some(Location::LineCount);
                        return Some(State::NewLine(self.line_count));
                    }
                }
                Location::LineCount =&gt; self.line_count += 1,
                Location::ReturnBrace(ch) =&gt; {
                    self.token.push(ch as char);
                    return Some(State::Token(mem::take(&amp;mut self.token), self.line_count));
                }
            }
        }
        for ch in &amp;mut self.content {
            if self.skipping_comment {
                if ch != '\n' {
                    continue;
                } else {
                    // ch == '\n'
                    self.skipping_comment = false;
                }
            }
            match ch {
                '\n' =&gt; {
                    if !self.token.is_empty() {
                        self.token_empty = false;
                        // 如果这是在 C 里面，也许可以记录下标签，下次直接 goto 到特定位置。但这是 Rust，只能记录下次要做什么
                        self.location = Some(Location::TokenEmptyLineCount);
                        return Some(State::Token(mem::take(&amp;mut self.token), self.line_count));
                    }
                    if !self.token_empty {
                        self.token_empty = true;
                        // 其实这里也可以不要 `LineCount` 分支的，先加一，再返回旧的值就行了。但是这么实现的好处是，返回之后，状态是一致的，
                        // 既可以使用返回的 line_count，也可以直接从结构体取 line_count，值都是一样的。
                        self.location = Some(Location::LineCount);
                        return Some(State::NewLine(self.line_count));
                    }
                    self.line_count += 1;
                }
                _ if ch.is_ascii_whitespace() =&gt; {
                    if !self.token.is_empty() {
                        self.token_empty = false;
                        self.location = None;
                        return Some(State::Token(mem::take(&amp;mut self.token), self.line_count));
                    }
                    continue;
                }
                '{' | '}' =&gt; {
                    if !self.token.is_empty() {
                        self.token_empty = false;
                        self.location = Some(Location::ReturnBrace(ch as u8));
                        return Some(State::Token(mem::take(&amp;mut self.token), self.line_count));
                    }
                    self.token.push(ch);
                    return Some(State::Token(mem::take(&amp;mut self.token), self.line_count));
                }
                // skip comment
                '#' =&gt; {
                    self.skipping_comment = true;
                }
                _ =&gt; {
                    self.token.push(ch);
                }
            }
        }
        if !self.token.is_empty() {
            self.token_empty = false;
            self.location = Some(Location::TokenEmptyLineCount);
            return Some(State::Token(mem::take(&amp;mut self.token), self.line_count));
        }
        if !self.token_empty {
            self.token_empty = true;
            // 如果代码运行到这里，证明最后一行缺少换行符，这已经是最后一行了，所以之后不需要再增加计数，也就不需要设置 location
            return Some(State::NewLine(self.line_count));
        }
        None
    }
}
// ANCHOR_END

fn main() {
    let tokenizer = Tokenizer::new(CONTENT);
    for token in tokenizer {
        println!(&quot;{:?}&quot;, token);
    }
}
</code></pre>
<p>状态机示意图<br />
<img src="/static/img/posts/build-my-doas-1/call next_impl.webp" alt="Image" width="1271" /></p>
<h2>最后</h2>
<p>以上就是本篇文章的全部内容了，用三种方式实现了完全相同的词法解析器，感觉还是挺有意思的。</p>
<p>虽然这系列的文章还没有写完，但我的 doas 已经写好了，并且已经使用了一段时间，可以去我的 <a href="https://github.com/lhz07/authentication">GitHub 仓库</a> 看看。目前还是 macOS only 的，因为我做它的初衷就是想在 macOS 上同时用上 insults 和 Touch ID，如果是在 Linux 上的话，已经有 sudo, sudo-rs, opendoas 可以用了，选择很多。</p>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-1">
<p>就是在你输错密码的时候会狠狠骂你，像这样：<br />
<img src="/static/img/posts/build-my-doas-1/insult.webp" alt="Image" width="500" /> <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
<li id="fn-2">
<p>OpenDoas: <a href="https://github.com/Duncaen/OpenDoas/blob/b96106b7e34ac591ae78b1684e9be3a265122463/parse.y">parse.y</a>. OpenBSD doas: <a href="https://github.com/openbsd/src/blob/f3dea0ff1fe6d5ef930cdf5d955f0e57d19f2632/usr.bin/doas/parse.y">parse.y</a> <a href="#fnref-2" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="2" aria-label="Back to reference 2">↩</a></p>
</li>
<li id="fn-3">
<p>目前 Rust 的生成器是 nightly 版本的 feature，详见 <a href="https://github.com/rust-lang/rfcs/pull/3513">RFC 3513</a> ，需要启用生成器的 feature，并使用 nightly 版本的 Rust 编译。 <a href="#fnref-3" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="3" aria-label="Back to reference 3">↩</a></p>
</li>
</ol>
</section>
]]>
      </description>
      </item>
    
    <item>
      <title>给博客搭建搜索系统</title>
      <link>https://lhz07.com/posts/build-my-search-engine</link>
      <guid>https://lhz07.com/posts/build-my-search-engine</guid>
      <pubDate>Sun, 29 Mar 2026 18:43:34 +0800</pubDate>
      <description
      ><![CDATA[<img src=/static/img/posts/build-my-search-engine/cover.webp alt="Image" />
      <p><em>探索如何利用 Tantivy 库为博客构建高效的全文搜索系统。顺带介绍一些信息检索与 NLP 的知识。</em></p>
      <br>
      <h2>核心思路</h2>
<p>搜索系统是一个比较复杂的东西，大概分为以下几点：</p>
<ol>
<li>分词：包括中文分词与英文分词</li>
<li>倒排索引：建立 token -&gt; posting list 的映射</li>
<li>模糊匹配：包括编辑距离，n-gram 相似度</li>
<li>查询解析与组合：将用户查询拆分为 tokens，再使用布尔查询、加权查询、短语查询等，组合后进行查询</li>
<li>生成摘要，其中高亮显示关键词</li>
</ol>
<h2>分词</h2>
<h3>英文分词</h3>
<p>英文分词相对简单，基本上分为3步</p>
<h4>按空格分割，并去除标点符号 (Punctuation)</h4>
<img src="/static/img/posts/build-my-search-engine/arch-btw.webp" alt="Image" width="310" />
<h4>小写化 (Lowercasing)</h4>
<img src="/static/img/posts/build-my-search-engine/lowercase.webp" alt="Image" width="200" />
<h4>词根化 (Stemming)</h4>
<p>英文里同一个词可能有进行时、过去式等，需要统一成它们的词根</p>
<img src="/static/img/posts/build-my-search-engine/stem.webp" alt="Image" width="200" />
<h3>中文分词</h3>
<p>中文分词就复杂多了，但是好在有现成的库可以用，我选择了 <code>jieba-rs</code> ，它是 jieba 中文分词的 Rust 实现。可以很好地把一句话切分成 tokens，如：</p>
<pre><code class="language-Rust">fn main() {
    // ANCHOR
    let jieba = jieba_rs::Jieba::new();
    let sentence = &quot;我们中出了一个叛徒&quot;;
    let words = jieba.cut(sentence, false);
    assert_eq!(words, [&quot;我们&quot;, &quot;中&quot;, &quot;出&quot;, &quot;了&quot;, &quot;一个&quot;, &quot;叛徒&quot;])
    // ANCHOR_END
}
</code></pre>
<p>如果中文里面夹杂英文，jieba 会顺便把英文按空格和标点符号分割输出：</p>
<pre><code class="language-Rust">fn main() {
    // ANCHOR
    let jieba = jieba_rs::Jieba::new();
    let sentence = &quot;btw, 我用的是Arch Linux系统&quot;;
    let words = jieba.cut(sentence, false);
    assert_eq!(
        words,
        [
            &quot;btw&quot;, &quot;,&quot;, &quot; &quot;, &quot;我&quot;, &quot;用&quot;, &quot;的&quot;, &quot;是&quot;, &quot;Arch&quot;, &quot; &quot;, &quot;Linux&quot;, &quot;系统&quot;
        ]
    )
    // ANCHOR_END
}
</code></pre>
<p>这样我们就无需使用两个 tokenizer 了，对中英文分别使用 tokenizer，再分别搜索的话，效果确实很好，但是后面生成摘要和高亮的时候就麻烦了，中英文 tokenizer 生成的摘要内容和高亮位置会有差异，很难合并，而且处理中英混合的搜索词时也很复杂。毕竟我的博客文章以中文为主，对英文搜索没什么需求，在这方面浅尝辄止，最后还是选择统一使用 jieba 分词。</p>
<h3>停用词 (Stop Words)</h3>
<p>在中英文中，有些字词使用极其广泛，却没什么实际含义（如“的”&quot;the&quot;），或者有实际含义，但是因为使用过于广泛，在大量的搜索结果中出现，使这些结果的评分都很高，无法帮助缩小搜索范围，这类字词被称为停用词。为了提高搜索的准确性和效率，一般会把这些词从 tokens 中移除。<br />
tantivy 使用的英文停用词，和 Lucene 是一样的，来自<a href="https://github.com/apache/lucene/blob/d5d6dc079395c47cd6d12dcce3bcfdd2c7d9dc63/lucene/analysis/common/src/java/org/apache/lucene/analysis/en/EnglishAnalyzer.java#L46">这里</a> ，大部分都是虚词。</p>
<pre><code class="language-Rust">const STOP_WORDS: &amp;str = [
    &quot;a&quot;, &quot;an&quot;, &quot;and&quot;, &quot;are&quot;, &quot;as&quot;, &quot;at&quot;, &quot;be&quot;, &quot;but&quot;, &quot;by&quot;, &quot;for&quot;, &quot;if&quot;, &quot;in&quot;, &quot;into&quot;, &quot;is&quot;, &quot;it&quot;,
    &quot;no&quot;, &quot;not&quot;, &quot;of&quot;, &quot;on&quot;, &quot;or&quot;, &quot;such&quot;, &quot;that&quot;, &quot;the&quot;, &quot;their&quot;, &quot;then&quot;, &quot;there&quot;, &quot;these&quot;,
    &quot;they&quot;, &quot;this&quot;, &quot;to&quot;, &quot;was&quot;, &quot;will&quot;, &quot;with&quot;,
];
</code></pre>
<p>而中文停用词，我在网上找了一番，最后决定用的是<a href="https://github.com/goto456/stopwords/blob/bf8b03b9d3709222804ae89578156d1a0d8bf2b2/cn_stopwords.txt">这个</a></p>
<h3>实现</h3>
<p>得益于 tantivy 这一强大框架，实现起来相当简单<br />
此处的代码仅供参考，实际使用的源码都在 <a href="https://github.com/lhz07/my_blog/tree/master/src/search_utils">my_blog</a> 项目里的 search_utils 模块下。</p>
<pre><code class="language-Rust">fn cut() {
    let mut jieba_analyzer =
    // 这里的 `JiebaMode::CutAll` 是我给 tantivy 写的 jieba wrapper 里面加的，
    // 内部就是调用 jieba 的 cutall 模式，也就是列出所有可能的分词，这样我们的索引会更丰富
	TextAnalyzer::builder(jieba::JiebaTokenizer::with_mode(jieba::JiebaMode::CutAll))
		// 分词后，移除过长的token
		.filter(RemoveLongFilter::limit(40))
		// 词根化
		.filter(Stemmer::new(tantivy::tokenizer::Language::English))
		// 英语停用词
		.filter(StopWordFilter::new(tantivy::tokenizer::Language::English).unwrap())
		// 小写化
		.filter(LowerCaser)
		.build();
    let mut token_stream = jieba_analyzer.token_stream(query_text);
    let mut tokens = Vec::new();
    while let Some(token) = token_stream.next() {
        // 去除空格token
        if !token.text.trim().is_empty()
		// 只保留中英文字符
		&amp;&amp; is_cjk_or_en(&amp;token.text)
		// 移除中文停用词
		&amp;&amp; !is_stop_word(&amp;token.text)
        {
            tokens.push(token.text.to_string());
        }
    }
}
</code></pre>
<h2>倒排索引 (Inverted index)</h2>
<p>既然已经分好了词，那就可以开始建立索引了。<br />
倒排索引的本质就是一个文本--&gt;文本位置的映射，和前向索引 (forward index) 相反。<br />
前向索引一般是这样：</p>
<table>
<thead>
<tr>
<th>Document</th>
<th>Tokens</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>arch, linux, is, open, source</td>
</tr>
<tr>
<td>2</td>
<td>it, is, arch, linux</td>
</tr>
<tr>
<td>3</td>
<td>what, is, it</td>
</tr>
</tbody>
</table>
<p>而倒排索引则是反过来，并且会按 token 分割</p>
<table>
<thead>
<tr>
<th>Tokens</th>
<th>Document</th>
</tr>
</thead>
<tbody>
<tr>
<td>arch</td>
<td>1, 2</td>
</tr>
<tr>
<td>linux</td>
<td>1, 2</td>
</tr>
<tr>
<td>is</td>
<td>1, 2, 3</td>
</tr>
<tr>
<td>open</td>
<td>1</td>
</tr>
<tr>
<td>source</td>
<td>1</td>
</tr>
<tr>
<td>it</td>
<td>2, 3</td>
</tr>
<tr>
<td>what</td>
<td>3</td>
</tr>
</tbody>
</table>
<p>之后假如用户输入了 arch linux，那么我们可以按照取交集的方式，得出包含关键字的文档是 1 和 2<br />
当然，具体实现更加复杂，不仅要记录 token 在哪个文档，还要记录具体的位置，并进行速度与存储占用的优化等等。</p>
<h3>文本预处理</h3>
<p>我的博客文章都是用 markdown 格式写的，对于分词来说，符号没什么影响，但是生成摘要的时候就会出现一堆 “##” 和 “**”，看起来十分难受。所以这个阶段主要是为了搜索后结果的可读性服务，并不会明显影响搜索的效果。<br />
首先，使用 <code>comrak</code> 这个 crate 生成语法树，然后遍历它，并输出纯文本。</p>
<pre><code class="language-Rust">/// Recursively walk the AST and collect only plain text.
fn render_plain&lt;'a&gt;(node: &amp;'a AstNode&lt;'a&gt;, output: &amp;mut String) {
    for child in node.children() {
        match &amp;child.data.borrow().value {
            NodeValue::Text(t) =&gt; output.push_str(t),
            NodeValue::Code(t) =&gt; output.push_str(&amp;t.literal),
            NodeValue::LineBreak | NodeValue::SoftBreak =&gt; output.push('\n'),
            NodeValue::Paragraph
            | NodeValue::Heading(_)
            | NodeValue::Item(_)
            | NodeValue::BlockQuote
            | NodeValue::List(_)
            | NodeValue::Table(_)
            | NodeValue::TableRow(_)
            | NodeValue::TableCell
            | NodeValue::FootnoteDefinition(_) =&gt; {
                render_plain(child, output);
                output.push('\n');
            }
            NodeValue::CodeBlock(block) =&gt; {
                output.push_str(&amp;block.literal);
                output.push('\n');
            }
            NodeValue::Link(_)
            | NodeValue::Image(_)
            | NodeValue::Emph
            | NodeValue::Strong
            | NodeValue::Strikethrough
            | NodeValue::Superscript
            | NodeValue::Subscript =&gt; {
                // Just render the children, ignore formatting/URLs
                render_plain(child, output);
            }
            _ =&gt; render_plain(child, output),
        }
    }
}
</code></pre>
<p>在这之后，输出的文本还不是特别美观，所以再用三个正则表达式处理一下。</p>
<pre><code class="language-Rust">use regex::Regex;
use std::sync::LazyLock;

// ANCHOR
pub fn preprocess_text(text: &amp;str) -&gt; String {
    // 这里的 \p{Han} 是指匹配所有汉字，包括简中，繁中，日韩文中的汉字
    static RE1: LazyLock&lt;Regex&gt; = LazyLock::new(|| Regex::new(r&quot;([a-zA-Z])(\p{Han})&quot;).unwrap());
    static RE2: LazyLock&lt;Regex&gt; = LazyLock::new(|| Regex::new(r&quot;(\p{Han})([a-zA-Z])&quot;).unwrap());
    static RE3: LazyLock&lt;Regex&gt; = LazyLock::new(|| Regex::new(r&quot;\s+&quot;).unwrap());
    // 把所有挨在一起的英文和汉字分开，比如“rust语言” -&gt; “rust 语言”
    let iter1 = RE1.replace_all(text, &quot;$1 $2&quot;);
    // 把所有挨在一起的汉字和英文分开，比如“使用regex” -&gt; “使用 regex”
    let iter2 = RE2.replace_all(&amp;iter1, &quot;$1 $2&quot;);
    // 把所有连续的空白（包括空格、换行符等）合并为一个空格
    RE3.replace_all(&amp;iter2, &quot; &quot;).to_string()
}
// ANCHOR_END
</code></pre>
<p>处理过后的文本大致长这样</p>
<img src="/static/img/posts/build-my-search-engine/plain-text.webp" alt="Image" width="700" />
<h3>建立索引</h3>
<p>我在此省略掉了一些和构建索引无关的代码，因此这段代码仅供示意流程，实际上无法直接运行。<br />
这里我们将内容存储到了不同的 field 里，这是为了后续搜索时更方便。比如文章内容和标题是分开存储的，这样查询的时候，可以区分匹配标题和内容，若关键词匹配标题则可以获得更高的评分。文件的实际路径也存储到单独的位置，它和搜索的内容无关。文章的 tags 则存储到 facet 里面，之后可以根据 facet 来匹配，如果查询已经选定了某些 tags，那么不含这些 tags 的文章可以直接被过滤掉。</p>
<pre><code class="language-Rust">pub fn build_index() -&gt; Result&lt;(), CatError&gt; {
    let mut schema_builder = Schema::builder();
    // prepare indexing options per-field with tokenizer name
    let zh_indexing = TextFieldIndexing::default()
        .set_tokenizer(&quot;jieba&quot;)
        .set_index_option(IndexRecordOption::WithFreqsAndPositions);
    let text_options_zh = TextOptions::default()
        .set_indexing_options(zh_indexing)
        .set_stored();
    let content_zh = schema_builder.add_text_field(&quot;content_zh&quot;, text_options_zh.clone());
    let title_field = schema_builder.add_text_field(&quot;title&quot;, text_options_zh);
    let tag_facet = schema_builder.add_facet_field(&quot;tags&quot;, FacetOptions::default());
    let path_field = schema_builder.add_text_field(&quot;path&quot;, STORED);
    let schema = schema_builder.build();
    let index = Index::create_in_dir(index_path, schema.clone())?;
    // Register jieba tokenizer for Chinese
    index.tokenizers().register(&quot;jieba&quot;, JIEBA_ANALYZER.clone());
    let mut writer = index.writer(50_000_000)?;
    for content in contents {
        let mut doc = TantivyDocument::default();
        for tag in tags.iter() {
            let facet = Facet::from(&amp;format!(&quot;/{}&quot;, tag.to_lowercase()));
            doc.add_facet(tag_facet, facet);
        }
        // 在不同的位置存储不同的文本
        doc.add_text(content_zh, &amp;text);
        doc.add_text(title_field, &amp;title);
        doc.add_text(path_field, &amp;file_name);
        writer.add_document(doc)?;
    }
    writer.commit()?;
    Ok(())
}
</code></pre>
<h2>模糊匹配</h2>
<p>我们简单介绍一下模糊匹配，它有助于提高搜索结果的召回率，也就是能搜到的东西更多，但是会降低搜索的精度，也就是更容易搜到不相关的东西。</p>
<h3>编辑距离</h3>
<p>什么是编辑距离？顾名思义，就是将一个词更改为另一个词，所需要的最少编辑操作次数。编辑距离是一个泛化的概念，在搜索中一般用的是 Levenshtein distance。Levenshtein distance 允许的编辑操作包括：</p>
<table>
<thead>
<tr>
<th>操作</th>
<th>示例</th>
</tr>
</thead>
<tbody>
<tr>
<td>将一个字符替换为另一个字符</td>
<td>deah -&gt; dear</td>
</tr>
<tr>
<td>插入一个字符</td>
<td>grat -&gt; great</td>
</tr>
<tr>
<td>删除一个字符</td>
<td>suggesstion -&gt; suggestion</td>
</tr>
</tbody>
</table>
<p>每进行一次上述的任意操作，编辑距离+1</p>
<p>当然，也有一些其他的编辑距离定义，比如，允许交换字符 (dera -&gt; dear)，或者，给不同的操作进行加权，比如替换操作，实际上是先删除，后插入，所以编辑距离为2，而交换操作，实际上是两次删除+插入，所以编辑距离为4.</p>
<h3>n-gram</h3>
<p>n-gram 是指文本中连续出现的 <strong>n</strong> 个单元（可以是字、词或字母）组成的序列。这里的 &quot;n&quot; 代表片段的长度。<br />
比如当 n = 2 时，称之为 bigram，&quot;Apple&quot; 会被切成 &quot;Ap&quot;&quot;pp&quot;&quot;pl&quot;&quot;le&quot;，而 &quot;Aple&quot; 会被切成 &quot;Ap&quot;&quot;pl&quot;&quot;le&quot;，这两个词的 bigram 非常相似，所以即使用户输入 &quot;Aple&quot;，依旧能查询到 &quot;Apple&quot; 相关的信息。<br />
n-gram 算法中，切分的 token 是有大量重合部分的，所以 n 越大，空间占用就会越大，计算速度也会更慢，但是保留的信息更多，匹配更精准。n 比较小的话，计算速度会很快，但是保留的信息很少，比如 n = 1 时，很容易匹配到大量无关信息。</p>
<h3>实际上...</h3>
<p>模糊匹配是我在搭建搜索系统的过程中尝试过的技术，但是发现不太符合需求。比如，将允许的编辑距离设为 1，看起来好像很小，但当关键词很短的时候，会匹配到大量不相关的结果，比如，&quot;git&quot; -&gt; &quot;get&quot;, &quot;rust&quot; -&gt; &quot;rush&quot;, &quot;apple&quot; -&gt; &quot;apply&quot;.<br />
而 n-gram 也是类似，不相关结果太多，我希望我的搜索引擎还是能尽量准确些，所以没有用这些模糊匹配的技术。</p>
<h2>查询解析与组合</h2>
<h3>处理用户输入</h3>
<p>比如用户输入了：“Linux下Qt程序如何打包”，我们要如何处理呢？<br />
非常简单，和刚刚分词那块的实现几乎一样，使用 jieba 处理输入，但这一次我们选择 search 模式，也就是为搜索专门优化的模式，很少重复分词，再过滤一下输出的 tokens 就行了。处理结果是这样：[&quot;linux&quot;, &quot;qt&quot;, &quot;程序&quot;, &quot;打包&quot;]<br />
看起来很不错，jieba 顺利地把输入切分成了 tokens，我们又过滤掉了停用词。<br />
假如 search 模式分词没分好怎么办？<br />
我们来考虑一些极端情况：“乒乓球拍卖完了”，search 模式会分成：[&quot;乒乓&quot;, &quot;乒乓球&quot;, &quot;拍卖&quot;, &quot;完&quot;]，而一般的理解，应该是[&quot;乒乓球拍&quot;, &quot;卖完&quot;]<br />
可见 search 模式也不是万能的，此时如果用 cutall 模式，可以得到：[&quot;乒&quot;, &quot;乒乓&quot;, &quot;乒乓球&quot;, &quot;乒乓球拍&quot;, &quot;乓&quot;, &quot;球&quot;, &quot;球拍&quot;, &quot;拍&quot;, &quot;拍卖&quot;, &quot;卖&quot;, &quot;卖完&quot;, &quot;完&quot;]，这样一来，我们就能得到“乒乓球拍”“卖完”这两个词了。<br />
但是这样也有明显的问题，cutall 模式切出来太多单字了，而在现代汉语中，很少单独使用一个字，一般都是组成词语使用，所以单字大部分时候都是噪声，如果我们直接用 cutall 模式输出的 tokens 去搜索，那就会得到很多不相关的结果，比如包含“球”字和“完”字，“篮球赛完美结束”，和乒乓球拍和卖东西半点关系没有，但是直接被包含进来了。<br />
所以我采取了组合并排除单字的方式，用 cutall 切一次，过滤掉所有单字，再用 search 模式切一次，再把两次的结果用集合去重。这样我们得到的结果就是：{&quot;卖完&quot;, &quot;乒乓&quot;, &quot;乒乓球拍&quot;, &quot;乒乓球&quot;, &quot;拍卖&quot;, &quot;完&quot;, &quot;球拍&quot;}，相关性就高多了。</p>
<h3>查询算法</h3>
<p>Tantivy 是如何决定哪些结果可以显示，哪些结果要排在前面的？它使用打分制，评分越高的结果排名越靠前，没有任何得分的结果就不会显示。<br />
Tantivy 使用的核心打分算法是 BM25，它具有以下特点：</p>
<ul>
<li>TF (Term Frequency)：即词频，一个词出现次数越多，分数就越高</li>
<li>IDF (Inverse Document Frequency)：DF 是指有多少文档包含这个词，显然 DF 越高，这个词就越常见，IDF 可以简单理解为 DF 的倒数，基本上是 DF 越小，IDF 就越大，越意味着这个词更罕见。一般来说，我们认为越罕见的词提供的信息量越高，越有助于筛选文档。所以 IDF 越大，文档的评分就会越高。</li>
<li>词频饱和：TF 分数的增长会趋于平缓，当关键词出现的次数很多时，比如在 1000 字的文章中，出现了 20 次或 30 次，此时对分数的贡献差别不大。</li>
<li>文档长度归一化：BM25 会将文章长度也纳入考虑，长文章不会因为包含的关键词数量更多而占优势</li>
</ul>
<h3>构造查询</h3>
<h4>基础查询</h4>
<p>之前也提到过，我的文章以中文为主，英文出现得比较少，所以接下来构造查询时，我们也要给它们加上不同的权重。</p>
<pre><code class="language-Rust">fn basic_query() {
    for tk in tokens.iter() {
        // jieba 已经把中英文分到不同的 token 里去了，所以只要这个 token 包含任何
        // 汉字，就当作中文来处理
        if contains_han(tk) {
            // Chinese token: use exact TermQuery and boost against content
            let term = tantivy::Term::from_field_text(content, tk);
            let term_title = tantivy::Term::from_field_text(title_field, tk);
            // 给在文章内容里出现的 token 赋予 1.5 的权重
            let query = BoostQuery::new(
                Box::new(TermQuery::new(
                    term,
                    IndexRecordOption::WithFreqsAndPositions,
                )),
                1.5,
            );
            // 在标题里出现的权重为 2
            let title_query = BoostQuery::new(
                Box::new(TermQuery::new(
                    term_title,
                    IndexRecordOption::WithFreqsAndPositions,
                )),
                2.0,
            );
            // should 表示出现能够增加评分，是“或”的关系，如果一篇文档一个 should 或 must
            // 都没有出现，则得分为 0
            clauses.push((Occur::Should, Box::new(query)));
            clauses.push((Occur::Should, Box::new(title_query)));
        } else {
            // short token
            let term = tantivy::Term::from_field_text(content, tk);
            // 出现在文章内容里的英文权重为 1，也就是没有权重
            let term_title = tantivy::Term::from_field_text(title_field, tk);
            // 出现在标题里的英文权重为 2
            let title_query = BoostQuery::new(
                Box::new(TermQuery::new(
                    term_title,
                    IndexRecordOption::WithFreqsAndPositions,
                )),
                2.0,
            );
            let q1 = TermQuery::new(term, IndexRecordOption::WithFreqsAndPositions);
            // must 表示必须出现，出现可以增加得分，如果没有出现，则这篇文档得分为 0，是“与”的关系
            // 在这里用来提高搜索的精度，就是考虑到我文章里的英文本来就不多，而且多为专业术语，如果
            // 搜索关键词里有英文，那文档里就必须出现，这样可以保证当搜索英文专业术语时，搜索结果里
            // 的无关内容很少
            clauses.push((Occur::Must, Box::new(q1)));
            clauses.push((Occur::Should, Box::new(title_query)));
        }
    }
}
</code></pre>
<h4>优化</h4>
<p>目前我们的查询基本做到了，输入关键词，如果有内容就一定会被查到，如果只匹配了部分关键词，也能查到。但是还有个小问题，就是只要包含了我们输入的关键词，比方说输入了 3 个关键词，3 篇文章都包含了，虽然有的可能是在同一句话中，有的可能分散得很开，但得分都差不多，并没有让最相关的文章排在最前面。<br />
举个例子，搜索“人工智能”的时候，一篇文章是讲<strong>人工智能</strong>的，另一篇是说：“<strong>智能</strong>冰箱是我们耗费了大量<strong>人工</strong>做出来的产品”，显然第一篇更符合，应该排在前面，但实际上并不一定。<br />
因此，我们可以使用短语查询 (Phrase Query) 来优化，也就是不仅要求有这些词出现，还要求词之间挨得很近，并且顺序和输入的关键词完全一样，可以通过 slop 来调整允许的距离，比如设置为 10，举个简单的例子</p>
<table>
<thead>
<tr>
<th>tokens</th>
<th>document</th>
<th>出现位置</th>
</tr>
</thead>
<tbody>
<tr>
<td>人工</td>
<td>1</td>
<td>[0]</td>
</tr>
<tr>
<td>智能</td>
<td>1</td>
<td>[25]</td>
</tr>
<tr>
<td>人工</td>
<td>2</td>
<td>[7]</td>
</tr>
<tr>
<td>智能</td>
<td>2</td>
<td>[8]</td>
</tr>
</tbody>
</table>
<p>那么按照先前的查询逻辑，文档 1 和 2 都能成功匹配，但是按照短语查询的逻辑，文档 2 的“人工”和“智能”靠得很近，slop 为 1，小于 10，成功匹配，在短语查询获得了分数；而文档 1 的“人工”和“智能”的 slop 为 25，大于 10，无法匹配，在短语查询这块不得分。<br />
先使用 jieba 的 search 模式切出来 tokens，再在 tokens 数量大于 1 时，加上额外的 PhraseQuery</p>
<pre><code class="language-Rust">fn phrase_query() {
    if proximity_subs.len() &gt; 1 {
        let mut proximity_query = tantivy::query::PhraseQuery::new(proximity_subs);
        // 设置适当的 slop
        proximity_query.set_slop(10);
        // 给匹配的文档设置高权重
        let proximity_query = BoostQuery::new(Box::new(proximity_query), 5.0);
        boolean_query = BooleanQuery::from(vec![
            // 这里的 `boolean_query` 是我们之前的查询，所以设置成 must，必须要匹配
            (Occur::Must, Box::new(boolean_query) as Box&lt;dyn Query&gt;),
            // 短语查询是加分项，如果没有匹配，就失去了额外分数，但不会抛弃这个文档
            (Occur::Should, Box::new(proximity_query)),
        ]);
    }
}
</code></pre>
<h4>过滤 tags</h4>
<p>如果搜索的时候指定了 tags，那就应该只在包含这些 tags 的文章里查询，实现非常简单，使用 <code>Occur::Must</code> 即可。</p>
<pre><code class="language-Rust">fn filter_tags() {
    if let Some(tags) = tags {
        // 从我们先前构建的索引中取出 tags 部分的内容
        let tag_facet = schema.get_field(&quot;tags&quot;)?;
        let mut queries: Vec&lt;(Occur, Box&lt;dyn Query&gt;)&gt; = Vec::with_capacity(tags.len());
        for tag in tags {
            let facet = Facet::from(&amp;format!(&quot;/{}&quot;, tag));
            let term = Term::from_facet(tag_facet, &amp;facet);
            let tag_query = TermQuery::new(term, IndexRecordOption::Basic);
            // 要求必须包含选定的 tags
            queries.push((Occur::Must, Box::new(tag_query)));
        }
        let tag_boolean_query = BooleanQuery::from(queries);
        // 与之前的查询相组合，是“与”的关系
        boolean_query = BooleanQuery::from(vec![
            (Occur::Must, Box::new(boolean_query) as Box&lt;dyn Query&gt;),
            (Occur::Must, Box::new(tag_boolean_query)),
        ]);
    }
}
</code></pre>
<h2>生成摘要</h2>
<p>这部分就很简单了，基本上是按照 Tantivy 的接口，把结果对应的文档路径提取出来，再生成摘要，Tantivy 生成的 snippet 有个方便的 <code>to_html()</code> 方法，可以直接生成 html，并在里面用 <code>&lt;b&gt;&lt;/b&gt;</code> 包住需要高亮的关键词。</p>
<pre><code class="language-Rust">fn generate_snippets() {
    // 创建一个 snippet 生成器
    let mut zh_snippet_gen = SnippetGenerator::create(&amp;searcher, &amp;boolean_query, content).unwrap();
    // 设置最大的摘要长度
    zh_snippet_gen.set_max_num_chars(200);
    // get total matched results count
    let count = searcher.search(&amp;boolean_query, &amp;tantivy::collector::Count)?;
    let terms_iter =
        top_docs
            .into_iter()
            .map(|(score, doc_addr)| -&gt; Result&lt;SearchTerm, CatError&gt; {
                let doc: TantivyDocument = searcher.doc(doc_addr)?;
                let file_name = doc
                    .get_first(path_field)
                    .and_then(|v| v.as_str())
                    .ok_or(CatError::internal(&quot;Can not get file name&quot;))?;

                let text_zh = doc
                    .get_first(content)
                    .and_then(|v| v.as_str())
                    .ok_or(CatError::internal(&quot;Can not get file content&quot;))?;

                // through testing, we find that snippet is a very expensive operation
                let ins = Instant::now();
                let snippet_zh = zh_snippet_gen.snippet(text_zh);
                log::info!(&quot;Snippet gen took: {:?}&quot;, ins.elapsed());

                let fm = extract_frontmatter(file_name)?;
                let res = SearchTerm {
                    score,
                    fm,
                    snippet: snippet_zh.to_html().trim().to_string(),
                };
                Ok::&lt;SearchTerm, CatError&gt;(res)
            });
}
</code></pre>
<h2>优化速度</h2>
<p>目前还没有给搜索进行任何优化，我们来简单测试一下速度吧。<br />
编译 release 版本在我的电脑上运行，搜索“rust“，搜到 4 个结果，耗时 100 多毫秒，嗯，谷歌级的速度。<br />
仔细看看代码，发现每次搜索的时候，都会重新加载词库和索引等，使用 static 变量缓存这些内容后，程序的内存占用会从 14 m 左右来到 60 m 左右，但是搜索耗时大幅缩减，耗时 13.2 ms</p>
<p>还能不能再优化？经过反复测试，我发现生成摘要竟然是最耗时的操作，整个搜索耗时 13.2ms ，但生成 snippet 前只用了 916 µs，也就是说，生成 snippet 的时间占了整个搜索的时间的 93%<br />
再仔细一看，发现每生成一个摘要，都要耗时 2-5 ms 左右，所以搜索结果越多，搜索耗时就越高。<br />
于是我使用 rayon 库对生成摘要的过程进行了并行化，优化完后，整个搜索耗时就只有 3.5 ms 了，比之前快多了。</p>
<pre><code class="language-Rust">// DONT_FORMAT
// 使用 rayon 库并行化生成摘要的过程
let terms_iter = top_docs.into_par_iter();
</code></pre>
<p>当然这是在我自己电脑上的测试，实际运行这个网站的服务器 cpu 只有 2 核，所以速度要慢得多。</p>
<h2>其他</h2>
<p>完成上述内容后，博客的搜索系统基本就算是搭建完成了，其他的工作无非是写点 html 和 js，做个搜索页面出来。<br />
有的人可能会问：直接用 google 搭配 site:lhz07.com 不好吗？其实我都没想到 google 爬我网站那么勤快，我既没写 robots.txt，也没写 sitemap，本来没指望 google 爬的，但是可能是因为 js 用得少吧，再加上 .com 域名，google 爬得又快又全面。<br />
经过我的简单测试，我的搜索结果和 google 搭配 site 搜索的结果，大部分情况下基本一致，只有两点明显差别：</p>
<ol>
<li>不知道为什么，google 把我的首页、搜索页和归档页都放到结果里面了，显得很乱，而且没什么用</li>
</ol>
<img src="/static/img/posts/build-my-search-engine/google1.webp" alt="Image" width="522" />
<img src="/static/img/posts/build-my-search-engine/my1.webp" alt="Image" width="472" />
<ol start="2">
<li>我的搜索更倾向于只要有任一关键词匹配，就有结果，当然，匹配所有关键词的肯定会在最前面，但是如果有只能匹配一个关键词的文章，那就也放上来，或许有用呢；但是 google 的要求所有关键词都匹配，才会有结果</li>
</ol>
<img src="/static/img/posts/build-my-search-engine/my2.webp" alt="Image" width="470" />
<img src="/static/img/posts/build-my-search-engine/google2.webp" alt="Image" width="629" />
<p>还有个好处，大概是因为我的索引小，可以全部缓存，所以速度比 google 快多了。</p>
<p>其实博客的搜索系统，大概在去年10月份，国庆节的时候就搭好了，一开始功能比较少，后面才加的按 tag 过滤的功能，之后想着说写一篇博客记录一下，但是也一直拖着没写，跑去写了一堆没什么用但很有意思的小项目。最近也是疏于更新，在群友的催更下，就写了这一篇。这篇文章里主要介绍了目前的搜索系统是如何实现的，至于我真正搭建搜索系统时遇到的问题，比如之前尝试过中英文分别搜索、模糊搜索之类的，就一笔带过了（主要是我也不记得了）</p>
]]>
      </description>
      </item>
    
    <item>
      <title>Subtyping and Variance</title>
      <link>https://lhz07.com/posts/subtyping-and-variance</link>
      <guid>https://lhz07.com/posts/subtyping-and-variance</guid>
      <pubDate>Tue, 2 Dec 2025 11:17:07 +0800</pubDate>
      <description
      ><![CDATA[<img src=/static/img/posts/subtyping-and-variance/cover.webp alt="Image" />
      <p><em>详解 Rust 中的子类型和变型</em></p>
      <br>
      <p>Rust 使用生命周期来追踪借用与所有权之间的关系。但是，实现生命周期检查并不简单，如果实现得不好，要么就限制太严，要么就允许了未定义行为。</p>
<p>为了能够灵活地使用生命周期，同时避免误用，Rust 使用了 <strong>subtyping（子类型）</strong> 和 <strong>variance（变型）</strong>。</p>
<p>让我们以这个例子开始。</p>
<pre><code class="language-Rust">// runnable
// Note: debug expects two parameters with the *same* lifetime
fn debug&lt;'a&gt;(a: &amp;'a str, b: &amp;'a str) {
    println!(&quot;a = {a:?} b = {b:?}&quot;);
}
fn main() {
    let hello: &amp;'static str = &quot;hello&quot;;
    {
        let world = String::from(&quot;world&quot;);
        let world = &amp;world; // 'world has a shorter lifetime than 'static
        debug(hello, world);
    }
}
</code></pre>
<p>如果以一种保守的方式实现生命周期检查，那么，因为 <code>hello</code> 和 <code>world</code> 拥有不同的生命周期，我们可能看到下面的错误：</p>
<pre><code class="language-text">error[E0308]: mismatched types
 --&gt; src/main.rs:10:16
   |
10 |         debug(hello, world);
   |                      ^
   |                      |
   |                      expected `&amp;'static str`, found struct `&amp;'world str`
</code></pre>
<p>这相当令人遗憾。在这种情况下，我们希望的是：接受任何跟 <code>'world</code> 至少活得一样长的类型。下面，让我们试试在生命周期中使用 subtyping。</p>
<h2>Subtyping</h2>
<p>subtyping 的概念是：某个类型可以替代另一种类型来使用。<br />
我们设想 <code>Sub</code> 是 <code>Super</code> 的子类型（在本文中会使用这样的标记 <code>Sub &lt;: Super</code> )。<br />
这意味着，<code>Sub</code> 满足了成为 <code>Super</code> 类型的所有条件，并且 <code>Sub</code> 还有可能满足了其他的额外条件。<br />
那么，为了在生命周期中使用 subtyping，我们需要明确生命周期应当满足什么条件。</p>
<blockquote>
<p><code>'a</code> 定义了一片代码区</p>
</blockquote>
<p>之后我们可以定义它们之间是如何互相关联的</p>
<blockquote>
<p><code>'long &lt;: 'short</code> : 当且仅当 <code>'long</code> 定义了一片<strong>完全包含</strong> <code>'short</code> 的代码区</p>
</blockquote>
<p><code>'long</code> 可能定义了一块大于 <code>'short</code> 的区域，但这仍然符合我们的定义。</p>
<blockquote>
<p>在之后的内容里，我们将发现 subtyping 其实远比这复杂和巧妙得多，但这条简单的规则已经覆盖了 99% 的情况，并且相当符合直觉。除非你在写 unsafe 代码，否则编译器会自动帮你处理所有的例外情况。<br />
但是这是 Rustonomicon，我们正在写 unsafe 代码，所以我们需要理解这东西到底是如何工作的，以及什么情况下我们会搞坏它。</p>
</blockquote>
<p>回到我们上面的例子，我们可以说 <code>'static &lt;: 'world</code>。目前，让我们先接受一个这样的概念：生命周期的子类型可以通过引用传递（关于这一点稍后会在 Variance 节中讨论）。比如：<code>&amp;'static str</code> 是 <code>&amp;'world str</code> 的子类型，我们可以把<code>&amp;'static str</code> 降级为 <code>&amp;'world str</code> ，就像把子类降级为基类一样。这样一来，上面的例子就可以编译了。</p>
<pre><code class="language-Rust">// runnable
fn debug&lt;'a&gt;(a: &amp;'a str, b: &amp;'a str) {
    println!(&quot;a = {a:?} b = {b:?}&quot;);
}

fn main() {
    let hello: &amp;'static str = &quot;hello&quot;;
    {
        let world = String::from(&quot;world&quot;);
        let world = &amp;world; // 'world has a shorter lifetime than 'static
        debug(hello, world); // hello silently downgrades from `&amp;'static str` into `&amp;'world str`
    }
}
</code></pre>
<h2>Variance</h2>
<p>在上面一节中，我们略过了这样一个条件：<code>'static &lt;: 'b</code> 意味着 <code>&amp;'static T &lt;: &amp;'b T</code>。要使这样的条件成立，需要使用一个叫做 Variance 的属性。但它并不总是像这个例子那么简单。为了理解它，让我们稍稍扩展一下这个例子：</p>
<pre><code class="language-Rust">// runnable
fn assign&lt;T&gt;(input: &amp;mut T, val: T) {
    *input = val;
}

fn main() {
    let mut hello: &amp;'static str = &quot;hello&quot;;
    {
        let world = String::from(&quot;world&quot;);
        assign(&amp;mut hello, &amp;world);
    }
    println!(&quot;{hello}&quot;); // use after free 😿
}
</code></pre>
<p>在 <code>assign</code> 函数中，我们让 <code>hello</code> 指向了 <code>world</code>，但是在 <code>hello</code> 被 println使用之前， <code>world</code> 就离开了作用域。<br />
这是一个经典的 use-after-free 问题！<br />
我们的第一反应可能是把问题归咎于 <code>assign</code> 的实现，但是它确实没有任何问题。我们想把 <code>T</code> 赋值给另一个 <code>T</code>，这并不奇怪。<br />
问题在于，当我们把 <code>hello</code> 作为 <code>input</code> 传入时，因为 <code>T</code> 对于两个参数都是相同的，对于 <code>val</code> ，<code>T</code> 被推断为 <code>&amp;'b str</code> ；对于 <code>input</code> ，<code>T</code> 也需要是 <code>&amp;'b str</code> ，实际上是把 <code>hello</code> 的类型 <code>&amp; mut &amp;‘static str</code> 转换为 <code>&amp;mut &amp;'b str</code> 传入了 <code>input</code> 。但是，我们不能假定 <code>&amp;mut &amp;'static str</code> 和 <code>&amp;mut &amp;'b str</code> 是兼容的。这意味着，<code>&amp;mut &amp;'static str</code> 不能成为 <code>&amp;mut &amp;'b str</code> 的子类型，尽管 <code>'static</code> 是 <code>'b</code> 的子类型。</p>
<p>Variance 是 Rust 引入的一个概念，用于通过泛型参数描述子类型之间的关系。</p>
<blockquote>
<p>NOTE: 我们定义泛型 <code>F&lt;T&gt;</code> ，以便讨论 <code>T</code> 。</p>
</blockquote>
<p><code>F</code> 的 variance 类型取决于它的输入如何影响输出，设有两个类型 <code>Sub</code> 和 <code>Super</code>，<code>Sub</code> 是 <code>Super</code> 的子类型，则：</p>
<ul>
<li><code>F</code> 是协变 ( covariant )的——如果 <code>F&lt;Sub&gt;</code> 是 <code>F&lt;Super&gt;</code> 的子类型（子类型的关系可以传递）</li>
<li><code>F</code> 是逆变 ( contravariant ) 的——如果<code>F&lt;Super&gt;</code> 是 <code>F&lt;Sub&gt;</code> 的子类型（子类型的关系反转了）</li>
<li>否则 <code>F</code> 是不变 ( invariant ) 的，即子类型的关系不复存在</li>
</ul>
<blockquote>
<p>NOTE: 与其他有继承功能的语言不同，比如 <code>class Cat</code> 协变为 <code>class Animal</code> ，之后就真的只能当 <code>Animal</code> 来用了（虽然可能指向的还是同一个对象，但原来的类型已经被完全隐藏了）。生命周期作为泛型参数传入，这里的 variance 只是用于约束传入的生命周期，并不会影响实际使用的生命周期。比如函数参数需要 <code>'a</code> 生命周期，返回值也是 <code>'a</code> 生命周期，但实际上用了一个 <code>'static</code>，那么返回的也是 <code>'static</code> ，而不会被降级。</p>
</blockquote>
<p>如果我们还记得上面的例子，就可以知道如果 <code>'a &lt;: 'b</code> ，那么<code>&amp;'a T</code> 是 <code>&amp;'b T</code> 的子类型，因此我们可以说 <code>&amp;'a T</code> 在 <code>'a</code> 上是协变的。<br />
同时，我们已经发现不能认为 <code>&amp;mut &amp;'a T</code> 是 <code>&amp;mut &amp;'b T</code> 的子类型，因此我们可以说 <code>&amp;mut T</code> 在 <code>T</code> 上是不变的。<br />
这里列出了常见类型的 variances:</p>
<table>
<thead>
<tr>
<th></th>
<th>'a</th>
<th>T</th>
<th>U</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>&amp;'a T </code></td>
<td>covariant</td>
<td>covariant</td>
<td></td>
</tr>
<tr>
<td><code>&amp;'a mut T</code></td>
<td>covariant</td>
<td>invariant</td>
<td></td>
</tr>
<tr>
<td><code>Box&lt;T&gt;</code></td>
<td></td>
<td>covariant</td>
<td></td>
</tr>
<tr>
<td><code>Vec&lt;T&gt;</code></td>
<td></td>
<td>covariant</td>
<td></td>
</tr>
<tr>
<td><code>UnsafeCell&lt;T&gt;</code></td>
<td></td>
<td>invariant</td>
<td></td>
</tr>
<tr>
<td><code>Cell&lt;T&gt;</code></td>
<td></td>
<td>invariant</td>
<td></td>
</tr>
<tr>
<td><code>fn(T) -&gt; U</code></td>
<td></td>
<td><strong>contra</strong>variant</td>
<td>covariant</td>
</tr>
<tr>
<td><code>*const T</code></td>
<td></td>
<td>covariant</td>
<td></td>
</tr>
<tr>
<td><code>*mut T</code></td>
<td></td>
<td>invariant</td>
<td></td>
</tr>
</tbody>
</table>
<p>注意这里的 <code>&amp;'a mut T</code> 对 <code>'a</code> 是协变的，因为协变只是缩短生命周期，这是完全没问题的。<br />
但对于 <code>fn(T) -&gt; U</code>，这里函数参数是逆变的，所以对于 <code>fn(&amp;'a mut T) -&gt; U</code> 来说，它对 <code>'a</code> 也是逆变的，但对 <code>T</code> 仍然是不变的。</p>
<p>回到刚才的例子，因为<code>&amp;mut T</code> 在 <code>T</code> 上是不变的，所以如果我们去掉那对大括号，仍然不能编译。</p>
<pre><code class="language-Rust">// runnable
fn assign&lt;T&gt;(input: &amp;mut T, val: T) {
    *input = val;
}

fn main() {
    let mut hello: &amp;'static str = &quot;hello&quot;;
    let world = String::from(&quot;world&quot;);
    assign(&amp;mut hello, &amp;world);
    println!(&quot;{hello}&quot;);
}
</code></pre>
<pre><code class="language-text">error[E0597]: `world` does not live long enough
  --&gt; src/main.rs:8:24
   |
 6 |     let mut hello: &amp;'static str = &quot;hello&quot;;
   |                    ------------ type annotation requires that `world` is borrowed for `'static`
 7 |     let world = String::from(&quot;world&quot;);
   |         ----- binding `world` declared here
 8 |     assign(&amp;mut hello, &amp;world);
   |                        ^^^^^^ borrowed value does not live long enough
 9 |     println!(&quot;{hello}&quot;);
10 | }
   | - `world` dropped here while still borrowed
</code></pre>
<p>因为 <code>hello</code> 的类型被我们声明为 <code>&amp;'static str</code> ，不能改变，而因为 <code>&amp;mut T</code> 的不变性，在传入 <code>assign</code> 函数时，<code>T</code> 只能被推导为 <code>&amp;'static str</code> ，所以会要求 <code>val</code> 的类型也是<code>&amp;'static str</code> 。<br />
解决方法也很简单，去掉对 <code>hello</code> 的类型声明，让编译器自动推导，这样 <code>hello</code> 就会被推导为 <code>&amp;'b str</code> ，和 <code>&amp;world</code> 的类型一样，代码能够正常编译运行了。</p>
<h3>更精确的生命周期</h3>
<p>我们下面来看一个更复杂的例子</p>
<pre><code class="language-Rust">// runnable
struct Interface&lt;'a&gt; {
    manager: &amp;'a mut Manager&lt;'a&gt;,
}

impl&lt;'a&gt; Interface&lt;'a&gt; {
    pub fn noop(self) {
        println!(&quot;interface consumed&quot;);
    }
}

struct Manager&lt;'a&gt; {
    text: &amp;'a str,
}

struct List&lt;'a&gt; {
    manager: Manager&lt;'a&gt;,
}

impl&lt;'a&gt; List&lt;'a&gt; {
    pub fn get_interface(&amp;'a mut self) -&gt; Interface&lt;'a&gt; {
        Interface {
            manager: &amp;mut self.manager,
        }
    }
}

fn main() {
    // 1 start
    let text = &quot;hello&quot;;
    let mut list = List {
        manager: Manager { text },
    };
    // 1 end
    // 2 start
    list.get_interface().noop();
    // 2 end
    // 3 start

    println!(&quot;Interface should be dropped here and the borrow released&quot;);

    // this fails because inmutable/mutable borrow
    // but Interface should be already dropped here and the borrow released
    use_list(&amp;list);
    // 3 end
}

fn use_list(list: &amp;List) {
    println!(&quot;{}&quot;, list.manager.text);
}
</code></pre>
<pre><code class="language-text">error[E0502]: cannot borrow `list` as immutable because it is also borrowed as mutable
  --&gt; src/main.rs:38:14
   |
32 |     list.get_interface().noop(); // 2
   |     ---- mutable borrow occurs here
...
38 |     use_list(&amp;list); // 4
   |              ^^^^^
   |              |
   |              immutable borrow occurs here
   |              mutable borrow later used here
</code></pre>
<p>这段代码的表现非常奇怪，对 <code>list</code> 的可变引用应该在使用完 <code>noop</code> 方法后就销毁了，但实际上却没有，导致编译器抱怨我们在持有可变引用的同时尝试借用新的不可变引用。<br />
我们来分析一下这段代码：</p>
<ol>
<li><code>text</code> 的生命周期是 <code>'static</code></li>
<li><code>list</code> 的生命周期设为 <code>'l</code> ，作用域为 1-3，由于 <code>'static</code> 是 <code>'a</code> 的子类型，可以协变，所以 <code>list</code> 现在持有生命周期为 <code>'static</code> 的引用</li>
<li>然后 <code>get_interface</code> 方法创建了一个 <code>list</code> 的可变引用 <code>&amp;'a mut list</code> ，并返回了一个生命周期为 <code>'a</code> 的 <code>Interface&lt;'a&gt;</code>，<code>'a</code> 的作用域暂时未知</li>
<li><code>noop</code> 方法销毁了 <code>Interface</code> ，但是引用只会失效（由编译器自动推导何时失效），不能被主动销毁，所以对 <code>&amp;'a mut list</code> 没有影响</li>
<li><code>use_list</code> 函数尝试对 <code>list</code> 创建新的不可变引用 <code>&amp;'b list</code> ，作用域为 3。显然 <code>list</code> 此时仍然需要有效，所以 <code>'a</code> 的作用域是 1-3，则 <code>&amp;'a mut list</code> 的作用域也是 1-3，所以无法再创建重叠的不可变引用 <code>&amp;'b list</code></li>
</ol>
<p>我们可以简化一下这个例子：</p>
<pre><code class="language-Rust">// runnable
struct List&lt;'a&gt; {
    text: &amp;'a str,
}

impl&lt;'a&gt; List&lt;'a&gt; {
    pub fn get_str(&amp;'a mut self) -&gt; &amp;'a mut &amp;str {
        &amp;mut self.text
    }
}

fn main() {
    let mut list = List { text: &quot;hello&quot; };

    let _ = list.get_str();

    println!(&quot;Text should be dropped here and the borrow released&quot;);

    // this fails because inmutable/mutable borrow
    // but text should be already dropped here and the borrow released
    use_list(&amp;list);
}

fn use_list(list: &amp;List) {
    println!(&quot;{}&quot;, list.text);
}
</code></pre>
<p>这样问题就很明显了，并且也很好修复，只需要改一下 <code>get_str</code> 方法：</p>
<pre><code class="language-Rust">// runnable
struct List&lt;'a&gt; {
    text: &amp;'a str,
}

// ANCHOR
impl&lt;'a&gt; List&lt;'a&gt; {
    // 改成这样
    pub fn get_str_1&lt;'b&gt;(&amp;'b mut self) -&gt; &amp;'b mut &amp;'a str {
        &amp;mut self.text
    }
    // 或者
    // 因为只需要增加 'b 这一个生命周期，可以省略
    pub fn get_str_2(&amp;mut self) -&gt; &amp;mut &amp;'a str {
        &amp;mut self.text
    }
}

fn main() {
    let mut list = List { text: &quot;hello&quot; };

    let _ = list.get_str_1();
    use_list(&amp;list);

    let _ = list.get_str_2();
    use_list(&amp;list);
}
// ANCHOR_END

fn use_list(list: &amp;List) {
    println!(&quot;{}&quot;, list.text);
}
</code></pre>
<p>更精确地表示生命周期，字符串本身的生命周期和 <code>List</code> 对象一致，但是对字符串的可变引用的生命周期可以更短。</p>
<h3>逆变</h3>
<p>前面我们已经详细讨论过协变与不变了，下面我们来看看逆变。<br />
在 Rust 中，只有一种类型是逆变的，就是函数指针的参数。<br />
我们先来解释为什么 <code>fn(T) -&gt; U</code> 对 <code>U</code> 是协变的。<br />
考虑这样的函数签名：</p>
<pre><code class="language-Rust">// DONT_FORMAT
fn get_str() -&gt; &amp;'a str;
</code></pre>
<p>也就是说，调用方期望，调用这个函数之后，获得一个生命周期为 <code>'a</code> 的引用<br />
如果我实际传入这样的函数：</p>
<pre><code class="language-Rust">// DONT_FORMAT
fn get_static() -&gt; &amp;'static str;
</code></pre>
<p>显然是没问题的，调用方期望能得到一个生命周期为 <code>'a</code> 的引用，就是说引用至少能活得跟 <code>'a</code> 一样长，但实际上得到了一个生命周期为 <code>'static</code> 的引用，活得比 <code>'a</code> 更长。</p>
<p>但是对于参数来说就不一样了。<br />
考虑这样的函数签名：</p>
<pre><code class="language-Rust">// DONT_FORMAT
fn store_ref(&amp;'a str);
</code></pre>
<p>意味着调用方实际上会往函数里传一个生命周期为 <code>'a</code> 的引用，所以这个函数需要能处理任何活得至少跟 <code>'a</code> 一样久的引用。<br />
如果我实际传入这样的函数：</p>
<pre><code class="language-Rust">// DONT_FORMAT
fn store_static(&amp;'static str);
</code></pre>
<p>这个函数只能处理活得至少跟 <code>'static</code> 一样久的引用，但调用方传的是 <code>'a</code> ，根据协变规则，<code>'static</code> 可以协变为 <code>'a</code> ，但是不能反过来，所以传入这样的函数是不可行的。<br />
来看一个具体的例子</p>
<pre><code class="language-Rust">// runnable
use std::cell::RefCell;

// ANCHOR
thread_local! {
    pub static StaticVecs: RefCell&lt;Vec&lt;&amp;'static str&gt;&gt; = RefCell::new(Vec::new());
}

/// saves the input given into a thread local `Vec&lt;&amp;'static str&gt;`
fn store(input: &amp;'static str) {
    StaticVecs.with_borrow_mut(|v| v.push(input));
}

/// Calls the function with it's input (must have the same lifetime!)
fn demo&lt;'a&gt;(input: &amp;'a str, f: fn(&amp;'a str)) {
    f(input);
}

fn main() {
    demo(&quot;hello&quot;, store); // &quot;hello&quot; is 'static. Can call `store` fine

    {
        let smuggle = String::from(&quot;smuggle&quot;);

        // 设这里 smuggle 是 'b，此处自动推导会得出 demo 要求 &lt;'a&gt; 为 'static, 但 'b 无法协变为 'static，因此无法编译
        demo(&amp;smuggle, store);
    }

    // 如果允许编译，那这里就会出现 use after free 😿
    StaticVecs.with_borrow(|v| println!(&quot;{v:?}&quot;));
}
// ANCHOR_END
</code></pre>
<p>但是，如果我们把这两个函数签名对调，就会发现引用可以自动协变了！<br />
如果需要接收 <code>'static</code> 的函数，但实际上传入了接收 <code>'a</code> 的函数。当调用方传参的时候，传的是 <code>'static</code> ，可以自动协变为 <code>'a</code> ，完全没问题。<br />
因此我们可以得出，函数指针对于参数是逆变的，对于返回值是协变的。</p>
<h3>variance in struct</h3>
<p>简单来说，struct 继承了它的字段的 variance<br />
比如一个 struct <code>MyType</code> ，它有一个泛型参数 <code>T</code> ，字段 <code>p</code> 的类型是 <code>T</code> ，那么 Mytype 对于 <code>T</code> 的 variance 就是 <code>p</code> 对于 <code>T</code> 的 variance<br />
如果 <code>T</code> 被用在了多个字段呢？<br />
有这样的规则：</p>
<ul>
<li>如果所有使用了 <code>T</code> 的字段都是协变的，那么 <code>MyType</code> 对 <code>T</code> 是协变的</li>
<li>如果所有使用了 <code>T</code> 的字段都是逆变的，那么 <code>MyType</code> 对 <code>T</code> 是逆变的</li>
<li>否则，<code>MyType</code> 对 <code>T</code> 是不变的</li>
</ul>
<!-- end list -->
<pre><code class="language-Rust">// runnable
use std::cell::Cell;

// ANCHOR
struct MyType&lt;'a, 'b, A: 'a, B: 'b, C, D, E, F, G, H, In, Out, Mixed&gt; {
    a: &amp;'a A,     // covariant over 'a and A
    b: &amp;'b mut B, // covariant over 'b and invariant over B

    c: *const C, // covariant over C
    d: *mut D,   // invariant over D

    e: E,       // covariant over E
    f: Vec&lt;F&gt;,  // covariant over F
    g: Cell&lt;G&gt;, // invariant over G

    h1: H,       // would also be covariant over H except...
    h2: Cell&lt;H&gt;, // invariant over H, because invariance wins all conflicts

    i: fn(In) -&gt; Out, // contravariant over In, covariant over Out

    k1: fn(Mixed) -&gt; usize, // would be contravariant over Mixed except..
    k2: Mixed,              // invariant over Mixed, because invariance wins all conflicts
}
// ANCHOR_END

fn main() {}
</code></pre>
<h2>其他</h2>
<p>本来是想翻译《The Rustonomicon》的这篇文章：<a href="https://doc.rust-lang.org/nomicon/subtyping.html#subtyping-and-variance">Subtyping and Variance</a> ，但是翻译了一半之后就有点懒了🫠，所以前半部分基本是直译，加了一点自己的东西，后半部分就只是参考这篇文章写的相关内容。<br />
还参考了这篇博客：<a href="https://dev.to/arichy/variance-best-perspective-of-understanding-lifetime-in-rust-m84">Variance - best perspective of understanding lifetime in Rust</a> ，虽然感觉里面有挺多错误的，但是总体来说讲得很好，用 class 作为例子很清晰地说明了 variance 的各种内容。我估计之后会参考这篇博客再写一篇，用 class 的例子讲解 variance<br />
这篇博客的作者是个前端程序员，然后从 JS/TS 转到 Rust 了，我看了一下他的其他文章，挺多都写得很不错，作为 Rust 的入门或进阶理论都很合适，比如这篇 <a href="https://dev.to/arichy/pin-in-rust-the-why-and-how-of-immovable-memory-481b">Pin in Rust: The Why and How of Immovable Memory</a> 和这篇 <a href="https://dev.to/arichy/a-journey-from-js-to-rust-3oa1">A Journey From JS To Rust</a> ，里面基本没有错误，讲解得也比较清楚。</p>
]]>
      </description>
      </item>
    
    <item>
      <title>Rust 中的各种 String（一）</title>
      <link>https://lhz07.com/posts/rust-string-1</link>
      <guid>https://lhz07.com/posts/rust-string-1</guid>
      <pubDate>Tue, 18 Nov 2025 11:07:26 +0800</pubDate>
      <description
      ><![CDATA[<img src=/static/img/posts/rust-string-1/1.webp alt="Image" />
      <p><em>介绍 Rust 中的各种 String 类型，以及其他...</em></p>
      <br>
      <h2>String 与 str</h2>
<h3>String</h3>
<p>这是最常见的字符串类型，内部是合法的 UTF-8，从其他字符串转换时，需要经过检查，以保证合法性。<br />
与 C 语言中的字符串不同，它不以 '\0' 结尾，而是在里面记录了字符串的长度信息，以较小的空间占用换来了更高效的读取方式，也使得获取字符串的长度这一操作几乎零成本，不需要遍历整个字符串。同时也更灵活了，可以在字符串里存储 '\0'（好奇怪的需求）<br />
这是标准库里的定义：</p>
<pre><code class="language-Rust">#[derive(PartialEq, PartialOrd, Eq, Ord)]
#[stable(feature = &quot;rust1&quot;, since = &quot;1.0.0&quot;)]
#[lang = &quot;String&quot;]
pub struct String {
    vec: Vec&lt;u8&gt;,
}
</code></pre>
<p>可以看出来，没有任何神秘的操作，里面就是个存储着 u8 的 vector，构造字符串、转换字符串时的安全性，完全由它的方法来保证。</p>
<h3>str</h3>
<p>它是 Rust 的 primitive type (原始类型)，就像 i32，u8，bool 一样。&amp;str 表示的是字符串切片，它指向一块存储着合法的 UTF-8 数组的内存。str 不能直接存储或者用于函数参数 / 返回值等，因为它表示的是一块动态大小的内存，编译时无法知道它的大小，所以只能以引用/指针的形式来使用它，比如：&amp;str, *const str, Box&lt;str&gt; 等等。<br />
&amp;str 最常用的地方有两个，string literals (字符串字面量)和表示字符串切片。<br />
string literals 就是代码里的常量字符串，比如写 <code>let text = &quot;abc&quot;;</code> ， 其中 &quot;abc&quot; 就是常量字符串，它直接被编码进二进制文件中，在整个程序运行期间都有效。<br />
字符串切片通常是对 String 的切片，从 String 产生一个 &amp;str 是几乎零成本的，当然，其他类型也可以通过一些转换，表示为字符串切片，此时 &amp;str 指向的内存就不属于 String 了，但是仍然是合法的 UTF-8 数组。<br />
&amp;str 和 String 非常像，它是一个胖指针，由两个部分组成：指向 [u8] 的指针，和表示字符串长度的 usize 。</p>
<h4>len 方法</h4>
<p>String 和 str 都有 len 方法，但是它们都只表示内部存储的 u8 数组的长度，并不一定是人类可读的字符串长度，因为 UTF-8 是可变长度编码，当一个 u8 存不下字符时，会使用多个 u8。比如</p>
<pre><code class="language-Rust">// runnable
fn main() {
    // ANCHOR
    let len = &quot;foo&quot;.len();
    assert_eq!(3, len);

    assert_eq!(&quot;ƒoo&quot;.len(), 4); // fancy f!
    assert_eq!(&quot;ƒoo&quot;.chars().count(), 3);
    // ANCHOR_END
}
</code></pre>
<p>当然，实际上字符串的处理更加复杂<sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup>。</p>
<h4>小歪个题</h4>
<p>Rust 和 C 在处理数组/切片这方面的设计有很大的区别，C 里面倾向于更小的内存占用，比如字符串使用 '\0' 结尾，数组长度只能使用 <code>sizeof</code> 获取，<code>sizeof</code> 会在编译时就进行计算，无法动态获取数组长度，所以数组长度和数组完全是分开管理的，函数传参也需要传数组本身和长度两个参数。<br />
而 Rust 更倾向于安全可控，数组/切片/字符串都会存储自己的长度，如果是数组本身，长度就存储在类型里面，如果是数组指针/引用/切片，则是一个胖指针，里面存储着数据和长度两个信息。<br />
一个有趣的设计是 unsized type，即编译时未知大小的类型，str 就是一种 unsized type，只能使用胖指针来进行存储。<br />
主要问题在于，要访问一个内存中的数据，只有单个指针是不够的，指针只指向数据起始处，数据长度或终止处是未知的。如果指针指向的是一种 sized type，那编译时就能知道它的大小，可以在编译时就自动把相关操作都加上长度，这样就能读取到完整的数据，从而进行解引用。如果编译时大小未知，那就要靠运行时动态存储大小了，比如在结构体里单独用一个字段存储，或者用胖指针。</p>
<h2>CString 与 CStr</h2>
<h3>CString</h3>
<p>与 String 类似，也是具有所有权的字符串类型，但它是 C-compatible 的。它以 '\0' 结尾，并且中间不含任何 '\0'，且内部可以是任意编码。对于 CString，Rust 也尝试让它变得更加安全，就是在构造的时候检查中间是否有 '\0'，从而保证 CString 内部一定是有效的 C 字符串。<br />
这是标准库里对它的定义：</p>
<pre><code class="language-Rust">#[derive(PartialEq, PartialOrd, Eq, Ord, Hash, Clone)]
#[rustc_diagnostic_item = &quot;cstring_type&quot;]
#[stable(feature = &quot;alloc_c_string&quot;, since = &quot;1.64.0&quot;)]
pub struct CString {
    // Invariant 1: the slice ends with a zero byte and has a length of at least one.
    // Invariant 2: the slice contains only one zero byte.
    // Improper usage of unsafe function can break Invariant 2, but not Invariant 1.
    inner: Box&lt;[u8]&gt;,
}
</code></pre>
<p>可以看出，内部是一个长度不可变的动态分配的 u8 数组，与 C 中的字符串很相似。并且它始终保证只在结尾有一个 '\0'，当需要传给 C 的函数时，直接传这个数组的指针即可。当然，这里的 Box 也是一个胖指针，记录了长度数据，所以可以很方便地获取字符串长度，并且不会在计算长度的时候发生越界错误。</p>
<p>标准库还为它实现了额外的 Drop 方法</p>
<pre><code class="language-Rust">// Turns this `CString` into an empty string to prevent
// memory-unsafe code from working by accident. Inline
// to prevent LLVM from optimizing it away in debug builds.
#[stable(feature = &quot;cstring_drop&quot;, since = &quot;1.13.0&quot;)]
#[rustc_insignificant_dtor]
impl Drop for CString {
    #[inline]
    fn drop(&amp;mut self) {
        unsafe {
            *self.inner.get_unchecked_mut(0) = 0;
        }
    }
}
</code></pre>
<p>这里非常有意思，首先说说这个方法本身的作用，它是帮助进行 use after free 检测的，当 CString 离开作用域，被自动销毁的时候，会调用这个 Drop 方法，自动将数组的首位设为 '\0'，这样即使还持有它的指针，并且指针指向的内存还没有被销毁，尝试使用的时候，也会发现字符串为空，可以更早地发现错误。因为 CString 的实现保证了 &quot;the slice ends with a zero byte and has a length of at least one&quot;，所以这里访问首位永远都不会越界。<br />
再说说优化，这里使用了 <code>#[rustc_insignificant_dtor]</code>, &quot;A type marked with the attribute <code>rustc_insignificant_dtor</code> is considered to not be significant. A drop is significant if it is implemented by the user or does anything that will have any observable behavior (other than freeing up memory)&quot;. 也就是说，虽然手动为它实现了 drop 方法，但这个 drop 方法不重要，可以在必要的时候优化掉，避免在 release 模式下影响性能。<br />
这个标注是给 rustc 的，rustc 会生成 LLVM IR，然后由 LLVM 后端进行编译，LLVM 的优化十分优秀，它会认为 drop 方法尝试写入了一块即将释放的内存，并且在释放之前也没被再次读取，就把 drop 方法给优化掉了。<br />
所以在这里还标注了 <code>#[inline]</code>，把代码内联进去，这样在 debug build 下，LLVM 不会优化掉 drop 方法，但是在 release build 下，LLVM 经过分析后仍然能够优化掉 drop 方法。</p>
<h3>CStr</h3>
<p>这是一种 unsized type，它的大小在编译时未知，就像 str 一样，常常使用 <code>&amp;CStr</code> 来表示。<br />
它是对 CString 的借用，一般来说也就是对 CString 的切片，这是标准库对它的定义</p>
<pre><code class="language-Rust">#[repr(transparent)]
pub struct CStr {
    inner: [c_char],
}
</code></pre>
<p><code>#[repr(transparent)]</code> 表示内存布局和 inner 完全一致，struct 是完全透明的，不存在额外的对齐。在传递字符串作为 C 函数的参数时，可以直接传递 CStr 的指针。</p>
<h2>Cow</h2>
<p>顺便来说一下 Rust 里的 Cow 字符串。Cow，即 copy on write，写时复制，即在复制的时候，并不真正深拷贝，而是只复制指向原来字符串的指针/引用，在需要修改字符串的时候，才实际进行深拷贝，复制整个字符串。<br />
典型的 Cow 实现，比如 Qt 的 QString，更类似于 Rust 里的 Arc&lt;String&gt;，每次复制的时候只增加引用计数，需要修改的时候才调用 Arc 的 <code>make_mut()</code> 方法，该方法会检查当前的引用计数是否为1，如果为1，则直接返回 String 的可变引用，否则就深拷贝整个 String，并放到新的 Arc 里，再返回对它的可变引用。</p>
<p>而 Rust 的 Cow 更加简单，它本质上是个枚举，定义如下：</p>
<pre><code class="language-Rust">#[stable(feature = &quot;rust1&quot;, since = &quot;1.0.0&quot;)]
#[rustc_diagnostic_item = &quot;Cow&quot;]
pub enum Cow&lt;'a, B: ?Sized + 'a&gt;
where
    B: ToOwned,
{
    /// Borrowed data.
    #[stable(feature = &quot;rust1&quot;, since = &quot;1.0.0&quot;)]
    Borrowed(#[stable(feature = &quot;rust1&quot;, since = &quot;1.0.0&quot;)] &amp;'a B),

    /// Owned data.
    #[stable(feature = &quot;rust1&quot;, since = &quot;1.0.0&quot;)]
    Owned(#[stable(feature = &quot;rust1&quot;, since = &quot;1.0.0&quot;)] &lt;B as ToOwned&gt;::Owned),
}
</code></pre>
<p>它可能为 owned，也就是具有所有权；或者 borrowed，也就是借用状态。它的好处是，灵活地把借用状态和 owned 状态整合成了一种类型。<br />
比如说，我有一个操作是对字符串进行替换，比如把字符串里的 '\' 都替换成 '\\'，如果字符串里真的有 '\'，那替换之后肯定需要重新分配内存，返回的是一个具有所有权的 String；但如果没有需要替换的字符，显然没必要重新分配，应该返回一个对原字符串的借用。但很显然，方法的返回值必须是固定的一种类型，这个时候就可以用 Cow 把返回值包起来，不管是 String 还是借用的 &amp;str，都可以表示为 Cow。<br />
在实际使用的时候呢，如果是只读操作，可以把 Cow 当作 &amp;str 使用，如果是写操作，就判断是否为 Owned，如果为 Owned 就把 String 取出来，否则就调用 <code>to_owned()</code> 方法得到 Owned 的值。</p>
<p>那复制 Cow 的时候会发生什么呢？这是 <code>clone()</code> 方法的实现：</p>
<pre><code class="language-Rust">#[stable(feature = &quot;rust1&quot;, since = &quot;1.0.0&quot;)]
impl&lt;B: ?Sized + ToOwned&gt; Clone for Cow&lt;'_, B&gt; {
    fn clone(&amp;self) -&gt; Self {
        match *self {
            Borrowed(b) =&gt; Borrowed(b),
            Owned(ref o) =&gt; {
                let b: &amp;B = o.borrow();
                Owned(b.to_owned())
            }
        }
    }
}
</code></pre>
<p>可见，当 b 是借用状态时，则再次产生一个借用状态的 Cow；当 b 是 owned 状态时，就对 b 调用 <code>to_owned()</code> 方法（大概率进行了深拷贝），产生一个 owned 的 Cow。<br />
得益于 Rust 较为智能的生命周期推导，大部分情况下，这个 Cow 实现已经足够灵活了，并且没有引用计数的开销。</p>
<p>当然，生命周期本身是个非常复杂的东西，我实际使用的时候，会稍微简化一下，像这样，用于错误处理。<br />
这是我定义的自己的错误类型</p>
<pre><code class="language-Rust">// runnable
use std::borrow::Cow;
use std::io;

// ANCHOR
#[derive(Debug)]
enum CatError {
    Custom(Cow&lt;'static, str&gt;),
}

impl CatError {
    pub fn custom&lt;S: Into&lt;Cow&lt;'static, str&gt;&gt;&gt;(s: S) -&gt; Self {
        CatError::Custom(s.into())
    }
}

fn main() {
    let err1 = CatError::custom(format!(
        &quot;error: {}&quot;,
        io::Error::from_raw_os_error(libc::EACCES)
    ));
    let err2 = CatError::custom(&quot;io error&quot;);
    println!(&quot;err1: {:?}&quot;, err1);
    println!(&quot;err2: {:?}&quot;, err2);
}
// ANCHOR_END
</code></pre>
<p>将 Cow 的借用限制为静态生命周期，也就是字符串常量。<br />
配合 <code>custom</code> 方法，在构造错误时，如果需要携带额外信息，就使用 <code>CatError::custom(format!(&quot;error: {e}&quot;))</code>—— 构造一个字符串，再用 Cow 包起来；如果不需要额外信息，就可以 <code>CatError::custom(&quot;an error occured&quot;)</code> ，直接把字符串常量包起来，没有额外分配。</p>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-1">
<p><a href="https://kaisery.github.io/trpl-zh-cn/ch08-02-strings.html#%E7%B4%A2%E5%BC%95%E5%AD%97%E7%AC%A6%E4%B8%B2">索引字符串</a>, <a href="https://doc.rust-lang.org/stable/book/ch08-02-strings.html#indexing-into-strings">indexing-into-strings</a> <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
</ol>
</section>
]]>
      </description>
      </item>
    
    <item>
      <title>Search</title>
      <link>https://lhz07.com/posts/search</link>
      <guid>https://lhz07.com/posts/search</guid>
      <pubDate>Sat, 8 Nov 2025 22:55:46 +0800</pubDate>
      <description
      ><![CDATA[<img src=/static/img/posts/search/Pasted%20image%2020250414170213.webp alt="Image" />
      <p><em>Search in artificial intelligence</em></p>
      <br>
      <h3>Search is about choice, not map</h3>
<h3>Option</h3>
<table>
<thead>
<tr>
<th></th>
<th>Backtracking</th>
<th>Use Enqueued List</th>
<th>Informed</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>British Museum</strong></td>
<td>X</td>
<td>X</td>
<td>X</td>
</tr>
<tr>
<td><strong>DFS</strong></td>
<td>✔️</td>
<td>✔️</td>
<td>X</td>
</tr>
<tr>
<td><strong>BFS</strong></td>
<td>X</td>
<td>✔️</td>
<td>X</td>
</tr>
<tr>
<td><strong>Hill Climbing</strong> (Improved DFS)</td>
<td>✔️</td>
<td>✔️</td>
<td>✔️</td>
</tr>
<tr>
<td><strong>Beam</strong> (Improved BFS)</td>
<td>X</td>
<td>✔️</td>
<td>✔️</td>
</tr>
</tbody>
</table>
<h3>Enqueue</h3>
<h4>Example Queue</h4>
<p><img src="/static/img/posts/search/Pasted%20image%2020250414170213.webp" alt="Image" /><br />
From <strong>S</strong> to <strong>G</strong> (Using DFS)<br />
<del>(S)</del><br />
<del>(S A)</del> (S B)<br />
<del>(S A B)</del> (S A D) (S B)<br />
<del>(S A B C)</del> (S A D) (S B)<br />
<del>(S A B C E)</del> (S A D) (S B)<br />
<del>(S A D)</del> (S B)<br />
(S A D G) (S B)</p>
<h3>How to Enqueue</h3>
<p><strong>DFS:</strong> Front</p>
<p><strong>BFS:</strong> Back<br />
<strong>Hill Climbing:</strong> Front Sorted<br />
<strong>Beam:</strong> Keep <em><code>W</code></em> Best</p>
<p><strong>Hill Climbing</strong> and <strong>Beam</strong>: Consider the distance to the goal</p>
<p>Consider how far we've gone so far</p>
<ol>
<li>Initialize the queue</li>
<li>Test first path in the queue</li>
</ol>
<p><img src="/static/img/posts/search/Pasted%20image%2020250414182611.webp" alt="Image" /></p>
<h3>Admissible Heuristic</h3>
<p>The heuristic estimate is guaranteed to be less than the actual distance</p>
<p>It is a perfectly sound way of doing an optimal search when about a map, but it may not work when it's not about a map.<br />
So we need <strong>Consistency</strong>.</p>
<h3>Consistent Heuristic</h3>
]]>
      </description>
      </item>
    
    <item>
      <title>实现 retain_mut_value</title>
      <link>https://lhz07.com/posts/implement-retain_mut_value</link>
      <guid>https://lhz07.com/posts/implement-retain_mut_value</guid>
      <pubDate>Tue, 7 Oct 2025 23:03:40 +0800</pubDate>
      <description
      ><![CDATA[<img src=/static/img/posts/analyze-the-retain-method-of-Vec/cover.webp alt="Image" />
      <p><em>用多种方式实现一个可以获取所有权的 retain_mut 方法</em></p>
      <br>
      <h2>前言</h2>
<p>上一次我们分析了 Vec 的 <code>retain_mut</code> 方法，这一方法可以让你安全地进行边遍历边删除的操作，并在这个过程中修改值，但是仍然不够灵活，毕竟 <code>remove</code> 方法都可以返回删除的值，凭什么 <code>retain</code> 方法就不能让我拿到被删除的值的所有权呢？这一次，我们来尝试一下实现 <code>retain_mut_value</code>，即可以边遍历边删除元素，同时还能得到被移出 Vec 的 value，从而避免 clone。<br />
<code>retain_mut</code> 函数已经为我们搭好了一个安全、高性能的框架，并且具有 panic safety，所以以下的函数都会在它的基础上进行修改。<br />
那让我们先从最简单的开始吧！</p>
<h2>retain_option</h2>
<p><code>retain_mut</code> 方法依靠返回的 <code>bool</code> 值判断是否需要删除，一个自然的想法是，我能不能不要返回 <code>bool</code> 值，而是返回 <code>Option</code> 呢？每次遍历的时候直接给闭包值的所有权，然后在闭包里可以选择把值取出来，并返回一个 <code>None</code>，或者不取出来，并返回一个 <code>Some</code>，里面包着原来的值。<br />
那么写出来大概是这样：</p>
<pre><code class="language-Rust">// runnable
use std::ptr;

struct BackshiftOnDrop&lt;'a, T&gt; {
    v: &amp;'a mut Vec&lt;T&gt;,
    processed_len: usize,
    deleted_cnt: usize,
    original_len: usize,
}

// ANCHOR
fn process_loop&lt;F, T, const DELETED: bool&gt;(
    original_len: usize,
    f: &amp;mut F,
    g: &amp;mut BackshiftOnDrop&lt;'_, T&gt;,
) where
    F: FnMut(T) -&gt; Option&lt;T&gt;,
{
    while g.processed_len != original_len {
        // SAFETY: Unchecked element must be valid.
        let cur = unsafe { g.v.as_mut_ptr().add(g.processed_len) };
        // SAFETY: We never touch the content of `cur` again after reading,
        // because the content in `cur` is invalid now.
        // However, `cur` is still a valid pointer to a blank memory.
        let element = unsafe { ptr::read(cur) };
        // Advance early to avoid double drop if `f` panicked.
        g.processed_len += 1;
        g.deleted_cnt += 1;
        match f(element) {
            None =&gt; {
                // We already advanced the counter.
                if DELETED {
                    continue;
                } else {
                    break;
                }
            }
            Some(element) =&gt; {
                // This means we don't want to delete this element, so we should restore the counter.
                g.processed_len -= 1;
                g.deleted_cnt -= 1;
                if DELETED {
                    // SAFETY: `deleted_cnt` &gt; 0, so the hole slot must not overlap with current element.
                    // We use write 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::write(hole_slot, element);
                    }
                } else {
                    // If there is not a hole slot, we move it back to the current position
                    // The element may be changed, so we should always write it back.
                    // SAFETY: `cur` is a valid pointer to a blank memory.
                    unsafe {
                        ptr::write(cur, element);
                    };
                }
                g.processed_len += 1;
            }
        }
    }
}
// ANCHOR_END

fn main() {}
</code></pre>
<p>相比 <code>retain_mut</code>，有几个需要注意的问题：</p>
<ol>
<li>我们把具有所有权的值本身传给了闭包，闭包一旦 panic，这个值会被自动释放，此时 Vec 里面绝对不能留着这个值的空洞了。所以我们需要提前增加 <code>processed_len</code> 和 <code>deleted_cnt</code>（第 15～16 行），并在返回 <code>Some</code>的情况下把值减回去。你可能注意到这里先把 <code>processed_len</code> 减回去了，后面又再加上，很奇怪的操作对吗？注意按照原方法的逻辑，此时是还没有处理过这个元素的，此时使用<code>g.v.as_mut_ptr().add(g.processed_len - g.deleted_cnt)</code> 来计算当前的空洞元素的位置是正确的。如果我们不想减回去又再加回来，那就需要改成 <code>g.v.as_mut_ptr().add(g.processed_len - 1 - g.deleted_cnt)</code>，感觉代码可读性更糟糕了，没有必要。</li>
<li>（第 36 行）当移动元素去填补空洞的时候，需要用 <code>ptr::write()</code>，而不是 <code>ptr::copy</code> 或者直接解引用赋值 <code>*hole_slot = element</code>。<code>hole_slot</code> 的值已经被释放掉了，此时再解引用显然是未定义行为。而相比 <code>retain_mut</code>，我们这里是已经取得了元素的所有权，直接 copy 的话，后面还需要 <code>mem::forget()</code> 去避免离开作用域时元素被自动释放。这里用 write 就很合适，它的语义是写一个具有所有权的值到一个地址中，里面已经处理好了自动释放的问题。</li>
<li><code>if DELETED</code>（第 30 行）在这里还有个 else 分支，这是为什么呢？因为我们已经把所有权交给闭包了，我们无法确定使用者是否会在里面修改元素的值：如果元素的值没有被修改，那当然可以不用处理，直接 forget 闭包返回的值就行，但是如果元素的值被修改了，我们需要重新把元素写回去。这就体现出这一实现的最大问题了，我们无法得知用户是否修改了元素的值，因此始终都需要把返回的 <code>Some</code> 里的元素重新写回 Vec，最坏情况下，用户一个元素都没改，却需要把整个 Vec的值都重写一遍，这显然是不合理的，我们需要一个性能更好的，不那么愚蠢的实现。</li>
</ol>
<h2>retain_mut_leak</h2>
<p>从上面的经验可以得知，把所有权交给闭包相当麻烦，并不合适，我们或许该走引用这条道。<br />
在介绍下一个方法前，我们来回顾一下 Rust 的安全保证，safe Rust 需要保证内存安全，但是需要保证内存不泄漏吗？<br />
并不需要，Rust 没有打算保证内存不泄漏，<code>Box::leak()</code> 被标记为安全方法。原因有两点：</p>
<ol>
<li>内存泄漏相较于其他的内存安全问题（解引用空指针、野指针等），影响较小，不会立刻导致程序崩溃等问题。并且，事实上并不存在真正的内存泄漏，程序退出时一定会释放掉申请的所有内存，所谓的内存泄漏，只是相当于把释放内存的代码放到了最后，当程序结束时才执行。</li>
<li>Rust 有引用计数指针，很容易就可以写出循环引用的代码，互相引用的内存永远不会被释放，而 Rust 没有 gc (垃圾回收器)，无法自动运行检测循环引用的算法并清理循环引用。这就意味着，除非把引用计数指针也标记为 unsafe，safe Rust 中依旧存在内存泄漏。</li>
</ol>
<p>综上所述，我们可以实现一个会泄漏内存的但是仍然符合 Rust 安全模型的 <code>retain_mut_leak</code> 方法，在闭包里提供对元素的可变引用，如果需要保留元素，那就不用动它，如果不需要保留元素，那就把它从 Vec 里移出来，但是不释放它的内存。<br />
在调用这个方法时，如果不需要移除元素，返回 <code>true</code> 即可，如果需要移除元素，就需要使用 unsafe 方法 <code>ptr::read()</code> 读出元素，并返回 <code>false</code> 。<br />
这个方法的实现很简单，只需要去掉标准库版本的 <code>unsafe { ptr::drop_in_place(cur) };</code> 就可以了。<br />
但是对于 panic safety 来说，还有点问题。当调用者在闭包中读出了元素，此后又 panic 了，元素会被自动 drop，从而在 Vec 里留下空洞。虽然说，这是由于调用 unsafe 方法导致的，我们可以不对此负责，但是最好还是像上个实现那样，在调用闭包前就增加 <code>processed_len</code> 和 <code>deleted_cnt</code>，并在返回 <code>true</code> 的时候减回去。这样，当调用闭包时发生了 panic，如果在读出元素之前，那就是内存泄漏，如果在读出元素之后，那就没有任何问题，无论怎样都不会在 Vec 里留下空洞。</p>
<p>这一方法完美吗？似乎还是不好，如果想获取所有权，必须使用 unsafe 操作，但其实这都还在其次，最大的问题是很容易发生内存泄漏，如果用户返回了 <code>false</code>，但是忘记了读出元素，内存会泄漏，如果读出元素前就 panic 了，还是会内存泄漏。<br />
究其原因，是由于返回 <code>false</code>( 表示保留元素与否 )，和读出元素是两个独立的操作，很容易就因为疏忽或 panic 而导致漏掉一个操作。</p>
<h2>retain_mut_value</h2>
<p>有什么办法能把两个操作封装在一起，并且保证它们之间不会发生 panic 呢？<br />
也许，就不应该直接给闭包提供元素本身的引用，我们把元素封装起来，就像标准库把指针用 Box 封装起来那样，再定义一些方法和实现 Deref trait，似乎也足够灵活，并且能把多个操作封装成一个方法调用。</p>
<h3>实现 TakeCell</h3>
<p>首先可以肯定的是，我们需要能原地修改元素，这就意味着，结构体里面存储的必须是引用或者指针。<br />
还有一点可以肯定的是，我们需要获取元素的所有权，所以这个结构体里面，必须能表达空或者非空，此处可以用 <code>Option</code> 或者指针来表示。<br />
这样一来我们就遇到了一个问题，结构体里面可能为空，难不成我们每次修改元素的时候，都要用 <code>unwrap</code> 或者直接解引用指针？还是说，每次修改元素都要做非空判断？虽然说对性能的影响很小，但是这也太难受了吧。<br />
让我们再次回顾一下 Rust 的安全模型，safe Rust 里面无论怎么操作，都不会出现 UB (未定义行为)，但是一旦涉及了 unsafe，即使是 safe Rust 代码，也可能出现 UB。比方说标准库的 <code>retain_mut</code>，如果只使用 safe 代码，确实是不会出现 UB 的，但是如果我们用了 unsafe 代码，比方说，我用 <code>ptr::read()</code> 把可变引用里的值读出来，并把值 drop 掉，再返回一个 <code>false</code>，表示这个值需要删除。那么当执行到 <code>unsafe { ptr::drop_in_place(cur) };</code> 就会发生二次释放，在标准库的 safe 方法里产生了 UB 😱</p>
<p>所以，我们完全可以采取不同的思路，Rust 的设计是，产生指针、获取指针是安全的，只有解引用指针是不安全的。我们可以反过来，只有创建结构体或者把值从结构体里面取出来的时候是 unsafe，其他时候都是 safe。这样一来，我们确保了只有 safe 方法不会导致 UB，而涉及到 unsafe 的时候，就不需要我们负责了。<br />
使用指针的实现如下：</p>
<pre><code class="language-Rust">// runnable
use std::ops::{Deref, DerefMut};
use std::ptr;

// ANCHOR
pub struct TakeCell&lt;T&gt; {
    value: *mut T,
}

impl&lt;T&gt; TakeCell&lt;T&gt; {
    /// # Safety
    /// The caller must ensure one of the following rules:
    /// - the `value` is valid.
    /// - the `value` is null, and will never be used.
    unsafe fn new(value: *mut T) -&gt; Self {
        TakeCell { value }
    }
    /// # Safety
    /// - The caller must ensure that `TakeCell` is not taken before calling this method.
    pub unsafe fn take_inner(&amp;mut self) -&gt; T {
        debug_assert!(self.is_valid(), &quot;TakeCell is already taken&quot;);
        // SAFETY: We ensure that `value` is valid and not null.
        let inner = unsafe { ptr::read(self.value) };
        self.value = ptr::null_mut(); // Mark as taken
        inner
    }
    pub fn is_valid(&amp;self) -&gt; bool {
        !self.value.is_null()
    }
}

impl&lt;T&gt; Deref for TakeCell&lt;T&gt; {
    type Target = T;

    fn deref(&amp;self) -&gt; &amp;Self::Target {
        debug_assert!(self.is_valid(), &quot;TakeCell is already taken&quot;);
        // SAFETY: We ensure that `value` is valid and not null.
        unsafe { &amp;*self.value }
    }
}

impl&lt;T&gt; DerefMut for TakeCell&lt;T&gt; {
    fn deref_mut(&amp;mut self) -&gt; &amp;mut Self::Target {
        debug_assert!(self.is_valid(), &quot;TakeCell is already taken&quot;);
        // SAFETY: We ensure that `value` is valid and not null.
        unsafe { &amp;mut *self.value }
    }
}
// ANCHOR_END

fn main() {}
</code></pre>
<p>实现相当简单，我就不过多赘述了。当然，操作指针可能还显得有点危险，我们完全可以实现一个使用 <code>Option</code> 与引用的版本。</p>
<h3>实现 TakeRef</h3>
<pre><code class="language-Rust">// runnable
use std::ops::{Deref, DerefMut};
use std::ptr;

// ANCHOR
pub struct TakeRef&lt;'a, T&gt; {
    value: Option&lt;&amp;'a mut T&gt;,
}

impl&lt;'a, T&gt; TakeRef&lt;'a, T&gt; {
    fn new(value: &amp;'a mut T) -&gt; Self {
        TakeRef { value: Some(value) }
    }

    /// # Safety
    /// The caller must ensure the `TakeRef` will never be used.
    unsafe fn new_none() -&gt; Self {
        TakeRef { value: None }
    }

    /// # Safety
    /// - The caller must ensure that `TakeRef` is not taken before calling this method.
    pub unsafe fn take_inner(&amp;mut self) -&gt; T {
        debug_assert!(self.is_valid(), &quot;TakeRef is already taken&quot;);
        // SAFETY: We ensure that `value` is some.
        let inner = unsafe { ptr::read(self.value.take().unwrap_unchecked()) };
        inner
    }

    pub fn is_valid(&amp;self) -&gt; bool {
        self.value.is_some()
    }
}

impl&lt;'a, T&gt; Deref for TakeRef&lt;'a, T&gt; {
    type Target = T;

    fn deref(&amp;self) -&gt; &amp;Self::Target {
        debug_assert!(self.is_valid(), &quot;TakeRef is already taken&quot;);
        // SAFETY: We ensure that `value` is some.
        unsafe { self.value.as_deref().unwrap_unchecked() }
    }
}

impl&lt;'a, T&gt; DerefMut for TakeRef&lt;'a, T&gt; {
    fn deref_mut(&amp;mut self) -&gt; &amp;mut Self::Target {
        debug_assert!(self.is_valid(), &quot;TakeRef is already taken&quot;);
        // SAFETY: We ensure that `value` is some.
        unsafe { self.value.as_deref_mut().unwrap_unchecked() }
    }
}
// ANCHOR_END

fn main() {}
</code></pre>
<p>这里的 new 可以被标记为 safe 方法，因为 Rust 的引用总是有效的，存入一个有效的引用，后续使用的时候总是安全的。<br />
在初始化 DropGuard 的时候，我们需要一个占位的 TakeRef，它永远不会被使用，所以我在这里还写了一个 unsafe 的 <code>new_none</code> 方法。<br />
在两种实现里我都加入了许多 <code>debug_assert</code>，用于保证这个结构体的实现没有出错。其实，如果你想要一个完全安全的版本，可以把所有的 <code>debug_assert</code> 都改为 <code>assert</code>，再把所有的 unsafe 标记移除掉就行了。但我觉得这样性能太差了，适当地用一点 unsafe，可以在符合 Rust 的安全模型的情况下，移除掉这些 <code>assert</code>。</p>
<h3>实现 retain_mut_value</h3>
<p>在实现了 TakeRef / TakeCell 之后，我们就可以着手实现方法本身了，使用两种结构体的实现差不多，在此仅介绍使用 TakeRef 的实现。</p>
<pre><code class="language-Rust">// runnable
use std::ops::{Deref, DerefMut};
use std::ptr;

pub struct TakeRef&lt;'a, T&gt; {
    value: Option&lt;&amp;'a mut T&gt;,
}

impl&lt;'a, T&gt; TakeRef&lt;'a, T&gt; {
    fn new(value: &amp;'a mut T) -&gt; Self {
        TakeRef { value: Some(value) }
    }

    /// # Safety
    /// The caller must ensure the `TakeRef` will never be used.
    unsafe fn new_none() -&gt; Self {
        TakeRef { value: None }
    }

    /// # Safety
    /// - The caller must ensure that `TakeRef` is not taken before calling this method.
    /// - The caller must ensure that the `TakeRef` is not used after taking the inner value.
    pub unsafe fn take_inner(&amp;mut self) -&gt; T {
        debug_assert!(self.is_valid(), &quot;TakeRef is already taken&quot;);
        // SAFETY: We ensure that `value` is some.
        let inner = unsafe { ptr::read(self.value.take().unwrap_unchecked()) };
        inner
    }

    pub fn is_valid(&amp;self) -&gt; bool {
        self.value.is_some()
    }
}

impl&lt;'a, T&gt; Deref for TakeRef&lt;'a, T&gt; {
    type Target = T;

    fn deref(&amp;self) -&gt; &amp;Self::Target {
        debug_assert!(self.is_valid(), &quot;TakeRef is already taken&quot;);
        // SAFETY: We ensure that `value` is some.
        unsafe { self.value.as_deref().unwrap_unchecked() }
    }
}

impl&lt;'a, T&gt; DerefMut for TakeRef&lt;'a, T&gt; {
    fn deref_mut(&amp;mut self) -&gt; &amp;mut Self::Target {
        debug_assert!(self.is_valid(), &quot;TakeRef is already taken&quot;);
        // SAFETY: We ensure that `value` is some.
        unsafe { self.value.as_deref_mut().unwrap_unchecked() }
    }
}

trait RetainValue&lt;T&gt; {
    fn retain_mut_value_another&lt;F&gt;(&amp;mut self, f: F)
    where
        F: FnMut(&amp;mut TakeRef&lt;T&gt;);
}

// ANCHOR
impl&lt;T&gt; RetainValue&lt;T&gt; for Vec&lt;T&gt; {
    fn retain_mut_value_another&lt;F&gt;(&amp;mut self, mut f: F)
    where
        F: FnMut(&amp;mut TakeRef&lt;T&gt;),
    {
        struct BackshiftOnDrop&lt;'a, T&gt; {
            v: &amp;'a mut Vec&lt;T&gt;,
            processed_len: usize,
            deleted_cnt: usize,
            original_len: usize,
            take_ref: TakeRef&lt;'a, T&gt;,
            processed: bool,
        }

        impl&lt;T&gt; Drop for BackshiftOnDrop&lt;'_, T&gt; {
            fn drop(&amp;mut self) {
                if !self.processed &amp;&amp; !self.take_ref.is_valid() {
                    self.deleted_cnt += 1;
                    self.processed_len += 1;
                }
                if self.deleted_cnt &gt; 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);
                }
            }
        }

        let original_len = self.len();
        if original_len == 0 {
            // Empty case: explicit return allows better optimization, vs letting compiler infer it
            return;
        }
        // Avoid double drop if the drop guard is not executed,
        // since we may make some holes during the process.
        unsafe { self.set_len(0) };

        let mut g = BackshiftOnDrop {
            v: self,
            processed_len: 0,
            deleted_cnt: 0,
            original_len,
            // SAFETY: `original_len` is always greater than 0, so the loop will always run at least once.
            // Therefore, this `none` will definitely be replaced.
            take_ref: unsafe { TakeRef::new_none() },
            processed: false,
        };

        fn process_loop&lt;F, T, const DELETED: bool&gt;(
            original_len: usize,
            f: &amp;mut F,
            g: &amp;mut BackshiftOnDrop&lt;'_, T&gt;,
        ) where
            F: FnMut(&amp;mut TakeRef&lt;T&gt;),
        {
            while g.processed_len != original_len {
                // SAFETY: Unchecked element must be valid.
                let cur = unsafe { &amp;mut *g.v.as_mut_ptr().add(g.processed_len) };
                let take_ref = TakeRef::new(cur);
                // Reset processed flag for each element
                g.processed = false;
                // Update the TakeRef
                g.take_ref = take_ref;
                f(&amp;mut g.take_ref);
                if !g.take_ref.is_valid() {
                    // advance the counter
                    g.processed_len += 1;
                    g.deleted_cnt += 1;
                    // Mark as processed
                    g.processed = true;
                    if DELETED {
                        continue;
                    } else {
                        break;
                    }
                }
                if DELETED {
                    // SAFETY: `deleted_cnt` &gt; 0, so the hole slot must not overlap with current element.
                    // We use write 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(
                            g.take_ref.value.as_deref().unwrap_unchecked(),
                            hole_slot,
                            1,
                        );
                    }
                }
                // Advance the counter and mark as processed
                g.processed_len += 1;
                g.processed = true;
            }
        }
        // Stage 1: Nothing was deleted.
        process_loop::&lt;F, T, false&gt;(original_len, &amp;mut f, &amp;mut g);
        // Stage 2: Some elements were deleted.
        process_loop::&lt;F, T, true&gt;(original_len, &amp;mut f, &amp;mut g);
        // All item are processed. This can be optimized to `set_len` by LLVM.
        drop(g);
    }
}

fn main() {
    let mut list = [&quot;apple&quot;, &quot;banana&quot;, &quot;cat&quot;]
        .into_iter()
        .map(|s| s.to_string())
        .collect::&lt;Vec&lt;_&gt;&gt;();
    let mut fruit_list = Vec::new();
    list.retain_mut_value_another(|s| {
        if &amp;**s == &quot;apple&quot; || &amp;**s == &quot;banana&quot; {
            let str = unsafe { s.take_inner() };
            fruit_list.push(str);
        }
    });
    println!(&quot;list: {:?}&quot;, list);
    println!(&quot;fruits: {:?}&quot;, fruit_list);
}
// ANCHOR_END
</code></pre>
<p>相比标准库，我们这里要改动的东西不少：</p>
<ol>
<li>如果闭包在执行的时候 panic 了，那么我们需要移动元素，填补 Vec 里的空洞，这里的情况有些不同，我们无法直接确定要填补的空洞，因为 TakeRef 此时可能被 <code>take_inner</code> 了，也可能没有。如果里面的值已经被取走了，那此时 Vec 里出现了一个空洞，应该把它填上；如果里面的值还没有被取走，也就是说，可能维持原样，或者被修改了，此时就不能动 Vec 里的值。因此，我们需要在 drop guard 里存储当前的 TakeRef，用于在 <code>drop</code> 方法中进行判断。我们还需要在 drop guard 里存储一个布尔值，在这里是 <code>processed</code>，用于判断当前的 TakeRef 是否被妥善处理了。只有当 <code>processed</code> 为 <code>false</code>，且 TakeRef 里的值已经被取走了，才需要在 drop 方法里额外填补一个空洞，否则无需处理。</li>
<li>由于无论是否 panic，TakeRef 都会记录下来当前的值是否已经被取走，所以我们无需提前增加 <code>processed_len</code> 和 <code>deleted_cnt</code>，维持标准库的写法即可。</li>
<li>你可能会注意到，在第 50 行，初始化 <code>g</code> 的时候，使用了一个 <code>new_none</code>，这个 TakeRef 有没有可能被实际使用呢？绝无可能。当 Vec 的长度为 0 时，我们早就在第 43 行显式返回了。所以如果能继续往下走，意味着至少需要进行一次 <code>process_loop</code>，只要进入了循环，这个存储着 <code>None</code> 的 TakeRef 立刻会被换成一个存储着有效引用的 TakeRef。</li>
</ol>
<h2>结束了</h2>
<p>大概就是这些内容，全套源码和测试在我的 <a href="https://github.com/lhz07/my_rust">GitHub 仓库</a> 中。使用 <code>cargo +nightly miri test</code> 进行测试，避免出现 UB 或者其他的小问题。</p>
<h2>其他</h2>
<h3>关于 panic safety</h3>
<p>Rust 的 panic safety 一般对应于 C++ 的 Basic exception safety，也有的会对应于 Strong exception safety，比如 Vec 的 <code>insert</code> 方法。但不同的是，C++ 很依赖异常来处理错误，除了 Google 的代码规范不使用异常以外，大部分 C++ 代码都会使用异常。而 Rust 的 panic 主要用于不可恢复的错误，大部分错误处理其实是由 Result 和 Option 来完成的。<br />
那为什么还要实现 panic safety？<br />
一方面是为了保证 safe Rust 的安全性，另一方面则是，你不知道别人会怎么处理错误，有的人觉得 panic 虽然慢一点，但是也能接受，标准库需要照顾到各种需求，所以我们也顺便实现一下。<br />
举一个使用 panic 处理错误的例子：很多时候会用到线程来并发地执行任务，如果不需要频繁地、快速地从线程中恢复错误，其实 panic 是一个可接受的选项，此时 panic safety 就很重要了，如果保证了 panic safety，那即使线程 panic 了，共享的数据结构也还能用，否则一旦线程 panic，整个数据结构就像被污染了一样，完全不能用了。<br />
还有一点则是，有的库内部认为这个错误不可恢复，所以使用了 panic，但是可能在实际使用中，需要恢复这个错误，这个时候会用 <code>catch_unwind</code> 来处理，此时 panic safety 也很重要。</p>
]]>
      </description>
      </item>
    
    <item>
      <title>Qt 程序的打包部署</title>
      <link>https://lhz07.com/posts/qt-deployment</link>
      <guid>https://lhz07.com/posts/qt-deployment</guid>
      <pubDate>Thu, 25 Sep 2025 19:17:53 +0800</pubDate>
      <description
      ><![CDATA[<img src=/static/img/posts/qt-deployment/cover.webp alt="Image" />
      <p><em>介绍如何将 Linux 上的 Qt 程序打包部署到其他 Linux 系统上运行</em></p>
      <br>
      <p>一直以来，Qt<sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup> 程序的打包部署都相当困难，今天我们进行一些简单的尝试。<br />
由于静态编译较复杂，且可能涉及违反 Qt 的 LGPL 协议问题，故仅介绍动态编译。<br />
Windows 和 macOS 下都有官方提供的 windowsdeployqt 或 macdeployqt，一键部署还是比较方便的，但是 Linux 下官方没有提供类似的软件，可以尝试 <a href="https://github.com/probonopd/linuxdeployqt">linuxdeployqt</a>，但是有两点限制：</p>
<ol>
<li>不支持部署 Wayland 程序</li>
<li>必须使用当前仍受支持的最低版本 Ubuntu LTS 进行部署</li>
</ol>
<p>所以我们还是来看看 Qt 官方的解决方案吧。</p>
<h4>链接动态库</h4>
<p>根据 Qt 的官方文档 <a href="https://doc.qt.io/qt-6/linux-deployment.html">Qt for Linux/X11 - Deployment</a>，动态编译的主要困难在于如何让编译出来的可执行文件能够找到需要的动态链接库。在文档中列举了三种方法：</p>
<ol>
<li>Install the Qt libraries in one of the system library paths (e.g. <code>/usr/lib</code> on most systems).</li>
<li>Pass a predetermined path to the <code>-rpath</code> command-line option when linking the application. This will tell the dynamic linker to look in this directory when starting your application.</li>
<li>You can write a startup script for your application, where you modify the dynamic linker configuration (e.g., adding your application's directory to the <code>LD_LIBRARY_PATH</code> environment variable).</li>
</ol>
<p>第一种方法其实是最不容易出现找不到库的错误的，只要使用者将所需的 Qt 及其他的动态库都安装到系统的 <code>/usr/lib</code> 下就可以。但是缺点也很明显，系统的动态库目录下可能已经有 Qt 库了（比如已经安装了 KDE ），显然不能重复安装，这个时候你的 Qt 程序是否能正确运行，就要看你使用的 Qt 版本和系统已有的 Qt 版本的兼容性如何了。</p>
<p>第三种方法也很简单，但是有以下两个缺点：</p>
<ul>
<li><strong>Note:</strong> If your application will be running with &quot;Set user ID on execution,&quot; and if it will be owned by root, then LD_LIBRARY_PATH will be ignored on some platforms. In this case, use of the LD_LIBRARY_PATH approach is not an option). 就是说这个方法有可能会失效，并且也没有任何补救的办法</li>
<li>不能直接运行可执行文件，而是要运行脚本来启动程序，显得非常不优雅，并且不符合使用直觉</li>
</ul>
<p>下面我们来介绍第二种方法，Qt 的文档给出的方法是修改编译程序的命令，但是在较新版本的 Linux 下，CMake 编译时设置的 rpath 其实是 runpath，而 runpath 的优先级要低于环境变量 LD_LIBRARY_PATH和系统默认路径，这就意味着，如果你的程序使用的库较新，而实际运行的系统库较旧，且系统库里恰好有同名的运行库，就会导致严重的兼容性问题，因此我们使用 patchelf 来设置动态库的路径。</p>
<p>我们需要用到如下软件：</p>
<ul>
<li><code>strings</code></li>
<li><code>patchelf</code></li>
<li><code>ldd</code></li>
</ul>
<p>对于不同的 GNU/Linux 发行版，它们可能包含在不同的软件包中。</p>
<p>首先使用 <code>ldd ./application</code> 命令查找程序依赖，其中的<code>application</code>参数可以是可执行文件，也可以是动态库，对于 Wayland 程序，我们主要是查找程序本身的依赖，以及<code>libqwayland-generic.so</code>(这是什么？请看插件一节) 的依赖。并将所需依赖复制到<code>lib</code>文件夹中去（假设我们的目录结构分为<code>bin</code>、<code>lib</code>、<code>plugins</code>三个文件夹）。<br />
这里可以使用一个脚本来帮我们快速地复制这些动态库。</p>
<pre><code class="language-bash">#!/bin/bash
exe=&quot;application&quot;
des=&quot;$(pwd)&quot;
deplist=$(ldd $exe | awk  '{if (match($3,&quot;/&quot;)){ printf(&quot;%s &quot;),$3 } }')
mkdir ./lib
cp $deplist $des/lib
</code></pre>
<p>将这个脚本和需要查找依赖的文件放在同一目录，并将<code>exe</code>的值改为该文件的名字，运行脚本即可。</p>
<p><strong>注意：是将脚本放在需要查找依赖的文件的目录下，如果将 Qt 的动态库文件移动到了别的目录下，可能导致查找到的依赖是系统的 Qt 库，而 Qt Creator 编译出来的可执行文件一般会链接到你用于开发的 Qt 库，一旦这两个库的版本不一致，就会导致不同版本的 Qt 库混用问题，程序根本无法运行。</strong></p>
<p>下面使用<code>patchelf</code>修改<code>rpath</code>，运行如下命令</p>
<pre><code class="language-bash">patchelf --force-rpath --set-rpath '$ORIGIN/to/lib' ./application
</code></pre>
<p><code>--force-rpath</code>的作用是确保设置的是rpath而不是runpath</p>
<p>其中的<code>$ORIGIN/to/lib</code>是程序查找动态库的路径，<code>$ORIGIN</code>是应用程序所在的目录</p>
<p><strong>注意：</strong></p>
<ol>
<li><strong><code>patchelf</code>会修改可执行文件，操作前建议备份原始文件</strong></li>
<li><strong>一定要使用单引号，否则终端会对<code>$ORIGIN</code>进行解释，导致路径设置出错</strong></li>
</ol>
<p>完成后使用如下命令查看<code>rpath</code>是否成功修改</p>
<pre><code class="language-bash">readelf -d ./application
</code></pre>
<p>如果你使用的 glibc 版本较高，应用程序在其他系统上运行时可能会出现兼容问题，因此还要设置<code>interpreter</code></p>
<pre><code class="language-bash">patchelf --set-interpreter /path/to/lib/ld-linux ./application
</code></pre>
<p><code>lib</code>目录下有一个文件的名字是以<code>ld-linux</code>开头的，将它的路径填入即可。</p>
<p><strong>注意：此处的路径只能使用绝对路径，因此，如果希望程序能在较低版本的系统上运行，要么是能保证安装在固定位置，要么就使用低版本的系统进行编译，低版本的 glibc 对高版本有一定的兼容性。</strong></p>
<p>可通过如下命令查看是否设置成功</p>
<pre><code class="language-bash">strings ./application | grep ld-linux
</code></pre>
<p>现在，如果你使用<code>ldd</code>命令查看程序依赖的话，应该会发现绝大多数的依赖都已经指向了你新设置的目录（当然要先把<code>lib</code>文件夹拷到对应的路径啊）。这意味着我们大部分的工作已经完成了，但是并不代表程序已经可以在任何系统上都正常运行了。</p>
<h4>插件</h4>
<p>根据 Qt 官方文档，任何 Qt GUI程序运行时，都需要一个用于实现 QPA 层的插件。因此，我们需要将<code>path/to/qt/version/gcc_64/plugins</code>目录下的<code>platforms</code>文件夹复制到程序的运行目录下。其中就包含上面提到的<code>libqwayland-generic.so</code>。当然，你的 Qt 程序可能还用到了其他插件，需要一并拷贝到程序的运行目录下。</p>
<h5>如何知道需要哪些插件？</h5>
<p>目前尚没有简单的解决办法，只能根据经验和插件名称进行推测，以及在不同系统上测试时有无功能缺失来了解。</p>
<h4>测试</h4>
<p>测试系统需要是没有安装 Qt 环境的，并且系统版本最好旧一些。然后根据程序运行时的报错，来添加所需的动态库。</p>
<p>比如我尝试在 Ubuntu 22.04.5 上运行我的程序，出现如下错误：</p>
<pre><code class="language-shell">QSystemTrayIcon::setVisible: No Icon set
Could not create decoration from factory! Running with no decorations.
</code></pre>
<p>第一个错误，推测可能是 iconengines 缺少依赖，使用<code>ldd</code>查找对应的依赖，发现少了<code>libQt6Svg.so.6</code><br />
第二个错误，推测可能是 wayland-decoration-client 缺少依赖，查找后发现也是少了<code>libQt6Svg.so.6</code><br />
将用于开发的 Qt 库目录下的<code>libQt6Svg.so.6</code>文件复制到<code>lib</code>文件夹中，程序不再报错。</p>
<h4>其他</h4>
<h5>设置 rpath 后有部分动态库还是找不到</h5>
<p>使用如下命令手动添加找不到的库</p>
<pre><code class="language-bash">patchelf --add-needed /path/to/library.so ./application
</code></pre>
<h5>调整 plugins 文件夹的位置</h5>
<p>比如我不想在程序运行目录下放 plugins 文件夹，那么我可以在程序目录下放一个<code>qt.conf</code>文件，在其中指定 plugins 文件夹的位置</p>
<pre><code class="language-conf">[Paths]
Prefix = ./../                       # 下面的设置项的路径前缀
Plugins = plugins                  # plugins文件夹的路径
</code></pre>
<p>实际上的 plugins 文件夹路径等于 Prefix 加上 Plugins，即 <code>./../plugins</code></p>
<p>等同于这么写</p>
<pre><code class="language-conf">[Paths]
Prefix = ./                       # 下面的设置项的路径前缀
Plugins = ../plugins                  # plugins文件夹的路径
</code></pre>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-1">
<p>Qt is pronounced as &quot;cute&quot;, not &quot;cu-tee&quot; <a href="https://wiki.qt.io/Qt_for_Beginners#Introduction_to_Qt">https://wiki.qt.io/Qt_for_Beginners#Introduction_to_Qt</a> <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
</ol>
</section>
]]>
      </description>
      </item>
    
    <item>
      <title>Vec 的 retain 方法分析</title>
      <link>https://lhz07.com/posts/analyze-the-retain-method-of-Vec</link>
      <guid>https://lhz07.com/posts/analyze-the-retain-method-of-Vec</guid>
      <pubDate>Tue, 23 Sep 2025 20:15:07 +0800</pubDate>
      <description
      ><![CDATA[<img src=/static/img/posts/analyze-the-retain-method-of-Vec/cover.webp alt="Image" />
      <p><em>简单分析 Rust 中 Vec 的 retain 方法的实现</em></p>
      <br>
      <pre><code class="language-Rust">/// Retains only the elements specified by the predicate, passing a mutable reference to it.
///
/// In other words, remove all elements `e` such that `f(&amp;mut e)` returns `false`.
/// This method operates in place, visiting each element exactly once in the
/// original order, and preserves the order of the retained elements.
///
/// # Examples
///
/// ```
/// let mut vec = vec![1, 2, 3, 4];
/// vec.retain_mut(|x| if *x &lt;= 3 {
///     *x += 1;
///     true
/// } else {
///     false
/// });
/// assert_eq!(vec, [2, 3, 4]);
/// ```
#[stable(feature = &quot;vec_retain_mut&quot;, since = &quot;1.61.0&quot;)]
pub fn retain_mut&lt;F&gt;(&amp;mut self, mut f: F)
where
    F: FnMut(&amp;mut T) -&gt; bool,
{
    let original_len = self.len();

    if original_len == 0 {
        // Empty case: explicit return allows better optimization, vs letting compiler infer it
        return;
    }

    // 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]
    //      |&lt;-              processed len   -&gt;| ^- next to check
    //                  |&lt;-  deleted cnt     -&gt;|
    //      |&lt;-              original_len                          -&gt;|
    // 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.
    struct BackshiftOnDrop&lt;'a, T, A: Allocator&gt; {
        v: &amp;'a mut Vec&lt;T, A&gt;,
        processed_len: usize,
        deleted_cnt: usize,
        original_len: usize,
    }

    impl&lt;T, A: Allocator&gt; Drop for BackshiftOnDrop&lt;'_, T, A&gt; {
        fn drop(&amp;mut self) {
            if self.deleted_cnt &gt; 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);
            }
        }
    }

    let mut g = BackshiftOnDrop {
        v: self,
        processed_len: 0,
        deleted_cnt: 0,
        original_len,
    };

    fn process_loop&lt;F, T, A: Allocator, const DELETED: bool&gt;(
        original_len: usize,
        f: &amp;mut F,
        g: &amp;mut BackshiftOnDrop&lt;'_, T, A&gt;,
    ) where
        F: FnMut(&amp;mut T) -&gt; bool,
    {
        while g.processed_len != original_len {
            // SAFETY: Unchecked element must be valid.
            let cur = unsafe { &amp;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.
                if DELETED {
                    continue;
                } else {
                    break;
                }
            }
            if DELETED {
                // SAFETY: `deleted_cnt` &gt; 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;
        }
    }

    // Stage 1: Nothing was deleted.
    process_loop::&lt;F, T, A, false&gt;(original_len, &amp;mut f, &amp;mut g);

    // Stage 2: Some elements were deleted.
    process_loop::&lt;F, T, A, true&gt;(original_len, &amp;mut f, &amp;mut g);

    // All item are processed. This can be optimized to `set_len` by LLVM.
    drop(g);
}
</code></pre>
<h3>为什么需要两个<code>process_loop</code>？</h3>
<p>在不删除元素的情况下，只需要遍历一遍所有元素，无需多余操作。<br />
但是一旦需要删除元素，即使只删除 1 个元素，也需要把后面的元素挨个向左移动一次。<br />
所以第一个 <code>process_loop</code> 就是处理没有删除元素的情况，此时：</p>
<ol>
<li>如果遍历完整个 <code>Vec</code> 也没有遇到需要删除的元素，第二个 <code>process_loop</code> 也不会运行。</li>
<li>如果遇到需要删除的元素，证明后面的元素都要进行一次移动了，所以运行第二个 <code>process_loop</code>。在这个循环里面，将从之前未处理过的元素继续遍历，每次遍历的时候，都将后面的元素经过一次移动直接到位——从当前位置往回移动 x 位（ x 即为已经删除掉的元素数量）。因为此时至少有一个元素被删除了，所以绝对需要复制到前面的位置，而不是原地复制，这里使用 <code>copy_nonoverlapping</code> 可以进行更激进的优化</li>
</ol>
<h4>模版优化</h4>
<p>这里还有一个十分有用的优化，<code>fn process_loop&lt;F, T, A: Allocator, const DELETED: bool&gt;</code> 这个函数定义中的 <code>const DELETED: bool</code> ，表明 <code>DELETED</code> 是一个常量，编译的时候，会根据模版生成对应的两套代码，并且在这两套代码中，由于 <code>DELETED</code> 是常量，两个 <code>if DELETED</code> 分支都可以直接被优化掉！<br />
事实上，只用一个 <code>process_loop</code> ，再加上一个 bool 变量用于判断，也是可以实现的，但是每次循环都需要多加一个 if 判断，而这个实现可以把 if 判断给优化掉</p>
<h3>时间复杂度</h3>
<p>虽然进行了两次 <code>process_loop</code> ，但第二次是接着第一次继续运行的，没有重复处理元素，时间复杂度为 O(n)，n 为原 <code>Vec</code> 的长度，非常高效。</p>
<h3>空间复杂度</h3>
<p>只使用了数个 <code>usize</code> 变量和引用/指针，复杂度为 O(1)，非常高效。</p>
<h3><code>panic</code> 时的安全处理</h3>
<p>传入 <code>retain</code> 的用于判断的闭包，和 <code>Vec</code> 中元素的 <code>Drop</code> 方法，都是有可能 <code>panic</code> 的，所以还需要在发生 <code>panic</code> 的时候进行安全处理，不能把已经产生的空洞留在 <code>Vec</code> 里面，也就是说，我们要保证即使发生了 <code>panic</code> ，这个 <code>Vec</code> 里的元素依旧全部都是有效的，没有空洞（已经释放掉的元素）。<br />
在这里使用了常见的 drop guard 方式，为<code>struct BackshiftOnDrop&lt;'a, T, A: Allocator&gt;</code> 实现了 <code>Drop</code> trait，这样它会在被释放的时候自动调用我们给它实现的 <code>drop</code> 方法。在我们实现的 <code>drop</code> 方法里面，需要做两件事：</p>
<ol>
<li>把后面未处理的元素全部搬到前面来，补上之前由于删除造成的空洞</li>
<li>重新设置 <code>Vec</code> 的长度，把已经删除的元素数量从中减去</li>
</ol>
<h4>如果我们没有发生 panic 呢？</h4>
<ol>
<li>当 drop 发生时，所有元素都被处理完了，这段代码</li>
</ol>
<!-- end list -->
<pre><code class="language-Rust">struct BackshiftOnDrop&lt;'a, T, A: Allocator&gt; {
    v: &amp;'a mut Vec&lt;T, A&gt;,
    processed_len: usize,
    deleted_cnt: usize,
    original_len: usize,
}

impl&lt;T, A: Allocator&gt; Drop for BackshiftOnDrop&lt;'_, T, A&gt; {
    fn drop(&amp;mut self) {
        if self.deleted_cnt &gt; 0 {
            // ANCHOR
            // 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,
                );
            }
            // ANCHOR_END
        }
        // SAFETY: After filling holes, all items are in contiguous memory.
        unsafe {
            self.v.set_len(self.original_len - self.deleted_cnt);
        }
    }
}
</code></pre>
<p>实际上是 <code>ptr::copy(x, y, 0);</code> ，需要复制的长度为 0 ，编译器会把它优化掉。<br />
2. 仍然会重新设置 <code>Vec</code> 的长度，把已经删除的元素数量从中减去</p>
<h4>drop guard 方法的有效性</h4>
<p>当程序 <code>panic</code> 的时候，会自动调用当前使用的所有变量的 <code>drop</code> 方法，在同作用域内按照声明的顺序释放，先释放当前作用域，再从里到外挨个释放外部的作用域。但是，有两种情况会使 drop guard 失效：</p>
<ol>
<li>在 <code>panic</code> 之后，调用 <code>drop</code> 方法的时候又发生了 <code>panic</code> ，此时 Rust 为了避免无限 <code>panic</code> ，会直接 <code>abort</code> ，即中止程序，剩下变量的 <code>drop</code> 方法都不会被运行。</li>
<li>有时为了缩小程序的体积，可能会在 <code>Cargo.toml</code> 中把 <code>panic</code> 都改为 <code>abort</code> (来源：<a href="https://github.com/johnthagen/min-sized-rust?tab=readme-ov-file#abort-on-panic">min-sized-rust</a>)，这样程序一旦出错会直接中止，不会打印 <code>panic</code> 的 traceback 信息，这时也会导致 drop guard 失效。</li>
</ol>
]]>
      </description>
      </item>
    
  </channel>
</rss>
