MuPDF C API 编程实战:fz_context、异常处理与多线程渲染完全指南(基于 SumatraPDF 仓库源码)
2026/9/21 15:35:26 网站建设 项目流程

MuPDF C API 编程实战:fz_context、异常处理与多线程渲染完全指南(基于 SumatraPDF 仓库源码)

【免费下载链接】sumatrapdfSumatraPDF reader项目地址: https://gitcode.com/gh_mirrors/su/sumatrapdf

导读

本文以 SumatraPDF 仓库中内置的 MuPDF 源码(位于 ext/mupdf)为核心,系统讲解 MuPDF C 接口的三大基石:fz_context 上下文模型基于宏的 fz_try/fz_always/fz_catch 异常处理,以及多线程渲染。读者将掌握 context 的创建、克隆与释放,理解 MuPDF 不使用 C++ 却实现 try/catch 语义的底层原理,并学会在多线程应用中正确配置 fz_locks_context、利用 display list 实现跨线程渲染。文中所有关键结论均可在仓库源码与示例程序(example.c、multi-threaded.c)中找到依据,并辅以 SumatraPDF 自身在 src/EngineMupdf.cpp 中的真实集成方式作为印证。


一、最小可用示例:从打开文档到输出像素

MuPDF 官方在ext/mupdf/docs/examples/目录下提供了完整可编译的示例。其中 example.c 演示了最基础的用法——打开一个 PDF/XPS/CBZ/EPUB 文档,渲染某一页,并以 ASCII PPM 格式输出到标准输出。

#include <mupdf/fitz.h> #include <stdio.h> #include <stdlib.h> int main(int argc, char **argv) { char *input; float zoom, rotate; int page_number, page_count; fz_context *ctx; fz_document *doc; fz_pixmap *pix; fz_matrix ctm; int x, y; if (argc < 3) { fprintf(stderr, "usage: example input-file page-number [ zoom [ rotate ] ]\n"); fprintf(stderr, "\tinput-file: path of PDF, XPS, CBZ or EPUB document to open\n"); fprintf(stderr, "\tPage numbering starts from one.\n"); fprintf(stderr, "\tZoom level is in percent (100 percent is 72 dpi).\n"); fprintf(stderr, "\tRotation is in degrees clockwise.\n"); return EXIT_FAILURE; } input = argv[1]; page_number = atoi(argv[2]) - 1; zoom = argc > 3 ? atof(argv[3]) : 100; rotate = argc > 4 ? atof(argv[4]) : 0; /* Create a context to hold the exception stack and various caches. */ ctx = fz_new_context(NULL, NULL, FZ_STORE_UNLIMITED); if (!ctx) { fprintf(stderr, "cannot create mupdf context\n"); return EXIT_FAILURE; } /* Register the default file types to handle. */ fz_try(ctx) fz_register_document_handlers(ctx); fz_catch(ctx) { fz_report_error(ctx); fprintf(stderr, "cannot register document handlers\n"); fz_drop_context(ctx); return EXIT_FAILURE; } /* Open the document. */ fz_try(ctx) doc = fz_open_document(ctx, input); fz_catch(ctx) { fz_report_error(ctx); fprintf(stderr, "cannot open document\n"); fz_drop_context(ctx); return EXIT_FAILURE; } /* Count the number of pages. */ fz_try(ctx) page_count = fz_count_pages(ctx, doc); fz_catch(ctx) { fz_report_error(ctx); fprintf(stderr, "cannot count number of pages\n"); fz_drop_document(ctx, doc); fz_drop_context(ctx); return EXIT_FAILURE; } if (page_number < 0 || page_number >= page_count) { fprintf(stderr, "page number out of range: %d (page count %d)\n", page_number + 1, page_count); fz_drop_document(ctx, doc); fz_drop_context(ctx); return EXIT_FAILURE; } /* Compute a transformation matrix for the zoom and rotation desired. */ /* The default resolution without scaling is 72 dpi. */ ctm = fz_scale(zoom / 100, zoom / 100); ctm = fz_pre_rotate(ctm, rotate); /* Render page to an RGB pixmap. */ fz_try(ctx) pix = fz_new_pixmap_from_page_number(ctx, doc, page_number, ctm, fz_device_rgb(ctx), 0); fz_catch(ctx) { fz_report_error(ctx); fprintf(stderr, "cannot render page\n"); fz_drop_document(ctx, doc); fz_drop_context(ctx); return EXIT_FAILURE; } /* Print image data in ascii PPM format. */ printf("P3\n"); printf("%d %d\n", pix->w, pix->h); printf("255\n"); for (y = 0; y < pix->h; ++y) { unsigned char *p = &pix->samples[y * pix->stride]; for (x = 0; x < pix->w; ++x) { if (x > 0) printf(" "); printf("%3d %3d %3d", p[0], p[1], p[2]); p += pix->n; } printf("\n"); } /* Clean up. */ fz_drop_pixmap(ctx, pix); fz_drop_document(ctx, doc); fz_drop_context(ctx); return EXIT_SUCCESS; }

