一旦您超过了Borrow-Checker的痛苦阈值,并意识到Rust可以让您完成其他语言无法想象的(有时是危险的)事情,那么您可能也会有同样的不可抗拒的渴望,将所有内容重写为Rust 。 尽管在最好的情况下这是平庸的徒劳(对几个项目毫无意义的浪费),但在最坏的情况下却导致代码质量下降(毕竟,为什么您认为自己在使用库方面比原始作者更有经验?)
通过重用原始代码为原始库提供安全的接口将更加有用。
• 第一步
• 我们收集chmlib-sys
• 在Rust中编写安全的包装器
• 按名称搜索项目
• 通过过滤器绕过元素
• 读取文件内容
• 添加示例
• CHM文件目录
• 将CHM文件解压缩到磁盘
• 接下来呢?
本文讨论了一个真实的项目。 我不得不从现有的CHM文件中提取信息,但是没有时间了解该格式。 懒惰是进步的动力。
chmlib板条箱发布在crates.io上 ,其源代码可在GitHub上获得 。 如果您发现它有用或发现问题,请通过bugtracker告诉我。
第一步
首先,有必要了解库的工作最初是如何构想的。
这不仅会教您如何使用它,而且还会确保一切正常。 如果幸运的话,您甚至可以找到现成的测试和示例。
不要跳过这一步!
我们将使用CHMLib (一个用于读取Microsoft编译的HTML帮助 ( .chm
)文件的C库)进行工作。
首先创建一个新项目,并将CHMLib作为git子模块连接:
$ git init chmlib && cd chmlib Initialized empty Git repository in /home/michael/Documents/chmlib/.git/ $ touch README.md Cargo.toml $ cargo new --lib chmlib Created library `chmlib` package $ cargo new --lib chmlib-sys Created library `chmlib-sys` package $ cat Cargo.toml [workspace] members = ["chmlib", "chmlib-sys"] $ git submodule add git@github.com:jedwing/CHMLib.git vendor/CHMLib Cloning into '/home/michael/Documents/chmlib/vendor/CHMLib'... remote: Enumerating objects: 99, done. remote: Total 99 (delta 0), reused 0 (delta 0), pack-reused 99 Receiving objects: 100% (99/99), 375.51 KiB | 430.00 KiB/s, done. Resolving deltas: 100% (45/45), done.
之后,看看使用tree
的内部内容:
$ tree vendor/CHMLib vendor/CHMLib ├── acinclude.m4 ├── AUTHORS ├── ChangeLog ├── ChmLib-ce.zip ├── ChmLib-ds6.zip ├── configure.in ├── contrib │ └── mozilla_helper.sh ├── COPYING ├── Makefile.am ├── NEWS ├── NOTES ├── README └── src ├── chm_http.c ├── chm_lib.c ├── chm_lib.h ├── enum_chmLib.c ├── enumdir_chmLib.c ├── extract_chmLib.c ├── lzx.c ├── lzx.h ├── Makefile.am ├── Makefile.simple └── test_chmLib.c 2 directories, 23 files
看起来该库使用GNU Autotools构建。 这不好,因为chmlib板条箱的所有用户(及其用户)都需要安装Autotools。
我们将尝试通过手动收集C代码来摆脱这种“传染性”的依赖关系,但稍后会对此进行更多介绍。
lzx.h和lzx.c文件包含LZX压缩算法的实现。 通常,最好使用某种liblzx库免费获取所有更新,但是愚蠢地编译这些文件可能会更容易。
enum_chmLib.c,enumdir_chmLib.c,extract_chmLib.c似乎是使用函数chm_enumerate(),chm_enumerate_dir(),chm_retrieve_object()的示例。 它将派上用场...
文件test_chmLib.c包含另一个示例,这次从CHM文件中提取一页到磁盘。
chm_http.c实现了一个简单的HTTP服务器,该浏览器在浏览器中显示.chm文件。 这可能不再有用。
因此,我们整理了供应商/ CHMLib / src中的所有内容。 我们会收集图书馆吗?
老实说,它很小,足以应用科学的戳法。
$ clang chm_lib.c enum_chmLib.c -o enum_chmLib /usr/bin/ld: /tmp/chm_lib-537dfe.o: in function `chm_close': chm_lib.c:(.text+0x8fa): undefined reference to `LZXteardown' /usr/bin/ld: /tmp/chm_lib-537dfe.o: in function `_chm_decompress_region': chm_lib.c:(.text+0x18ca): undefined reference to `LZXinit' /usr/bin/ld: /tmp/chm_lib-537dfe.o: in function `_chm_decompress_block': chm_lib.c:(.text+0x2900): undefined reference to `LZXreset' /usr/bin/ld: chm_lib.c:(.text+0x2a4b): undefined reference to `LZXdecompress' /usr/bin/ld: chm_lib.c:(.text+0x2abe): undefined reference to `LZXreset' /usr/bin/ld: chm_lib.c:(.text+0x2bf4): undefined reference to `LZXdecompress' clang: error: linker command failed with exit code 1 (use -v to see invocation)
好吧,也许仍然需要此LZX ...
$ clang chm_lib.c enum_chmLib.c lzx.c -o enum_chmLib
呃...全部?
为了确保代码正常工作,我从互联网上下载了一个示例:
$ curl http://www.innovasys.com/static/hs/samples/topics.classic.chm.zip \ -o topics.classic.chm.zip $ unzip topics.classic.chm.zip Archive: topics.classic.chm.zip inflating: output/compiled/topics.classic.chm $ file output/compiled/topics.classic.chm output/compiled/topics.classic.chm: MS Windows HtmlHelp Data
让我们看看enum_chmLib是如何处理的:
$ ./enum_chmLib output/compiled/topics.classic.chm output/compiled/topics.classic.chm: spc start length type name === ===== ====== ==== ==== 0 0 0 normal dir / 1 5125797 4096 special file /#IDXHDR ... 1 4944434 11234 normal file /BrowserView.html ... 0 0 0 normal dir /flash/ 1 532689 727 normal file /flash/expressinstall.swf 0 0 0 normal dir /Images/Commands/RealWorld/ 1 24363 1254 normal file /Images/Commands/RealWorld/BrowserBack.bmp ... 1 35672 1021 normal file /Images/Employees24.gif ... 1 3630715 200143 normal file /template/packages/jquery-mobile/script/ jquery.mobile-1.4.5.min.js ... 0 134 1296 meta file ::DataSpace/Storage/MSCompressed/Transform/ {7FC28940-9D31-11D0-9B27-00A0C91E9C7C}/ InstanceData/ResetTable
主啊, 即使在这里 jQuery¯\ _(ツ)_ /¯
编译chmlib-sys
现在我们知道足够在chmlib -sys crate中使用CHMLib,它负责构建本机库,将其与Rast编译器链接,并提供C函数的接口。
要构建该库,您需要编写build.rs
文件。 他将使用cc crate调用C编译器并进行其他友谊操作,以使一切正常工作。
幸运的是,我们可以将大部分工作转移到cc,但是有时要困难得多。 阅读更多有关汇编脚本的文档 。
首先添加cc作为chmlib-sys的依赖项:
$ cd chmlib-sys $ cargo add --build cc Updating 'https://github.com/rust-lang/crates.io-index' index Adding cc v1.0.46 to build-dependencies
然后我们编写build.rs
:
您还需要告诉Cargo chmlib-sys链接到chmlib库。 然后,Cargo可以确保在整个依赖关系图中只有一个机架,具体取决于特定的本机库。 这样可以避免有关重复字符或意外使用不兼容库的模糊错误消息。
接下来,我们需要声明chmlib库导出的所有函数,以便可以从Rast使用它们。
为此,有一个很棒的bindgen项目。 将C头文件提供给输入,并输出带有Rast的FFI绑定的文件。
$ cargo install bindgen $ bindgen ../vendor/CHMLib/src/chm_lib.h \ -o src/lib.rs \ --raw-line '#![allow(non_snake_case, non_camel_case_types)]' $ head src/lib.rs /* automatically generated by rust-bindgen */ #![allow(non_snake_case, non_camel_case_types)] pub const CHM_UNCOMPRESSED: u32 = 0; pub const CHM_COMPRESSED: u32 = 1; pub const CHM_MAX_PATHLEN: u32 = 512; pub const CHM_PARAM_MAX_BLOCKS_CACHED: u32 = 0; pub const CHM_RESOLVE_SUCCESS: u32 = 0; pub const CHM_RESOLVE_FAILURE: u32 = 1; $ tail src/lib.rs extern "C" { pub fn chm_enumerate_dir( h: *mut chmFile, prefix: *const ::std::os::raw::c_char, what: ::std::os::raw::c_int, e: CHM_ENUMERATOR, context: *mut ::std::os::raw::c_void, ) -> ::std::os::raw::c_int; }
我强烈建议您阅读Bindgen用户手册 ,以解决其不足 。
在此阶段,编写冒烟测试将很有用,它将验证所有操作均按预期进行,并且可以实际调用原始C库的函数。
cargo test
一切似乎井井有条:
$ cargo test Finished test [unoptimized + debuginfo] target(s) in 0.03s Running ~/chmlib/target/debug/deps/chmlib_sys-2ffd7b11a9fd8437 running 1 test test bindgen_test_layout_chmUnitInfo ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Running ~/chmlib/target/debug/deps/smoke_test-f7be9810412559dc running 1 test test open_example_file ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests chmlib-sys running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
在Rust中编写安全包装
从技术上和技术上,我们现在可以从Rasta调用CHMLib,但这需要不安全的堆。 它可能适用于老练的手工艺品,但对于在crates.io上发布,则值得为所有不安全的代码编写安全的包装。
如果使用cargo doc --open
查看chmlib-sys API,则可以看到许多将*mut ChmFile
作为第一个参数的函数。 这类似于对象和方法。
CHMLib头文件 #ifndef INCLUDED_CHMLIB_H #define INCLUDED_CHMLIB_H #ifdef __cplusplus extern "C" { #endif #ifdef PPC_BSTR #include <wtypes.h> #endif #ifdef WIN32 #ifdef __MINGW32__ #define __int64 long long #endif typedef unsigned __int64 LONGUINT64; typedef __int64 LONGINT64; #else typedef unsigned long long LONGUINT64; typedef long long LONGINT64; #endif /* the two available spaces in a CHM file */ /* NB: The format supports arbitrarily many spaces, but only */ /* two appear to be used at present. */ #define CHM_UNCOMPRESSED (0) #define CHM_COMPRESSED (1) /* structure representing an ITS (CHM) file stream */ struct chmFile; /* structure representing an element from an ITS file stream */ #define CHM_MAX_PATHLEN (512) struct chmUnitInfo { LONGUINT64 start; LONGUINT64 length; int space; int flags; char path[CHM_MAX_PATHLEN+1]; }; /* open an ITS archive */ #ifdef PPC_BSTR /* RWE 6/12/2003 */ struct chmFile* chm_open(BSTR filename); #else struct chmFile* chm_open(const char *filename); #endif /* close an ITS archive */ void chm_close(struct chmFile *h); /* methods for ssetting tuning parameters for particular file */ #define CHM_PARAM_MAX_BLOCKS_CACHED 0 void chm_set_param(struct chmFile *h, int paramType, int paramVal); /* resolve a particular object from the archive */ #define CHM_RESOLVE_SUCCESS (0) #define CHM_RESOLVE_FAILURE (1) int chm_resolve_object(struct chmFile *h, const char *objPath, struct chmUnitInfo *ui); /* retrieve part of an object from the archive */ LONGINT64 chm_retrieve_object(struct chmFile *h, struct chmUnitInfo *ui, unsigned char *buf, LONGUINT64 addr, LONGINT64 len); /* enumerate the objects in the .chm archive */ typedef int (*CHM_ENUMERATOR)(struct chmFile *h, struct chmUnitInfo *ui, void *context); #define CHM_ENUMERATE_NORMAL (1) #define CHM_ENUMERATE_META (2) #define CHM_ENUMERATE_SPECIAL (4) #define CHM_ENUMERATE_FILES (8) #define CHM_ENUMERATE_DIRS (16) #define CHM_ENUMERATE_ALL (31) #define CHM_ENUMERATOR_FAILURE (0) #define CHM_ENUMERATOR_CONTINUE (1) #define CHM_ENUMERATOR_SUCCESS (2) int chm_enumerate(struct chmFile *h, int what, CHM_ENUMERATOR e, void *context); int chm_enumerate_dir(struct chmFile *h, const char *prefix, int what, CHM_ENUMERATOR e, void *context); #ifdef __cplusplus } #endif #endif /* INCLUDED_CHMLIB_H */
让我们从数据类型开始,该数据类型在构造函数中调用chm_open(),在析构函数中调用chm_close()。
pub unsafe extern "C" fn chm_open(filename: *const c_char) -> *mut chmFile; pub unsafe extern "C" fn chm_close(h: *mut chmFile);
为了简化错误处理,我们使用thiserror crate ,它会自动实现std::error::Error
。
$ cd chmlib $ cargo add thiserror
现在您需要弄清楚如何将std::path::Path
转换为*const c_char
。 不幸的是,由于各种 具有兼容性的 玩笑 ,这并非易事。
现在让我们定义ChmFile结构。 它存储指向chmlib_sys :: chmFile的非空指针。 如果chm_open()返回空指针,则表示由于某种错误她无法打开文件。
为了确保没有内存泄漏,请在Valgrind下运行一个简单的测试。 他将创建一个ChmFile并立即释放它。
Valgrind说没有剩余的未记内存:
$ valgrind ../target/debug/deps/chmlib-8d8c740d578324 open_valid_chm_file ==8953== Memcheck, a memory error detector ==8953== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. ==8953== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info ==8953== Command: ~/chmlib/target/debug/deps/chmlib-8d8c740d578324 open_valid_chm_file ==8953== running 1 test test tests::open_valid_chm_file ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ==8953== ==8953== HEAP SUMMARY: ==8953== in use at exit: 0 bytes in 0 blocks ==8953== total heap usage: 249 allocs, 249 frees, 43,273 bytes allocated ==8953== ==8953== All heap blocks were freed -- no leaks are possible ==8953== ==8953== For counts of detected and suppressed errors, rerun with: -v ==8953== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
按名称搜索项目
接下来的是chm_resolve_object()函数:
pub const CHM_RESOLVE_SUCCESS: u32 = 0; pub const CHM_RESOLVE_FAILURE: u32 = 1; pub unsafe extern "C" fn chm_resolve_object( h: *mut chmFile, objPath: *const c_char, ui: *mut chmUnitInfo ) -> c_int;
搜索可能会失败,因此chm_resolve_object()返回报告成功或失败的错误代码,有关所找到对象的信息将由传递给chmUnitInfo的指针记录。
仅针对我们的案例使用out参数ui创建了std::mem::MaybeUninit
类型。
现在,让我们将UnitInfo结构保留为空-这与chmUnitInfo C结构的Rust等效。 从ChmFile开始读取时,我们将添加字段。
请注意,尽管Rast上的代码不包含显式的状态更改,但ChmFile :: find()接受&mut self
。 事实是,C实现使用各种fseek()在文件中移动,因此内部状态在搜索过程中仍会更改。
让我们在先前下载的实验文件中检查ChmFile :: find():
过滤旁路项目
CHMLib提供了一个API,用于通过位掩码过滤器查看CHM文件的内容。
使用方便的位标志板条箱来处理掩码和标志:
$ cargo add bitflags Updating 'https://github.com/rust-lang/crates.io-index' index Adding bitflags v1.2.1 to dependencies
并根据chm_lib.h中的常量定义“ 过滤器”复选框:
我们还需要一个用于Rastovyh闭包的extern "C"
适配器,该适配器可以以指向函数的指针的形式传递给C:
function_wrapper
包含一个棘手的不安全代码,您需要使用这些代码:
state
指针必须指向闭包F的实例。- 闭包执行的其他代码可能会引起恐慌。 它不应跨越Rast和C之间的边界,因为使用不同语言进行堆栈升级是未定义的行为。 可能的恐慌应该使用
std::panic::catch_unwind()
拦截。 - 传递给function_wrapper的指向chmlib_sys :: chmFile的指针也存储在调用ChmFile中。 在调用过程中,必须确保只有闭包才能操作chmlib_sys :: chmFile,否则可能会出现竞争状况。
- 需要传递闭包
&mut ChmFile
,为此,您将需要使用现有指针在堆栈上创建一个临时对象。 但是,如果在这种情况下运行ChmFile析构函数,则chmlib_sys :: chmFile将被释放得太早。 为了解决这个问题,有std::mem::ManuallyDrop
。
这是使用function_wrapper来实现ChmFile::for_each()
:
注意F参数如何与通用function_wrapper函数交互。 当您需要通过FFI传递Rust闭包以使用另一种语言进行编码时,通常使用此技术。
读取文件内容
我们需要的最后一个函数负责使用chm_retrieve_object()实际读取文件。
它的实现非常简单。 这类似于典型的std :: io :: Read特性,但显式文件偏移量除外。
, , « », , chm_retrieve_object() :
- 0, ;
- 0 : ;
- −1 ( errno);
- −1 , , , malloc().
ChmFile::read() :
API CHMLib , . , . — , Rust Go ( , rustdoc godoc ).
, CHMLib , .
, , .
CHM-
CHM- .
#include "chm_lib.h" #include <stdio.h> #include <stdlib.h> #include <string.h> /* * callback function for enumerate API */ int _print_ui(struct chmFile *h, struct chmUnitInfo *ui, void *context) { static char szBuf[128]; memset(szBuf, 0, 128); if(ui->flags & CHM_ENUMERATE_NORMAL) strcpy(szBuf, "normal "); else if(ui->flags & CHM_ENUMERATE_SPECIAL) strcpy(szBuf, "special "); else if(ui->flags & CHM_ENUMERATE_META) strcpy(szBuf, "meta "); if(ui->flags & CHM_ENUMERATE_DIRS) strcat(szBuf, "dir"); else if(ui->flags & CHM_ENUMERATE_FILES) strcat(szBuf, "file"); printf(" %1d %8d %8d %s\t\t%s\n", (int)ui->space, (int)ui->start, (int)ui->length, szBuf, ui->path); return CHM_ENUMERATOR_CONTINUE; } int main(int c, char **v) { struct chmFile *h; int i; for (i=1; i<c; i++) { h = chm_open(v[i]); if (h == NULL) { fprintf(stderr, "failed to open %s\n", v[i]); exit(1); } printf("%s:\n", v[i]); printf(" spc start length type\t\t\tname\n"); printf(" === ===== ====== ====\t\t\t====\n"); if (! chm_enumerate(h, CHM_ENUMERATE_ALL, _print_ui, NULL)) printf(" *** ERROR ***\n"); chm_close(h); } return 0; }
_print_ui() Rust. UnitInfo , , .
main() , , describe_item() ChmFile::for_each().
:
$ cargo run --example enumerate-items topics.classic.chm > rust-example.txt $ cd vendor/CHMLib/src $ clang chm_lib.c enum_chmLib.c lzx.c -o enum_chmLib $ cd ../../.. $ ./vendor/CHMLib/src/enum_chmLib topics.classic.chm > c-example.txt $ diff -u rust-example.txt c-example.txt $ echo $? 0
diff , , , , . - , diff.
diff --git a/chmlib/examples/enumerate-items.rs b/chmlib/examples/enumerate-items.rs index e68fa58..ef855ac 100644 --- a/chmlib/examples/enumerate-items.rs +++ b/chmlib/examples/enumerate-items.rs @@ -36,6 +36,10 @@ fn describe_item(item: UnitInfo) { description.push_str("file"); } + if item.length() % 7 == 0 { + description.push_str(" :)"); + } + println!( " {} {:8} {:8} {}\t\t{}", item.space(),
:
$ cargo run --example enumerate-items topics.classic.chm > rust-example.txt $ diff -u rust-example.txt c-example.txt --- rust-example.txt 2019-10-20 16:51:53.933560892 +0800 +++ c-example.txt 2019-10-20 16:40:42.007053966 +0800 @@ -1,9 +1,9 @@ topics.classic.chm: spc start length type name
!
CHM-
, CHMLib, «» .
#include "chm_lib.h" #include <stdio.h> #include <stdlib.h> #include <string.h> #ifdef WIN32 #include <windows.h> #include <direct.h> #define mkdir(X, Y) _mkdir(X) #define snprintf _snprintf #else #include <unistd.h> #include <sys/stat.h> #include <sys/types.h> #endif struct extract_context { const char *base_path; }; static int dir_exists(const char *path) { #ifdef WIN32 /* why doesn't this work?!? */ HANDLE hFile; hFile = CreateFileA(path, FILE_LIST_DIRECTORY, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile != INVALID_HANDLE_VALUE) { CloseHandle(hFile); return 1; } else return 0; #else struct stat statbuf; if (stat(path, &statbuf) != -1) return 1; else return 0; #endif } static int rmkdir(char *path) { /* * strip off trailing components unless we can stat the directory, or we * have run out of components */ char *i = strrchr(path, '/'); if(path[0] == '\0' || dir_exists(path)) return 0; if (i != NULL) { *i = '\0'; rmkdir(path); *i = '/'; mkdir(path, 0777); } #ifdef WIN32 return 0; #else if (dir_exists(path)) return 0; else return -1; #endif } /* * callback function for enumerate API */ int _extract_callback(struct chmFile *h, struct chmUnitInfo *ui, void *context) { LONGUINT64 ui_path_len; char buffer[32768]; struct extract_context *ctx = (struct extract_context *)context; char *i; if (ui->path[0] != '/') return CHM_ENUMERATOR_CONTINUE; /* quick hack for security hole mentioned by Sven Tantau */ if (strstr(ui->path, "/../") != NULL) { /* fprintf(stderr, "Not extracting %s (dangerous path)\n", ui->path); */ return CHM_ENUMERATOR_CONTINUE; } if (snprintf(buffer, sizeof(buffer), "%s%s", ctx->base_path, ui->path) > 1024) return CHM_ENUMERATOR_FAILURE; /* Get the length of the path */ ui_path_len = strlen(ui->path)-1; /* Distinguish between files and dirs */ if (ui->path[ui_path_len] != '/' ) { FILE *fout; LONGINT64 len, remain=ui->length; LONGUINT64 offset = 0; printf("--> %s\n", ui->path); if ((fout = fopen(buffer, "wb")) == NULL) { /* make sure that it isn't just a missing directory before we abort */ char newbuf[32768]; strcpy(newbuf, buffer); i = strrchr(newbuf, '/'); *i = '\0'; rmkdir(newbuf); if ((fout = fopen(buffer, "wb")) == NULL) return CHM_ENUMERATOR_FAILURE; } while (remain != 0) { len = chm_retrieve_object(h, ui, (unsigned char *)buffer, offset, 32768); if (len > 0) { fwrite(buffer, 1, (size_t)len, fout); offset += len; remain -= len; } else { fprintf(stderr, "incomplete file: %s\n", ui->path); break; } } fclose(fout); } else { if (rmkdir(buffer) == -1) return CHM_ENUMERATOR_FAILURE; } return CHM_ENUMERATOR_CONTINUE; } int main(int c, char **v) { struct chmFile *h; struct extract_context ec; if (c < 3) { fprintf(stderr, "usage: %s <chmfile> <outdir>\n", v[0]); exit(1); } h = chm_open(v[1]); if (h == NULL) { fprintf(stderr, "failed to open %s\n", v[1]); exit(1); } printf("%s:\n", v[1]); ec.base_path = v[2]; if (! chm_enumerate(h, CHM_ENUMERATE_ALL, _extract_callback, (void *)&ec)) printf(" *** ERROR ***\n"); chm_close(h); return 0; }
. , .
extract(). , .
main() , extract(), .
CHM- HTML-, -.
$ cargo run --example extract -- ./topics.classic.chm ./extracted $ tree ./extracted ./extracted ├── default.html ├── BrowserForward.html ... ├── Images │ ├── Commands │ │ └── RealWorld │ │ ├── BrowserBack.bmp ... ├── script │ ├── _community │ │ └── disqus.js │ ├── hs-common.js ... └── userinterface.html $ firefox topics.classic/default.html ( default.html Firefox)
JavaScript ( - Microsoft Help), , .
接下来是什么?
chmlib , , crates.io.
:
- ChmFile::for_each() ChmFile::for_each_item_in_dir() , , .
- , ChmFile
Continuation::Continue
. , F: FnMut(&mut ChmFile, UnitInfo) -> C
C: Into<Continuation>
, impl From<()> for Continuation
. - (, extract()) ChmFile::for_each() .
impl<E> From<Result<(), E>> for Continuation where E: Error + 'static
. - -
std::fs::File
. , ChmFile::read() - std::io::Writer
.