2011年13月32日24点60分60秒

这是一个测试时候发现的问题,2011 年 13 月 32 日 24 点 60 分 60 秒,这个时间存在吗?

我觉得这应该是个错误的时间,但很不幸地是,如果往 struct tm 中填入这个时间,mktime() 时并不会报错。下面是一段简单的测试代码:

#include <stdio.h>
#include <string.h>
#include <time.h>

int main()
{
    struct tm t;
    memset(&t, 0, sizeof(t));
    t.tm_year = 2011-1900;
    t.tm_mon = 13-1;
    t.tm_mday = 32;
    t.tm_hour = 24;
    t.tm_min = 60;
    t.tm_sec = 60;
    printf("tm_mon=%d, tm_mday=%d, tm_hour=%d, tm_min=%d, tm_sec=%d\n",
            t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec);
    time_t tt = mktime(&t);
    printf("%s", ctime(&tt));
    printf("tm_mon=%d, tm_mday=%d, tm_hour=%d, tm_min=%d, tm_sec=%d\n",
            t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec);
}

这个程序执行的结果是:

$ ./a.out  
tm_mon=12, tm_mday=32, tm_hour=24, tm_min=60, tm_sec=60
Thu Feb  2 01:01:00 2012
tm_mon=1, tm_mday=2, tm_hour=1, tm_min=1, tm_sec=0

查看 man mktime,的确有对这个特性的解释:

...if structure members are outside their valid interval, they will be normalized
 (so that, for example,  40  October  is  changed into  9  November);...

这就意味着,对输入时间做严格的检查无法依赖于标准库中的 mktime() 函数,只能自己来进行。这应该也是 date 命令曾经改进过的地方。

