对RAII的思考

对RAII的思考

Some random thoughts about RAII

写程序时常常需要申请系统资源,比如打开文件,申请一块内存。申请到这些资源后,在程序退出或者资源使用完毕后,应当正确的释放。如果不能正确释放,会造成一系列问题。比如申请的内存没有释放造成内存泄露Memory Leak,申请的进程锁没有被解锁Unlock,造成进程间的死锁DeadLock。在C++语言里,解决这类资源管理问题的管用手法是RAII (Resource Acuiquistion Is Initialization)

这篇笔记是对RAII的一点思考。

 

1. 什么是RAII

简单来讲,把获取资源的代码放到类的构造函数里,把释放资源的代码放到析构函数里。比如用ofstream file(“output.txt”) 可以打开文件,当file变量不起作用是,文件会被自动关闭。

比如下面这张图(from:The RAII Programming Idiom),看看这里面有多少地方需要写释放资源的代码。如果使用RAII,这些其实地方都不用留代码。

RAII  Example
RAII Example

 

 

2. RAII的优缺点

RAII的好处是利用C++语言优势安全、正确的管理资源。同时RAII是C++建议的资源获取方式,这种代码可以被广大C++用户理解。

不方便之处是,使用RAII有一些陷阱。比如不要用RAII一次获取多个资源。

 

3.为什么C++有RAII

C++语言保证了一个类构造之后,析构函数会被自动调用。这个使用方式与资源管理的方式相似。因此可以用类的生命周期来管理资源。

 

4. 为什么C/Java/Python没有RAII

C语言没有原生的构造和析构函数,获取的资源不能有任何自动机制来释放。

Java/Python有语言中的支持,即Dispose Pattern。举例来说就是 try…catch..finally语句。使用者只要把释放资源的语句写到finally,资源就会被释放。

 

4. RAII 和Exception的关系

RAII和Exception紧密相关,更确切的说,构造函数和异常这两个特性在某种程度上互相依赖。

对于构造函数来说(获取资源的语句在构造函数里),构造函数没有返回值,因此想知道资源是否成功获取是不能从函数返回值来判断的,唯一可以用的手法是在资源获取失败时抛出异常。也就是说构造函数需要使用异常。

另一方面,使用异常之后,需要用构造函数来管理资源。因为异常抛出以后,很可能处理异常的代码和异常发生的代码不在一个层次(异常在Call Stack上逐层向上)。为了实现异常安全(Exception Safe),应该使用构造函数(另一个选择是智能指针,但智能指针有智能指针的问题,详见C++FAQ的讨论)。

对已有的C++代码来说,实现或检查代码是不是异常安全不是一个的简单人物。这种情况下,异常这个特性往往会被禁用(比如Google C++ style guide)。如果异常被禁用了,我们就没法从构造函数本身获知资源是否成功获取,那是不是说我们没法使用RAII特性呢?

答案是否定的。我们可以在获取资源后,用其他的类函数来检查资源获取是否成功。比如ostream::is_open()就可以检查文件是否被正常打开。

 

5. 怎么绕开RAII

在C等不提供RAII支持的语言里,可以直接绕开RAII,即保证获取资源后,程序的每一个出口都有释放资源的语句。

这种方法有可能造成多处重复的资源释放代码,或者使用goto语句把所有程序跳转到一处资源释放代码。

 

6. 实践中怎么用RAII

实践中除了把资源获取的语句写到构造函数,把资源释放的语句写到析构函数,还应当注意:

1)获取多个资源时,可以写在多个类的构造函数里,使得每一个类的构造函数对应一个资源。这样在任何资源获取失败时,已经获得的资源会得到释放

2)有时候获取资源失败等于程序失败(Fatal Condition),这种情况下可以直接退出(exit),把清理资源的任务留给操作系统。

 

给作者编号

给作者编号

Author Number

http://zhanxw.com/author

 