该示例的运行方式(源码注释中有完整说明):

# 在源码树内构建并渲染第一页:100% 缩放、0 度旋转 make examples ./build/debug/example document.pdf 1 100 0 > page1.ppm # 使用已安装的库编译 gcc -I/usr/local/include -o example \ /usr/local/share/doc/mupdf/examples/example.c \ /usr/local/lib/libmupdf.a \ /usr/local/lib/libmupdfthird.a \ -lm ./example document.pdf 1 100 0 > page1.ppm

几个关键参数需要留意:

  • 页码从 1 开始(代码中atoi(argv[2]) - 1转为内部从 0 计数);
  • 缩放比例是百分比,100% 对应 72 dpi 基准分辨率;
  • 旋转单位为顺时针角度,通过fz_scalefz_pre_rotate组合出变换矩阵ctm
  • 渲染目标是 RGB 颜色空间(fz_device_rgb(ctx)),输出 pixmap 的samplesstridewhn字段直接可读。

示例特意指出:这段代码没有任何错误处理,目的是降低入门复杂度;任何严肃的程序都必须使用下文描述的 fz_try/fz_catch 异常处理策略——示例中每个fz_try/fz_catch块实际上就是这一策略的最小形态。


二、公共函数参数:fz_context 到底是什么

2.1 为什么几乎所有 MuPDF 函数都带一个 ctx

MuPDF 接口中绝大多数函数都接受一个fz_context *ctx参数。这个 context 承载了 MuPDF 在解析、渲染文档时的全部全局状态,官方文档明确列举了它包含的内容:

  • 异常栈(exception stack):支撑下文 fz_try/fz_catch 宏机制的运行环境;
  • 内存分配器(memory allocator):允许调用方注入自定义 malloc/realloc/free;
  • 资源存储(resource store):用于缓存图片、字体等渲染资源;
  • 一组锁及加锁/解锁函数(locks):用于多线程安全。

如果没有提供锁及配套函数,那么该 context(及其克隆)只能在单线程应用中使用

2.2 源码级验证:fz_context 的内部结构

在 ext/mupdf/include/mupdf/fitz/context.h 中可以查到struct fz_context的完整定义。从源码结构看,它内部明确分为:

  • alloc(fz_alloc_context)、locks(fz_locks_context):用户在创建时注入的分配器与锁;
  • error(fz_error_context):持有异常栈槽位stack[256]、当前错误码、errno 与错误消息缓冲(最大 256 字节消息),这是每个 context独立拥有的部分;
  • warn(fz_warn_context):警告消息缓冲与去重计数;
  • aa(fz_aa_context):抗锯齿位数(0~8)、最小线宽等渲染参数,也是每个 context 独立的;
  • 共享部分:font(字体上下文)、store(资源存储)、glyph_cache(字形缓存)、colorspace(颜色空间)等,这些在克隆 context 之间共享

