TML是一種超文本標記語言,易于編碼、編寫,而PDF因為其不易修改便于傳輸的特性成為一種辦公常用到的文件格式,作為職場人的我們,有時候就需要對這兩種格式進行轉換,將拿到手的PDF文件轉換成HTML格式,然后再進行編碼等其它操作,那么如何將PDF轉HTML格式?來看看這些方法吧。
方法一:借助“全能PDF轉換助手”進行操作
該軟件支持PDF與其它多種格式進行相互轉換操作,還有PDF編輯、加密、壓縮、處理等工具。它的“PDF轉HTML”功能,可以添加多個文件進行批量轉換;對于頁數比較多的PDF文件,如果我們只需要其中幾頁,就可以自定義轉換頁面,勾選頁面或者輸入頁碼都可以實現自定義操作。
具體操作步驟如下:
步驟一:
點擊“PDF選其他”,然后選擇“PDF轉HTML”,添加需要轉換的文件,可以添加單個或者多個PDF文件。
步驟二:選擇要轉換的頁面,點擊“頁碼選擇”,支持輸入頁碼選擇頁面,也可以手動勾選,在如果需要勾選的頁面過多,可以勾選不需要轉換的頁面,然后點擊“反選”,就可以選擇需要的頁面了,之后點擊“開始轉換”,等待轉換完成就可以了。
它還有APP可以使用,我們可以在手機上對PDF進行格式轉換、編輯、解密等操作,方便了我們的辦公生活。
方法二:借助“WPS office”進行操作
這是一款支持Word、Excel等格式進行編輯的軟件,我們可以通過它將PDF轉成HTML格式,但是它不能直接將PDF轉換成HTML格式,需要先轉換成Word的格式再進行下一步操作。
操作步驟如下:
步驟一:打開PDF文件,點擊上方工具欄的“轉換”,然后選擇“PDF轉Word”,等待轉換完成。
步驟二:打開轉成Word格式的文件,點擊上方工具欄的“文件”,然后選擇“另存為”,在彈出的頁面,更改文件類型為“單一網頁文件*.mht;*mhtml”,接下來點擊“保存”,就可以得到HTML格式的文件了。
看了上面的方法,你知道如何將PDF轉HTML格式了嗎?操作不算難,跟著上面的步驟操作就可以實現PDF轉成HTML格式啦,有需要的朋友,可以試一試哦。
大家知道HTML格式嗎?我們通常上網瀏覽的網頁就是HTML格式。而PDF格式是我們常用的一種文件格式,在不同的設備上打開,既不會影響到PDF內容的排版,也不容易被修改。在工作中,有時為了查看PDF文件在網頁狀態下的排版,以及對內容進行編輯修改,我們需要將PDF轉成HTML。可能有些小伙伴們不知道如何轉換。別著急,今天這期PDF轉HTML轉換器推薦,給大家做一下詳細介紹。
轉換方法一:借助“萬能文字識別軟件”完成轉換
安利指數:★★★★☆
安利理由:功能豐富,支持多種格式進行轉換
這款軟件主打文字識別功能,它能夠準確識別圖片、視頻、音頻中的文字內容,并將它們轉換成文字。不止這些,它還能實現全能翻譯、AI修復照片、PDF轉換處理等操作。
像PDF轉成HTML就可以使用這款軟件完成。它的轉換速度很快,如果文件數量較多也不用擔心,我們可以將文件批量上傳,大大提高我們的效率。
轉換流程:
步驟一:打開軟件,找到【PDF轉換處理】,選擇【PDF轉HTML】按鈕。
步驟二:將需要轉換的PDF文件直接拖拽進軟件。
步驟三:點擊【開始轉換】,轉換后的HTML文件默認保存到電腦桌面。轉換成功后,可以點擊查看。
告訴大家一個好消息,我們除了可以在電腦上操作,也可以在手機下載它的APP進行使用哦。如果遇到需要進行翻譯、掃描、PDF轉換處理等情況,也可以使用APP來操作,非常方便!
轉換方法二:借助“WPS”完成轉換
安利指數:★★★☆☆
安利理由:支持文檔表格編輯處理
WPS作為我們經常使用的辦公軟件,擁有對word,PPT等文檔進行編輯的能力,那你知道它還能實現PDF轉HTML的操作嗎?
轉換流程:
首先打開WPS軟件,新建文檔。然后將PDF文件的內容復制到word文檔后保存。保存的格式選擇【單一網頁文件】即可。
以上就是今天的PDF轉HTML轉換器推薦。看完這篇文章,大家知道如何轉換了嗎?有需要的小伙伴,趕快收藏起來吧!
版本 56 開始,Firefox 瀏覽器支持一種新的字符編碼轉換庫,叫做 encoding_rs。它是用 Rust 編寫的,代替了從 1999 年就開始使用的 C++ 編寫的字符編碼庫 uconv。最初,所有調用該字符編碼轉換庫的代碼都是 C++,所以盡管新的庫是用 Rust 編寫的,它也必須能被 C++ 代碼調用。實際上,在 C++ 調用者看來,這個庫跟現代的 C++ 庫沒什么區別。下面是我實現這一點采用的開發方式。
相關閱讀:
所謂“現代”C++的意思就是從 C++ 調用者來看,函數庫遵循 C++ 的核心指南(https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines),并具備以下新特性:
上面的 gsl:: 表示 Guidelines Support Library(https://github.com/microsoft/GSL),這個庫能提供核心指南要求、但尚未存在于 C++ 標準庫中的東西。
“用 Rust”寫 C++ 庫的意思是指庫中的大部分是用 Rust 寫的,但提供給 C++ 調用者的接口至少在 C++ 調用者來看就像個真正的 C++ 庫一樣。
C++ 的 ABI 非常復雜,而 Rust ABI 尚未完全確定。但是,C++ 和 Rust 都支持一些使用 C ABI 的函數。因此,要想讓 C++ 和 Rust 擁有互操作性,就需要通過某種方法,讓 C++ 把 Rust 代碼看成 C 代碼,Rust 把 C++ 代碼看成 C 代碼。
這篇文章并不是 Rust 與 C++ 互聯的完整指南。encoding_rs 的接口非常簡單,缺乏兩種語言之間的互操作性上的常見問題。但是,encoding_rs 簡化 C++ 接口的例子可以作為一個指南,給那些希望在設計函數庫時了解跨語言互操作性的人們提供一些幫助。具體來說:
為了理解我們討論的 Rust API(https://docs.rs/encoding_rs/0.8.13/encoding_rs/),先來從高層次看看。整個函數庫有三個公開的結構體(struct):Encoding,Decoder 和 Encoder。從函數庫的使用者角度來看,這些結構能為各種具體的編碼提供統一的接口,所以可以像 traits、父類或接口一樣使用, 但嚴格來說它們實際上是結構體。Encoding 的實例是靜態分配的。Decoder 和 Encoder 封裝了流轉換的狀態,是在運行時動態分配的。
Encoding 實例的引用(即&'static Encoding)可以通過標簽獲得(從協議文本中提取的文本識別信息),或通過命名靜態變量(named static)獲得。然后 Encoding 可以作為 Decoder 的參數使用,后者是在棧上分配的。
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 中有個方法可以將流從調用者分配的一個切片解碼到調用者分配的一個切片。解碼器不進行堆分配操作。
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) }
在處理流之外的情況時,調用者完全不需要處理 Decoder 和 Encoder 的任何東西。Encoding 會提供方法在一個緩沖區中處理整個邏輯輸入流。
impl Encoding { pub fn decode_without_bom_handling_and_without_replacement<'a>( &'static self, bytes: &'a [u8], ) -> Option<Cow<'a, str>> }
0. 對 FFI 友好的設計
有些設計來自于問題域本身的簡化因素。而有些只是選擇。
字符編碼庫可以合理地將編碼、解碼器和編碼器的概念表示成 traits(類似于 C++ 中沒有字段的抽象父類),但是,encoding_rs 對這些概念采用了結構體(struct),以便在分發的時候能 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 本身,而是故意讓層次結構不能擴展。其背后反映的哲學是,添加字符編碼不應該是程序員應當關心的事情。相反,程序應當使用 UTF-8 作為數據交換,而且程序不應當支持古老的編碼,除非需要兼容已有的內容。這種不可擴展的層次結構能帶來強類型安全。如果你從 encoding_rs 得到一個 Encoding 實例,那么你可以信任它絕不會給出任何編碼標準中沒有給出的特性。也就是說,你可以相信它絕不會表現出 UTF-7 或 EBCDIC 的行為。
此外,通過分發 enum,一個編碼的解碼器可以在內部根據 BOM 嗅探的結果變成另一個編碼的解碼器。
有人可能會說,Rust 提供編碼轉換器的方式是將它變成迭代適配器,接受字節迭代器的輸入,然后輸出 Unicode 的標量值,或者相反。然而迭代器不僅在跨越 FFI 邊界時更復雜,還使得加速 ASCII 處理等技巧更難以實現。而直接接受一個切片進行讀取和寫入操作,不僅使得提供 C API 更容易(用 C 的術語來說,Rust 切片解構成對齊的非空指針和一個長度值),而且可以通過觀察多個代碼單元能放入單個寄存器(ALU 寄存器或 SIMD 寄存器)的情況,實現一次處理多個代碼單元,從而實現 ASCII 處理加速。
如果 Rust 的原生 API 只處理基本類型、切片和(非 trait 對象的)結構體,那么與支持高級 Rust 特性的 API 相比,這個 API 更容易映射到 C API。(在 Rust 中,發生類型擦除時會產生一個 trait 對象。也就是說,你得到的是一個 trait 類型的引用,它并沒有給出該引用指向的那個結構體的類型信息。)
1. 建立 C API
當涉及到的類型足夠簡單時,C 和 Rust之間的主要鴻溝,一是 C 語言缺乏方法、缺乏多返回值功能,二是不能以值形式傳送 C 結構體之外的類型。
2.在 C++ 中根據 C API 重建 API
即使是慣用的 C API(https://github.com/hsivonen/encoding_c/blob/master/include/encoding_rs.h)也不能當做現代 C++ API 使用。幸運的是,類似于多重返回值、切片等 Rust 概念可以在 C++ 中表示,只需將 C API 返回的指針解釋成指向 C++ 對象的指針,就能展示出 C++ 的優雅。
大部分例子來自一個使用了 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)。這里我假設標準庫類型的例子更容易被更多讀者接受。
方法的優雅
對于每個 C 語言中不透明的構造體指針,C++ 中都會定義一個類,C 的頭文件也會修改,使得從 C++ 編譯器的角度來看,指針類型變成指向 C++ 類實例的指針。這些放在一起就相當于一個 reinterpret_cast 過的指針,而不需要實際寫出 reinterpret_cast。
由于指針并不真正指向它們看似指向的類的實例,而是指向 Rust 結構體的實例,因此應該事先做好預防措施。這些類中沒有定義任何字段。默認的無參數構造函數和復制構造方法被刪除,默認的 operator= 也被刪除。此外,這些類還不能包含虛方法。(最后一點是個重要的限制條件,稍后會討論。)
class Encoding final { // ... private: Encoding() = delete; Encoding(const Encoding&) = delete; Encoding& operator=(const Encoding&) = delete; ~Encoding() = delete; };
對于 Encoding 來說,所有實例都是靜態的,因此析構函數也被刪掉了。如果是動態分配的 Decoder 和 Encoder,還要添加一個空的析構函數和一個 static void operator delete。(后面會給一個例子。)這樣能讓這個偽 C++ 類的析構過程導向 C API 中相應類型的釋放函數。
這些基礎工作將指針變得看上去像是 C++ 類實例的指針。有了這些,就能在這些指針上實現方法調用了。(介紹完下一個概念后也會給出實例。)
返回動態分配的對象
前面說過,Rust API 以值方式返回 Encoder 或 Decoder,這樣調用者可以將返回值放在棧上。這種情況被 FFI 的包裹代替,因此 C API 只需通過指針暴露堆上分配的對象。而且,這些指針也被重新解釋為可 delete 的 C++ 對象指針。
不過還需要確保這些 delete 會在正確的時機被調用。在現代 C++ 中,如果對象在同一時刻只能有一個合法的所有者,那么對象指針會被包裹在 std::unique_ptr 或 mozilla::UniquePtr 中。老的 uconv 轉換器支持引用計數,但在 Gecko 代碼中所有實際的應用中,每個轉換器都只有一個所有者。由于編碼器和解碼器的使用方式使得同一時刻只有一個合法的所有者,因此 encoding_rs 的兩個 C++ 包裹就使用了 std::unique_ptr 和 mozilla::UniquePtr。
我們來看看 Encoding 中那個返回 Decoder 的工廠方法。在 Rust 中,這個方法接收 self 的引用,通過值返回 Decoder。
impl Encoding { pub fn new_decoder(&'static self) -> Decoder { // ... } }
在 FFI 層,第一個參數是顯式的指針類型,對應于 Rust 的 &self 和 C++ 的 this(具體來說,是 const 版本的 this)。我們在堆上分配內存(Box::new())然后將 Decoder 放進分配好的內存中。然后忘記內存分配(Box::into_row),這樣可以將指針返回給 C,而不會在作用域結束時釋放。為了能夠釋放內存,我們引入了一個新的函數,將 Box 放回,然后將它賦給一個變量,以便立即離開作用域,從而釋放堆上分配的內存。
#[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++ 環境中使用(而不是作為純 C API 使用)時將其替換成正確的 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 導向回 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 類型的引用方法調用只不過是普通的函數調用,但第一個參數是指向 self 的引用。在 C++ 一側,非虛方法的調用原理相同:非虛 C++ 方法調用只不過是函數調用,但第一個函數是 this 指針。
在 FFI/C 層,我們可以將同樣的指針顯式地作為第一個參數傳遞。
在調用 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 類型”和“非虛”是非常重要的。要想讓上面的代碼正確工作,那么無論那一側都不能有 vtable。這就是說,Rust 不能有 trait,C++ 也不能有繼承。在 Rust 中,trait 對象(指向任何實現了 trait 的結構體的 trait 類型的引用)實現為兩個指針:一個指向結構體實例,另一個指向對應于數據的具體類型的 vtable。我們需要能夠把 self 的引用作為單一指針跨越 FFI 傳遞,所以在跨越 FFI 時無法攜帶 vtable 指針。為了讓 C++ 對象指針兼容 C 的普通指針,C++ 將 vtable 指針放在了對象自身上。由于我們的指針指向的并不是真正帶有 vtable 的 C++ 對象,而是 Rust 對象,所以必須保證 C++ 代碼不會在指針目標上尋找 vtable 指針。
其結果是,Rust 中的結構對應的 C++ 中的類不能從 C++ 框架中的通用基類繼承。在 Gecko 的情況中,C++ 類不能繼承 nsISupports。例如,在 Qt 的語境下,對應的 C++ 類不能從 QObject 繼承。
非空指針
Rust API 中有的方法會返回 &'static Encoding。Rust 的引用永遠不會為 null,因此最好是將這個信息傳遞給 C++ API。C++ 中對應于此的是 gsl::not_null和mozilla::NotNull。
由于 gsl::not_null 和 mozilla::NotNull 只不過是類型系統層面的寫法,它并不會改變底層指針的機器表示形式,因此對于有 Rust 保證的指針,跨越 FFI 之后可以認為它們絕不會為 null,所以我們想做的是,利用與之前將 FFI 返回的指針重新解釋為指向無字段、無虛方法的 C++ 對象的指針同樣的技巧來騙過 C++ 編譯器,從而在頭文件中聲明那些 FFI 返回的絕不會為 null 的指針為類型 mozilla::NotNull<const Encoding*>。不幸的是,實際上這一點無法實現,因為在 C++ 中,涉及模板的類型不能在 extern "C" 函數的定義中使用,所以 C++ 代碼最后只能在從 C API 接收到指針、包裹在 gsl::not_null 或 mozilla::NotNull 時進行一系列的 null 檢查。
但是,也有一些定義是指向編碼對象常量的靜態指針(指向的目標是在 Rust 中定義的),而且恰巧 C++ 允許將這些定義為 gsl::not_null<const Encoding*>,所以我們這樣實現了。(感謝 Masatoshi Kimura 指出這一點的可行性。)
Rust 中靜態分配的 Encoding 實例的定義如下:
pub static UTF_8_INIT: Encoding = Encoding { name: "UTF-8", variant: VariantEncoding::Utf8, }; pub static UTF_8: &'static Encoding = &UTF_8_INIT;
在 Rust 中,通用的規則(https://twitter.com/tshepang_dev/status/1051558270425591808)是 static 用來聲明不會改變的內存地址,const 用來聲明不會改變的值。因此,UTF_8_INIT 應當為 static,而 UTF_8 應當為 const:指向 static 實例的引用的值不會改變,但為這個引用靜態分配的內存地址則不一定。不幸的是, Rust 有一條規則說,const 的右側不能包含任何 static 的東西,因此這一條阻止了對 static 的引用,以確保 const 定義的右側可以被靜態檢查,確定它是否適合任何假想的 const 定義——甚至是那些在編譯時就試圖解引用(dereference)的定義。
但對于 FFI,我們需要為 UTF_8_INIT 分配一塊不會改變的內存,因為這種內存能在 C 的連接器中使用,可以讓我們為 C 提供命名的指針類型的東西。上面說的 UTF_8 的表示形式已經是我們需要的了,但為了讓 Rust 更優雅,我們希望 UTF_8 能參與到 Rust 的命名空間中。這意味著從 C 的角度來看,它的名字需要被改變(mangle)。我們浪費了一些空間來重新靜態分配指針來避免改變名稱,以供 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 引用類型擁有同樣的表現形式)。但是,Rust 編譯器拒絕編譯帶有全局可視性指針的程序。由于全局變量可以被任何線程訪問,多線程同時訪問指針指向的目標可能會引發問題。這種情況下,指針目標不會被修改,因此全局可視性是沒問題的。為了告訴編譯器這一點,我們需要為指針實現 Sync 這個 marker trait。但是,trait 不能在指針類型上實現。作為迂回方案,我們為*const Encoding創建了一個新的類型。新的類型擁有與它包裹的類型同樣的表現形式,但我們可以在新類型上實現 trait。實現 Sync 是 unsafe 的,因為我們告訴了編譯器某些東西可以接受,這并不是編譯器自己發現的。
在 C++ 中我們可以這樣寫(宏擴展之后的內容):
extern "C" { extern gsl::not_null<const encoding_rs::Encoding*> const UTF_8_ENCODING; }
指向編碼器和解碼器的指針也絕不會為 null,因為內存分配失敗會直接終止程序。但是 std::unique_ptr / mozilla::UniquePtr 和 gsl::nul / mozilla::NotNull 不能結合使用。
可選值
Rust 中常見的做法是用 Option<T> 表示返回值可能有值也可能沒有值。現在的 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> 中提取出包裹值時最優雅的方法就是使用 operator*(),但這個也是沒有檢查的,因此也是不安全的。
多返回值
盡管 C++ 在語言層面缺少對于多返回值的支持,但多返回值可以從庫的層次實現。比如標準庫,相應的部分是 std::tuple,std::make_tuple 和 std::tie。在 Gecko 中,相應的庫是 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 切片包裹了一個自己不擁有的指針,和指針指向內容的長度,表示數組中的一段連續內容。相應的 C 代碼為:
src: &[u8] const uint8_t* src, size_t src_len dst: &mut [u8] uint8_t* dst, size_t dst_len
C++ 的標準庫中并沒有對應的東西(除了 std::string_view 可以用來表示只讀字符串切片之外),但 C++ 核心指南中已經有一部分叫做 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 發布時,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 關鍵字,并用 mozilla:: 的類型和類型 trait 代替標準庫中的類型。在 Gecko 改成 C++14 后,部分 constexpr 關鍵字被恢復了。
不論如何, 我們有了自己的 mozilla::Span,現在可以添加 gsl::span 中缺少的、像 Rust 一樣的子 span 了。如果你需要子 span 下標 i 開始直到 j,但不包括 j,那么 gsl::span 的實現方法是:
&slice[i..] span.subspan(i) &slice[..i] span.subspan(0, i) &slice[i..j] span.subspan(i, j - i)
而 mozilla::Span 的實現方法是:
&slice[i..] span.From(i) &slice[..i] span.To(i) &slice[i..j] span.FromTo(i, j)
gsl::span 和 Rust 的切片有一個重要的區別:它們解構成指針和長度的方式不同。對于零長度的 gsl::span,指針可能會解構為 nullptr。而 Rust 切片中,指針必須不能為 null 且必須對齊,甚至零長度切片也是如此。乍一看起來似乎有點違反直覺:當長度為零時,指針永遠不會解引用,那么它是否為 null 有什么關系嗎?實際上,在優化 Option 之類的枚舉之中的 enum 差異時這一點非常重要。None 表示為全零比特,所以如果包裹在 Some() 中,那么指針為 null、長度為零的切片就可能偶然被當做 None。通過要求指針不為 null 指針,Option 中的零長度切片就可以與 None 區分開來。通過要求指針必須對齊,當切片元素類型的對齊大于一時,就有可能進一步使用指針的低位比特。
在意識到我們不能將從 C++ 的 gsl::span::data() 中獲得的指針直接傳遞給 Rust 的 std::slice::from_raw_parts() 后,我們必須決定要在哪里將 nullptr 替換成 reinterpret_cast<T*>(alignof(T))。如果使用 gsl::span 則有兩個候選的位置:提供 FFI 的 Rust 代碼中,或者在調用 FFI 的 C++ 代碼中。而如果使用 mozilla::Span,我們可以改變 span 的實現代碼,因此還有另外兩個候選的位置:mozilla::Span 的構造函數,和指針的 getter 函數。
在這些候選位置中,mozilla::Span 的構造函數似乎是編譯器最有可能優化掉某些檢查的地方。這就是為什么我決定將檢查放在這里的原因。這意味著如果使用 gsl::span,那么檢查的代碼必須移動到FFI的調用中。所有從 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 的調用者即使在長度為零時也不要傳遞 NULL。但是,C API 在未定義行為方面已經有很多問題了,所以再加一個未定義行為似乎也不是什么大事兒。
合并到一起
我們來看看上面這些特性結合后的例子。首先,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 } } }
由于它是個靜態方法,因此不存在指向 self 的引用,在 FFI 函數中也沒有相應的指針。該切片解構成一個指針和一個長度。長度變成 in/out 參數,用來返回切片刀長度,以及 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 上重建對應于 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,因為隱式構造函數在 std::tuple 嵌入到 std::optional 中時不能正確工作。
代數類型
之前,我們看到了 Rust 側的流 API 可以返回這個 enum:
pub enum DecoderResult { InputEmpty, OutputFull, Malformed(u8, u8), }
現在 C++ 也有了類似 Rust 的 enum 的東西:std::variant<Types...>。但在實踐中,std::variant 很難用,因此,從優雅的角度來看,Rust 的 enum 本應是個輕量級的東西,所以沒有道理使用 std::variant 代替。
首先,std::variant 中的變量是沒有命名的。它們通過位置或類型來識別。命名變量曾經作為 lvariant(http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0095r1.html)提議過,但并沒有被接受。其次,即使允許重復的類型,使用它們也是不現實的。第三,并沒有語言層面上相當于 Rust 的 match(https://doc.rust-lang.org/book/second-edition/ch06-02-match.html)的東西。曾經提議過的inspect(http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0095r1.html)相當于 match 的機制,但并未被接受。
在 FFI/C 層,上面 enum 的信息被打包到一個 u32 中。我們沒有試圖將它在 C++ 側擴展成更漂亮的東西,而是簡單地使用了與 C API 同樣的 uint32_t。如果調用者需要在異常情況下從中提取出兩個小的整數,那么調用者可以自己用位操作從 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 的魔術值是個微優化。在某些架構上,與零比較的代價要比與其他常量比較更低,而表示解碼時的異常情況和無法映射的情況的值不會與零重疊。
通知整數溢出
Decoder 和 Encoder 擁有一些方法用于查詢最壞情況下的緩沖區大小需求。 調用者提供輸入的代碼單元的數量,方法返回要保證相應的轉換方法不會返回OutputFull 所需的最小緩沖區大小(以代碼單元為單位)。
例如,將 UTF-16 編碼成 UTF-8,最壞情況下等于乘以三。至少在原理上,這種計算可以導致整數溢出。在 Rust 中,整數溢出被認為是安全的,因為即使由于整數溢出而分配了太少的緩沖區,實際上訪問緩沖區也會進行邊界檢查,所以整體的結果是安全的。但是,緩沖區訪問在 C 或 C++ 中通常是沒有邊界檢查的,所以 Rust 中的整數溢出可能會導致 C 或 C++ 中的內存不安全,如果溢出的計算結果被用來確定緩沖區分配和訪問時的大小的話。對于 encoding_rs 而言,即使是 C 或 C++ 負責分配緩沖區,寫入操作也是由 Rust 進行的, 所以也許是沒問題的。但為了確信起見,encoding_rs 提供的最壞情況的計算也進行了溢出檢查。
在 Rust 中,經過溢出檢查的結果會返回 Option<usize>。為保持 C API 中類型的簡單性,C API 會返回 size_t,并用 SIZE_MAX 通知溢出。因此,C API 實際上使用的是飽和算術(saturating arithmetic)。
在使用標準庫類型的 C++ API 中,返回類型是 std::optional<size_t>。在 Gecko 中,我們使用了一個整數類型的包裹,提供溢出檢查和有效性標志。在 Gecko 版本的 C++ API 中,返回值是 mozilla::CheckedInt<size_t>,這樣處理溢出信號的方式與 Gecko 的其他部分一致。(邊注:我發現 C++ 標準庫依然沒有提供類似 mozilla::CheckedInt 的包裹以進行整數運算中的溢出檢查時感到非常震驚——這應該是標準就支持的避免未定義行為的方式。)
我們再來看看 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 語言沒有標準類型來保存堆上分配的字符串,從而知道字符串的長度和容量,從而能在字符串被修改時重新分配其緩沖區。也許可以建立一種新的 C 類型,其緩沖區由 Rust 的 String 負責管理,但這種類型就沒辦法兼容 C++ 的字符串了。第三,借來的 C 字符串切片在 C 語言中將會表現成原始的指針和一個長度,一些文檔說這個指針僅在輸入指針有效的時候才有效。因此并沒有語言層面的機制來防止指針在釋放之后被使用。
解決方案并不是完全不在 C 層面提供非流式 API。下 Rust 側,非流式 API 只是個構建在流式 API 和一些驗證函數(ASCII 驗證、UTF-8 驗證、ISO-2022-JP ASCII 狀態驗證)上的便利 API。
盡管 C++ 的類型系統能夠表示與 Rust 的 Cow<'a, str> 相同的結構體,如std::variant<std::string_view, std::string>,但這種C++的Cow是不安全的,因為它的生存期限'a無法被 C++ 強制。盡管 std::string_view(或gsl::span)可以(在大多素情況下)在 C++ 中作為參數使用,但作為返回值類型,它會導致在釋放后發生訪問。與 C 一樣,最好的情況就是有某個文檔能說明只要輸入的 gsl::span 有效,輸出的 std::string_view 就有效。
為了避免發生釋放后訪問,我們在依然使用 C++17 的 C++ API 的版本中,簡單地令 C++ 的 decode_without_bom_handling_and_without_replacement() 函數永遠復制并返回一個 std::optional<std::string>。
但在 Gecko 的情況中,我們能夠在保證安全的情況下做得更好。Gecko 使用了 XPCOM 字符串(https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Guide/Internal_strings),它提供了多種存儲選項,特別是:從其他人那里(不安全地)借來的 dependent string,自動用將短字符串行內嵌入至行內緩沖區的 auto string,以及指向堆上分配的引用計數器緩沖區的 shared string。
如果要解碼的緩沖區是個指向堆上分配的引用計數器緩沖區的 XPCOM 字符串,而且我們需要解碼至 UTF-8(而不是 UTF-16),而在這種情況下本應該從 Rust 那里借(除非是刪除 BOM 的情況),現在我們可以另輸出字符串指向與輸入相同的堆上分配的引用計數器緩沖區(并增加引用計數)。這正是 mozilla::Encoding 的非流式 API 做法。
與 Rust 相比,除了輸入字符串必須使用引用計數存儲以便復制能正確工作之外,還有另外一個限制:如果 BOM 被移除,那么輸入不能有 UTF-8 BOM。雖然 Rust 可以從輸入中借出不帶 BOM 的那一段切片,但對于 XPCOM 字符串,增加引用計數的方式只有在輸入和輸輸出的字節內容完全一致的情況下才能正確工作。如果省略掉開頭的三個字節,它們就不是完全一致了。
雖然使用 C++17 標準庫類型的 C++ API 中的非流式 API 是在 C++ 流式 API 的基礎上構建的,但為了更多的安全性,mozilla::Encoding 的非流式 API 并不是基于流式 C++ API 構建的,而是在 Rust 語言的流式 Rust API 的基礎上構建的(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 字符串。
由于 C++ 沒有安全的借用機制而導致必須在非流式 API 中進行復制之外,還有一點點令人失望的是,從 C++ 中實例化 Decoder 和 Encoder 需要進行堆分配操作,而 Rust 調用者是在棧上分配這些類型。我們能讓 C++ 的使用者也避免堆分配操作嗎?
答案是可以,但正確地實現這一點需要讓 C++ 的構建系統查詢 rustc 以構建常量,使得系統變得異常復雜。
我們不能跨越 FFI 直接用值的形式返回非 C 的結構體,但如果一個恰當地對齊的指針有足夠多的內存,我們可以將非 C 的結構體寫到由 FFI 的另一側提供的內存中。實際上,API 支持這個功能,作為之前在堆上實例化新的 Decoder 的操作的一種優化措施:
#[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() 應當旨在之前從 API 獲得了 Decoder 的指針的情況下使用,對于 Decoder 的情況來說,使用 = 進行賦值操作應當不是問題,就算指針指向的內存地址沒有初始化,因為 Decoder 并沒有實現 Drop。用 C++ 的術語來說,Rust 中的 Decoder 沒有析構函數,所以只要該指針之前指向合法的 Decoder,那么使用 = 進行賦值不會進行任何清理工作。
如果編寫一個 Rust 結構體并實現 Drop 使之析構成未初始化的內存,那就應該使用 std::ptr::write() 代替 =。std::ptr::write() 能“用給定的值覆蓋內存地址,而不會讀取或放棄舊的值”。也許,上面的情況也能作為使用 std::ptr::write() 的很好的例子,盡管嚴格來說并不那么必要。
從 Rust 的 Box 中獲得的指針能保證正確地對齊,并且指向足夠大小的一片內存。如果 C++ 要分配棧內存供 Rust 代碼寫入,就要讓 C++ 代碼使用正確的大小和對齊。而從 Rust 向 C++ 傳遞這兩個值的過程,就是整個代碼變得不穩定的開始。
C++ 代碼需要自己從結構體發現正確的大小和對齊。這兩個值不能通過調用 FFI 函數獲得,因為 C++ 必須在編譯時就確定這兩個值。大小和對齊并不是常量,因此不能手動寫到頭文件中。首先,每當 Rust 結構體改變時這兩個值都會改變,因此直接寫下來有可能過會導致它們不能適應 Rust 結構體改變后的真實需求。其次,這兩個值在 32 位體系和 64 位體系上不一樣。第三,也是最糟糕的一點,一個 32 位體系上的對齊值可能與另一個 32 位體系的對齊值不一樣。具體來說,絕大多數目標體系上的 f64 的對齊值是 8,如 ARM、MIPS 和 PowerPC,而 x86 上的 f64 的對齊值是 4。如果 Rust 有 m68k 的移植(https://lists.llvm.org/pipermail/llvm-dev/2018-August/125325.html),那么有可能會使 32 位平臺上的對齊值產生更多不確定性(https://bugzilla.mozilla.org/show_bug.cgi?id=1325771#c49)。
似乎唯一的正確方法就是,作為構建過程的一部分,從 rustc 中提取出正確的大小和對齊信息,然后再編譯 C++ 代碼,這樣就可以將兩個數字寫入生成的 C++ 頭文件中,供 C++ 代碼參考。更簡單的方法是讓構建系統運行一小段Rust程序,利用 std::mem::size_of和std::mem:align_of 獲取這兩個數值并輸出到 C++ 頭文件中。這個方案假定構建和實際運行發生在同一個目標體系上,所以不能在交叉編譯中使用。這一點可不太好。
我們需要從 rustc 中提取給定的結構體在特定體系下的大小和對齊值,但不能通過執行程序的方式。我們發現(https://blog.mozilla.org/nnethercote/2018/11/09/how-to-get-the-size-of-rust-types-with-zprint-type-sizes/)rustc有個命令行選項,-Zprint-type-sizes,能夠輸出類型的大小和對齊值。不幸的是,這個選項僅存在于每日構建版本上……不過不管怎樣,最正確的方法還是讓一個構架腳本首先用該選項調用 rustc,解析我們關心的大小和對齊,然后將它們作為常量寫入 C++ 頭文件總。
或者,由于“過對齊”(overalign)是允許的,我們可以信任結構體不會包含 SIMD 成員(對于128位向量來說對齊值為 16),因此對齊值永遠為 8。我們還可以檢查在 64 位平臺上的對齊值,然后永遠使用該值,希望其結果是正確的(特別是希望在 Rust 中結構體增長時,有人能記得更新給 C++ 看的大小)。但寄希望于有人記得什么事情,使用 Rust 就失去了意義。
不管怎樣,假設常量 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; // ... };
其中:
然后,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 的實現之外試圖定義 Decoder decoder;而不立即初始化會導致編譯錯誤,因為 Decoder() 構造函數是私有的。
我們來分析發生了什么:
原文:https://hsivonen.fi/modern-cpp-in-rust/
作者:Henri Sivonen,Mozilla 的軟件開發者,致力于網絡層和底層,如HTML解析器、字符編碼轉換器等。
譯者:彎月,責編:屠敏
*請認真填寫需求信息,我們會在24小時內與您取得聯系。