最近写了一个网页,主要解决写文章给作者编号的问题。这个问题怎么来的呢?现在的文章需要提供作者,作者工作单位和作者的贡献,但是提供的方式是给每个作者单位按照作者出现的顺序编号。比如我的文章有79个作者,现在突然要加一个新的合作者,把他拍到第50位,那么从第51位到最后一位作者的编号可能都需要改变。这个过程很容易出错。我想写个软件给所有人用,顺便凑个热闹,学习一下jQuery + Django,最后的作品放到我的网页(link)。在这个过程中,还学习了Nginx, Emacs web-mode, 这篇笔记主要是把我的经验记录下来。

 

1. 学习Django

Django是Python语言实现的Web架构,它最开始是用于展示新闻(Newroom),国内的豆瓣也用Django。作为Python的“粉丝”,我学一下Django,希望以后可以把有用的工具放到网上。

想入门,最好最省时间的方法是Django tutorial,就在Django的官方网页。这个Tutorial一共有六个部分,介绍的常用Django的功能,包括: 模型(Model),模板(Template),视图(View),静态文件(Static),管理界面(Admin),网址分发(URL Dispatching)等等。学习之后,我感到Django的强项是利用Python语言做到来简化数据库使用。对于一个简单的App,基本开发的流程是: (1)设定project 的settings.py 和urls.py,注册新的app ;(2)设定urls.py来确定网址和视图(View)的关系;(3)用HTML开发模板(/template/app/XXX.html)同时实现各种需要的视图(views.py)

架设Django,一般简易使用uWSGI。我使用的是Nginx处理静态页面,同时用uWSGI处理Django相关界面。简易先在本机用uWSGI调好程序,再放到服务器上并配置Nginx和uWSGI。

Djano默认使用Unicode,而我们一般都用str(),这是ASCII编码。两种编码对于字符串处理来讲(包括join, index)没有什么区别,但在print的时候,Unicode的字符应该先转成UTF-8字符,比如: print u”aaa”.encode(‘UTF-8’)

 

2. 学习jQuery

用jQuery的主要目的是用Ajax来更新网页数据,这样用户用起来有行云流水的感觉。jQuery有.get 和 .post两个方法,使用起来和访问网页很相似。不过这里面有几个陷阱。第一个是GET方式适合小数据,对于Django来讲,默认的大小是4096个字符,大一点的数据应该用POST;第二个陷阱就是POST,为了安全(CSRF: cross-site request forgery),Django要求POST的数据必须有csrftoken,一般的HTML表格Form必须有csrf的标记才会被Django接收。

为了调试Ajax的输入输出,简易用Firefox下的FireBug或者最新版本的Firefox,它们都可以显示Ajax请求的参数和返回值。最新版本的Firefox还可以给Javascript设置断点,这就更加降低了调试的难度。

jQuery的功能很多,这个网页把jQuery 1.9以及之前版本的功能用不同颜色区分出来,很方便查询(link)。

在这个网页中,我还用了handsontable,这是一个在jQuery的基础上开发的实用的javascript spreadsheet,语法简单,使用方便。

3. 一点Nginx经验

对Nginx来讲,我们的配置是用location语句来把特定的网址传给uWSGI进程。注意Nginx设置里如果有多个location语句,Nginx并不一定选择最先匹配的网址,而是选择最长的网址。比如:

location ~* /author/static/.+.(jpg|jpeg|gif|css|png|js|ico|xml)$ {
root /var/django/zhanxw/; # STATIC_ROOT
expires 30d;
}

location ~* ^.+.(jpg|jpeg|gif|css|png|js|ico|xml)$ {
access_log        off;
expires           30d;
root /var/www;
}

如果第一个location写成: “location /author/static”,那么.jpg之类的文件会被第二个location处理,这就不是Django static文件的正确处理方式。

 

4. 一点Emacs web-mode经验

用Emacs写Django的模板HTML,最好用的不是django-html-mode,而是web-mode。只有这个mode可以识别 “{% static ‘polls/index.html’ %}”这样的记号,并正确缩进。

