Emacs和它的朋友们——阅读源代码篇(转)

正如那本《Code Reading》一书中指出的那样,源代码阅读一直没有被很好的重 视:你上大学的时候有“代码阅读”这门课吗?相信没有。

1 Source Insight

谈到阅读源代码,不得不提一下很多人都用过并且现在也还用着的一个工具: Source Insight。很多年前我最早接触的源代码阅读工具就是这个。不可否认, 它有很多优点:非常直观,非常容易上手,该有的功能基本上都有…

但是,它也有一些缺点:

是商业软件:要花钱买或者使用盗版
是Windows软件:在Linux下用的话需要用 Wine 虚拟。
可扩展性极差:如同Perl语言作者对Perl的评价,一个好的语言,应该让简单 的事情更简单,难的事情有可能;一个代码阅读工具也应如此,但是Source Insight对难的事情(扩展性、定制性)基本上不可能。
基本是一个只读软件:想使它可写、用它来编辑源代码简直要弱爆了,跟 Vim/Emacs等比起来。
使用开源软件 Emacs 和它的朋友们,我们可以实现Source Insight所拥有的大部 分(也是最有价值的那部分)功能。并且能极大的超越。

2 Emacs

Emacs 是一个很强悍的编辑器,基本上只有 Vim 可与之匹敌。(鄙人是一个狂热 的 Emacs 粉,但绝不是 Vim 黑,因为我学Emacs的过程,最大的受益来自于Vim 作者所写的“Seven habits of effective text editing”)。

没错,Emacs的学习曲线可能是陡一些,但是话说回来,学会它绝对是值得的,并 且不然的话,就没意思了——爬个香山,能和珠峰比哪个更带劲儿么?

作为一个Emacs的老用户,我想我应该说说为什么我向大家推荐Emacs。这是一件 看上去很简单,实际上很困难的事情,因为太熟悉了,就像要解释人为什么要呼 吸空气一样。但无论如何,我会认真试一下。

2.1 强大的社区和库的支持——内功

任何一个好的软件背后都会有一个强大的社区以及库。Java语言为什么那么火? 因为它自带的标准库提供了很强大的功能。Perl的背后有cpan(相信我,里面 真的简直什么都有)。TeX背后有ctan。Emacs的背后有EmacsWiki。

有了这样一个社区之后,无论你想要什么样的功能,很可能你到社区里找一找 就发现别人已经帮你实现了;或者如果没找到的话,你自己实现它,再把它发 布到社区里,从而帮助到其它的用户——整个社区就这样慢慢变得越来越强大。

这些社区(库)的实现,一般都是用Emacs自带的语言Elisp来实现的。所以我 们把它称为内功。说到Elisp,它是最古老而伟大的Lisp语言(听起来有点宗 教狂热?)的一个专门为Emacs的编辑器功能而量身定做的一个变种。学点 Lisp是很有好处的(据说:-)。

2.2 友好的与其他强大的工具配套使用——外功

简单地说,就是好学、好用。

2.2.1 试用

当你对某个外部程序不是很熟的时候,你可以很轻松地试着调用它一下,从而更 快学会它。

比如 make 程序,我想知道某段 makefile 脚本是不是可以这样写,如果不跟 Emacs配套试用的话,你需要用很多步骤:

创建一个临时的Makefile
打开此Makefile,编辑,输入以下内容,并关闭。
out/target/product/%/kernel:
     cp $@-out/uImage $@
退出编辑器,回到命令行,运行 make,查看结果,如果出错,回第2步。
删除此临时文件。
验证通过,把上面的内容再输入到你真正要写的Makefile脚本里去(或者可以 用copy/paste)。
而如果你在Emacs里的话,你可以直接在你想改的Makefile里开始写以上的脚本片 段,然后选上此片段,按 M-| (也就是shell-command-on-region命令),然 后输入 make -f - out/target/product/xx/kernel ,底下的临时输出窗口就 会显示:

