Python 在 Mac OS X 系统下使用 UCL 库

背景

最近 QQ 上突然冒出了一个很久没有联系的网友,跟我说 ShiningLore (天使) 有一个新的韩国私服,突然一下又热血沸腾了,毕竟这是我人生中的第一个网游。到处 Google 了一下,也没有发现太多的介绍和说明,最后在 17173 的某个角落找到了这个游戏的新手入门

好了,话题扯远了。今天要说的是 UCL,为什么扯到游戏上了。因为这个游戏的资源打包文件中,使用 UCL 的库进行压缩。UCLMarkus F.X.J. Oberhumer 开发的基于 GPL 协议的一个开源库。要想要把游戏里面的资源文件解压缩出来,就非得要用这个 UCL 库不可。

在网上到处搜索了一下,并没有发现有 Python 下的 UCL 实现。毕竟,这个库的版本已经比较旧了,到现在都已经有 10 年的历史了。其官网显示的最后更新日期为 2014 年 7 月 20 号。不过好在,这货是用标准的 ANSI C 编写的,外加 Python 这一胶水语言,一定会有解决办法的。

再次搜索,发现了几个 Python 和 C 语言对接的方法,最后选择了 CFFI (C Foreign Function Interface for Python)。不过要使用 CFFI,还需要准备好编译器。在 OS X 系统下自然是 Xcode 了,建议同时安装上 Command line tools。而 Windows 平台下自然是 Microsoft Visual Studio 了。

编译 UCL

我们要做的第一件事情,自然是编译 UCL 了。首先先获得 UCL 的源代码,在写这篇文章的时候,UCL 的最新版本是 1.03,不过我估计它也不会再更新了,毕竟已经过去 10 年之久了。

1
$ curl -OL http://www.oberhumer.com/opensource/ucl/download/ucl-1.03.tar.gz

然后解压缩下载回来的压缩包。

1
$ tar zxvf ucl-1.03.tar.gz

编译并生成 UCL 的链接库。

1
2
3
$ cd ucl-1.03
$ ./configure
$ make

最终生成的链接库文件保存在:ucl-1.03/src/.libs/libucl.a

安装 pip

pip 是 Python 的一个包管理软件,要想安装 CFFI,我们需要先安装 pip。如果系统中已经安装有 pip,可以直接跳过这个步骤,直接开始安装 CFFI。

1
$ sudo easy_install pip

安装 CFFI

前面已经介绍过了,CFFI 是 Python 下的 C 语言对接的一个 interface,通过 CFFI,我们可以在 Python 中直接调用 C 中的函数。

只需要一条命令就可以轻松的安装 CFFI。

1
$ sudo pip install cffi

Python 中调用 UCL

大部分的工作我们都已经搞掂了,接下来我们还需要一个 wrapper。感谢伟大的互联网,已经有好心人做好了 pyucl,我们直接拿来主意就可以了。

这里我只用了 src/pyucl 目录下的 ucl.py 文件,并没有使用模块安装的方法。

ucl.py 放在与 ucl-1.03.tar.gz 同级的目录,并修改 ucl.py,增加 include_dirslibrary_dirs 为相对应的路径。

修改部分的代码如下:

1
2
C = ffi.verify("#include <ucl/ucl.h>",
libraries=['ucl'], include_dirs=['ucl-1.03/include'], library_dirs=['ucl-1.03/src/.libs'])

更多的配置参数、编译参数和链接参数,如 sources, include_dirs, define_macros, undef_macros, libraries, library_dirs, extra_objects, extra_compile_args, extra_link_args 等,可以参考 CFFI 官方文档:http://cffi.readthedocs.org/

最后,启动 Python 解析器,验证一下效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
>>> import ucl
>>> @ucl.callback
... def a_callback(n0, n1, n, v):
... print "CALLBACK GOT CALLED: %d %d %d" % (n0, n1, n)
...
>>> string_in = "henkitsminehenkhenkhenkitsmine"
>>> string_out = ucl.nrv2d_99_compress(string_in, level=10, callback=a_callback)
CALLBACK GOT CALLED: 0 0 -1
CALLBACK GOT CALLED: 1 0 3
CALLBACK GOT CALLED: 30 24 4
>>> print len(string_in), len(string_out)
30 24
>>> string_dec = ucl.nrv2d_decompress(string_out, len(string_in))
>>> print string_dec == string_in
True
>>> string_dec = ucl.nrv2d_decompress(string_out, len(string_in) - 10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "ucl.py", line 144, in nrv2d_decompress
return _ucl_decompress(C.ucl_nrv2d_decompress_safe_8, data, outsize)
File "ucl.py", line 138, in _ucl_decompress
ucl_errors[retval]))
RuntimeError: Decompression failed: -202 (UCL_E_OUTPUT_OVERRUN)
>>> ucl.nrv2b_decompress(string_out, 2*len(string_in))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "ucl.py", line 142, in nrv2b_decompress
return _ucl_decompress(C.ucl_nrv2b_decompress_safe_8, data, outsize)
File "ucl.py", line 138, in _ucl_decompress
ucl_errors[retval]))
RuntimeError: Decompression failed: -203 (UCL_E_LOOKBEHIND_OVERRUN)

一切正常,搞掂,收工!