此外web-mode可以自动补全HTML tab,比如你在<p>后面打</,web-mode会帮你补全</p>。还可以用C-c C-e b 和C-c C-e e跳到一对tag的最前面(beginning)和最后面(end)。

 

5. 一般性经验

最开始设计应该以最少功能,最小实现为好,不要一下子把界面设计复杂。可以想几个用例(Use Case),保证最基本最重要的功能,其他功能应该越少越好。

网页的布局应该少用<br/>这种硬回车。在有Bootstrap的情况下,完全可以用<div>和<p>来用更少的HTML tag来实现更自然、自动的布局。

 

PyCon2013 有意思的幻灯

Interesting slides from PyCon 2013
今年PyCon在加州的Santa Clara召开。我虽然没去,但一如既往的关心。
挑出和我相关的一些有意思的幻灯,在此分享。

1. BeautifulCode
Raymond Hettinger是一个善用Python的高手,他的code非常简洁,有Python的风味。
这个幻灯里,他介绍很多Python中常用的简洁的写法,包括怎么用iterator, list comprehension.
我感觉有意思的是defaultDict (不需要再用dict.get()), decorator(修饰方法,类比CSS)和context(干净的获取和释放资源)

原始链接

2. Python: A “Toy” Language

David Beazley是一个Python作家,对Python GIL有过详细的介绍,以前写过如何把Generator组合成一个workflow的幻灯。
现在在芝加哥教Python。这个人擅长Python的教学,并能给出有趣的例子。这次也不例外,他介绍了如何用Python和Shapeoko (包括Arduino)来组装并使用CNC (数控机床?)。这个例子告诉我们Python可以做计算机程序之外很有用的 应用。

原始链接

3. Awesome Big Data Algorithms

作者是MSU的老师。这个Blog的背景是土壤Genetics。因为土壤中的genetics比单纯人的DNA更复杂,数据量更大,因此需要Big Data Algorithm。这个幻灯介绍三种算法:SkipList, HyperLogLog, Bloom filter(CountMin Sketch)。

SkipList是一种基于链表的数据结构,相对羽平衡二叉树,这个算法的优点是更好的支持并发操作。本质上,SkipList是一个分层次的链表。在最底层,链表元素按顺序排列。在更高的层次,(按照概率)简历一部分低层的数据的索引。这种数据结构在查找时非常有效:从高层开始查找,直到最底层顺序查找,整个查找是log(N)

HyperLogLog是应用于大数据的算法,用来计算一个很大集合的基数(即合理总共有多少不相同的元素)。大致思路是用一组相互独立的哈希函数依次处理输入,然后对哈希值分块计数:对高位统计有多少连续的0;用低位的值当做数据块。比如:011000|01,就是高位有3个连续的0,低位是1,就表示第一个数据块。因为连续观测的三个0的概率大约是1/8,所以对数据块1来讲,可以把计数乘8,作为集合基数的估计。因为低位可能有0,1,2,3这四种数值,总基数可以取上述4中技术的几何平均数。在HyperLogLog中,具体的还有一些系数可以调整,使得估计更准确。
这片Blog详细介绍了HyperLogLog算法,图文并茂。

BloomFilter或CountMin Sketch是两个不同的算法,但又有紧密联系。相似之处是两个算法都需要一族独立的哈希函数。不同之处是处理的问题不同。对BloomFilter,在预处理阶段对每一个特定的输入算出所有哈希函数的值,并在这些值上做出标记。最后,当查找一个特定的输入是否出现过,只需查找这一系列的哈希函数对应值上有没有标记。对于BloomFilter,可能有False Positive,但不可能有False Negative。此外,BloomFilter可看做查找一个数据有或者没有的数据结构(数据的频率是否大于1)。CountMin Sketch在BloomFilter的基础上更进一步,它可用来估算某一个输入的频率(不局限于大于1)。具体思路是对哈希函数值对应的sketch上计数(对BloomFilter则只是标记是或否)。最后估计频率的时候,对每个估算出的频率取最小值。

原始链接

4. Why you should use Python 3 for text processing