$ date -d "2011-13-32" # date (coreutils) 5.2.1
Wed Feb  1 00:00:00 CST 2012
$ date -d "2011-13-32" # date (GNU coreutils) 8.5
date: invalid date `2011-13-32' 

epoll 事件之 EPOLLRDHUP

在对系统问题进行排查时,我发现了一个奇怪的现象:明明是对方断开请求,系统却报告一个查询失败的错误,但从用户角度来看请求的结果正常返回,没有任何问题。

对这个现象深入分析后发现,这是一个基于 epoll 的连接池实现上的问题,或者说是特性 :)

首先解释一下导致这个现象的原因。

在使用 epoll 时,对端正常断开连接(调用 close()),在服务器端会触发一个 epoll 事件。在低于 2.6.17 版本的内核中,这个 epoll 事件一般是 EPOLLIN,即 0x1,代表连接可读。

连接池检测到某个连接发生 EPOLLIN 事件且没有错误后,会认为有请求到来,将连接交给上层进行处理。这样一来,上层尝试在对端已经 close() 的连接上读取请求,只能读到 EOF,会认为发生异常,报告一个错误。

因此在使用 2.6.17 之前版本内核的系统中,我们无法依赖封装 epoll 的底层连接库来实现对对端关闭连接事件的检测,只能通过上层读取数据时进行区分处理。

不过,2.6.17 版本内核中增加了 EPOLLRDHUP 事件,代表对端断开连接,关于添加这个事件的理由可以参见 “[Patch][RFC] epoll and half closed TCP connections”。

在使用 2.6.17 之后版本内核的服务器系统中,对端连接断开触发的 epoll 事件会包含 EPOLLIN | EPOLLRDHUP,即 0x2001。有了这个事件,对端断开连接的异常就可以在底层进行处理了,不用再移交到上层。

重现这个现象的方法很简单,首先 telnet 到 server,然后什么都不做直接退出,查看在不同系统中触发的事件码。

注意,在使用 2.6.17 之前版本内核的系统中,sys/epoll.h 的 EPOLL_EVENTS 枚举类型中是没有 EPOLLRDHUP 事件的,所以带 EPOLLRDHUP 的程序无法编译通过。

僵尸对象或 RAII

我最近在想这个问题,到底要不要在程序中使用异常?

以前写的 C 代码比较多,即使写 C++,基本上也是把它当成 C with object 来用。对异常的了解偏少,使用更是极少。最近评审别人代码的时候遇到一个问题:如果构造函数中 new 失败了,会发生什么事情?

工程的代码一般提倡哪里出错在哪里处理,不能恢复的要返回错误码给调用者。在一般情况下,使用 new(std::no_throw) 保证 new 不抛出异常(否则结果是灾难性的),并且检查分配是否成功是可以实现这一点的。

遗憾的是构造函数没有返回值,我们不能返回构造失败。那么只有用迂回的办法,为类定义一个成员变量 bool inited。初始化为 false,只有在构造的工作都完成之后,才将它置为 true。如果一个对象的 inited 成员为 false,就意味着它构造过程中出了问题,不能被使用。这就是一个僵尸对象,“活死人”。

看,我们成功地规避了使用异常。但是慢着,不是只有 bad_alloc 这一个异常啊!还有 bad_cast、runtime_error、logic_error,还有:

$ grep class /usr/include/c++/4.5/stdexcept 
// Standard exception classes  -*- C++ -*-
// ISO C++ 19.1  Exception classes
   *  program runs (e.g., violations of class invariants).
   *  @brief One of two subclasses of exception.
  class logic_error : public exception 
  class domain_error : public logic_error 
  class invalid_argument : public logic_error 
  class length_error : public logic_error 
  class out_of_range : public logic_error 
   *  @brief One of two subclasses of exception.
  class runtime_error : public exception 
  class range_error : public runtime_error 
  class overflow_error : public runtime_error 
  class underflow_error : public runtime_error 

天那,我未曾注意过标准库有那么多异常!那么如果在使用标准库时,不小心触发了什么异常,OMG!

这样看来,使用异常是很有必要的。但是,麻烦的问题又来了,一旦使用异常,函数的退出过程就变了。使用错误码有一个好处,就是你可以在函数返回前擦干净自己的屁股;但是使用异常呢?你既要保证对象能够自己擦屁股(RAII),还要保证函数能自己擦屁股(在正确的位置使用异常处理),这样才能在 stack unwinding 时不会导致内存泄露。哦,auto_ptr 可以帮上一些忙,但如果是分配的资源是数组呢?

还有一个麻烦是,你要遵从约定——特别是对于一个程序库作者来说。如果约定出错时抛出异常,那么可以抛;如果约定出错时返回错误码,或者这个库可能被 C 调用,那么抛出异常就可能是灾难。

现在看来,如果想实现更健壮的 C++ 程序,那么异常处理是不可或缺的。但在使用异常处理之前,必须得了解在哪里、怎样抛出和捕获异常,如果是团队合作,可能还需要有简单的操作指导手册,否则使用不当或者过量的异常也可能带来麻烦。

我还在路上!

编程杂感 20110313

唉,最近表达的欲望很小,这篇日志也仅仅是凑数而已。前一段时间周旋在几个项目之间,忙的没什么时间思考问题或者写字。从上周开始,退出了一个跨部门合作的项目,专心于自己的事情。其中的原因有很多,不好说也不可说。

这半年来,对我所负责的系统,我致力于的是消灭各种 bug,提高稳定性,添加新功能以及为系统的未来发展做一个较为长远的规划。这半年里的程序升级要么从非常微小之处着手,要么是新的模块,擅长的是以最小的代价修复一个具体问题,但不曾仔细思考过如何对一个模块的设计缺陷进行逐步的全面的修正。这种修正类似于重构,但不是推倒重来,而是慢慢地逐步演进。我现在感觉到,在这方面的能力或经验,我还是缺乏的。

这样看来,被分配去做一个已有系统的维护和升级也不是坏事。最近可自己支配的时间多了些,我就沉浸在代码的阅读里。常常在思考的问题是如果让我来,这部分代码的结构该如何设计,才能够提供最大的灵活性,例如易复用、易扩展。好的代码能让你赞叹,差的代码也让你嗟叹,对已有代码的反思和修正,未始不能让自己得到成长。在这个方向上,我和我们的项目都还有很多功课需要做。

在 shell 脚本里打日志

今天小弟在重构代码中的一个脚本模块,其中涉及到日志功能。上午花了点儿时间想出了个在 shell 打日志的技巧,觉得值得写一下。

希望要实现的效果是:实现一个 write_log 命令,给一条出错消息作为输入,write_log 记录日志时自动加上 时间戳、脚本文件名和行号。形如:

2010-12-17 19:13:44 [work.sh:24] FATAL: mkdir -p /x.

时间戳、脚本文件名都好获得,但是行号就没那么容易实现了。shell 中的 $LINENO 变量只能展开成当前行的行号,如果把 write_log 实现成函数的话,势必在函数中无法使用 $LINENO。

开始我想了好大一会儿,觉得 eval 能干这个事情。但是如果用 eval 的话,还不如直接把 $LINENO 传给 write_log 函数呢,与我的初衷不是太相符。我拉来同事讨论了一把,也没解决问题。正当我准备放弃了,计划每次传 $LINENO 参数时,忽然想起来,怎么把 alias 给忘了呢?

于是,write_log 的实现就是这个样子了:

function _write_log()
{
  if [ $# -eq 2 ]; then
    if [ -z $LOGFILE ]; then
      echo "$(date "+%Y-%m-%d %H:%M:%S") [$0:$1] $2"
    else
      echo "$(date "+%Y-%m-%d %H:%M:%S") [$0:$1] $2" >> $LOGFILE
    fi
  elif [ $# -eq 1 ]; then
    if [ -z $LOGFILE ]; then
      echo "$(date "+%Y-%m-%d %H:%M:%S") [$0] $1"
    else
      echo "$(date "+%Y-%m-%d %H:%M:%S") [$0] $1" >> $LOGFILE
    fi
  else
    return 1
  fi
}
alias write_log='_write_log $LINENO' # 这里必须使用单引号

存在的问题是:上面这段代码在 bash 里是不工作的,但是用 sh 可以——即使 sh 也是链接到 bash 的。问题出在 alias 上,可以把问题简化成这样,有一个脚本 a.sh:

$ cat a.sh
alias lss='ls -l'
lss /tmp

这个脚本用 /bin/sh 执行是这样的:

$ sh a.sh 
total 8
drwx------ 2 gdm gdm 4096 2010-12-17 19:34 orbit-gdm
drwx------ 2 gdm gdm 4096 2010-12-17 11:04 pulse-PKdhtXMmr18n

用 /bin/bash 执行是这样的:

$ bash a.sh 
a.sh: line 2: lss: command not found

把 bash 随便 link 成一个叫 sh 的链接文件,再执行是类似这样的:

$ ln -s /bin/bash ~/sh
$ ~/sh a.sh 
total 8
drwx------ 2 gdm gdm 4096 2010-12-17 19:34 orbit-gdm
drwx------ 2 gdm gdm 4096 2010-12-17 11:04 pulse-PKdhtXMmr18n

这个问题肯定是有原因的,我不愿意去翻 bash 源代码,也不知道哪里去找答案,所以我放弃了,直接在文件头加上

#!/bin/sh

如果哪位兄台知道这种“奇怪”现象的原因所在,请不吝赐教 :)

WordPress博客评论合并工具

上篇,这里共享我写的一个用来合并 WordPress 博客评论的小工具。该工具可以将两个镜像 WordPress 博客上对同一篇文章的评论合并起来。

下面先介绍合并的步骤:

1. 首先到这里下载我修改的 WordPress 导入插件,并按照安装一般 WordPress 插件的方式,安装并启用该插件。

2. 然后在 WP 管理后台选择“工具->导入->WordPress”,然后上传从镜像 WP 博客导出的 xml 文件。

3. 在下一步选择“Only Merge Comments” 很重要!!!

Wordpress博客评论合并工具

4. submit,稍等片刻即可。

其实我没有重新制造轮子,只是修改了一下 WordPress 默认的博客导入工具 WordPress Importer,给它加了点儿功能。只要选中“Only Merge Comments”,使用这个工具是很安全的,它只会将 xml 中与当前博客中存在的文章对应的评论添加上去,而不处理任何不存在的文章,也不会重复添加已有的评论,而且会过滤某些垃圾评论。用这个选项,你可以重复导入很多次 :)

可能的缺陷有:这个工具判断文章是否存在的唯一标准是文章标题,因此如果有多篇文章标题一样,可能会存在问题(未测试)。本人不保证它是充分测试的,因此在应用之前最好还是在本地的镜像测试后进行;如果没有进行测试,请一定在合并之前对博客进行备份

下面是我修改的 patch:

--- wordpress-importer/wordpress-importer.php    2010-06-02 00:38:23.000000000 +0800
+++ ../../www/blog/wp-content/plugins/wordpress-importer/wordpress-importer.php    2010-09-29 19:33:57.953790929 +0800
@@ -49,2 +49,3 @@
     var $fetch_attachments = false;
+    var $only_merge_comments = false;
     var $url_remap = array ();
@@ -258,2 +259,7 @@

+<h2><?php _e('Only Merge Comments', 'wordpress-importer'); ?></h2>
+<p>
+    <input type="checkbox" value="1" name="comments" id="merge-comments" />
+    <label for="merge-comments"><?php _e('Only merge comments, ignore post, tags...', 'wordpress-importer') ?></label>
+</p>
<?php
@@ -483,3 +489,7 @@

-        $post_exists = post_exists($post_title, '', $post_date);
+        if ($this->only_merge_comments) {
+            $post_exists = post_exists($post_title, '', '');
+        } else {
+            $post_exists = post_exists($post_title, '', $post_date);
+        }

@@ -489,4 +499,7 @@
             $comment_post_ID = $post_id = $post_exists;
-        } else {
-
+        } else if ( $this->only_merge_comments) {
+            echo '<li>';
+            printf(__('Post <em>%s</em> not found, comments not updated.', 'wordpress-importer'), stripslashes($post_title));
+            $comment_post_ID = $post_id = $post_exists;
+        } else {
             // If it has parent, process parent first.
@@ -605,3 +618,11 @@
                 // if this is a new post we can skip the comment_exists() check
-                if ( !$post_exists || !comment_exists($comment['comment_author'], $comment['comment_date']) ) {
+                if ($this->only_merge_comments) {
+                    if ( $post_exists && !comment_exists($comment['comment_author'], $comment['comment_date']) && $comment['comment_author'] != 'Unknown') {
+                        if (isset($inserted_comments[$comment['comment_parent']]))
+                            $comment['comment_parent'] = $inserted_comments[$comment['comment_parent']];
+                        $comment = wp_filter_comment($comment);
+                        $inserted_comments[$key] = wp_insert_comment($comment);
+                        $num_comments++;
+                    }
+                } else if ( !$post_exists || !comment_exists($comment['comment_author'], $comment['comment_date']) ) {
                     if (isset($inserted_comments[$comment['comment_parent']]))
@@ -847,5 +868,7 @@
         $this->get_entries();
-        $this->process_categories();
-        $this->process_tags();
-        $this->process_terms();
+        if ($this->only_merge_comments) {
+            $this->process_categories();
+            $this->process_tags();
+            $this->process_terms();
+        }
         $result = $this->process_posts();
@@ -891,2 +914,4 @@
                 $fetch_attachments = ! empty( $_POST['attachments'] );
+                $only_merge_comments = ! empty( $_POST['comments'] );
+                $this->only_merge_comments = (bool) $only_merge_comments;
                 $result = $this->import( $_GET['id'], $fetch_attachments);

支持多浏览器的网站变灰方法

这篇文章中给出了针对 IE 浏览器的使网站变灰的方法,具体做法是在 CSS 文件的开头添加这样一行:

html { filter:progid:DXImageTransform.Microsoft.BasicImage(grayscale=1); }

但是很遗憾这种方法并不能支持 Firefox 和 Chrome。这篇博客介绍一种支持各种浏览器的网站变灰方法,试验其支持 Firefox、Chrome 和 IE,据说可以支持 Opera(未测试),方法比较简单,就直接介绍步骤了。

1. 到这里下载 grayscale.js 文件到你网站的根目录(或者也可以不下载,直接引入该 js,未测试)。

2. 在网站的 footer 或者 header 等全局的文件中插入以下代码(注意,$() 和 .load handler需要 jquery.js 的支持,不使用 jquery 的同学可以自行搜索解决标签查找和 window onload 事件处理问题,例如这里这里):

<script type="text/javascript" src="/grayscale.js"></script>
<script type="text/javascript">
$(window).load(function () {
  grayscale( $('body') );
});
</script>

该方法的缺点是:

1. 页面加载完后才对整个页面进行变灰操作;
2. 在非 IE 浏览器中不支持来自其它域名的网站图片的变灰;
3. 造成非 IE 浏览器在加载完页面后进行大量 js 计算,该计算负担可以通过仅变灰 img, a 等标签而不是 body 来优化。

使用 Sikuli 实现同时登录两个 Dropbox 帐户

来自 MIT 的用图片编程的 Sikuli 语言最近着实火了一把,看着对岸的程序员 Vgod 开发出如此酷的软件着实令人羡慕。但除了 Demo 之外,能不能拿 Sikuli 来 engineer a better life 呢?显然是可以的,就如 Vgod 这篇文章所说,Sikuli 有无穷的潜力,那我们就来玩儿一把,展示一下 Sikuli 的一个现实应用。

1. Dropbox

Dropbox 是一个在线文件存储系统,可以用来存储和在不同电脑间共享文件,但是一个 Dropbox 用户只有 2G 的存储空间,当我们文件多的时候,就受到限制了。而一般情况下 Dropbox 只能运行一个例程,使用多个用户貌似不可行。但是到底可能吗?

当然可能,只是我们需要多个 Windows 帐户。也就是说,每个 Windows 帐户可以运行一个 Dropbox,如果你系统里有多个帐户,就可以运行多个 Dropbox。注意,受到安全策略的限制,这些帐户必须设置密码。比如我们新建一个"dropbox"帐户,密码也是"dropbox"。

2. 笨的方法

一般情况下使用其它帐户运行程序的方式为:在程序或者快捷方式上点右键,选择“运行方式”,然后选择“下列用户”,输入你期望的用户和密码(dropbox:dropbox)来执行该程序。

3. 聪明的方法

但是这样做太麻烦了,我们可以用批处理脚本做这件事情:

start D:\Program\Dropbox\Dropbox.exe
runas /user:dropbox D:\Program\Dropbox\Dropbox.exe

但这样还要手工输入密码,有很多种方法可以避免手工输入 runas 密码,但很遗憾它们大多在 Windows XP Home Edition 上不可用。

用 Home Edition 的同志还是得交互式的输入密码。能不能不手工输呢?可以,比如 expect 就是专门处理交互的语言。不过,学起来太麻烦了吧,要不来看看 Sikuli 怎么做?

4. 使用 Sikuli

下面这个图就是完成启动两个 Dropbox 的 Sikuli 程序:

使用 Sikuli 同时启动两个 Dropbox

首先switchApp("cmd")启动 Windows 的命令行,然后wait等待那个提示符出现,然后 type() 键入一行 runas 命令,wait 等待提示输入密码,type 输入密码 dropbox 加回车 \n,bingo,出来一个 dropbox 了,最后再 type 一行启动非 runas 的 dropbox,又出来一个 dropbox。

上述程序运行结果如下图所示:

两个 Dropbox 在运行

好玩吧!Sikuli 程序就是那么简单,我从下载 Sikuli 到完成这个程序大约花了四十分钟的时间,这可比去学 expect 快多了。这下 expact 之类的交互语言在简单的场景下可以无视了。

你可以将 Sikuli 程序导出成一个 .skl 文件,据说可以双击运行,不过我尝试未成功,这是一个遗憾,希望后续版本可以解决这个问题。

5. 注册 Dropbox

您如果对 Dropbox 感兴趣的话,可以点击下面我的两个邀请链接注册,这样咱们的空间都可以增加 250M。本人将非常感谢您的支持。(如果您打算再注册一个的话,最好不要用自己的邀请链接,因为同一台电脑上激活的用户不会奖励空间。)

https://www.dropbox.com/referrals/NTE2NjMyMTU5

RSS Feed 迁移方法

由于政策的调整,目前很多博主都将博客域名从 .cn 迁出,相信很多朋友都会遇到 RSS Feed 迁移的问题。如果一直使用 Feedburner/Feedsky 这种第三方烧录网站管理订阅,只需要更换第三方抓取的源即可;但是如果之前订户多用 WordPress 原始的源 example.cn/feed/、example.cn/?feed=rss2,或者使用自定的域名 feed.example.cn 的话,当域名迁移时,原来的 example.cn 被弃用后,订户就无法得到文章更新了。

我之前一直使用 feed.solrex.cn 作为 Feedsky 的自定义域名,因为我觉得 solrex.cn 可能比 feedsky.com 更长久,后来发现这是非常愚蠢的想法。当我把域名迁移到 .org 时,就面临 feed 迁移的问题。

最简单的方法是将原来的 feed url 重定向到 Feedburner/Feedsky,但这要求网站主必须仍然控制原来域名,那就没有更换域名的必要了。

或者使用一篇博客来通知订户更换 feed url,但是实践证明这种方法收效甚微。很多人(包括我)不会去看自己使用的是什么源,认为自己使用的就是正确的 feed url。

起初我是使用的直接重定向,但后来一封域名注册商的邮件,威胁如果不办理某些手续,24日之后会停止我的 .cn 域名解析。我想,还是用一些略显卑劣的手段通知大家更换订阅源吧。这种卑劣的方法是:如果使用原来的源订阅本站,就会看到每天一篇的“网站迁移通知”,直到用户更改订阅源,或者无法忍受直接删除 feed。

其技术实现方法是:使用 php 模仿 WP 的 rss 源生成一个 xml 文件,该文件只包含一篇文章,将原来的源指向它(或者 url 重定向到它)。该 xml 中的更新日期、文章 url 每天更新一次,这样阅读器就会认为博客有更新,把这篇文章抓取回去。我本以为阅读器是根据更新日期判断文章是否重复,后来发现是根据文章 url 来判断。为减少工作量,我们可以将文章的 url 指向某篇目标文章,然后在 url 后面加上 “?date=***”,这样阅读器就不会认为是同一篇文章,而且读者仍然能够点入目标文章。

方法很简单,如果您比较懒的话,可以参考我使用的文件(也可以从这里直接下载 php 源代码):

<?php echo '<?xml version="1.0" encoding="UTF-8"?>'."\n"; ?>
<?php echo '<?xml-stylesheet type="text/xsl" media="screen" href="http://feeds.feedburner.com/~d/styles/rss2chinesetwfull.xsl"?><?xml-stylesheet type="text/css" media="screen" href="http://feeds.feedburner.com/~d/styles/itemcontent.css"?>'; ?><rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:creativeCommons="http://backend.userland.com/creativeCommonsRssModule" version="2.0">

<channel>
    <title>Solrex Shuffling</title>
   
    <link>http://blog.solrex.org</link>
    <description>Engineering a better life, programming a great future.</description>
    <pubDate><?php echo date('D, d M Y ', strtotime("+7 hour")); echo '00:00:00 GMT'; ?></pubDate>
    <generator>http://wordpress.org/?v=2.7.1</generator>

    <language>en</language>
    <sy:updatePeriod>hourly</sy:updatePeriod>
    <sy:updateFrequency>1</sy:updateFrequency>
        <item>
        <title>站点迁移通知-<?php echo date('d M Y', strtotime("+7 hour")); ?></title>
        <link>http://blog.solrex.org/?p=638679&amp;q=<?php echo date('Ymd', strtotime("+7 hour")); ?></link>
        <comments>http://blog.solrex.org/?p=638679&amp;q=<?php echo date('Ymd', strtotime("+7 hour")); ?>#comments</comments>
        <pubDate><?php echo date('D, d M Y ', strtotime("+7 hour")); echo '00:00:00 GMT'; ?></pubDate>
        <dc:creator>Solrex Yang</dc:creator>
       
        <guid isPermaLink="false">http://blog.solrex.org/?p=638679&amp;q=<?php echo date('Ymd', strtotime("+7 hour")); ?></guid>
        <description><![CDATA[您好,您之所以看到这篇文章是因为您仍在使用被遗弃的 feed 地址 http://feed.solrex.cn 订阅我的博客Solrex Shuffling。我已经将网站从 http://blog.solrex.cn 迁移到了 http://blog.solrex.org。由于 .cn 域名潜在被删除的危险,为了不丢失和您交流的渠道,我不得不出此下策以每天一篇博客的方式提醒您更新 feed 地址,希望您能谅解!...
]]></description>
            <content:encoded><![CDATA[<p>您好,您之所以看到这篇文章是因为您仍在使用被遗弃的 feed 地址 http://feed.solrex.cn 订阅我的博客<a href="">Solrex Shuffling</a>。我已经将网站从 <a href="">http://blog.solrex.cn</a> 迁移到了 <a href="">http://blog.solrex.org</a>。由于 .cn 域名潜在被删除的危险,为了不丢失和您交流的渠道,我不得不出此下策以每天一篇博客的方式提醒您更新 feed 地址,希望您能谅解!</p>
<p>如果您觉得<a href="">本站</a>对您还有点儿用处,可以使用以下方式继续订阅:</p>
<ul>
<li><p>如果您使用离线阅读器,请将本站的 feed 地址 <a href="http://feeds.feedburner.com/solrex">http://feeds.feedburner.com/solrex</a> 或者 <a href="http://feed.feedsky.com/solrex">http://feed.feedsky.com/solrex</a> 添加到您的订阅器中,并删除现有这个 feed。</p></li>
<li><p>如果您使用在线阅读器,比如 Google Reader、抓虾 之类,您可以点击<a href="">这里</a>到本站首页,在右侧选择您的在线阅读器,重新订阅,并将现在这个 feed 删除。</p></li>
</ul>
<p>如果您觉得<a href="">本站</a>对您不再有用,可以使用以下方式退订:</p>
<ul>
<li><p>如果您使用离线阅读器,请咨询阅读器帮助如何删除 feed,一般情况下在 feed 上直接点 del 键即可。</p></li>
<li><p>Google Reader 用户可以在左侧 Subscriptions 中找到本 feed(一般名为 Solrex Shuffling),将鼠标移动至其上,您会发现右侧有一个向下的小箭头,点击箭头,您就会发现有 Unsubscribe 的选项;或者您也可以到右上角的 Setting 中,点入 Subscriptions 标签页,对所有 feed 进行管理时删除 Solrex Shuffling 这个 feed。您可以在<a href="http://www.google.com/support/reader/bin/answer.py?hl=zh_CN&answer=73062">这个页面</a>找到更多帮助。</p></li>
<li><p>抓虾用户可以在<a href="http://zhuaxia.com/help.php#3_3">这个页面</a>找到退订的帮助。</p></li>
<li><p>其它在线阅读器用户请咨询该网站帮助。</p></li>
</ul>
<p>无论如何,感谢您一直以来对本站的支持,我希望能在<a href="">新的站点</a>继续收到您的批评或支持!祝您好运!</p>
<p>Solrex Yang</p>
<p><?php echo date('D, d M Y ', strtotime("+7 hour")); ?></p>
]]></content:encoded>
            <wfw:commentRss>http://blog.solrex.org/?p=638679&amp;q=<?php echo date('Ymd', strtotime("+7 hour")); ?>/feed/ ?></wfw:commentRss>
        </item>
</channel>
</rss>

您可以到 feed.solrex.cn 查看效果。

Cygwin GCC qsort 函数错误(续)

上一篇文章中提到我在为 qsort 写 compare 函数时犯了一个愚蠢的错误:我脑袋陷入了一个错误的逻辑,以为 compare 函数嘛,就是要 compare 一下,那么我用 '>' 或者 '< ' 这种比较算符就可以满足要求(潜意识里认为 > 会返回 1 或者 -1,显然是错的,上篇文章的评论者 Stephen 开始也犯了同样的直觉错误,不过他马上就醒悟过来了)。我当时脑袋里也犹豫了一下要不要处理相等的情况,后来想快排算法中没有判断相等的情况,那么我没必要加上等号。

这个错误直接导致了快排算法失效。

但是为什么在 Linux 下的 gcc 可以输出正确的排序结果呢?我想了很久,最终还是把 glibc 的代码看了一下,才发现,原来当数组规模比较小时时(数组大小小于物理内存的四分之一),glibc 的 qsort 其实不使用 quick sort(_quicksort),而是使用 merge sort(msort_with_tmp)。而且在 msort_with_tmp 中,对 compare 的处理是比较其返回值是否 <=0,这样排序的结果就是正确的了。[1]

事实上最简单的快排算法是只使用 '<' 号或者 '<='的,比如 Wikipedia 上给出的快排算法,那么我们的 compare 只返回 -1 和 0 行吗?这取决于实现,比如对快排算法的优化中有一个就是对数组中有大量相等元素情况下的优化,其中一种实现 Three-way partition, 就需要使用到三种情况:大于、小于或等于。原始的快排 partition 是将数组按照与 pivot 的比较分为两段,Three-way partition 则是将数组分为三段,中间增加一段与 pivot 值相等的子数组。C 玩具代码的实现如下:

void qsort_3way(int a[], int lo, int hi)
{
  if (hi <= lo) return;
  int lt = lo, gt = hi, i = lt;
  int v = a[lo], t;
  while (i <= gt) {
    if (a[i] < v) {
      t = a[i]; a[i] = a[lt]; a[lt] = t;
      ++i; ++lt;
    } else if (a[i] > v) {
      t = a[i]; a[i] = a[gt]; a[gt] = t;
      --gt;  
    } else i++;
  }
  qsort_3way(a, lo, lt - 1);
  qsort_3way(a, gt + 1, hi);
}

但是 '<' 和 '>' 真的都需要吗?理论上来讲,'>' 是不需要的,我们显然可以将 a[i] > v 改成 v < a[i]。这也是 C++ 里面做的,C++ 中的 sort 函数只需要类重载 '< ' 运算符。但是 C 中并没有这种约定,我们不能预设 qsort 如何拿 compare() 的返回值与 0 比较。因此让 compare() 按照 C 的约定,返回大于、小于和等于 0 的三种情况是绝对正确的而且必要的。

我了解了正确的结果怎么得来的,但是我仍然不知道错误的结果是怎么得来的。看起来 Cygwin 使用的 libc 中没有采取类似 Linux 下 gcc 的策略(比如无法取到物理内存大小?)。quick sort 算法有很多优化的技巧和实现:有的使用 '< ' 符号比较,有的在分支数组足够小时采用插入排序,有的同时使用 '<', '> 两个符号,有的随机取 pivot,有的取三点中值作为 pivot。[2] 没有看到代码和调试,很难判断 Cygwin 的 libc 使用了什么算法(当然,尝试分析不同的输入输出是可以得到规律的,比密码分析还是要简单一些)。

[1] glibc/stdlib/msort.c.
[2] Jon Bentley and M. Douglas McIlroy, "Engineering a sort function", Software - Practice and Experience, Vol. 23 (11), 1249-1265, 1993.

Cygwin GCC qsort 函数错误

我平时在 Windows 下写代码时,经常使用 Cygwin 的 gcc。但是今天我居然发现 Cygwin 下 gcc 的 qsort 函数是错误的!这种基本的函数出错,太让人惊讶了。为了验证是不是代码有错,我使用 tcc 和 Linux 下的 gcc 都编译了同样一段程序,它们两个都输出了期望的结果,只有 Cygwin 的 gcc 是错的。下面是示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int compare(const void *p, const void *q)
{
  return *(const char *)p > *(const char *)q;
}

int main()
{
  char a[] = "1312515";
  printf("%sn", a);
  qsort(a, strlen(a), sizeof(char), compare);
  printf("%sn", a);
  return 0;
}

按说它应该输出:

1312515
1112355

但是我用 Cygwin gcc 编译后,它居然运行出这样的结果:

1312515
2111355

太诡异了。我尝试调试它,结果 gdb 无法步入 qsort 代码中。谁能告诉我是为什么?

附 Cygwin gcc 信息:

$ gcc -v
Using built-in specs.
Target: i686-pc-cygwin
Configured with: /gnu/gcc/package/gcc4-4.3.2-2/src/gcc-4.3.2/configure --srcdir=/gnu/gcc/package/gcc4-4.3.2-2/src/gcc-4.3.2 --prefix=/usr --exec-prefix=/usr --bindir=/usr/bin --sbindir=/usr/sbin --libexecdir=/usr/sbin --datadir=/usr/share --localstatedir=/var --sysconfdir=/etc --infodir=/usr/share/info --mandir=/usr/share/man --datadir=/usr/share --infodir=/usr/share/info --mandir=/usr/share/man -v --with-gmp=/usr --with-mpfr=/usr --enable-bootstrap --enable-version-specific-runtime-libs --with-slibdir=/usr/bin --libexecdir=/usr/lib --enable-static --enable-shared --enable-shared-libgcc --enable-__cxa_atexit --with-gnu-ld --with-gnu-as --with-dwarf2 --disable-sjlj-exceptions --enable-languages=ada,c,c++,fortran,java,objc,obj-c++ --disable-symvers --enable-libjava --program-suffix=-4 --enable-libgomp --enable-libssp --enable-libada --enable-threads=posix AS=/opt/gcc-tools/bin/as.exe AS_FOR_TARGET=/opt/gcc-tools/bin/as.exe LD=/opt/gcc-tools/bin/ld.exe LD_FOR_TARGET=/opt/gcc-tools/bin/ld.exe
Thread model: posix
gcc version 4.3.2 20080827 (beta) 2 (GCC)

我犯了一个愚蠢的错误,感谢来自 Stephen 的评论

你的compare函数有问题,你的compare函数不会返回负数。修改compare为:
int compare(const void *p, const void *q)
{
return *(const char *)p - *(const char *)q;
}
再编译运行就正确了。

字符串参数的模板函数推导问题(续)

前面一篇文章我们讨论了字符串作为参数的模板函数推导问题,下面我们看一下使用不同字符串参数类型对模板函数实例化的影响。代码如下,在语句后面的注释为该句的输出。该输出是 g++ 编译后产生的输出,主要是因为输出简洁,而且我们这里只关心模板函数的不同实例,并不关心 const 类型。

#include <iostream>
#include <typeinfo>
#include <vector>
#include <string>
using namespace std;

template<typename T>
void foo(const T& t)
{
  cout << "foo: generic(" << t << ") " << typeid(t).name() << endl;
}

template<typename T>
void bar(const T t)
{
  cout << "bar: generic(" << t << ") " << typeid(t).name() << endl;
}

/*
$ c++filt [-t] A1_c A2_c A3_c Ss PKc
char [1]
char [2]
char [3]
std::basic_string<char, std::char_traits<char>, std::allocator<char> >
char const*
*/
int main()
{
  foo("");                              // foo: generic() A1_c
  foo("0");                             // foo: generic(0) A2_c
  foo("01");                            // foo: generic(01) A3_c
  foo(static_cast<string>(""));         // foo: generic() Ss
  foo(static_cast<string>("0"));        // foo: generic(0) Ss
  foo(static_cast<string>("01"));       // foo: generic(01) Ss
  foo(static_cast<const char *>(""));   // foo: generic() PKc
  foo(static_cast<const char *>("0"));  // foo: generic(0) PKc
  foo(static_cast<const char *>("01")); // foo: generic(01) PKc
  foo(*(new string("")));               // foo: generic() Ss
  foo(*(new string("0")));              // foo: generic(0) Ss
  foo(*(new string("01")));             // foo: generic(01) Ss
  bar("");                              // foo: generic() PKc
  bar("0");                             // foo: generic(0) PKc
  bar("01");                            // foo: generic(01) PKc
  bar(static_cast<string>(""));         // foo: generic() Ss
  bar(static_cast<string>("0"));        // foo: generic(0) Ss
  bar(static_cast<string>("01"));       // foo: generic(01) Ss
  bar(static_cast<const char *>(""));   // foo: generic() PKc
  bar(static_cast<const char *>("0"));  // foo: generic(0) PKc
  bar(static_cast<const char *>("01")); // foo: generic(01) PKc
  bar(*(new string("")));               // foo: generic() Ss
  bar(*(new string("0")));              // foo: generic(0) Ss
  bar(*(new string("01")));             // foo: generic(01) Ss
  return 0;
}

基于前一篇博客的分析,我们知道形如 "hello" 的常量字符串在编译时的类型是 char 数组。不同长度的 char 数组,其类型是不一样的,我们可以使用下面语句:

cout << (typeid(char [1]) == typeid(char [2])) << endl;

来验证这一想法。因此,如果我们使用不同长度的字符串作为参数调用 foo,编译器就会为模板函数 foo 实例化不同的实例函数,这一点已经由 foo 的前三个输出验证。我们还可以通过 readelf 来读取目标文件符号表,或者 objdump 查看目标文件反汇编代码中 foo 的实例函数的数量。

$ readelf -s test.o | c++filt -t | less
$ objdump -S test.o | c++filt -t | less

这也就是说,我们使用原始字符串调用了三次 foo,其实是三个不同的实例函数,这样显然会导致目标代码臃肿。那么怎么避免这种情况出现呢?下面我们使用了三种不同的方法,将字符串 static_cast 成 string 或者 const char * 类型,或者使用字符串构造一个 string 对象作为参数,这三种情况都能保证不同(内容)字符串参数的调用使用的是同一个实例化的模板函数。

有没有方法避免类型转换呢?我们可以使用非引用参数类型作为模板函数的模板参数,如 bar 模板函数所示。如前一篇中的分析,此时 char 数组类型会被隐式转换成 char 指针类型,然后进行模板函数推导。所以我们看到即使传的是原始字符串参数,其调用的实例化函数仍然是 char const * 类型的。由于这里类型 T 被推导为 char const * 类型,所以传递的仍然是指针。

但是下面的 string 类型的实例化模板函数实现的就是值传递了,这在函数运行效率上可能会有一些影响。不过现代的函数库对 string 都实现为 copy-on-write(例如 MFC 的 CString 和 Qt 的 QString),我想 STL 的 string 应该也不例外,而 const T 参数并不允许对参数修改,所以效率上的影响应该还是比较小的。只是在语义上与传一个指针就有不同了,假如不限定 T 是 const,那么值传递 string 时,对 string 的修改就无法反映到原来 string 上了。

最后,到底哪个方法好呢?我不知道,我没有足够的实践经验来评论哪种方法更好。我这两篇文章的目的仅仅是探讨一下使用不同形式字符串作为模板函数参数时可能发生的奇怪现象,以及要注意的方面,至于哪种方法更好,可能要留待实际需求来决定。

附:第一段代码的 VS 2008 编译器编译结果执行的输出:

foo: generic() char const [1]
foo: generic(0) char const [2]
foo: generic(01) char const [3]
foo: generic() class std::basic_string,class std::allocator >
foo: generic(0) class std::basic_string
,class std::allocator >
foo: generic(01) class std::basic_string
,class std::allocator >
foo: generic() char const *
foo: generic(0) char const *
foo: generic(01) char const *
foo: generic() class std::basic_string
,class std::allocator >
foo: generic(0) class std::basic_string
,class std::allocator >
foo: generic(01) class std::basic_string
,class std::allocator >
bar: generic () char const *
bar: generic (0) char const *
bar: generic (01) char const *
bar: generic () class std::basic_string
,class std::allocator >
bar: generic (0) class std::basic_string
,class std::allocator >
bar: generic (01) class std::basic_string
,class std::allocator >
bar: generic () char const *
bar: generic (0) char const *
bar: generic (01) char const *
bar: generic () class std::basic_string
,class std::allocator >
bar: generic (0) class std::basic_string
,class std::allocator >
bar: generic (01) class std::basic_string
,class std::allocator >

字符串参数的模板函数推导问题

国庆长假期间又翻了翻 《C++ Primer》,看到模板函数特化,就想起来以前遇到的一个问题。这个问题我曾经在 TopLanguage 讨论组请教过(链接),今天翻出来又仔细想了想,做一个总结吧。

困惑起源于以字符串作为参数,如何匹配到特化的模板函数。代码如下,其中注释部分是该句对应的输出(使用 VS2008 编译器,一会儿再讨论 g++ 的问题):

#include <iostream>
#include <typeinfo>
using namespace std;

template<typename T>
void foo(const T& t)
{
  cout << "foo: generic " << typeid(t).name() << endl;
}

template<>
void foo<const char *>(const char * const& t)
{
  cout << "foo: special " << typeid(t).name() << endl;
}

template<typename T>
void bar(const T t)
{
  cout << "bar: generic " << typeid(t).name() << endl;
}

template<>
void bar<const char *>(const char * t)
{
  cout << "bar: special " << typeid(t).name() << endl;
}

int main()
{
  char str[] = "hello";
  const char con_str[] = "hello";
  const char * const p = "hello";
  foo("hello");                                  // foo: generic char const [6]
  foo(static_cast<const char * const>("hello")); // foo: special char const *
  foo(static_cast<const char *>("hello"));       // foo: special char const *
  foo(str);                                      // foo: generic char const [6]
  foo(con_str);                                  // foo: generic char const [6]
  foo(p);                                        // foo: special char const *
  bar("hello");                                  // bar: special char const *
  bar(str);                                      // bar: generic char *
  bar(con_str);                                  // bar: special char const *
  bar(p);                                        // bar: special char const *
  cout << typeid("hello").name() << endl;        // char const [6]
  cout << typeid(str).name() << endl;            // char [6]
  cout << typeid(con_str).name() << endl;        // char const [6]
  cout << typeid(p).name() << endl;              // char const *
  return 0;
}

首先让我奇怪的问题是,第一个 foo 函数调用 foo("hello"),为什么实际调用的不是特化的 foo 函数?

其实这个例子是有起源的,《C++ Primer》第四版 Section 16.6.1 的最后给出这样一个例子:

// define the general compare template
template <class T>
int compare(const T& t1, const T& t2) { /* ... */ }

int main() {
    // uses the generic template definition
    int i = compare("hello", "world");
    // ...
}

// invalid program: explicit specialization after call
template<>
int compare<const char*>(const char* const& s1,
                         const char* const& s2)
{ /* ... */ }

并解释说:

This program is in error because a call that would match the specialization is made before the specialization is declared. When the compiler sees a call, it must know to expect a specialization for this version. Otherwise, the compiler is allowed to instantiate the function from the template definition.

那么我认为作者暗含的意思里有,compare("hello", "world") 这个函数调用是 match 特化的 compare 函数的。但是从我们给出的第一段代码输出来看,并不是这个样子的,所以我谨慎地怀疑,《C++ Primer》给出的这个例子是有错的。虽然这段程序的确有错,但是即使将特化函数提到前面,compare("hello", "world") 仍然不会调用该特化函数。

请教了别人、书本和标准之后,下面我试着对上面每句的输出做一下解释(当然,可能有错,请指正):

1.   foo("hello");                                  // foo: generic char const [6]

"hello"具有类型 char const [6],由于 foo 模板使用的是引用参数,因此数组实参不会被转换成指针,而是追求一个较为精确的匹配,因此编译器实例化一个 void foo<char const [6]>(const char (& t)[6]) 模板函数(VS2008),这也是为什么我们能看到参数的类型输出是 char const [6];

2.   foo(static_cast<const char * const>("hello")); // foo: special char const *

"hello"被 cast 成了 const char * const 类型,自然与特化的函数 void foo<const char *>(const char * const& t) 能够精确匹配,因此调用的是特化的 foo;

3.   foo(static_cast<const char *>("hello"));       // foo: special char const *

"hello"被 cast 成了 const char * 类型,虽然少了一个 const,但是 C++ 标准中有这样的说法:

14.8.2.3
If the orignial A is a reference type, A can be more cv-qualified than the deduced A

这种 cv-qualifier 并不影响推导,最终仍然是匹配到特化的 foo 函数;

4.   foo(str);                                      // foo: generic char const [6]

str 和 "hello" 也是仅仅相差一个 cv-qualifier,也不影响推导,其结果与 1 是一致的;

5.   foo(con_str);                                  // foo: generic char const [6]

con_str 和 "hello" 的类型一样,显然其结果与 1 应是一致的;

6.   foo(p);                                        // foo: special char const *

p 的类型其实就是 2 中参数被 cast 之后的类型,显然其结果应该与 2 一致;

7.   bar("hello");                                  // bar: special char const *

乍一看就有些奇怪,为什么把模板参数换成值(而不是引用),特化的情况就与 foo 不同了呢?C++ 标准中有这样的规定:

14.8.2.3
If A is not a reference type:
-- If P is an array type, the pointer type produced by the array-to-pointer standard conversion (4.2) is used in place of P for type deduction;

因此,这里 "hello" 原本是一个数组类型,由于模板的参数不是引用类型,所以 "hello" 的类型被转换为指针类型 char const * 参加推导,正好与特化的 bar 函数匹配;

8.   bar(str);                                      // bar: generic char *

由于模板参数不是引用类型,没有 const 限定的 str 无法匹配特化的 bar,因此编译器实例化一个 void bar<char *>(char * t) 模板函数;

9.   bar(con_str);                                  // bar: special char const *

由于 con_str 与 "hello" 的类型一样,因此其结果与 7 是一致的;

10.   bar(p);                                        // bar: special char const *

这里 p 的类型本身就是特化函数的参数类型,显然要被推导为调用特化函数。

解释完了字符串参数的模板函数推导问题,下面来讨论一下 g++ 和 VS2008 的不同。上面同样的代码,使用 g++ 编译之后,输出是这个样子的:

foo: generic A6_c
foo: special PKc
foo: special PKc
foo: generic A6_c
foo: generic A6_c
foo: special PKc
bar: special PKc
bar: generic Pc
bar: special PKc
bar: special PKc
A6_c
A6_c
A6_c
PKc

当然,需要解释的是 g++ 内部对符号的字面做了一些变化,我们可以使用 c++filt demangle 这些符号:

$ c++filt [-t] A6_c PKc Pc
char [6]
char const *
char *

与 VS2008 的输出相比,我有一个疑问,为什么 g++ 没有为 const char [6] 输出正确的 const 类型名呢?

还有,我们提到了第 1 种情况下,编译器为 foo("hello") 调用实例化了一个 void foo<char const [6]>(const char (& t)[6]) 类型的函数。假如我们提供了一个类似的特化函数,那么 foo("hello") 会调用该特化函数;但是,使用 g++ 编译器时,特化函数的类型必须是 void foo<char [6]>(const char (& t)[6]) 而不是 void foo<char const [6]>(const char (& t)[6]),这让我感觉非常奇怪。只有不提供模板参数时,比如 void foo(const char (& t)[6]),两个编译器才能都推导出调用特化函数。

需要验证的话,您可以尝试在第一段代码中增加下面两个特化函数,再在两个编译器上编译那段代码:

template<>
void foo<char [6]>(const char (& t)[6])
{
  cout << "foo: special<char [6]> " << typeid(t).name() << endl;
}

template<>
void foo<char const [6]>(const char (& t)[6])
{
  cout << "foo: special<char const [6]> " << typeid(t).name() << endl;
}

将文本文件读入数组-C语言实现

要求:使用 C 语言将文本文件的每一行读入为数组的一个元素,返回一个 char ** 指针。

由于行长度和文本文件行数均未知,相当于二维 char 数组的两维长度都未定义。由于 getline 函数可以自动扩充 char 数组长度,我最初的想法是使用 getline 得到每行,然后每次对 char ** 进行 realloc,直到读完整个文件。

但是这种做法并不好,首先 getline 是 glibc 的扩展,而不是 C 语言的标准函数,使用除 gcc 以外的编译器是不一定能编译通过的;其次,每次对 char ** 指针进行 realloc 显得代码很 ugly。可以使用 fgets 替代 getline,但是就要自己来控制一维 char 数组的长度。

后来想想,换了一种思路,首先将整个文件读入内存,然后根据 '\n' 的个数来计算文件的行数,作为二维数组的长度,然后将所有的 '\n' 替换成 '\0',并将每一行的指针赋给二维 char 数组,代码如下:

char ** text_2_array(const char *filename)
{
  char *p, **array;
  int lines;
  if(filename == NULL) return NULL;

  FILE *fp = fopen(filename, "r");
  if(fp == NULL) return NULL;

  /* Get file size. */
  fseek(fp, 0L, SEEK_END);
  long int f_size = ftell(fp);
  fseek(fp, 0L, SEEK_SET);

  /* Allocate space for file content. */
  char *buf = (char *) calloc(f_size, sizeof(char));
  if(buf == NULL) return NULL;

  fread(buf, sizeof(char), f_size, fp);
  fclose(fp);

  /* Get number of lines. */
  for(p=strchr(buf, '\n'), lines=1; p!=NULL; p=strchr(p, '\n'), lines++) {
    if(*p == '\n') p++;
  }

  /* Allocate space for array; split file buffer to lines by change '\n' to
     '\0'. */
  array = (char **) calloc(lines+1, sizeof(char*));
  array[0] = buf;
  for(p=strchr(buf, '\n'), lines=1; p!=NULL; p=strchr(p, '\n')) {
    if(*p == '\n') *p++ = '\0';
    if(p != NULL) array[lines++] = p;
  }
  /* Add a terminate NULL pointer. */
  array[lines] = NULL;
  return array;
}

其实读文本文件入数组这个功能在很多语言中是很简单的操作,比如 PHP 的 file 函数,或者 Bash 的 (`cat filename`),都可以直接实现这个功能。但是对 C 这种更低级的语言来说,貌似就没那么简单了。我想要了解的是,除了我上面提到的两种思路,有没有更简单或者直接的方法来解决这个问题?比如一些我不熟悉的函数,或者一些 trick。

SVN 技巧:GUI 版本比较和可执行属性

我曾经在《使用 kdiff3 进行 svn 版本比较》中介绍了为什么以及如何使用 kdiff3 或者 meld 等 GUI 比较工具进行 SVN 版本比较。但这样做有个小问题,就是如果设置了 GUI 工具作为比较工具,那么就没办法输出 diff 文件,而且每次都要关掉窗口才会出现下一个文件,就无法比较多个文件了。所以我觉得下面这种做法会更好一些:

$ more svndiff
#!/bin/bash
sed -i -e 's/^# diff-cmd.*$/diff-cmd=meld/' ~/.subversion/config
svn diff
sed -i -e 's/^diff-cmd.*$/# diff-cmd = meld/' ~/.subversion/config

其实就是用一个脚本 svndiff 来做 GUI 比较的工作。svndiff 执行时首先将 svn 配置文件中的比较工具改为 meld,然后进行比较,比较完后再将修改注释掉,这样就不会影响正常 svn diff 的功能。这样一来,svndiff 是 GUI diff,svn diff 就是命令行 diff。

设置文件可执行属性对 Windows 用户来说可能没什么用,可是对 Linux 用户来说用处就大了。没人希望每次一 update,就要重新对需要执行的脚本 chmod 一下。svn 修改文件可执行属性的命令太长了,我老记不住,所以放在这里做个笔记吧:

svn propset svn:executable ON filename

豆瓣好友统计图标

自从 Feedburner 订阅数统计图标成为博客装逼工具之后,各种各样的统计图标层出不穷,比如我也使用的 Twitter Counter。但是我一直没发现我认为很有装逼范儿的豆瓣提供好友数统计图标,因此我就使用豆瓣的 API 自己搞了一个。其实我主要是觉得在博客侧栏放豆瓣图书列表太多了,而一个小“豆”字也没啥意思,搞个好友数图标就挺好玩了。

我想没准你也会感兴趣,所以我把这个服务发布了出来。豆瓣好友统计图标的主页在:

http://solrex.org/douyou/

下面是直接从主页拷贝过来的内容,您也可以到我的博客侧栏看看效果。

介绍

呃——我觉得写主页比写主程序还费劲。简单来说,这个东西就跟 Feedburner 的订阅数统计图标类似,利用豆瓣提供的 API 抓取你的豆瓣好友数量,并做成一个小图片出来让你可以放在自己的博客上秀一秀。比如下面就是我的豆瓣好友统计图标:

豆瓣

你还可以移步到我的博客右侧栏,看看豆瓣好友统计图标和其它统计图标共存的状况。本统计图标一天更新一次,因此统计数并不完全实时,这是为了减轻服务器负载,请理解。

这个小项目完全是出于兴趣写成的,因此很简陋且维持在可用的水平上,我也没有更优化它的想法。在我服务器能承受的情况下我会尽量维持它,但本人不对服务的有效性和可用性做出任何承诺。

我觉得这个服务本身应该由豆瓣提供,如果你是豆瓣的工作人员,觉得这个站点有趣并想在豆瓣中加入此服务的话,欢迎你和我联系,我将无偿提供所有的代码,仅仅希望在对应产品中加上一个 Thanks to 到我的链接。

生成图片

输入豆瓣 UID:
(豆瓣用户 UID,英文或数字,非登录 email 地址)

(若没有即刻显示请稍等后多提交一次,服务器抓取信息可能有延迟。不知道自己的 UID 的话,可以登录豆瓣,查看自己的设置->username项。)

豆瓣

您可以把下面这段代码嵌入到您的博客或者主页中来显示豆瓣好友统计:

<a href="http://www.douban.com/people/solrex" title="豆瓣好友统计"><img src="http://solrex.org/douyou/dc/solrex" style="border: 0pt none ;" alt="豆瓣" height="26" width="88"></a>

JPerf Single Jar with UDP BW Unit Fixed

JPerf is the GUI frond-end of IPerf, a TCP and UDP bandwidth performance measurement tool which allows the tuning of various parameters and UDP characteristics.

The official JPerf release (2.0.2 version) has some flaws. First, it mistakenly uses bytes/sec as the unit of UDP bandwidth, which should be bits/sec according to IPerf man-page:

-b, --bandwidth #[KM]
       for  UDP,  bandwidth  to  send  at  in  bits/sec (default 1 Mbit/sec,
       implies -u)

Second, starting it from command line is error prone. The command to start it (jperf.sh) is:

java -classpath jperf.jar:lib/forms-1.1.0.jar:lib/jcommon-1.0.10.jar:lib/jfreechart-1.0.6.jar:lib/swingx-0.9.6.jar net.nlanr.jperf.JPerf

We can see that all jar paths in classpath are relative paths. So if we create a symbol link to the jperf.sh script, e.g. /usr/bin/jperf -> /opt/jperf-2.0.2/jperf.sh. Then calling /usr/bin/jperf will result in some errors like:

Exception in thread "main" java.lang.NoClassDefFoundError: net/nlanr/jperf/JPerf
Caused by: java.lang.ClassNotFoundException: net.nlanr.jperf.JPerf
at java.net.URLClassLoader$1.run(URLClassLoader.java:200)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
at java.lang.ClassLoader.loadClass(ClassLoader.java:307)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
at java.lang.ClassLoader.loadClass(ClassLoader.java:252)
at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:320)
Could not find the main class: net.nlanr.jperf.JPerf. Program will exit.

This error can be fixed by resolving the real path of the symbol link, as I reported.

However, a better way to solve this problem is to pack all libs JPerf needed(i.e. forms*.jar, jcommon*.jar, jfreechart*.jar, swingx*.jar) to a single jar, and add a proper "Manifest". Then we will be able to start JPerf with a much simpler command:

java -jar jperf.jar

And finally, I gave a try to solve the above 2 flaws and put my work (deb/jar/src packets) on my site. You can find them here .

浏览器自动选择 Proxy 配置案例

本文主要讨论的是浏览器代理服务器设置技术,文中出现的人名、公司名或者域名均为化名,如有雷同,纯属巧合。

在某些地方上网时,比如南京大学的校园网中,某些公司的局域网中,我们可能需要用到代理服务器。代理服务器的切换一直是一个让人头痛的话题,IE 浏览器有一个 ProxySwither Lite 软件可以用来切换代理,Firefox 有一批插件可以用来切换代理,但是,很难用它们来解决全局性的问题,使用前的配置也是比较麻烦的事情。那么,有没有一种方法可以一劳永逸地解决这个问题呢?答案是有的,那就是 PAC(Proxy Auto-Config) 文件。

使用 PAC 文件我们可以做到:1. IE、Firefox、Opera...浏览器使用同一个代理配置方案,Windows、Linux多系统使用同一个代理配置方案;2. 针对特定的域名,使用特定的代理;3. 针对特定的 IP 范围,使用特定的代理;4. 针对特定的 URL 模式,使用特定的代理。

下面我们来看一个案例:

假设小明的电脑位于 C 公司的局域网中,C 公司为了某些需要禁止员工访问某些站点,例如: alogspot.com 和 bwitter.com ,但是小明的工作和学习需要经常访问这些站点,公司的网管给小明带来了很大不便。不过小明很聪明,他找到了一个可以访问被禁那些站点的一个代理 127.0.0.1:8000。虽然通过该代理小明可以访问这些站点,但是切换代理和浏览器设置始终是麻烦;特别是在用 doogle.com 搜索到的某些文章位于 alogspot.com 时,一不小心点了搜索结果,到搜索引擎 doogle.com 的连接就会有很大一会儿被重置。因为小明的代理速度比较慢,总不能用代理上所有网站吧?这真是件麻烦事,小明该怎么办呢?

虽然很头痛,但是互联网的开拓者们给我们留下了那么多遗产,怎么能不好好利用呢?小明翻出了一个尘封已久的 Wiki 页面,缓缓回忆起那古老的 Javascript 语言,顿时有了主意,于是他写出了下面这个 PAC 脚本:

// 看看域名是不是本地站点
function isLocalHost(host)
{
  if( dnsDomainIs(host, "localhost") )
    return true;
  return false;
}
// 看看域名是不是禁止访问的站点
function isBlockedHost(host)
{
  if( dnsDomainIs(host, "alogspot.com") ||
      dnsDomainIs(host, "bwitter.com") )
    return true;
  return false;
}
// 看看搜索结果 URL 中是不是包含被禁止访问的关键字
function isBlockedURL(url, host)
{
  if( dnsDomainIs(host, "doogle.com") ) {
    if ( shExpMatch(url, "*alogspot.com*") ||
         shExpMatch(url, "*bwitter.com*") )
      return true;
  }
  return false;
}
// 看看 IP 在不在本地 IP 范围内
function isLocalIP(addr)
{
  if( isInNet(addr,"127.0.0.0","255.0.0.0") ||
      isInNet(addr,"10.0.0.0","255.0.0.0") ||
      isInNet(addr,"192.168.0.0","255.255.0.0") ||
      isInNet(addr,"172.16.0.0","255.255.0.0") )
    return true;
  return false;
}
// 看看 IP 在不在被禁止访问的 IP 范围内
function isBlockedIP(addr)
{
  return false;
}
// 看看 IP 地址是不是 IPv6 地址
function isIPV6(addr)
{
  if( shExpMatch(addr, "*:*") )
    return true;
  return false;
}
// 这是浏览器默认调用的函数接口
function FindProxyForURL(url, host)
{
  var direct      = "DIRECT";
  var httpProxy   = "PROXY localhost:8000";
  var socksProxy  = "SOCKS localhost:9050"// 留着做个参考
 
  if(isLocalHost(host)) {
    // 如果是本地域名,那就直连
    return direct;
  } else if(isBlockedURL(url, host) || isBlockedHost(host)) {
    // 如果是被禁止访问的域名,或者搜索结果 URL 中含有被禁止访问的关键词,那就走代理
    return httpProxy;
  }

  if(!isResolvable(host)) {
    // 如果域名不能解析,那就直连
    return direct;
  }
  // 解析域名到 IP 地址
  var IpAddr = dnsResolve(host);

  if(isLocalIP(IpAddr) || isIPV6(IpAddr)) {
    // 如果是本地 IP 或者 IPv6 地址,那就直连
    return direct;
  } else if(isBlockedIP(IpAddr)) {
    // 如果是被禁止访问的地址,那就走代理
    return httpProxy;
  } else {
    // 剩下的,唉,就直连吧
    return direct;
  }
}

小明将以上内容保存为 C:proxy.pac(~/proxy.pac),然后到

Firefox 中,选择 工具->选项->高级->网络->设置(Edit->Preferences->Advanced->Network->Settings),将 file:///c:/proxy.pac(file:///home/username/proxy.pac)填入“自动代理配置 URL”(Automatic proxy configuration URL)文本框中;

再到

IE 中,选择 工具->Internet 选项->连接->局域网设置,勾选使用自动配置脚本,填入 file://c:/proxy.pac;

再到

Opera 中,选择 Tools->Preferences->Advanced->Network->Proxy Servers,勾选上 Use automatic proxy configuration,填入 file://c:/proxy.pac。

从此,小明就开始了自己幸福的互联网冲浪生活,再也没有看到那曾经熟悉的“到该网站的连接已被重置”消息了。

PS:若要 Firefox 和 Chrome 支持远端 DNS 解析,需使用 SOCKS5 作为代理的前缀。

vasprintf 会将空间分配到栈上吗?

由于提交过几次 Linux Fetion 的 bug 和 patch,Linux Fetion 的开发者邀请我加入了 Linux Fetion GUI 的维护者团队中。

昨天晚上和今天下午,我和邓东东(DDD)一直在调试一个 Linux Fetion 在 64 位电脑上的段错误 BUG。这是一个非常奇怪的 BUG,其表现为在 64 位电脑上(Ubuntu 9.04)运行 Linux Fetion 在登录成功后会经常出现 Segmentation Fault。DDD 确定该 BUG 存在于 Libfetion 库中,并且和读取联系人信息的函数有关。Libfetion 论坛上一直有人抱怨类似问题,但是在 DDD 的 64 位虚拟机上却无法重现此 BUG(他的 libc 是 2.7 版本的)。

由于 DDD 仍然不愿意公开 libfetion 库的源代码,我只好等每次他修改库文件之后发给我再调试。经过了好几个小时的努力,今天下午我发现,该 BUG 的主要成因非常有可能是:子函数中本应被动态分配到堆(heap)上的空间被分配(或误写)到了栈(stack)上,子函数返回调用者之后指向子函数栈内容的指针非法。

由于该动态分配的空间是使用 vasprintf 自动分配的,从 DDD 给我的部分代码来看指针传递出问题的可能性不大。那么我想,是不是 vasprintf 函数并不能保证动态分配的空间在 heap 上呢?希望对此有了解的朋友指点一下,谢谢!

$ uname -a
Linux Slytherin 2.6.28-12-generic #43-Ubuntu SMP Fri May 1 19:31:32 UTC 2009 x86_64 GNU/Linux
$ gcc -v
Using built-in specs.
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 4.3.3-5ubuntu4' --with-bugurl=file:///usr/share/doc/gcc-4.3/README.Bugs --enable-languages=c,c++,fortran,objc,obj-c++ --prefix=/usr --enable-shared --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --enable-nls --with-gxx-include-dir=/usr/include/c++/4.3 --program-suffix=-4.3 --enable-clocale=gnu --enable-libstdcxx-debug --enable-objc-gc --enable-mpfr --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 4.3.3 (Ubuntu 4.3.3-5ubuntu4)
$ ll /lib/libc.so.6
lrwxrwxrwx 1 root root 11 2009-04-13 10:26 /lib/libc.so.6 -> libc-2.9.so

PS:我在写这篇博文过程中搜索了一下 Wikipedia,发现这样一段话:

int asprintf(char **ret, const char *format, ...)

asprintf automatically allocates enough memory to hold the final string. It sets *ret to a pointer to the resulting string, or to an undefined value if an error occurred (GLibc is notable in being the only implementation that doesn't always set *ret to NULL on error).

那么是不是分配失败导致了错误的发生呢?但是如果分配失败,为什么子函数返回前的指针的确指向一段在栈上的字符串呢?

多余的逗号?

晚上看了两页 The Art of Unix Programming,其中提到了一个我以前一直感觉困惑的地方:

在我看过的 C/C++ 语言程序代码中,为什么有的列表初始化时在最后元素后会加逗号“,”,而有的不会?
例如:int[] a = { 1, 2, 3, };

书中的原话倒不是讨论逗号该不该加,而是说到了这样做能带来的好处:

A good example is C accommodating an extra comma at the end of an array initializer list, which makes both editing and machine generation of array initializers much easier.
-- The Art of Unix Programming (TAOUP) Ch8.3.1

哦,虽然我一直体会到这样做的好处(尤其当列表成员又臭又长且要经常修改时),也晓得这样做不会引起编译错误,但我经常是在代码 stable 之后将最后的逗号去掉——原因无它,不确定这样做是不是没有问题,那么还是尽量避免吧。今天忽然看到 TAOUP 提到这个,我就好奇:到底是 C/C++ 标准允许这样做呢?还是编译器的实现大部分支持这样做?于是就查了一下。

结果让我很开心,C/C++ 标准中就允许这样做:

initializer:
    assignment-expression
    { initializer-list }
    { initializer-list , }

-- ISO/IEC 9899:1999 (C99) Ch6.7.8 §1

initializer-clause:
    assignment-expression
    { initializer-list ,opt }
    { }

-- ISO/IEC 14882:1998 (C++98) Ch8.5 §1

K&R 中也用非常简短的一句话提到了这个特性:

A list may end with a comma, a nicety for neat formatting.
-- The C Programming Language (K&R) Appendix 8.7

这意味着(C/C++ 语言中)在元素列表最后加上一个逗号是一件非常安全的事情,看来我以后不必再考虑删除列表最后那个逗号了,这样能省却我很多麻烦。

延伸阅读:在其它编程语言中,是否支持这样做呢?Arrays: additionnal commas 这篇文章进行了一个很有意思的讨论。