正是"独立异常栈 + 共享缓存"这一划分,构成了多线程中"每个线程克隆一个 context"方案的基础。

2.3 创建与释放 context

创建与释放的入口(context.h):

  • fz_new_context(alloc, locks, max_store):三个参数分别为自定义分配器、锁集合、资源存储上限字节数。alloc 与 locks 均可传NULL(分别表示使用标准库分配器、单线程模式);max_store可传FZ_STORE_UNLIMITED(0,不设上限)或FZ_STORE_DEFAULT256 << 20,约 256 MiB 的合理上限)。创建失败时返回NULL
  • fz_clone_context(ctx):克隆一个 context,供多线程使用(详见第五节)。
  • fz_drop_context(ctx):释放 context 及其全局状态,会顺带 flush 缓冲的警告;传NULL则什么都不做。注意:不能在一个正处于活动状态的 fz_try/fz_always/fz_catch 块内释放该块所用的 context。

三、错误处理:不靠 C++ 的 try/catch

3.1 核心机制

MuPDF 使用一组异常处理宏来简化错误返回与资源清理。从概念上讲,它们与 C++ 的 try/catch 非常相似,但不需要任何特殊编译器支持——其底层是 C 的setjmp/longjmp

基本形式如下:

fz_try(ctx) { // Try to perform a task. Never 'return', 'goto' or // 'longjmp' out of here. 'break' may be used to // safely exit (just) the try block scope. } fz_always(ctx) { // Any code here is always executed, regardless of // whether an exception was thrown within the try or // not. Never 'return', 'goto' or longjmp out from // here. 'break' may be used to safely exit (just) the // always block scope. } fz_catch(ctx) { // This code is called (after any always block) only // if something within the fz_try block (including any // functions it called) threw an exception. The code // here is expected to handle the exception (maybe // record/report the error, cleanup any stray state // etc) and can then either exit the block, or pass on // the exception to a higher level (enclosing) fz_try // block (using fz_throw, or fz_rethrow). }

其中fz_always 块是可选的,可以安全省略。

宏的真实定义可以在 context.h 中看到,它们只是一段"黑盒"式的宏展开:

#define fz_var(var) fz_var_imp((void *)&(var)) #define fz_try(ctx) if (!fz_setjmp(*fz_push_try(ctx))) if (fz_do_try(ctx)) do #define fz_always(ctx) while (0); if (fz_do_always(ctx)) do #define fz_catch(ctx) while (0); if (fz_do_catch(ctx))

配套的抛错与查询 API(均声明于 context.h):

  • fz_throw(ctx, errcode, fmt, ...)/fz_vthrow:抛出异常,必须处于某个外层 fz_try 块内;
  • fz_rethrow(ctx):在 fz_catch 内把当前异常原样抛给上层(前提是期间没有介入其他 fz_try/fz_catch);
  • fz_morph_error(ctx, fromcode, tocode):在 catch 内修改异常类型,常用于"降级"异常严重程度;
  • fz_caught(ctx):取得当前异常的错误码;fz_caught_message(ctx):取得格式化后的消息字符串;fz_caught_errno(ctx):对 SYSTEM 类错误取回 errno;
  • fz_rethrow_if/fz_rethrow_unless:按错误码条件重抛;
  • fz_report_error(ctx):把异常上报到注册的错误回调(example.c 中每个 catch 块都在用它);
  • fz_ignore_error(ctx):彻底吞掉一个已处理的异常。

MuPDF 定义的错误类型枚举(context.h)包括:FZ_ERROR_NONEFZ_ERROR_GENERICFZ_ERROR_SYSTEM(致命的内存不足或系统调用错误)、FZ_ERROR_LIBRARYFZ_ERROR_ARGUMENT(参数非法/越界)、FZ_ERROR_LIMIT(资源或硬性限制)、FZ_ERROR_UNSUPPORTEDFZ_ERROR_FORMAT(不可恢复的语法/格式错误)、FZ_ERROR_SYNTAX(应被诊断并忽略的语法错误),以及仅供内部使用的FZ_ERROR_TRYLATERFZ_ERROR_ABORTFZ_ERROR_REPAIRED

3.2 宏方案的三大限制

基于宏的实现有 3 个主要限制,官方文档逐条强调:

  1. 绝不要从 try 块内return(也不能gotolongjmp跳出去)。这会破坏宏的内部簿记,在之后引发问题;代码虽然能检测到这类行为,但此时已来不及给出原始违规位置的可用错误报告。

  2. try/always/catch 不是一条原子 C 语句。例如下面的写法不会得到预期结果:

    if (condition) fz_try(ctx) { ... } fz_catch(ctx) { ... }

    必须改写为:

    if (condition) { fz_try(ctx) { ... } fz_catch(ctx) { ... } }
  3. 宏基于 setjmp/longjmp,因此 C 标准对这两个函数的一切限制同样适用于 fz_try/fz_catch。特别是:任何在 fz_try 开始之后、抛异常之前被赋值的"真正局部"变量,其值在抛异常过程中可能变成未定义

3.3 fz_var:防止局部变量在长跳转中"丢失"

为了缓解限制 3,MuPDF 提供了fz_var()宏,它告诉编译器确保该变量不会因抛异常而被重置。其展开为fz_var_imp((void *)&(var)),一个对变量的地址求值的调用,从而让编译器在 longjmp 跨越栈帧时保留其值。

官方文档给出的"模范代码"是一个盖房子的比喻,完整展示了 fz_try/fz_always/fz_catch + fz_var 的组合用法:

house build_house(plans *p) { material m = NULL; walls w = NULL; roof r = NULL; house h = NULL; tiles t = make_tiles(); fz_var(w); fz_var(r); fz_var(h); fz_try(ctx) { fz_try(ctx) { m = make_bricks(); } fz_catch(ctx) { // No bricks available, make do with straw? m = make_straw(); } w = make_walls(m, p); r = make_roof(m, t); // Note, NOT: return combine(w,r); h = combine(w, r); } fz_always(ctx) { drop_walls(w); drop_roof(r); drop_material(m); drop_tiles(t); } fz_catch(ctx) { fz_throw(ctx, "build_house failed"); } return h; }

这段代码值得逐条解读:

  1. make_tiles()在 fz_try 之前调用:若它抛异常,会直接由更外层的异常处理器接管;若成功,t在 fz_try 开始前就已赋值,因此无需对 t 调用 fz_var
  2. 先尝试用砖块(make_bricks)作为建材,失败则回退到稻草(make_straw);若再失败,会落入 fz_catch,整个流程干净地失败。
  3. 假设combine对传入的 walls 和 roof 各取新引用,因此无论成败,wr都必须清理——这正是 fz_always 块的职责。
  4. 遵循标准 C 惯例:销毁 NULL 是安全的(fz_drop_* 系列均允许传 NULL)。

此外官方文档强调:fz_always 块内绝不能调用可能抛异常的函数;而 fz_catch 内若想重抛,使用 fz_rethrow。SumatraPDF 的多线程渲染循环(multi-threaded.c)也严格遵循了这一模式:fz_alwaysfz_drop_devicefz_drop_pagefz_catchfz_rethrow

3.4 真实世界的错误回调

除了异常宏,context 还支持注册错误/警告回调:fz_set_error_callback/fz_set_warning_callback(context.h),回调会在异常处理过程中被调用,但回调本身绝不能抛异常。SumatraPDF 正是这样做的——在 src/EngineMupdf.cpp 中,InstallFitzErrorCallbacksfz_print_cb同时注册为 warning 与 error 回调,把 MuPDF 的警告/错误消息统一转发进 SumatraPDF 的日志系统,并对"找不到系统字体""未知 epub 版本"等可忽略信息做了过滤(src/EngineMupdf.cpp)。


四、多线程:规则、锁与两种架构选择

4.1 先想清楚:你真的需要多线程吗

官方文档首先给出了一个务实的提醒:MuPDF 可以在完全不感知线程的前提下被构建进多线程应用。如果应用在一个线程里打开文档并"充当服务器",为其他线程按需提供页面并渲染,那么 MuPDF 始终只被这一个线程调用——对其他线程而言没有任何线程安全问题,也就不需要任何锁。本节讨论的是更复杂的情形:在同一个应用里从多个线程并发调用 MuPDF

4.2 五条铁律

官方文档给出了确保多线程顺畅运行的 5 条规则:

  1. "不同线程不允许同时对同一 context 发起 MuPDF 调用。"大多数时候最简单的方式就是每个线程各用一个 context——在线程创建的同时创建新 context(细节见"克隆 context"一节)。

  2. "不同线程不允许同时使用同一个文档。"同一时刻只能有一个线程访问文档;但一旦从文档生成了 display list,多个线程就可以同时操作这些 display list。文档也可以被多个线程使用,前提是有防护措施保证使用不是同时的。

  3. "不同线程不允许同时调用同一个 device。"多线程同时调用一个 device 会使其状态错乱甚至崩溃;多个线程轮流调用同一 device 是完全可以的,只要有防护避免同时调用。

  4. "除非 MuPDF 纯粹单线程使用,否则必须在创建 context 时就提供 fz_locks_context。"MuPDF 需要用用户提供的锁函数来保护对某些结构/资源/库的不安全并发访问——即使使用完全独立的 MuPDF 实例也是如此

  5. "所有在用 context 必须共享同一个 fz_locks_context(或其底层锁)。"官方强烈建议:fz_new_context只调用一次,之后用fz_clone_context派生新 context,这样自动保证所有实例使用同一锁机制。当前虽然仍支持多次调用fz_new_context创建完全独立的 context,但这些 context必须共享同一个 fz_locks_context(或依赖同一组底层锁);创建不同独立 context 的能力将来可能被移除。

4.3 fz_locks_context 与 FZ_LOCK_MAX

调用方需要提供FZ_LOCK_MAX个互斥锁。MuPDF 调用锁结构里的 lock/unlock 函数指针时,传入的是该结构里的 user 指针与锁编号i0 <= i < FZ_LOCK_MAX)。这些互斥锁既可以是递归的也可以是非递归的,因为 MuPDF 只会以非递归风格调用。

锁结构的定义(context.h):

typedef struct { void *user; void (*lock)(void *user, int lock); void (*unlock)(void *user, int lock); } fz_locks_context; enum { FZ_LOCK_ALLOC = 0, FZ_LOCK_FREETYPE, FZ_LOCK_GLYPHCACHE, FZ_LOCK_MAX };

可以看到当前有 3 把内部锁:分配锁(FZ_LOCK_ALLOC)、FreeType 字体引擎锁(FZ_LOCK_FREETYPE)、字形缓存锁(FZ_LOCK_GLYPHCACHE)。MuPDF 内部为避免死锁有一条简单规则:"绝不在已经持有锁 i(0 <= i <= n)时再去拿锁 n";为验证这一规则还提供了调试代码,可通过定义FITZ_DEBUG_LOCKING启用(在 MEMENTO 或非 NDEBUG 构建下自动开启)。

context.h 中的fz_lock/fz_unlock内联函数展示了 MuPDF 内部加锁的统一入口:先做锁调试断言,再调用用户提供的ctx->locks.lock(ctx->locks.user, lock)fz_keep_imp/fz_drop_imp等引用计数辅助函数也都在FZ_LOCK_ALLOC上做加解锁,确保引用计数增减在多线程下是原子的(context.h)。

4.4 SumatraPDF 的锁实现:真实世界的样本

SumatraPDF 在 src/EngineMupdf.cpp 中实现了这套回调——用EngineMupdf对象自身的fz_locks[lock]互斥锁数组来充当FZ_LOCK_MAX把锁:

static void fz_lock_context_cs(void* user, int lock) { EngineMupdf* e = (EngineMupdf*)user; e->fz_locks[lock].Lock(); } static void fz_unlock_context_cs(void* user, int lock) { EngineMupdf* e = (EngineMupdf*)user; e->fz_locks[lock].Unlock(); }

并在构造函数里一次性组装(src/EngineMupdf.cpp):

fz_locks_ctx.user = this; fz_locks_ctx.lock = fz_lock_context_cs; fz_locks_ctx.unlock = fz_unlock_context_cs; _ctx = fz_new_context(nullptr, &fz_locks_ctx, FZ_STORE_DEFAULT); ... fz_register_document_handlers(_ctx);

这里user指针被用来携带EngineMupdf对象(从而找到锁数组),正是官方文档推荐"用 user 指针传递锁数组、避免全局变量"的实践。注册完文档处理器后,SumatraPDF 还会注入 Windows 系统字体加载与内嵌字体加载器(参见 src/mupdf/README.md 对mupdf_load_system_font.cnoto_sumatra.c的说明)。

4.5 多线程示例:主线程读页、每页一个渲染线程

multi-threaded.c 演示了官方推荐的第一种架构:一个主线程负责从文档读取页面并生成 display list,每页一个渲染线程负责把 display list 画成 pixmap。它的构建与运行方式:

# 源码树内构建:将每页渲染为独立 PNG make examples ./build/debug/multi-threaded document.pdf # 基于已安装库编译 gcc -I/usr/local/include -o multi-threaded \ /usr/local/share/doc/mupdf/examples/multi-threaded.c \ /usr/local/lib/libmupdf.a \ /usr/local/lib/libmupdfthird.a \ -lpthread -lm ./multi-threaded document.pdf

示例的头部注释特别提醒:所有页面会同时渲染,请选页数少的文件,以免过度压榨机器;同时不同环境的线程数量限制也可能成为瓶颈。

该示例的骨架值得拆解:

(1)初始化锁与主 context(multi-threaded.c)

pthread_mutex_t mutex[FZ_LOCK_MAX]; fz_locks_context locks; // 初始化 FZ_LOCK_MAX 个非递归互斥锁 for (i = 0; i < FZ_LOCK_MAX; i++) { if (pthread_mutex_init(&mutex[i], NULL) != 0) fail("pthread_mutex_init()"); } // user 指针指向锁数组,lock/unlock 函数据此定位具体锁 locks.user = mutex; locks.lock = lock_mutex; locks.unlock = unlock_mutex; ctx = fz_new_context(NULL, &locks, FZ_STORE_UNLIMITED);

配套的lock_mutex/unlock_mutex(multi-threaded.c)就是user转回pthread_mutex_t*数组后按下标加解锁:

void lock_mutex(void *user, int lock) { pthread_mutex_t *mutex = (pthread_mutex_t *) user; if (pthread_mutex_lock(&mutex[lock]) != 0) fail("pthread_mutex_lock()"); } void unlock_mutex(void *user, int lock) { pthread_mutex_t *mutex = (pthread_mutex_t *) user; if (pthread_mutex_unlock(&mutex[lock]) != 0) fail("pthread_mutex_unlock()"); }

(2)主线程"读页 + 生成 display list"(multi-threaded.c):主线程逐个fz_load_pagefz_bound_pagefz_new_display_listfz_new_list_devicefz_run_pagefz_close_device,把页面的全部绘制命令固化进 display list;随后通过struct thread_datactx(供渲染线程克隆)、display list、包围盒 bbox 等传给渲染线程。代码注释明确强调:"页面的加载不能放在工作线程里做,因为同一时刻只允许一个线程访问文档"。主线程在 fz_always 里丢弃 device 与 page,在 fz_catch 里 fz_rethrow。

(3)渲染线程:克隆 context + 渲染 display list(multi-threaded.c)

void * renderer(void *data_) { struct thread_data *data = (struct thread_data *)data_; int pagenumber = contenteditable="false">【免费下载链接】sumatrapdfSumatraPDF reader项目地址: https://gitcode.com/gh_mirrors/su/sumatrapdf

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询