这个讲座关注的Python3,而不是现在我使用的Python2.7.3。但在Python社区,有时好的功能会从版本3移植回版本2。
这个讲座介绍了Python3里面的新功能,例如ChainMap,startswith(tuple作为参数),unicode支持,textwrap模块(可以方便的排版)和email模块。

原始链接

发布Python程序

发布Python程序

Distribute Python script

最近需要把一个Python程序发布给其他学校的Collaborators使用,发现最主要的问题是版本不兼容:我使用的是Python 2.7版本,但其他学校可能使用2.5或者更老的2.4版本。因为我使用了大量新版的特有功能,直接把代码发布给对方是没法让对方使用的。因为老版本的Python不支持2.7版里的函数比如:os.SEEK_SET, all(), str.format() 。解决方法有两个:

1. 使用Freeze之类的工具

在Python source code里有一个Tools文件夹,里面有freeze这个工具。他可以把Python代码编译成C语言中的Python。这种方式可以解决简单的脚本,但复杂的脚本这种方法可能会带来其他的错误。

2. 重写代码

这是最根本的解决方法,把新版本特有的函数重新定义,或者用另一种方法实现。比如all()

# all is a keyword since Python 2.7                                                                                                                                                  
try:
    all
except:
    def all(iterable):
        for element in iterable:
            if not element:
                return False
            return True

有一个工具可以检查当前的Python的向前兼容性:

https://github.com/ghewgill/pyqver

检查checkVCF.py的结果如下所示:

/net/fantasia/home/zhanxw/rvtests/rvMeta/sftp.clean/checkVCF/checkVCF.py
2.5 all
2.4 set, generator expression
2.3 logging, sum, enumerate

就是说我使用了2.5特有的all()函数,如果想在2.4版的Python上运行,需要重新定义all()函数。
类似的,set(), generator expression是2.4版才加入的特性,如果要支持2.3版本,就必须改写这两个地方。

 

 

写统计软件的两个常见错误

Two common mistakes when writing statistics software

最近在写一个软件,发现两个常见的错误:

1. 缺失 数据的处理

对于缺失数据,表示上可以是NA, “.”, -9,应该先保证软件读入的数字是正确的。

之后,在模型中,是应该丢掉数据,还是impute数据?是impute到均值还是用随机数值?

在写软件的时候必须要清楚。

 

2. 数值运算的维数

在统计中,常常有大量的数值运算,比如矩阵相乘。

在文章中这类运算往往会忽略维数,比如矩阵的行数和列数。

这时候软件中应加入更多的边界检查,这样就能避免程序崩溃。

 

 

GCC 相关设置(跨平台、不同版本)

查看gcc build-in specs

gcc -dumpspecs
-dumpspecs
Print the compiler's built-in specs---and don't do anything else. (This is used when GCC itself is being built.)

看gcc到哪些目录查找头文件:

gcc -print-search-dirs
-print-search-dirs
Print the name of the configured installation directory and a list of program and library directories gcc will search---and don't do anything else.

This is useful when gcc prints the error message installation problem, cannot exec cpp0: No such file or directory. To resolve this you either need to put cpp0 and the other compiler components where gcc expects to find them, or you can set the environment variable
GCC_EXEC_PREFIX to the directory where you installed them. Don't forget the trailing /.

看gcc编译到那个平台