cp out/target/product/xx/kernel-out/uImage out/target/product/xx/kernel
cp: cannot stat `out/target/product/xx/kernel-out/uImage': No such file or directory
make: *** [out/target/product/xx/kernel] Error 1
于是你就知道试用通过了,并且这小段代码已经在它应该在的位置上,没那么多 乱七八糟非常不简约的步骤哦!

2.2.2 文档

如果试用不够的话,你可以在Emacs里直接打开该外部程序的文档来深入学习。 Linux下常见的man手册、info手册,perl的perldoc方档等都可以直接在 Emacs里打开看,并且比起在终端上用相应的man(1)等命令打开,在Emacs里你 可以调用更丰富的浏览、搜索功能(如果是在终端上打开,你的输出是 $PAGER 来控制的,可能是 less ,也可能是 more ,甚至可能是 cat )。

比如,在我知道perldoc怎么用之前,我一直直接用man perlfunc来看perl的 函数帮助,比如我想看read函数,我一般是先 man perlfunc 然后再用 occur 功能去列出 read 在这个 man 手册里出现的行,向下翻几页之后 你能很轻松地发现 read 函数是在哪一行上讲解的(因为开始讲解的地方是 会突出来一点的,纯文本的显示一般会用不同的缩进来表示不同的章节),然 后跳到那一行上就行了,见图(在这个例子中,read的定义开始于3162行):

orrur-read.png

当然,现在我知道Emacs有一个perldoc命令可以直接显示read函数了,可是在 此之前,这个小技巧真的让我很开心:-) 因为要不然的话,我只能在终端里用 /read 然后不停地按 n 去找下一个 read 出现的地方,而一行上可能会 有出现很多次 read。而这样不停地机械地按,还容易按过头啊。或者我可以 想一个更复杂的正则表达式比如 /^ *read (匹配一行开头任意个空格后跟 一个read单词),但这个真的很不习惯,我老是担心万一不是空格,而是制表 符怎么办,另外不同的工具有不同的正则表达式语法,我很不愿意去多记一个 less的正则表达式语法是怎样。

2.2.3 使用

当你想解决一个问题,发现Emacs本身不够用的时候,你可以很轻松地调用外 部程序来帮忙。Emacs主要通过直接运行、运行并获得输出、运行并喂以输入 并获得输出这三种方式来调用外部程序。基本上就是UNIX哲学最精华的部份都 用上了。如果现有的工具也无法满足的话,还可以用perl之类的脚本,现写一 个,只要符合UNIX的哲学,也是能被Emacs使用的。

2.2.4 逆袭の使用

偶尔地,我们也可能想在Emacs之外来调用Emacs的功能,这也是可以的。一段 elisp程序和一段perl程序其实都是程序。

这种用法我用的最多的是org-mode的发布功能,比如的我github page(您目前正 在看的这篇文章就是发布于github上),我用org-mode写完一篇文章之后,可能 忘了把它发布为html,所以我在git push的时候可以加个hook,自动检查一下, 相应的.html文件是不是没有更新,如果没有的话,就逆向调用Emacs一下,自动 完成.html的发布(要不然的话还手动打开Emacs,再打开这个.org文件,再手动 发布,就太烦了)。

所以以上就是我喜欢用Emacs的原因,内外兼修,无限可能。下面我们开始讲怎样 调用一个个具体的外部程序来把Emacs打造成强大的代码阅读工具吧!

3 Grep

没错,grep,最简单,最古老,最强大的工具之一。

grep与Emacs的结合相当紧密,Emacs专门有一个mode来处理grep的输出: compilation-mode(make出错,perl脚本里的die,还有很多其他程序的错误输出, 都可以用这个mode来捕获出错的文件与行号,从而实现跳转)。

从下面的例子可以看到,实际上Emacs使用的mode是grep-mode,但这个实际上是 从compilation-mode继承下来的。这种输出有一个基本模式,那就是 “ 文件名:行号: 内容 ”。所以Emacs很容易parse出应该跳转到哪个文件的哪一行上。

跳转到下一个匹配行的Emacs函数是 next-error (由此可见其与 compilation-mode的联系,把grep的匹配也称之为一个error,软件逻辑重用是好 的,但这样的名字重用真的会造成困惑吧)。

下面你会看到我把所有的代码阅读工具的界面都归一到grep-mode上来了,所以可见grep在我心目中的重要性:-)

(这种做法是非常极端的,当我们发现一样好的东西的时候,应该像一条疯狗一 样扑上去,死死咬住不放:-) grep很好用,所以让我们到处都用grep;文件很好 用,所以让我们做一个everything is a file的操作系统;面像对象很好,所以 让我们做一个一切皆对象的编程语言;…… 参见 极端编程。)

-*- mode: grep; default-directory: "~/system-config/gcode/fcitx/src/" -*-
Grep started at Fri Oct 19 16:25:46

grep -nH -e include *.cpp
ime-socket.cpp:1:#include <map>
ime-socket.cpp:2:#include <string>
...

Grep finished (matches found) at Fri Oct 19 16:25:46
4 ctags-exuberant

ctags-exuberant是emacs自带的etags的一个加强兼容版。用它可以查函数、类、 结构等定义于何处。它支持40多种语言。它可以轻易地扩展支持更多简单的语言, 比如Kernel的Kconfig脚本,可以通过一个这样的 $HOME/.ctags 文件来增加支 持:

--langdef=kconfig
--langmap=kconfig:(Kconfig)
--regex-kconfig=/^(menu)?config[ \t]*([a-zA-Z0-9_]+)/CONFIG_\2/d,definition/
4.1 打造个人的使用习惯

这里要叉开去说一下我对Emacs和Vim自带的tag功能有点不满,它们都不支持把所 有的定义点给列出来,只能让你自己一个一个地挨个看下去,看是不是你要找的。 (至少我没有发现,这点不如source-insight来得直观,它是默认把所有的定义 点都给列出来)。

幸好我们用的是开源软件,无限可能。让我们自己来!

ctags-exuberant支持一个 -x 选项,允许它:

Print a tabular, human-readable cross reference (xref) file to standard output instead of generating a tag file.

你可以这样调用它: ctags-exuberant -x scim_fcitx_imengine.cpp ,结果是这样的:

...
SCIM_CONFIG_IMENGINE_FCITX_LANGUAGES macro        53 scim_fcitx_imengine.cpp #define SCIM_CONFIG_IMENGINE_FCITX_LANGUAGES "/IMEngine/Fcitx/Languages"
SCIM_FCITX_ICON_FILE macro        71 scim_fcitx_imengine.cpp #define SCIM_FCITX_ICON_FILE (SCIM_ICONDIR "/fcitx.png")
SCIM_FULL_LETTER_ICON macro        62 scim_fcitx_imengine.cpp #define SCIM_FULL_LETTER_ICON (SCIM_ICONDIR "/full-letter.png")
...
剧透:文件名,行号。

哈,我们可以写一个perl小脚本,轻松地把它转换成grep的格式!见图:

grep-def.png

这样做的额外的好处是,查找上一个/下一个定义点的Emacs按键,也跟 grep/compilation统一起来了!(不去记两套不同但功能完全类似的按键,就像 保护自己的脑力不被 less 的不常用、irrelevant、insignificant的正则表达 式规则污染一样,非常必要。)

一个缺点是,所有使用grep-mode的外部输出,都共用一个buffer,看完ctags看 grep再回来看之前的ctags的话,就不得不重新查一遍(或许我应该去看看 multi-grep,但幸好这些grep/ctags查得都很快)。

4.2 多点定义下的距离计算

在一个大的项目下,一个函数有很多个定义是很常见的事。source-insight或我 们如上打造的Emacs默认查到这些定义之后,都是以一种随意的顺序列出这些定义。

我们的目标是给这些定义点排一个序。Google为何能大行其道,据说跟它有一个 很牛的申请了专利的排序算法 pagerank 有关。所谓排序,意思就是说,当出现 多个结果时,我们把这些结果按照一定的规则排列出来,理想的目标是:把最重 要的、最相关的、用户最想要的结果排在最前面。

如果你只是在读一个小的代码项目,那很可能不需要怎么排序,因为基本上你要 搜索什么东西,要么没有,要么出来只有一两条结果,所以就不需要排序了。

但是,如果是Android这种大项目呢?你可能会查出来有n个地方定义了你想查的 那个函数,你真正想要看的那个函数可能列在第一个,也可能出现在最后一个, 总之,你需要很费神费眼地自己去找。让我们想个办法保护我们的眼睛吧!

比如我们在Android里搜索 parse_state ,会出来6个地方有定义它:

Finding global definition: parse_state

Database directory: /home/bhj/src/android/
-------------------------------------------------------------------------------
gtags-cscope-bhj -f cscope.out -d -L -1 parse_state

*** /home/bhj/src/android/external/e2fsprogs/e2fsck/profile.c:
parse_state[152]               struct parse_state {

*** /home/bhj/src/android/system/core/init/parser.c:
parse_state[70]                struct parse_state

*** /home/bhj/src/android/external/freetype/include/freetype/internal/psaux.h:
parse_state[575]               T1_ParseState parse_state;

*** /home/bhj/src/android/external/iptables/extensions/libipt_conntrack.c:
parse_state[60]                parse_state(const char *state, size_t strlen, struct ipt_conntrack_info *sinfo)

*** /home/bhj/src/android/external/iptables/extensions/libipt_state.c:
parse_state[32]                parse_state(const char *state, size_t strlen, struct ipt_state_info *sinfo)

*** /home/bhj/src/android/external/iptables/extensions/libip6t_state.c:
parse_state[32]                parse_state(const char *state, size_t strlen, struct ipt_state_info *sinfo)
-------------------------------------------------------------------------------

Search complete.  Search time = 0.59 seconds.
这种情况我们应该怎么对它进行排序呢?要注意的是,“最重要的、最相关的”这 一标准是上下文相关的。如果用户当前在看system init相关的代码,很可能用户 希望第二个 parse_state 出现在最前面,如果用户在看iptables相关的代码,那么 很可能需要把4、5、6排在前面。

怎么办呢?我想到了一个“土”办法。

把用户开始搜索的时候正在阅读的文件,记为起始文件;把结果文件中的每一个, 都跟起始文件计算一下“距离”。距离最短的,就认为是最相关的。

怎么计算距离呢?哈哈,perl有一个module,叫 String::Approx ,可以直接 从cpan上安装,如果是用debian/ubuntu的话,也可以直接 apt-get install libstring-approx-perl 。它已经帮我们实现了计算两个字符串之间的距离的功 能,我们直接拿来用就行了(再一次让我们见识到了社区的力量)。

假设我们的起始文件是 /home/bhj/src/android/system/core/init/init.c , 在读这个文件代码的时候,我们想查一下 parse_state ,查出来有一 条 /home/bhj/src/android/system/core/init/parser.c 里面包含有这个定义, 那它当然应该排在最前面了,因为它距离最短,大家甚至都是在一个目录下的。

5 imenu

imenu是Emacs自带的一个命令,用它可以轻松地实现 source-insight 里类似于 下图的功能(1. 列出当前文件中的所有定义方便查看,2. 鼠标点击实现跳转):

si-imenu.png

我们的imenu的用法当然跟source insight有点不一样,虽然精神上是类似的。首 先,我不能一直炫炫地显示着有一个“定义窗口”,我想看有哪些定义的话,需要 自己手动打一下imenu命令(我曾经很偏执地也想要一个这样的窗口,并且这也是 可以通过 ecb 之类的插件实现的,但是后来我突然不想要了,嘻嘻)。

其次,如下图所示

emacs-imenu.png

我可以把它跟emacs的 anything.el 结合,对要显示何种定义做更精准的控制 (图中只显示了 macro )。Source insight做不到这一点,所以,Emacs加一分! anything 的好处在于你还可以接着打一个空格,再打一个正则表达式,继续过 滤出你真正想要的定义。

最后,我不需要用鼠标点,直接回车就跳到定义的点上了(看完不想跳的话就按 C-g )Source insight必须用鼠标点,所以 Emacs 再加一分!

5.1 imenu与ctags-exuberant的结合

最后,上面的图里显示的 imenu 定义跟默认的 Emacs 自带的有点不一样哦!那 是因为我发现 Emacs 自带的imenu找定义功能不够强大,所以我就把它定制了一 下,转而寻求强大的ctags-exuberant的帮助(详见 我的配置系统 ):

(setq-default imenu-create-index-function #'ajoke-create-index-function)
6 beagrep

终于说到我的 favorite,beagrep了。详见 两秒钟grep两G代码!。

6.1 beagrep与ctags-exuberant的结合使用

通过它们的结合使用,我们可以实现最后一个功能:查找一个函数在何处被调用。见下图:

func-called.png

其工作原理是这样的,以上图为例,先beagrep出readlink出现在哪些文件里,再 在这些文件上当场调用ctags-exuberant,获得类似这样的信息:某文件第100行 有一个函数定义 A_func ,150行有下一个函数 B_func ,而readlink出现在了125行, 那么,我们可以说, A_func 调用了readlink这个函数。

又一次,我使用了 grep-mode 来一统其输出格式。

7 生成调用关系图

source-insight有生成调用关系图的功能(有人用过吗?),我们用开源软件也 可以做到的(只不过弄完这个功能之后我再也没有用过它),见下图:

call-graph.png

8 cscope 和 gtags

我曾经也用过这两个工具,从上面的函数定义距离计算的例子中可以看到,写那 一段的时候我还是在用cscope。后来转成用gtags,最后用的是ctags-exuberant。

虽然现在已经不完全用这两个工具了,它俩的一些好的idea我还是吸收了,比如 在cscope.el里有一个ajoke–marker-ring,能够在查询定义之后回到开始查询的 地方,经过我的改良之后,纵横进退,异常方便。而gtags既有接口模拟cscope的 输出,又有plugin机制可以调ctags-exuberant来做定义点预索引,所以为我试用 这几个不同工具时的平滑过渡提供了很大的方便。目前我还在通过它来调用 ctags-exuberant,因为它有内建增量更新索引机制。

9 查找局部变量的定义

这个也是可以实现的,先记下当前的变量,再搜到当前函数开头(可以用 ctags-exuberant来记算),再搜到记下的变量的第一次出现的地方。

写到这里,我们的“Emacs和它的朋友们——代码阅读篇”就告一段落了。下一篇,你 猜对了,“Code Writing”,敬请期待!

posted @ 2014-06-18 17:29  微信公众号--共鸣圈  阅读(1108)  评论(0编辑  收藏  举报