Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 国产99久9在线,99视频久久,精品女同一区二区三区在线观看

          整合營銷服務(wù)商

          電腦端+手機端+微信端=數(shù)據(jù)同步管理

          免費咨詢熱線:

          如何將PDF轉(zhuǎn)HTML格式?試試這些PDF轉(zhuǎn)HTML轉(zhuǎn)換器

          TML是一種超文本標記語言,易于編碼、編寫,而PDF因為其不易修改便于傳輸?shù)奶匦猿蔀橐环N辦公常用到的文件格式,作為職場人的我們,有時候就需要對這兩種格式進行轉(zhuǎn)換,將拿到手的PDF文件轉(zhuǎn)換成HTML格式,然后再進行編碼等其它操作,那么如何將PDF轉(zhuǎn)HTML格式?來看看這些方法吧。

          方法一:借助“全能PDF轉(zhuǎn)換助手”進行操作

          該軟件支持PDF與其它多種格式進行相互轉(zhuǎn)換操作,還有PDF編輯、加密、壓縮、處理等工具。它的“PDF轉(zhuǎn)HTML”功能,可以添加多個文件進行批量轉(zhuǎn)換;對于頁數(shù)比較多的PDF文件,如果我們只需要其中幾頁,就可以自定義轉(zhuǎn)換頁面,勾選頁面或者輸入頁碼都可以實現(xiàn)自定義操作。

          具體操作步驟如下:

          步驟一:

          點擊“PDF選其他”,然后選擇“PDF轉(zhuǎn)HTML”,添加需要轉(zhuǎn)換的文件,可以添加單個或者多個PDF文件。

          步驟二:選擇要轉(zhuǎn)換的頁面,點擊“頁碼選擇”,支持輸入頁碼選擇頁面,也可以手動勾選,在如果需要勾選的頁面過多,可以勾選不需要轉(zhuǎn)換的頁面,然后點擊“反選”,就可以選擇需要的頁面了,之后點擊“開始轉(zhuǎn)換”,等待轉(zhuǎn)換完成就可以了。

          它還有APP可以使用,我們可以在手機上對PDF進行格式轉(zhuǎn)換、編輯、解密等操作,方便了我們的辦公生活。

          方法二:借助“WPS office”進行操作

          這是一款支持Word、Excel等格式進行編輯的軟件,我們可以通過它將PDF轉(zhuǎn)成HTML格式,但是它不能直接將PDF轉(zhuǎn)換成HTML格式,需要先轉(zhuǎn)換成Word的格式再進行下一步操作。

          操作步驟如下:

          步驟一:打開PDF文件,點擊上方工具欄的“轉(zhuǎn)換”,然后選擇“PDF轉(zhuǎn)Word”,等待轉(zhuǎn)換完成。

          步驟二:打開轉(zhuǎn)成Word格式的文件,點擊上方工具欄的“文件”,然后選擇“另存為”,在彈出的頁面,更改文件類型為“單一網(wǎng)頁文件*.mht;*mhtml”,接下來點擊“保存”,就可以得到HTML格式的文件了。

          看了上面的方法,你知道如何將PDF轉(zhuǎn)HTML格式了嗎?操作不算難,跟著上面的步驟操作就可以實現(xiàn)PDF轉(zhuǎn)成HTML格式啦,有需要的朋友,可以試一試哦。

          大家知道HTML格式嗎?我們通常上網(wǎng)瀏覽的網(wǎng)頁就是HTML格式。而PDF格式是我們常用的一種文件格式,在不同的設(shè)備上打開,既不會影響到PDF內(nèi)容的排版,也不容易被修改。在工作中,有時為了查看PDF文件在網(wǎng)頁狀態(tài)下的排版,以及對內(nèi)容進行編輯修改,我們需要將PDF轉(zhuǎn)成HTML。可能有些小伙伴們不知道如何轉(zhuǎn)換。別著急,今天這期PDF轉(zhuǎn)HTML轉(zhuǎn)換器推薦,給大家做一下詳細介紹。

          轉(zhuǎn)換方法一:借助“萬能文字識別軟件”完成轉(zhuǎn)換

          安利指數(shù):★★★★☆

          安利理由:功能豐富,支持多種格式進行轉(zhuǎn)換

          這款軟件主打文字識別功能,它能夠準確識別圖片、視頻、音頻中的文字內(nèi)容,并將它們轉(zhuǎn)換成文字。不止這些,它還能實現(xiàn)全能翻譯、AI修復(fù)照片、PDF轉(zhuǎn)換處理等操作。

          像PDF轉(zhuǎn)成HTML就可以使用這款軟件完成。它的轉(zhuǎn)換速度很快,如果文件數(shù)量較多也不用擔心,我們可以將文件批量上傳,大大提高我們的效率。

          轉(zhuǎn)換流程:

          步驟一:打開軟件,找到【PDF轉(zhuǎn)換處理】,選擇【PDF轉(zhuǎn)HTML】按鈕。

          步驟二:將需要轉(zhuǎn)換的PDF文件直接拖拽進軟件。

          步驟三:點擊【開始轉(zhuǎn)換】,轉(zhuǎn)換后的HTML文件默認保存到電腦桌面。轉(zhuǎn)換成功后,可以點擊查看。

          告訴大家一個好消息,我們除了可以在電腦上操作,也可以在手機下載它的APP進行使用哦。如果遇到需要進行翻譯、掃描、PDF轉(zhuǎn)換處理等情況,也可以使用APP來操作,非常方便!

          轉(zhuǎn)換方法二:借助“WPS”完成轉(zhuǎn)換

          安利指數(shù):★★★☆☆

          安利理由:支持文檔表格編輯處理

          WPS作為我們經(jīng)常使用的辦公軟件,擁有對word,PPT等文檔進行編輯的能力,那你知道它還能實現(xiàn)PDF轉(zhuǎn)HTML的操作嗎?

          轉(zhuǎn)換流程:

          首先打開WPS軟件,新建文檔。然后將PDF文件的內(nèi)容復(fù)制到word文檔后保存。保存的格式選擇【單一網(wǎng)頁文件】即可。

          以上就是今天的PDF轉(zhuǎn)HTML轉(zhuǎn)換器推薦。看完這篇文章,大家知道如何轉(zhuǎn)換了嗎?有需要的小伙伴,趕快收藏起來吧!

          版本 56 開始,F(xiàn)irefox 瀏覽器支持一種新的字符編碼轉(zhuǎn)換庫,叫做 encoding_rs。它是用 Rust 編寫的,代替了從 1999 年就開始使用的 C++ 編寫的字符編碼庫 uconv。最初,所有調(diào)用該字符編碼轉(zhuǎn)換庫的代碼都是 C++,所以盡管新的庫是用 Rust 編寫的,它也必須能被 C++ 代碼調(diào)用。實際上,在 C++ 調(diào)用者看來,這個庫跟現(xiàn)代的 C++ 庫沒什么區(qū)別。下面是我實現(xiàn)這一點采用的開發(fā)方式。

          相關(guān)閱讀:

          • 關(guān)于 encoding_rs 本身:https://hsivonen.fi/encoding_rs/
          • 演講視頻:https://media.ccc.de/v/rustfest18-5-a_rust_crate_that_also_quacks_like_a_modern_c_library
          • 幻燈片:https://hsivonen.fi/rustfest2018/

          1.怎樣寫現(xiàn)代 C++?

          所謂“現(xiàn)代”C++的意思就是從 C++ 調(diào)用者來看,函數(shù)庫遵循 C++ 的核心指南(https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines),并具備以下新特性:

          • 通過返回 std::unique_ptr / mozilla::UniquePtr 中在堆上分配的對象的指針進行堆內(nèi)存分配管理。
          • 調(diào)用者分配的緩沖區(qū)用 gsl::span / mozilla::Span 來表示,而不是用普通的指針和長度表示。
          • 多個返回值使用 std::tuple / mozilla::Tuple 傳遞,而不是使用輸出參數(shù)。
          • 非空的普通指針用 gsl::not_null / mozilla::NotNull 表示。

          上面的 gsl:: 表示 Guidelines Support Library(https://github.com/microsoft/GSL),這個庫能提供核心指南要求、但尚未存在于 C++ 標準庫中的東西。

          2.用 Rust 寫 C++ 庫?

          “用 Rust”寫 C++ 庫的意思是指庫中的大部分是用 Rust 寫的,但提供給 C++ 調(diào)用者的接口至少在 C++ 調(diào)用者來看就像個真正的 C++ 庫一樣。

          3.C++ 和 Rust 都與 C 有互操作性

          C++ 的 ABI 非常復(fù)雜,而 Rust ABI 尚未完全確定。但是,C++ 和 Rust 都支持一些使用 C ABI 的函數(shù)。因此,要想讓 C++ 和 Rust 擁有互操作性,就需要通過某種方法,讓 C++ 把 Rust 代碼看成 C 代碼,Rust 把 C++ 代碼看成 C 代碼。

          4.簡化的情形

          這篇文章并不是 Rust 與 C++ 互聯(lián)的完整指南。encoding_rs 的接口非常簡單,缺乏兩種語言之間的互操作性上的常見問題。但是,encoding_rs 簡化 C++ 接口的例子可以作為一個指南,給那些希望在設(shè)計函數(shù)庫時了解跨語言互操作性的人們提供一些幫助。具體來說:

          • encoding_rs 從來不會調(diào)用 C++:跨語言調(diào)用是單向的。
          • encoding_rs 在調(diào)用返回后,不持有指向 C++ 對象的引用:因此 Rust 代碼不需要管理 C++ 內(nèi)存。
          • encoding_rs 不需要用 Rust 或 C++ 語言提供繼承層次結(jié)構(gòu):因此兩種語言中都沒有 vtables。
          • encoding_rs 操作的數(shù)據(jù)類型非常簡單:只有基本類型的連續(xù)緩沖區(qū)(u8 / uint8_t 和 u16 / char16_t 的緩沖區(qū))。
          • 僅支持 panic=abort 配置(即 Rust 的崩潰會終止整個程序,無需回滾棧),而且這里給出的代碼只有在該配置下才是正確的。這里給出的代碼沒有去防止 Rust 崩潰跨越 FFI 邊界回滾,因此跨 FFI 邊界的崩潰是未定義的行為。

          5.API 快速概覽

          為了理解我們討論的 Rust API(https://docs.rs/encoding_rs/0.8.13/encoding_rs/),先來從高層次看看。整個函數(shù)庫有三個公開的結(jié)構(gòu)體(struct):Encoding,Decoder 和 Encoder。從函數(shù)庫的使用者角度來看,這些結(jié)構(gòu)能為各種具體的編碼提供統(tǒng)一的接口,所以可以像 traits、父類或接口一樣使用, 但嚴格來說它們實際上是結(jié)構(gòu)體。Encoding 的實例是靜態(tài)分配的。Decoder 和 Encoder 封裝了流轉(zhuǎn)換的狀態(tài),是在運行時動態(tài)分配的。

          Encoding 實例的引用(即&'static Encoding)可以通過標簽獲得(從協(xié)議文本中提取的文本識別信息),或通過命名靜態(tài)變量(named static)獲得。然后 Encoding 可以作為 Decoder 的參數(shù)使用,后者是在棧上分配的。

          let encoding: &'static Encoding =
           Encoding::for_label( // by label
           byte_slice_from_protocol
           ).unwrap_or(
           WINDOWS_1252 // by named static
           );
          let decoder: Decoder =
           encoding.new_decoder();
          

          在處理流時,Decoder 中有個方法可以將流從調(diào)用者分配的一個切片解碼到調(diào)用者分配的一個切片。解碼器不進行堆分配操作。

          pub enum DecoderResult {
           InputEmpty,
           OutputFull,
           Malformed(u8, u8),
          }
          impl Decoder {
           pub fn decode_to_utf16_without_replacement(
           &mut self,
           src: &[u8],
           dst: &mut [u16],
           last: bool
           ) -> (DecoderResult, usize, usize)
          }
          

          在處理流之外的情況時,調(diào)用者完全不需要處理 Decoder 和 Encoder 的任何東西。Encoding 會提供方法在一個緩沖區(qū)中處理整個邏輯輸入流。

          impl Encoding {
           pub fn decode_without_bom_handling_and_without_replacement<'a>(
           &'static self,
           bytes: &'a [u8],
           ) -> Option<Cow<'a, str>>
          }
          

          6.處理過程

          0. 對 FFI 友好的設(shè)計

          有些設(shè)計來自于問題域本身的簡化因素。而有些只是選擇。

          字符編碼庫可以合理地將編碼、解碼器和編碼器的概念表示成 traits(類似于 C++ 中沒有字段的抽象父類),但是,encoding_rs 對這些概念采用了結(jié)構(gòu)體(struct),以便在分發(fā)的時候能 match 成一個 enum,而不必依賴于 vtable(https://en.wikipedia.org/wiki/Virtual_method_table)。

          pub struct Decoder { // no vtable
           variant: VariantDecoder,
           // ...
          }
          enum VariantDecoder { // no extensibility
           SingleByte(SingleByteDecoder),
           Utf8(Utf8Decoder),
           Gb18030(Gb18030Decoder),
           // ...
          }
          

          這樣做的主要動機并不是消除 vtable 本身,而是故意讓層次結(jié)構(gòu)不能擴展。其背后反映的哲學(xué)是,添加字符編碼不應(yīng)該是程序員應(yīng)當關(guān)心的事情。相反,程序應(yīng)當使用 UTF-8 作為數(shù)據(jù)交換,而且程序不應(yīng)當支持古老的編碼,除非需要兼容已有的內(nèi)容。這種不可擴展的層次結(jié)構(gòu)能帶來強類型安全。如果你從 encoding_rs 得到一個 Encoding 實例,那么你可以信任它絕不會給出任何編碼標準中沒有給出的特性。也就是說,你可以相信它絕不會表現(xiàn)出 UTF-7 或 EBCDIC 的行為。

          此外,通過分發(fā) enum,一個編碼的解碼器可以在內(nèi)部根據(jù) BOM 嗅探的結(jié)果變成另一個編碼的解碼器。

          有人可能會說,Rust 提供編碼轉(zhuǎn)換器的方式是將它變成迭代適配器,接受字節(jié)迭代器的輸入,然后輸出 Unicode 的標量值,或者相反。然而迭代器不僅在跨越 FFI 邊界時更復(fù)雜,還使得加速 ASCII 處理等技巧更難以實現(xiàn)。而直接接受一個切片進行讀取和寫入操作,不僅使得提供 C API 更容易(用 C 的術(shù)語來說,Rust 切片解構(gòu)成對齊的非空指針和一個長度值),而且可以通過觀察多個代碼單元能放入單個寄存器(ALU 寄存器或 SIMD 寄存器)的情況,實現(xiàn)一次處理多個代碼單元,從而實現(xiàn) ASCII 處理加速。

          如果 Rust 的原生 API 只處理基本類型、切片和(非 trait 對象的)結(jié)構(gòu)體,那么與支持高級 Rust 特性的 API 相比,這個 API 更容易映射到 C API。(在 Rust 中,發(fā)生類型擦除時會產(chǎn)生一個 trait 對象。也就是說,你得到的是一個 trait 類型的引用,它并沒有給出該引用指向的那個結(jié)構(gòu)體的類型信息。)

          1. 建立 C API

          當涉及到的類型足夠簡單時,C 和 Rust之間的主要鴻溝,一是 C 語言缺乏方法、缺乏多返回值功能,二是不能以值形式傳送 C 結(jié)構(gòu)體之外的類型。

          • 方法用函數(shù)包裹起來,該函數(shù)的第一個參數(shù)是指向該方法所屬結(jié)構(gòu)體的指針。
          • 切片參數(shù)轉(zhuǎn)換為兩個參數(shù):指向切片開頭的指針,以及切片的長度。
          • 函數(shù)的返回值中,第一個基本類型的返回值作為返回值返回,其他返回值作為輸出參數(shù)。當輸出參數(shù)與同類型的輸入?yún)?shù)相關(guān)時,使用 in/out 參數(shù)是合理的。
          • 如果 Rust 方法以值的形式返回一個結(jié)構(gòu)體,那么封裝函數(shù)將打包該結(jié)構(gòu)體,并返回指向它的指針,因此 Rust 不必考慮該結(jié)構(gòu)體。此外還要添加一個函數(shù),用于釋放該指針指向的結(jié)構(gòu)體。這樣,Rust 方法只需將指針打包,或者拆包。從 C 指針的角度來看,結(jié)構(gòu)體是不透明的。
          • 作為特殊情況,獲取編碼名稱的方法在 Rust 中返回 &'static str,它被包裹在一個函數(shù)中,接收一個指向可寫入的緩沖區(qū)的指針,緩沖區(qū)的長度至少應(yīng)當為最長的編碼名稱的長度。
          • enum 用來表示輸入緩沖區(qū)的枯竭、輸出緩沖區(qū)占滿,或錯誤以及詳細情況,這些 enum 在 C API 中轉(zhuǎn)變成 uint32_t,并加上相應(yīng)的常量來表示“輸入空”或“輸出滿”以及一系列解釋其他錯誤的規(guī)則。這種方式不是最理想的,但在這種情況下很好用。
          • 越界檢查時的長度計算改成飽和運算(saturating)。也就是說,調(diào)用者需要將 SIZE_MAX 當作越界的信號。

          2.在 C++ 中根據(jù) C API 重建 API

          即使是慣用的 C API(https://github.com/hsivonen/encoding_c/blob/master/include/encoding_rs.h)也不能當做現(xiàn)代 C++ API 使用。幸運的是,類似于多重返回值、切片等 Rust 概念可以在 C++ 中表示,只需將 C API 返回的指針解釋成指向 C++ 對象的指針,就能展示出 C++ 的優(yōu)雅。

          大部分例子來自一個使用了 C++17 標準庫類型的 API(https://github.com/hsivonen/encoding_c/blob/master/include/encoding_rs_cpp.h)。在 Gecko 中,我們一般會避免使用 C++ 標準庫,而使用一個 encoding_rs 的特別版本的 C++ API,該版本使用了 Gecko 特有的類型(https://searchfox.org/mozilla-central/source/intl/Encoding.h)。這里我假設(shè)標準庫類型的例子更容易被更多讀者接受。

          方法的優(yōu)雅

          對于每個 C 語言中不透明的構(gòu)造體指針,C++ 中都會定義一個類,C 的頭文件也會修改,使得從 C++ 編譯器的角度來看,指針類型變成指向 C++ 類實例的指針。這些放在一起就相當于一個 reinterpret_cast 過的指針,而不需要實際寫出 reinterpret_cast。

          由于指針并不真正指向它們看似指向的類的實例,而是指向 Rust 結(jié)構(gòu)體的實例,因此應(yīng)該事先做好預(yù)防措施。這些類中沒有定義任何字段。默認的無參數(shù)構(gòu)造函數(shù)和復(fù)制構(gòu)造方法被刪除,默認的 operator= 也被刪除。此外,這些類還不能包含虛方法。(最后一點是個重要的限制條件,稍后會討論。)

          class Encoding final {
          // ...
          private:
           Encoding() = delete;
           Encoding(const Encoding&) = delete;
           Encoding& operator=(const Encoding&) = delete;
           ~Encoding() = delete;
          };
          

          對于 Encoding 來說,所有實例都是靜態(tài)的,因此析構(gòu)函數(shù)也被刪掉了。如果是動態(tài)分配的 Decoder 和 Encoder,還要添加一個空的析構(gòu)函數(shù)和一個 static void operator delete。(后面會給一個例子。)這樣能讓這個偽 C++ 類的析構(gòu)過程導(dǎo)向 C API 中相應(yīng)類型的釋放函數(shù)。

          這些基礎(chǔ)工作將指針變得看上去像是 C++ 類實例的指針。有了這些,就能在這些指針上實現(xiàn)方法調(diào)用了。(介紹完下一個概念后也會給出實例。)

          返回動態(tài)分配的對象

          前面說過,Rust API 以值方式返回 Encoder 或 Decoder,這樣調(diào)用者可以將返回值放在棧上。這種情況被 FFI 的包裹代替,因此 C API 只需通過指針暴露堆上分配的對象。而且,這些指針也被重新解釋為可 delete 的 C++ 對象指針。

          不過還需要確保這些 delete 會在正確的時機被調(diào)用。在現(xiàn)代 C++ 中,如果對象在同一時刻只能有一個合法的所有者,那么對象指針會被包裹在 std::unique_ptr 或 mozilla::UniquePtr 中。老的 uconv 轉(zhuǎn)換器支持引用計數(shù),但在 Gecko 代碼中所有實際的應(yīng)用中,每個轉(zhuǎn)換器都只有一個所有者。由于編碼器和解碼器的使用方式使得同一時刻只有一個合法的所有者,因此 encoding_rs 的兩個 C++ 包裹就使用了 std::unique_ptr 和 mozilla::UniquePtr。

          我們來看看 Encoding 中那個返回 Decoder 的工廠方法。在 Rust 中,這個方法接收 self 的引用,通過值返回 Decoder。

          impl Encoding {
           pub fn new_decoder(&'static self) -> Decoder {
           // ...
           }
          }
          

          在 FFI 層,第一個參數(shù)是顯式的指針類型,對應(yīng)于 Rust 的 &self 和 C++ 的 this(具體來說,是 const 版本的 this)。我們在堆上分配內(nèi)存(Box::new())然后將 Decoder 放進分配好的內(nèi)存中。然后忘記內(nèi)存分配(Box::into_row),這樣可以將指針返回給 C,而不會在作用域結(jié)束時釋放。為了能夠釋放內(nèi)存,我們引入了一個新的函數(shù),將 Box 放回,然后將它賦給一個變量,以便立即離開作用域,從而釋放堆上分配的內(nèi)存。

          #[no_mangle]
          pub unsafe extern "C" fn encoding_new_decoder(
           encoding: *const Encoding) -> *mut Decoder
          {
           Box::into_raw(Box::new((*encoding).new_decoder()))
          }
          #[no_mangle]
          pub unsafe extern "C" fn decoder_free(decoder: *mut Decoder) {
           let _ = Box::from_raw(decoder);
          }
          

          在 C 文件頭中看起來像這樣:

          ENCODING_RS_DECODER*
          encoding_new_decoder(ENCODING_RS_ENCODING const* encoding);
          void
          decoder_free(ENCODING_RS_DECODER* decoder);
          

          ENCODING_RS_DECODER 是一個宏,用于在 C 頭文件在 C++ 環(huán)境中使用(而不是作為純 C API 使用)時將其替換成正確的 C++ 類型。

          在 C++ 一側(cè),我們使用 std::unique_ptr,相當于 Rust 的 Box。實際上它們也非常相似:

          let ptr: Box<Foo>
          std::unique_ptr<Foo> ptr
          Box::new(Foo::new(a, b, c))
          make_unique<Foo>(a, b, c)
          Box::into_raw(ptr)
          ptr.release()
          let ptr = Box::from_raw(raw_ptr);
          std::unique_ptr<Foo> ptr(raw_ptr);
          

          我們把從 C API 獲得的指針包裹在 std::unique_ptr 中:

          class Encoding final {
          public:
           inline std::unique_ptr<Decoder> new_decoder() const
           {
           return std::unique_ptr<Decoder>(
           encoding_new_decoder(this));
           }
          };
          

          當 std::unique_ptr<Decoder> 離開作用域時,刪除操作會通過 FFI 導(dǎo)向回 Rust,這是因為定義是下面這樣的:

          class Decoder final {
          public:
           ~Decoder() {}
           static inline void operator delete(void* decoder)
           {
           decoder_free(reinterpret_cast<Decoder*>(decoder));
           }
          private:
           Decoder() = delete;
           Decoder(const Decoder&) = delete;
           Decoder& operator=(const Decoder&) = delete;
          };
          

          如何工作?

          在 Rust 中,非 trait 的方法只不過是語法糖:

          impl Foo {
           pub fn get_val(&self) -> usize {
           self.val
           }
          }
          fn test(bar: Foo) {
           assert_eq!(bar.get_val(), Foo::get_val(&bar));
          }
          

          對非 trait 類型的引用方法調(diào)用只不過是普通的函數(shù)調(diào)用,但第一個參數(shù)是指向 self 的引用。在 C++ 一側(cè),非虛方法的調(diào)用原理相同:非虛 C++ 方法調(diào)用只不過是函數(shù)調(diào)用,但第一個函數(shù)是 this 指針。

          在 FFI/C 層,我們可以將同樣的指針顯式地作為第一個參數(shù)傳遞。

          在調(diào)用 ptr->Foo() 時,其中的 ptr 是 T* 類型,而如果方法定義為 void Foo()(它在 Rust 中映射到 &mut self),那么 this 是 T* 類型,如果方法定義為 void Foo() const(在 Rust 中映射到 &self),則 this 是 const T* 類型,所以這樣也能正確處理 const。

          fn foo(&self, bar: usize) -> usize
          size_t foo(size_t bar) const
          fn foo(&mut self, bar: usize) -> usize
          size_t foo(size_t bar)
          

          這里“非 trait 類型”和“非虛”是非常重要的。要想讓上面的代碼正確工作,那么無論那一側(cè)都不能有 vtable。這就是說,Rust 不能有 trait,C++ 也不能有繼承。在 Rust 中,trait 對象(指向任何實現(xiàn)了 trait 的結(jié)構(gòu)體的 trait 類型的引用)實現(xiàn)為兩個指針:一個指向結(jié)構(gòu)體實例,另一個指向?qū)?yīng)于數(shù)據(jù)的具體類型的 vtable。我們需要能夠把 self 的引用作為單一指針跨越 FFI 傳遞,所以在跨越 FFI 時無法攜帶 vtable 指針。為了讓 C++ 對象指針兼容 C 的普通指針,C++ 將 vtable 指針放在了對象自身上。由于我們的指針指向的并不是真正帶有 vtable 的 C++ 對象,而是 Rust 對象,所以必須保證 C++ 代碼不會在指針目標上尋找 vtable 指針。

          其結(jié)果是,Rust 中的結(jié)構(gòu)對應(yīng)的 C++ 中的類不能從 C++ 框架中的通用基類繼承。在 Gecko 的情況中,C++ 類不能繼承 nsISupports。例如,在 Qt 的語境下,對應(yīng)的 C++ 類不能從 QObject 繼承。

          非空指針

          Rust API 中有的方法會返回 &'static Encoding。Rust 的引用永遠不會為 null,因此最好是將這個信息傳遞給 C++ API。C++ 中對應(yīng)于此的是 gsl::not_null和mozilla::NotNull。

          由于 gsl::not_null 和 mozilla::NotNull 只不過是類型系統(tǒng)層面的寫法,它并不會改變底層指針的機器表示形式,因此對于有 Rust 保證的指針,跨越 FFI 之后可以認為它們絕不會為 null,所以我們想做的是,利用與之前將 FFI 返回的指針重新解釋為指向無字段、無虛方法的 C++ 對象的指針同樣的技巧來騙過 C++ 編譯器,從而在頭文件中聲明那些 FFI 返回的絕不會為 null 的指針為類型 mozilla::NotNull<const Encoding*>。不幸的是,實際上這一點無法實現(xiàn),因為在 C++ 中,涉及模板的類型不能在 extern "C" 函數(shù)的定義中使用,所以 C++ 代碼最后只能在從 C API 接收到指針、包裹在 gsl::not_null 或 mozilla::NotNull 時進行一系列的 null 檢查。

          但是,也有一些定義是指向編碼對象常量的靜態(tài)指針(指向的目標是在 Rust 中定義的),而且恰巧 C++ 允許將這些定義為 gsl::not_null<const Encoding*>,所以我們這樣實現(xiàn)了。(感謝 Masatoshi Kimura 指出這一點的可行性。)

          Rust 中靜態(tài)分配的 Encoding 實例的定義如下:

          pub static UTF_8_INIT: Encoding = Encoding {
           name: "UTF-8",
           variant: VariantEncoding::Utf8,
          };
          pub static UTF_8: &'static Encoding = &UTF_8_INIT;
          

          在 Rust 中,通用的規(guī)則(https://twitter.com/tshepang_dev/status/1051558270425591808)是 static 用來聲明不會改變的內(nèi)存地址,const 用來聲明不會改變的值。因此,UTF_8_INIT 應(yīng)當為 static,而 UTF_8 應(yīng)當為 const:指向 static 實例的引用的值不會改變,但為這個引用靜態(tài)分配的內(nèi)存地址則不一定。不幸的是, Rust 有一條規(guī)則說,const 的右側(cè)不能包含任何 static 的東西,因此這一條阻止了對 static 的引用,以確保 const 定義的右側(cè)可以被靜態(tài)檢查,確定它是否適合任何假想的 const 定義——甚至是那些在編譯時就試圖解引用(dereference)的定義。

          但對于 FFI,我們需要為 UTF_8_INIT 分配一塊不會改變的內(nèi)存,因為這種內(nèi)存能在 C 的連接器中使用,可以讓我們?yōu)?C 提供命名的指針類型的東西。上面說的 UTF_8 的表示形式已經(jīng)是我們需要的了,但為了讓 Rust 更優(yōu)雅,我們希望 UTF_8 能參與到 Rust 的命名空間中。這意味著從 C 的角度來看,它的名字需要被改變(mangle)。我們浪費了一些空間來重新靜態(tài)分配指針來避免改變名稱,以供 C 使用:

          pub struct ConstEncoding(*const Encoding);
          unsafe impl Sync for ConstEncoding {}
          #[no_mangle]
          pub static UTF_8_ENCODING: ConstEncoding =
           ConstEncoding(&UTF_8_INIT);
          

          這里使用了指針類型,以明確 C 語言會將其當做指針(即使 Rust 引用類型擁有同樣的表現(xiàn)形式)。但是,Rust 編譯器拒絕編譯帶有全局可視性指針的程序。由于全局變量可以被任何線程訪問,多線程同時訪問指針指向的目標可能會引發(fā)問題。這種情況下,指針目標不會被修改,因此全局可視性是沒問題的。為了告訴編譯器這一點,我們需要為指針實現(xiàn) Sync 這個 marker trait。但是,trait 不能在指針類型上實現(xiàn)。作為迂回方案,我們?yōu)?const Encoding創(chuàng)建了一個新的類型。新的類型擁有與它包裹的類型同樣的表現(xiàn)形式,但我們可以在新類型上實現(xiàn) trait。實現(xiàn) Sync 是 unsafe 的,因為我們告訴了編譯器某些東西可以接受,這并不是編譯器自己發(fā)現(xiàn)的。

          在 C++ 中我們可以這樣寫(宏擴展之后的內(nèi)容):

          extern "C" {
           extern gsl::not_null<const encoding_rs::Encoding*> const UTF_8_ENCODING;
          }
          

          指向編碼器和解碼器的指針也絕不會為 null,因為內(nèi)存分配失敗會直接終止程序。但是 std::unique_ptr / mozilla::UniquePtr 和 gsl::nul / mozilla::NotNull 不能結(jié)合使用。

          可選值

          Rust 中常見的做法是用 Option<T> 表示返回值可能有值也可能沒有值。現(xiàn)在的 C++ 提供了同樣的東西:std::optional<T>。在 Gecko 中,我們使用的是 mozilla::Maybe<T>。

          Rust 的 Option<T> 和 C++ 的 std::optional<T> 實際上是一樣的:

          return None;
           return std::nullopt;
          return Some(foo);
           return foo;
          is_some()
           operator bool()
           has_value()
          unwrap()
           value()
          unwrap_or(bar)
           value_or(bar)
          

          但不幸的是,C++ 保留了安全性。從 std::optional<T> 中提取出包裹值時最優(yōu)雅的方法就是使用 operator*(),但這個也是沒有檢查的,因此也是不安全的。

          多返回值

          盡管 C++ 在語言層面缺少對于多返回值的支持,但多返回值可以從庫的層次實現(xiàn)。比如標準庫,相應(yīng)的部分是 std::tuple,std::make_tuple 和 std::tie。在 Gecko 中,相應(yīng)的庫是 mozilla::Tuple,mozilla::MakeTuple 和 mozilla::Tie。

          fn foo() -> (T, U, V)
           std::tuple<T, U, V> foo()
          return (a, b, c);
           return {a, b, c};
          let (a, b, c) = foo();
           const auto [a, b, c] = foo();
          let mut (a, b, c) = foo();
           auto [a, b, c] = foo();
          

          切片

          Rust 切片包裹了一個自己不擁有的指針,和指針指向內(nèi)容的長度,表示數(shù)組中的一段連續(xù)內(nèi)容。相應(yīng)的 C 代碼為:

          src: &[u8]
           const uint8_t* src, size_t src_len
          dst: &mut [u8]
           uint8_t* dst, size_t dst_len
          

          C++ 的標準庫中并沒有對應(yīng)的東西(除了 std::string_view 可以用來表示只讀字符串切片之外),但 C++ 核心指南中已經(jīng)有一部分叫做 span 的東西(https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#i13-do-not-pass-an-array-as-a-single-pointer):

          src: &[u8]
           gsl::span<const uint8_t> src
          dst: &mut [u8]
           gsl::span<uint8_t> dst
          &mut vec[..]
           gsl::make_span(vec)
          std::slice::from_raw_parts(ptr, len)
           gsl::make_span(ptr, len)
          for item in slice {}
           for (auto&& item : span) {}
          slice[i]
           span[i]
          slice.len()
           span.size()
          slice.as_ptr()
           span.data()
          

          GSL 依賴于 C++14,但在 encoding_rs 發(fā)布時,Gecko 由于 Android 的阻礙,不得不停留在 C++11 上(https://bugzilla.mozilla.org/show_bug.cgi?id=1325632#c25)。因此,GSL 不能原樣在 Gecko 中使用,我將 gsl::span 移植到了 C++11 上,變成 mozilla::Span(https://searchfox.org/mozilla-central/source/mfbt/Span.h#375)。移植的過程主要是去掉 constexpr 關(guān)鍵字,并用 mozilla:: 的類型和類型 trait 代替標準庫中的類型。在 Gecko 改成 C++14 后,部分 constexpr 關(guān)鍵字被恢復(fù)了。

          不論如何, 我們有了自己的 mozilla::Span,現(xiàn)在可以添加 gsl::span 中缺少的、像 Rust 一樣的子 span 了。如果你需要子 span 下標 i 開始直到 j,但不包括 j,那么 gsl::span 的實現(xiàn)方法是:

          &slice[i..]
           span.subspan(i)
          &slice[..i]
           span.subspan(0, i)
          &slice[i..j]
           span.subspan(i, j - i)
          

          而 mozilla::Span 的實現(xiàn)方法是:

          &slice[i..]
           span.From(i)
          &slice[..i]
           span.To(i)
          &slice[i..j]
           span.FromTo(i, j)
          

          gsl::span 和 Rust 的切片有一個重要的區(qū)別:它們解構(gòu)成指針和長度的方式不同。對于零長度的 gsl::span,指針可能會解構(gòu)為 nullptr。而 Rust 切片中,指針必須不能為 null 且必須對齊,甚至零長度切片也是如此。乍一看起來似乎有點違反直覺:當長度為零時,指針永遠不會解引用,那么它是否為 null 有什么關(guān)系嗎?實際上,在優(yōu)化 Option 之類的枚舉之中的 enum 差異時這一點非常重要。None 表示為全零比特,所以如果包裹在 Some() 中,那么指針為 null、長度為零的切片就可能偶然被當做 None。通過要求指針不為 null 指針,Option 中的零長度切片就可以與 None 區(qū)分開來。通過要求指針必須對齊,當切片元素類型的對齊大于一時,就有可能進一步使用指針的低位比特。

          在意識到我們不能將從 C++ 的 gsl::span::data() 中獲得的指針直接傳遞給 Rust 的 std::slice::from_raw_parts() 后,我們必須決定要在哪里將 nullptr 替換成 reinterpret_cast<T*>(alignof(T))。如果使用 gsl::span 則有兩個候選的位置:提供 FFI 的 Rust 代碼中,或者在調(diào)用 FFI 的 C++ 代碼中。而如果使用 mozilla::Span,我們可以改變 span 的實現(xiàn)代碼,因此還有另外兩個候選的位置:mozilla::Span 的構(gòu)造函數(shù),和指針的 getter 函數(shù)。

          在這些候選位置中,mozilla::Span 的構(gòu)造函數(shù)似乎是編譯器最有可能優(yōu)化掉某些檢查的地方。這就是為什么我決定將檢查放在這里的原因。這意味著如果使用 gsl::span,那么檢查的代碼必須移動到FFI的調(diào)用中。所有從 gsl::span 中獲得的指針必須進行如下清洗:

          template <class T>
          static inline T* null_to_bogus(T* ptr)
          {
           return ptr ? ptr : reinterpret_cast<T*>(alignof(T));
          }
          

          此外,由于這段檢查并不存在于提供 FFI 的 diamante 中,C API 變得有點不尋常,因為它要求 C 的調(diào)用者即使在長度為零時也不要傳遞 NULL。但是,C API 在未定義行為方面已經(jīng)有很多問題了,所以再加一個未定義行為似乎也不是什么大事兒。

          合并到一起

          我們來看看上面這些特性結(jié)合后的例子。首先,Rust 中的這個方法接收一個切片,并返回一個可選的 tuple:

          impl Encoding {
           pub fn for_bom(buffer: &[u8]) ->
           Option<(&'static Encoding, usize)>
           {
           if buffer.starts_with(b"\xEF\xBB\xBF") {
           Some((UTF_8, 3))
           } else if buffer.starts_with(b"\xFF\xFE") {
           Some((UTF_16LE, 2))
           } else if buffer.starts_with(b"\xFE\xFF") {
           Some((UTF_16BE, 2))
           } else {
           None
           }
           }
          }
          

          由于它是個靜態(tài)方法,因此不存在指向 self 的引用,在 FFI 函數(shù)中也沒有相應(yīng)的指針。該切片解構(gòu)成一個指針和一個長度。長度變成 in/out 參數(shù),用來返回切片刀長度,以及 BOM 的長度。編碼變成返回值,編碼指針為 null 表示 Rust 中的 tuple 為 None。

          #[no_mangle]
          pub unsafe extern "C" fn encoding_for_bom(buffer: *const u8,
           buffer_len: *mut usize)
           -> *const Encoding
          {
           let buffer_slice =
           ::std::slice::from_raw_parts(buffer, *buffer_len);
           let (encoding, bom_length) =
           match Encoding::for_bom(buffer_slice) {
           Some((encoding, bom_length)) =>
           (encoding as *const Encoding, bom_length),
           None => (::std::ptr::null(), 0),
           };
           *buffer_len = bom_length;
           encoding
          }
          

          C 頭文件中的簽名如下:

          ENCODING_RS_ENCODING const*
          encoding_for_bom(uint8_t const* buffer, size_t* buffer_len);
          

          C++ 層在 C API 上重建對應(yīng)于 Rust API 的部分:

          class Encoding final {
          public:
           static inline std::optional<
           std::tuple<gsl::not_null<const Encoding*>, size_t>>
           for_bom(gsl::span<const uint8_t> buffer)
           {
           size_t len = buffer.size();
           const Encoding* encoding =
           encoding_for_bom(null_to_bogus(buffer.data()), &len);
           if (encoding) {
           return std::make_tuple(
           gsl::not_null<const Encoding*>(encoding), len);
           }
           return std::nullopt;
           }
          };
          

          這里我們必須顯式使用 std::make_tuple,因為隱式構(gòu)造函數(shù)在 std::tuple 嵌入到 std::optional 中時不能正確工作。

          代數(shù)類型

          之前,我們看到了 Rust 側(cè)的流 API 可以返回這個 enum:

          pub enum DecoderResult {
           InputEmpty,
           OutputFull,
           Malformed(u8, u8),
          }
          

          現(xiàn)在 C++ 也有了類似 Rust 的 enum 的東西:std::variant<Types...>。但在實踐中,std::variant 很難用,因此,從優(yōu)雅的角度來看,Rust 的 enum 本應(yīng)是個輕量級的東西,所以沒有道理使用 std::variant 代替。

          首先,std::variant 中的變量是沒有命名的。它們通過位置或類型來識別。命名變量曾經(jīng)作為 lvariant(http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0095r1.html)提議過,但并沒有被接受。其次,即使允許重復(fù)的類型,使用它們也是不現(xiàn)實的。第三,并沒有語言層面上相當于 Rust 的 match(https://doc.rust-lang.org/book/second-edition/ch06-02-match.html)的東西。曾經(jīng)提議過的inspect(http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0095r1.html)相當于 match 的機制,但并未被接受。

          在 FFI/C 層,上面 enum 的信息被打包到一個 u32 中。我們沒有試圖將它在 C++ 側(cè)擴展成更漂亮的東西,而是簡單地使用了與 C API 同樣的 uint32_t。如果調(diào)用者需要在異常情況下從中提取出兩個小的整數(shù),那么調(diào)用者可以自己用位操作從 uint32_t 中提取。

          FFI 代碼如下:

          pub const INPUT_EMPTY: u32 = 0;
          pub const OUTPUT_FULL: u32 = 0xFFFFFFFF;
          fn decoder_result_to_u32(result: DecoderResult) -> u32 {
           match result {
           DecoderResult::InputEmpty => INPUT_EMPTY,
           DecoderResult::OutputFull => OUTPUT_FULL,
           DecoderResult::Malformed(bad, good) =>
           (good as u32) << 8) | (bad as u32),
           }
          }
          

          使用零作為 INPUT_EMPTY 的魔術(shù)值是個微優(yōu)化。在某些架構(gòu)上,與零比較的代價要比與其他常量比較更低,而表示解碼時的異常情況和無法映射的情況的值不會與零重疊。

          通知整數(shù)溢出

          Decoder 和 Encoder 擁有一些方法用于查詢最壞情況下的緩沖區(qū)大小需求。 調(diào)用者提供輸入的代碼單元的數(shù)量,方法返回要保證相應(yīng)的轉(zhuǎn)換方法不會返回OutputFull 所需的最小緩沖區(qū)大小(以代碼單元為單位)。

          例如,將 UTF-16 編碼成 UTF-8,最壞情況下等于乘以三。至少在原理上,這種計算可以導(dǎo)致整數(shù)溢出。在 Rust 中,整數(shù)溢出被認為是安全的,因為即使由于整數(shù)溢出而分配了太少的緩沖區(qū),實際上訪問緩沖區(qū)也會進行邊界檢查,所以整體的結(jié)果是安全的。但是,緩沖區(qū)訪問在 C 或 C++ 中通常是沒有邊界檢查的,所以 Rust 中的整數(shù)溢出可能會導(dǎo)致 C 或 C++ 中的內(nèi)存不安全,如果溢出的計算結(jié)果被用來確定緩沖區(qū)分配和訪問時的大小的話。對于 encoding_rs 而言,即使是 C 或 C++ 負責分配緩沖區(qū),寫入操作也是由 Rust 進行的, 所以也許是沒問題的。但為了確信起見,encoding_rs 提供的最壞情況的計算也進行了溢出檢查。

          在 Rust 中,經(jīng)過溢出檢查的結(jié)果會返回 Option<usize>。為保持 C API 中類型的簡單性,C API 會返回 size_t,并用 SIZE_MAX 通知溢出。因此,C API 實際上使用的是飽和算術(shù)(saturating arithmetic)。

          在使用標準庫類型的 C++ API 中,返回類型是 std::optional<size_t>。在 Gecko 中,我們使用了一個整數(shù)類型的包裹,提供溢出檢查和有效性標志。在 Gecko 版本的 C++ API 中,返回值是 mozilla::CheckedInt<size_t>,這樣處理溢出信號的方式與 Gecko 的其他部分一致。(邊注:我發(fā)現(xiàn) C++ 標準庫依然沒有提供類似 mozilla::CheckedInt 的包裹以進行整數(shù)運算中的溢出檢查時感到非常震驚——這應(yīng)該是標準就支持的避免未定義行為的方式。)

          7.重建非流式 API

          我們再來看看 Encoding 的非流式 API 中的方法:

          impl Encoding {
           pub fn decode_without_bom_handling_and_without_replacement<'a>(
           &'static self,
           bytes: &'a [u8],
           ) -> Option<Cow<'a, str>>
          }
          

          返回類型 Option 中的類型是 Cow<'a, str>,這個類型的值或者是自己擁有的 String,或者是從別的地方借來的字符串切片(&'a str)。借來的字符串切片的生存時間'a 就是輸入切片(bytes: &'a [u8])的生存時間,因為在借的情況下,輸出實際上是從輸入借來的。

          將這種返回值映射到 C 中面臨著問題。首先,C 不提供任何方式表示可能擁有也可能借的情況。其次,C 語言沒有標準類型來保存堆上分配的字符串,從而知道字符串的長度和容量,從而能在字符串被修改時重新分配其緩沖區(qū)。也許可以建立一種新的 C 類型,其緩沖區(qū)由 Rust 的 String 負責管理,但這種類型就沒辦法兼容 C++ 的字符串了。第三,借來的 C 字符串切片在 C 語言中將會表現(xiàn)成原始的指針和一個長度,一些文檔說這個指針僅在輸入指針有效的時候才有效。因此并沒有語言層面的機制來防止指針在釋放之后被使用。

          解決方案并不是完全不在 C 層面提供非流式 API。下 Rust 側(cè),非流式 API 只是個構(gòu)建在流式 API 和一些驗證函數(shù)(ASCII 驗證、UTF-8 驗證、ISO-2022-JP ASCII 狀態(tài)驗證)上的便利 API。

          盡管 C++ 的類型系統(tǒng)能夠表示與 Rust 的 Cow<'a, str> 相同的結(jié)構(gòu)體,如std::variant<std::string_view, std::string>,但這種C++的Cow是不安全的,因為它的生存期限'a無法被 C++ 強制。盡管 std::string_view(或gsl::span)可以(在大多素情況下)在 C++ 中作為參數(shù)使用,但作為返回值類型,它會導(dǎo)致在釋放后發(fā)生訪問。與 C 一樣,最好的情況就是有某個文檔能說明只要輸入的 gsl::span 有效,輸出的 std::string_view 就有效。

          為了避免發(fā)生釋放后訪問,我們在依然使用 C++17 的 C++ API 的版本中,簡單地令 C++ 的 decode_without_bom_handling_and_without_replacement() 函數(shù)永遠復(fù)制并返回一個 std::optional<std::string>。

          但在 Gecko 的情況中,我們能夠在保證安全的情況下做得更好。Gecko 使用了 XPCOM 字符串(https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Guide/Internal_strings),它提供了多種存儲選項,特別是:從其他人那里(不安全地)借來的 dependent string,自動用將短字符串行內(nèi)嵌入至行內(nèi)緩沖區(qū)的 auto string,以及指向堆上分配的引用計數(shù)器緩沖區(qū)的 shared string。

          如果要解碼的緩沖區(qū)是個指向堆上分配的引用計數(shù)器緩沖區(qū)的 XPCOM 字符串,而且我們需要解碼至 UTF-8(而不是 UTF-16),而在這種情況下本應(yīng)該從 Rust 那里借(除非是刪除 BOM 的情況),現(xiàn)在我們可以另輸出字符串指向與輸入相同的堆上分配的引用計數(shù)器緩沖區(qū)(并增加引用計數(shù))。這正是 mozilla::Encoding 的非流式 API 做法。

          與 Rust 相比,除了輸入字符串必須使用引用計數(shù)存儲以便復(fù)制能正確工作之外,還有另外一個限制:如果 BOM 被移除,那么輸入不能有 UTF-8 BOM。雖然 Rust 可以從輸入中借出不帶 BOM 的那一段切片,但對于 XPCOM 字符串,增加引用計數(shù)的方式只有在輸入和輸輸出的字節(jié)內(nèi)容完全一致的情況下才能正確工作。如果省略掉開頭的三個字節(jié),它們就不是完全一致了。

          雖然使用 C++17 標準庫類型的 C++ API 中的非流式 API 是在 C++ 流式 API 的基礎(chǔ)上構(gòu)建的,但為了更多的安全性,mozilla::Encoding 的非流式 API 并不是基于流式 C++ API 構(gòu)建的,而是在 Rust 語言的流式 Rust API 的基礎(chǔ)上構(gòu)建的(https://searchfox.org/mozilla-central/source/intl/encoding_glue/src/lib.rs)。在 Gecko 中,我們有 XPCOM 字符串的 Rust 綁定(https://searchfox.org/mozilla-central/source/servo/support/gecko/nsstring/src/lib.rs),所以可以從 Rust 中操作 XPCOM 字符串。

          8.結(jié)語:我們真的需要用指針來引用 Decoder 和 Encoder 嗎?

          由于 C++ 沒有安全的借用機制而導(dǎo)致必須在非流式 API 中進行復(fù)制之外,還有一點點令人失望的是,從 C++ 中實例化 Decoder 和 Encoder 需要進行堆分配操作,而 Rust 調(diào)用者是在棧上分配這些類型。我們能讓 C++ 的使用者也避免堆分配操作嗎?

          答案是可以,但正確地實現(xiàn)這一點需要讓 C++ 的構(gòu)建系統(tǒng)查詢 rustc 以構(gòu)建常量,使得系統(tǒng)變得異常復(fù)雜。

          我們不能跨越 FFI 直接用值的形式返回非 C 的結(jié)構(gòu)體,但如果一個恰當?shù)貙R的指針有足夠多的內(nèi)存,我們可以將非 C 的結(jié)構(gòu)體寫到由 FFI 的另一側(cè)提供的內(nèi)存中。實際上,API 支持這個功能,作為之前在堆上實例化新的 Decoder 的操作的一種優(yōu)化措施:

          #[no_mangle]
          pub unsafe extern "C" fn encoding_new_decoder_into(
           encoding: *const Encoding,
           decoder: *mut Decoder)
          {
           *decoder = (*encoding).new_decoder();
          }
          

          即使文檔說 encoding_new_decoder_into() 應(yīng)當旨在之前從 API 獲得了 Decoder 的指針的情況下使用,對于 Decoder 的情況來說,使用 = 進行賦值操作應(yīng)當不是問題,就算指針指向的內(nèi)存地址沒有初始化,因為 Decoder 并沒有實現(xiàn) Drop。用 C++ 的術(shù)語來說,Rust 中的 Decoder 沒有析構(gòu)函數(shù),所以只要該指針之前指向合法的 Decoder,那么使用 = 進行賦值不會進行任何清理工作。

          如果編寫一個 Rust 結(jié)構(gòu)體并實現(xiàn) Drop 使之析構(gòu)成未初始化的內(nèi)存,那就應(yīng)該使用 std::ptr::write() 代替 =。std::ptr::write() 能“用給定的值覆蓋內(nèi)存地址,而不會讀取或放棄舊的值”。也許,上面的情況也能作為使用 std::ptr::write() 的很好的例子,盡管嚴格來說并不那么必要。

          從 Rust 的 Box 中獲得的指針能保證正確地對齊,并且指向足夠大小的一片內(nèi)存。如果 C++ 要分配棧內(nèi)存供 Rust 代碼寫入,就要讓 C++ 代碼使用正確的大小和對齊。而從 Rust 向 C++ 傳遞這兩個值的過程,就是整個代碼變得不穩(wěn)定的開始。

          C++ 代碼需要自己從結(jié)構(gòu)體發(fā)現(xiàn)正確的大小和對齊。這兩個值不能通過調(diào)用 FFI 函數(shù)獲得,因為 C++ 必須在編譯時就確定這兩個值。大小和對齊并不是常量,因此不能手動寫到頭文件中。首先,每當 Rust 結(jié)構(gòu)體改變時這兩個值都會改變,因此直接寫下來有可能過會導(dǎo)致它們不能適應(yīng) Rust 結(jié)構(gòu)體改變后的真實需求。其次,這兩個值在 32 位體系和 64 位體系上不一樣。第三,也是最糟糕的一點,一個 32 位體系上的對齊值可能與另一個 32 位體系的對齊值不一樣。具體來說,絕大多數(shù)目標體系上的 f64 的對齊值是 8,如 ARM、MIPS 和 PowerPC,而 x86 上的 f64 的對齊值是 4。如果 Rust 有 m68k 的移植(https://lists.llvm.org/pipermail/llvm-dev/2018-August/125325.html),那么有可能會使 32 位平臺上的對齊值產(chǎn)生更多不確定性(https://bugzilla.mozilla.org/show_bug.cgi?id=1325771#c49)。

          似乎唯一的正確方法就是,作為構(gòu)建過程的一部分,從 rustc 中提取出正確的大小和對齊信息,然后再編譯 C++ 代碼,這樣就可以將兩個數(shù)字寫入生成的 C++ 頭文件中,供 C++ 代碼參考。更簡單的方法是讓構(gòu)建系統(tǒng)運行一小段Rust程序,利用 std::mem::size_of和std::mem:align_of 獲取這兩個數(shù)值并輸出到 C++ 頭文件中。這個方案假定構(gòu)建和實際運行發(fā)生在同一個目標體系上,所以不能在交叉編譯中使用。這一點可不太好。

          我們需要從 rustc 中提取給定的結(jié)構(gòu)體在特定體系下的大小和對齊值,但不能通過執(zhí)行程序的方式。我們發(fā)現(xiàn)(https://blog.mozilla.org/nnethercote/2018/11/09/how-to-get-the-size-of-rust-types-with-zprint-type-sizes/)rustc有個命令行選項,-Zprint-type-sizes,能夠輸出類型的大小和對齊值。不幸的是,這個選項僅存在于每日構(gòu)建版本上……不過不管怎樣,最正確的方法還是讓一個構(gòu)架腳本首先用該選項調(diào)用 rustc,解析我們關(guān)心的大小和對齊,然后將它們作為常量寫入 C++ 頭文件總。

          或者,由于“過對齊”(overalign)是允許的,我們可以信任結(jié)構(gòu)體不會包含 SIMD 成員(對于128位向量來說對齊值為 16),因此對齊值永遠為 8。我們還可以檢查在 64 位平臺上的對齊值,然后永遠使用該值,希望其結(jié)果是正確的(特別是希望在 Rust 中結(jié)構(gòu)體增長時,有人能記得更新給 C++ 看的大小)。但寄希望于有人記得什么事情,使用 Rust 就失去了意義。

          不管怎樣,假設(shè)常量 DECODER_SIZE和DECODER_ALIGNMENT 可以在 C++ 中使用,那么可以這樣做:

          class alignas(DECODER_ALIGNMENT) Decoder final
          {
           friend class Encoding;
          public:
           ~Decoder() {}
           Decoder(Decoder&&) = default;
          private:
           unsigned char storage[DECODER_SIZE];
           Decoder() = default;
           Decoder(const Decoder&) = delete;
           Decoder& operator=(const Decoder&) = delete;
           // ...
          };
          

          其中:

          • 構(gòu)造器 Decoder() 沒有被標記為 delete,而是標記為 default,但仍然是 private。
          • Encoding 被定義為 friend,使上面的構(gòu)造函數(shù)能夠訪問。
          • 添加了 public 的默認移動構(gòu)造函數(shù)。
          • 添加了一個 private 字段,類型為 unsigned char[DECODER_SIZE]。
          • Decoder 本身定義為 alignas(DECODER_ALIGNMENT)。
          • operator delete 不再重載。

          然后,Encoding 上的 new_decoder() 可以這樣寫(改名為 make_decoder 以避免在 C++ 中不尋常地使用“new”這個詞):

          class Encoding final
          {
          public:
           inline Decoder make_decoder() const
           {
           Decoder decoder;
           encoding_new_decoder_into(this, &decoder);
           return decoder;
           }
           // ...
          };
          

          使用方法:

          Decoder decoder = input_encoding->make_decoder();
          

          注意在 Encoder 的實現(xiàn)之外試圖定義 Decoder decoder;而不立即初始化會導(dǎo)致編譯錯誤,因為 Decoder() 構(gòu)造函數(shù)是私有的。

          我們來分析發(fā)生了什么:

          • unsigned char 數(shù)組提供了 Rust Decoder 的存儲。
          • C++ Decoder 沒有基類、虛方法等,所以實現(xiàn)沒有提供任何隱藏的成員,Decoder 的地址與它的 storage 成員的地址相同,因此可以簡單地把 Decoder 自身的地址傳遞給 Rust。
          • unsigned char 的對齊為 1(即不限制),因此 Decoder 上的 alignas 需要確定對齊。
          • 默認的移動構(gòu)造函數(shù)會 memmove Decoder 中的字節(jié),而 Rust 的 Decoder 是可以移動的。
          • 私有的默認、無參數(shù)的構(gòu)造函數(shù),使得任何在 Encoder 之外對 C++ Decoder 只作定義而不立即初始化的行為導(dǎo)致編譯錯誤。
          • 但是,Encoder 能實例化一個未初始化的 Decoder 并傳遞其指針給 Rust,這樣 Rust 代碼就能將 Rust 的 Decoder 示例寫入 C++ 通過指針提供的內(nèi)存中。

          原文:https://hsivonen.fi/modern-cpp-in-rust/

          作者:Henri Sivonen,Mozilla 的軟件開發(fā)者,致力于網(wǎng)絡(luò)層和底層,如HTML解析器、字符編碼轉(zhuǎn)換器等。

          譯者:彎月,責編:屠敏


          主站蜘蛛池模板: 无码国产伦一区二区三区视频 | 97久久精品无码一区二区天美| 国内国外日产一区二区| 精品欧洲av无码一区二区14| 日韩AV无码一区二区三区不卡毛片 | 中文字幕一区二区三区在线观看| 午夜精品一区二区三区在线观看| 国产精品视频一区国模私拍 | 春暖花开亚洲性无区一区二区| 一区二区三区四区精品视频| 一区二区三区国产| 中文字幕一区精品| 一区二区三区免费精品视频 | 午夜精品一区二区三区在线观看| 性色av闺蜜一区二区三区| 3D动漫精品一区二区三区| 夜色福利一区二区三区| 亚洲影视一区二区| 久久精品无码一区二区WWW| 亚洲Av高清一区二区三区| 2021国产精品视频一区| 亚洲一本一道一区二区三区| 97久久精品无码一区二区天美| 中文字幕人妻第一区| 亚洲日韩AV一区二区三区四区| 精品人妻系列无码一区二区三区| 亚洲av无码片区一区二区三区| 中字幕一区二区三区乱码 | 成人精品一区二区三区不卡免费看| 综合久久一区二区三区| 亚洲永久无码3D动漫一区| 久久se精品动漫一区二区三区| 久久国产精品视频一区| 国产精品高清一区二区人妖| 无码人妻一区二区三区免费| 在线观看一区二区三区视频 | 中文字幕乱码亚洲精品一区| 国产精品日韩一区二区三区| 一区二区三区免费在线视频| 国产无套精品一区二区 | 国产精品一区二区三区免费|