gcc -Q -v
-Q Makes the compiler print out each function name as it is compiled, and print some statistics about each pass when it finishes.
zhanxw@host10-41.sph.umich.edu: ~/temp> gcc -Q -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/opt/local/libexec/gcc/x86_64-apple-darwin10/4.7.1/lto-wrapper
Target: x86_64-apple-darwin10
Configured with: ../gcc-4.7.1/configure --prefix=/opt/local --build=x86_64-apple-darwin10 --enable-languages=c,c++,objc,obj-c++,lto,fortran,java --libdir=/opt/local/lib/gcc47 --includedir=/opt/local/include/gcc47 --infodir=/opt/local/share/info --mandir=/opt/local/share/man --datarootdir=/opt/local/share/gcc-4.7 --with-libiconv-prefix=/opt/local --with-local-prefix=/opt/local --with-system-zlib --disable-nls --program-suffix=-mp-4.7 --with-gxx-include-dir=/opt/local/include/gcc47/c++/ --with-gmp=/opt/local --with-mpfr=/opt/local --with-mpc=/opt/local --with-ppl=/opt/local --with-cloog=/opt/local --enable-cloog-backend=isl --enable-stage1-checking --disable-multilib --enable-lto --with-as=/opt/local/bin/as --with-ld=/opt/local/bin/ld --with-ar=/opt/local/bin/ar --with-bugurl=https://trac.macports.org/newticket --disable-ppl-version-check --with-pkgversion='MacPorts gcc47 4.7.1_2'
Thread model: posix
gcc version 4.7.1 (MacPorts gcc47 4.7.1_2)

看gcc默认开启了哪些FLAG(定义了哪些macro)

$cpp -dM <(echo "") #define __DBL_MIN_EXP__ (-1021) #define __FLT_MIN__ 1.17549435e-38F #define __DEC64_DEN__ 0.000000000000001E-383DD #define __CHAR_BIT__ 8 #define __WCHAR_MAX__ 2147483647 #define __DBL_DENORM_MIN__ 4.9406564584124654e-324 #define __FLT_EVAL_METHOD__ 0 #define __DBL_MIN_10_EXP__ (-307) #define __FINITE_MATH_ONLY__ 0 #define __DEC64_MAX_EXP__ 384 ... ...

和MacPorts 相关的——如何选择gcc

sudo port select --list gcc
sudo port select --set gcc gcc42 # default gcc in Mac Snow Leopard
sudo port select --set gcc mp-gcc47 # MacPorts GNU GCC 4.7

和平台相关的Macro定义:

其他参考
【1】StackOverflow上的讨论:哪个macro可以区分Mac和Linux Link
【2】SourceForge上Pre-defined C/C++ Compiler Macros,包括以下方面:
Standards
Compilers
Libraries
Operating Systems
Architectures
Devices
Link

Linux环境下测量程序的内存占用

Measure memory consumption of programs in Linux

在衡量程序性能时不仅需要测量运行时间,还常常需要测量其内存使用。比如如果一个程序比更一个程序快,有可能是以空间换取时间,因此单纯比较运行速度就失去了意义。我们这里演示如何在Linux环境下测量程序的内存占用。

我们先要弄清楚内存的含义:对于运行中的程序而言,它有四个部分:数据,文本,堆,栈(data, text, stack, heap)。对于操作系统而言,所有分配给程序的内存都用虚拟内存表示(Virtual Memory)。对硬件而言,内存分为物理内存(Physical)和交换内存(Swap)。这三种环境的关系有两种:(1)程序内存会被映射到虚拟内存,即虚拟内存中分段对应数据、文本、堆、栈; (2)虚拟内存中某些部分放在物理内存中,另一些放到交换内存中。

为了程序运行有空间上的效率,Linux系统下会共享内存。例如共享程序库(比如libc)或者是内存映射(memory map)。据我的理解,共享部分在程序空间中可以是文本部分,在虚拟内存中放在相同的地址,在硬件中可放在物理内存或交换内存。

对应上面不同的概念,Linux常用的内存相关术语有:
常驻内存(Resident memory), 表示硬件中物理内存的占用。
虚拟内存,Virtual memory,操作系统使用内存的一种抽象。
页(page),操作系统以页为单位管理(分配,回收)内存

测量程序的内存占用可以用不同的统计量。从程序的生命周期来看,可以分为最大(peak)内存占用,即时内存占用,和平均(average)内存占用。从内存的含义来讲,可以在程序本身的语境(context),或是操作系统,或是硬件的语境,比如:栈内存使用(heap memory),虚拟内存占用,物理内存占用。

测量一个程序的内存占用时,最理想的是我们知道(每时每刻)程序的四个部分(在虚拟内存中)分别有多大,对应的,在物理内存或者交换内存中占用了多少空间。但是这是一个很难达到的目标,因为:(1)操作系统以页方式分配内存,很难得到每一个页中具体多少空间被占用;(2)虚拟内存往往比物理内存大很多,当操作系统给一个运行的程序分配内存时总是提供多余程序精确需要的内存;(3)注程序在物理内存的占用加上程序在交换内存的占用构成了程序总的使用情况,但是现有的工具并不直接给出这一数值,需要手动把Resident Memory和Shared Memory相加 。

在实际情况下,通过一些现有的工具,我们往往更关心这几个易于获得统计量:
(1)max resident memory size
可以使用time -v命令(要指定全部路径,否则bash的time命令不识别-v)
注意这个测量值往往远远高于程序运行时消耗的内存。
(这是time 1.7版本的bug,time 报告的内存用量是实际的4倍,因为wait3/wait4返回的实际内存用量,单位是kbyte,但time错把单位设定为page)
本质上time使用了wait3或者wait4命令来获取程序的resources (见 ‘man wait3’)。
使用Python实现时,可以用resources module来获取。
另外,使用这个思路还可以得到程序的运行时间(用户时间,内核时间,实际时间)

(2)top 或者ps
可以查看程序的Resident Size(RES)列,Share Memory(SHR)以及Virtual Memory Size(VIRT),这一统计量默认每秒更新一次。这一方式的缺点是测量值为即时测量,每时每刻这个值都有可能变化。优点是使用起来很方便。
使用Python实现时,可以先使用multiprocesing.Process启动一个程序,然后使用psutil module,构造psutl.Process(),然后使用get_memory_info方式来获取rss,vms

(3)与(2)类似,但为了更多的信息,还可以读取/proc
这是通过读取/proc//status和/proc//smaps来获得程序的内存使用量的。理论上这是最精确的测量。这两个文件的具体格式可以用‘man proc’来获得。
在Python中,需要另外写程序来解释这两个文件,例如下面这样:

zhanxw@amd: ~> cat /proc/15648/status
Name:	takeMem
State:	R (running)
Tgid:	15648
Pid:	15648
PPid:	15163
TracerPid:	0
Uid:	248396	248396	248396	248396
Gid:	248396	248396	248396	248396
FDSize:	256
Groups:	500 1007 1013 1017 1028 1033 248396 
VmPeak:	   12752 kB
VmSize:	   12752 kB
VmLck:	       0 kB
VmHWM:	    1756 kB
VmRSS:	    1756 kB
VmData:	    1112 kB
VmStk:	     136 kB
VmExe:	       4 kB
VmLib:	    3232 kB
VmPTE:	      40 kB
VmSwap:	       0 kB
Threads:	1

在实践中,可以使用已有的程序,例如tmem【4】,以及lh3的runit【6】(下载udp,在runit/下,用make编译)。输出如下。

tmem

./tmem ./takeMem 1000000000
	    3908		    3908	      84	      84
Allocating 1000000000 memory
	  988336		  988336	  148012	  148012
	  988336		  988336	  308524	  308524
	  988336		  988336	  442636	  442636
	  988336		  988336	  567244	  567244
	  988336		  988336	  686308	  686308
	  988336		  988336	  796396	  796396
	  988336		  988336	  889588	  889588
	  988336		  988336	  949516	  949516
runit

./runit ~/mycode/smake/takeMem 1000
Allocating 1000 memory
-- CMD: ./runit /net/fantasia/home/zhanxw/mycode/smake/takeMem 1000

-- totalmem     198345252.000 kB
-- available    190966956.000 kB
-- free           1569216.000 kB

-- retval                   0
-- real                15.749 sec
-- user                15.350 sec
-- sys                  0.170 sec
-- maxrss             704.000 kB
-- avgrss             704.000 kB
-- maxvsize         11904.000 kB
-- avgvsize         11904.000 kB

参考资料:
【1】Python resource 模块
【2】Python os 模块,介绍wait3,wait4
【3】Python multiprocessing模块,介绍了Process这个易于使用的类,与subprocess相似,但有更灵活的用法
【4】Measuring Memory Usage,介绍了用C来读取/proc//status的程序,并给出tmem的下载链接
【5】Understanding memory,非常详细的介绍Linux内存分配,优化
【6】Heng Li’s personal website 可以下载比较hash library的程序,里面有一个runint/文件夹,编译之后可以用runit来测量内存使用

Embedding Python in C/C++

将Python嵌入C/C++

首先嵌入和扩展是相关联但不同的两个概念:嵌入是指在C语言中使用Python,而扩展是在Python中使用C语言(以library的形式)。

其次嵌入Python的本质是嵌入Python 解释器(Interpretor)。因此我们需要调用相应的Initialize, Finalize函数。另外,为了让C/C++识别Python相关的函数,我们还需要#include 以及相应的linker options:-Xlinker -export-dynamic -ldl -lrt -lutil

另外,Python语句有两种:statement 和 expression。注意statement是没有返回值的。因此Python语言里有exec和eval分别对应这两种情形。最本质的区别是statement有副作用(side effect),比如会把值绑定到一个名称上,比如: a = 3 。当我们用PyRun_SimpleString(“a=3”)时,这种副作用是在当前的environment下(内部实现是dict)多出一个变量(dict的key),名称是a。

此外要注意Python的执行代码是和environment相关的,比如global和local,想用的函数名称(例如:dir,str,print),变量都是保存在各自的environment里。平时我们写的 if name == "__main__" 就是说默认的环境是在模块__main__里面。我们要取出默认的函数或者变量(Python内部不严格区分这两个概念,知识函数可以callable),可以用下面的代码(以取出dir函数为例):

    PyObject* main_module =
        PyImport_AddModule("__main__");

// Get the main module's dictionary                                                                                                                                                  
// and make a copy of it.                                                                                                                                                            
    PyObject* main_dict =
        PyModule_GetDict(main_module);
    //  pFunc is also a borrowed reference 
    PyObject* pFunc = PyDict_GetItemString(pDict, "dir");

嵌入Python还应注意内存的使用。因为Python主要使用(另一种是Python的malloc, free)Reference count方式来管理新的变量,我们应记住在获得一个PyObject*类型的指针之后,用Py_DECREF或者Py_XDECREF来减少reference count (注意,特殊情况下取出的结果是不能减少reference count的,比如取出list中某个元素)。

最后给一个例子来说明怎么在Python里计算任何表达式expression (参考了FAQ[1])

#include <Python.h>
                                                                                                                                                                                     
double checkExpression(const char* formular, double gq, double dp) {
    Py_Initialize();

    // Get a reference to the main module.                                                                                                                                               
    PyObject* main_module =
        PyImport_AddModule("__main__");

    // Get the main module's dictionary                                                                                                                                                  
    // and make a copy of it.                                                                                                                                                            
    PyObject* main_dict =
        PyModule_GetDict(main_module);

    char s[1024];
    sprintf(s, "GQ=%lf", gq); //, dp, formular);                                                                                                                                     
    if ( 0 != PyRun_SimpleString(s) ) { // something wrong happen!                                                                                                                   
        fprintf(stderr, "\nSomething wrong in assigning GQ\n");
        return -1.;
    }
    sprintf(s, "DP=%lf", dp);
    if ( 0 != PyRun_SimpleString(s) ) { // something wrong happen!                                                                                                                   
        fprintf(stderr, "\nSomething wrong in assigning DP\n");
        return -1.;
    }

    PyObject* ret = PyRun_String(formular, Py_eval_input, main_dict, main_dict);
    if (ret == NULL) {
        Py_XDECREF(ret);
        PyErr_Clear();
        return -1.;
    };
    double res;
    if (PyInt_Check(ret)) {
        res = PyLong_AsLong(ret);
    } else if (PyFloat_Check(ret)) {
        res = PyFloat_AS_DOUBLE(ret);
    } else if (PyBool_Check(ret)) {
        res = ret == Py_True;
    }
    Py_XDECREF(ret);
    Py_Finalize();
    return res;
};

重要参考资料

【1】 扩展/嵌入Python的FAQ Extending/Embedding FAQ

【2】API 手册 Python/C API Reference Manual

【3】嵌入Python的流程性说明 Embedding Python in Another Application

【4】扩展Python的流程性说明,这里介绍了Python底层的知识,这些知识不会在”嵌入Python的流程性说明”中重复出现 Extending Python with C or C++

Valgrind 查内存错误的利器

Valgrind – a cool tool to check memory related problems

Valgrind是非常有用的检查内存相关问题的工具。比如: 内存泄漏,double free memory,内存非法访问。基本上Segmentation fault都能用Valgrind查出来。我刚刚查出了一个很刁钻的bug,在找bug的过程中发现valgrind非常有用,但要用好,还需要点技巧。

先描述一下问题:

自己的程序总是Segmentation Fault。我先用Valgrind运行,重要结果如下:


==11060== Invalid write of size 8
==11060== at 0x44FF07: FileReader::FileReader() (in /net/nfsb/dumbo/home/zhanxw/smallTool/BamPileup)
==11060== by 0x410D05: BufferedReader::BufferedReader(char const*, int) (IO.h:232)
==11060== by 0x41126C: LineReader::LineReader(char const*) (IO.h:332)
==11060== by 0x41047A: RangeList::addRangeFile(char const*) (RangeList.cpp:128)
==11060== by 0x405B28: main (BamPileup.cpp:258)
==11060== Address 0x75fc2e8 is 0 bytes after a block of size 40 alloc'd
==11060== at 0x4C27CC1: operator new(unsigned long) (vg_replace_malloc.c:261)
==11060== by 0x411252: LineReader::LineReader(char const*) (IO.h:332)
==11060== by 0x41047A: RangeList::addRangeFile(char const*) (RangeList.cpp:128)
==11060== by 0x405B28: main (BamPileup.cpp:258)
==11060==

因为我的BufferedReader包含FileReader类,我最开始的几个思路:
1. 自己的code有bug
BufferedReader 和 FileReader都是自己写的,用过很多次没有问题,这次出现Valgrind报错在IO.h:232,因此反复检查了那段代码。
2. 怀疑link有问题的library
重新编译整个code多次。

但是问题依旧,后来给Valgind 这几个参数 –show-reachable=yes –leak-check=full ,再重新运行:

==11908== Invalid write of size 8
==11908== at 0x4584BB: FileReader::FileReader() (BgzfFileTypeRecovery.cpp:239)
==11908== by 0x4106B5: BufferedReader::BufferedReader(char const*, int) (IO.h:232)
==11908== by 0x410C28: LineReader::LineReader(char const*) (IO.h:332)
==11908== by 0x40FE2A: RangeList::addRangeFile(char const*) (RangeList.cpp:128)
==11908== by 0x4054D8: main (BamPileup.cpp:258)
==11908== Address 0x75fc2e8 is 0 bytes after a block of size 40 alloc'd
==11908== at 0x4C27CC1: operator new(unsigned long) (vg_replace_malloc.c:261)
==11908== by 0x410C0E: LineReader::LineReader(char const*) (IO.h:332)
==11908== by 0x40FE2A: RangeList::addRangeFile(char const*) (RangeList.cpp:128)
==11908== by 0x4054D8: main (BamPileup.cpp:258)
==11908==

这次一下发现原来是我link别人代码的时候,我们都有一个类叫做FileReader,编译器把错误的FileReader代码链接给我,所以把程序搞崩溃了。

总结一下,要是:
1. 自己一下子就用到Valgrind的这些参数
2. 链接别人的代码前先测试一下,然后就能把问题的原因归于新加入的代码

可惜没那么多“要是”,以此文纪念一下刚刚过去的3个小时。