Category Archives: programming

C中的移位和undefined behavior

前一阵写一个程序,需要malloc 2G的buffer,于是第一次就这么写了

long size = 1 << 31;

结果malloc返回总是失败,把size打印出来发现是个负数。如果改成以下的写法就没问题了

long size = 1u << 31;

今天正好看到内部邮件列表里有人也在讨论这个问题,贴出了C99对移位操作的规定,也为我解决了这个疑问

ISO C99 6.5.7 Bitwise shift operators, paragraph 4:

 The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated
 bits are filled with zeros. If E1 has an unsigned type, the value of
 the result is E1 × 2^E2 , reduced modulo one more than the maximum
 value representable in the result type. If E1 has a signed type and
 nonnegative value, and E1 × 2^E2 is representable in the result
 type, then that is the resulting value; otherwise, the behavior is
 undefined.

也就是说 1 << 31 这种表达式是个未定义行为,因为1是一个signed type,并且 1 x 2^31已经超出了signed int的表达范围,所以gcc中得到负数只是gcc的一个实现。而 1u << 31 能够得到正确的2G数值,是因为1u是一个unsigned type,1u x 2^31 的结果正好能在unsigned int里表示,再赋值给long之后也就没问题了。

修复nctwit与locale相关的一个bug

nctwit开发笔记中已经给nctwit写了雏形,基本可用了,反正能达到我个人的需求。但是近来遇到一个bug,那就是不能用颜色区分已读和未读消息了,所有消息标记为已读,而且不能设置当前时间戳。这个问题在自己的台式机和笔记本上都没有,只在公司的台式机上才有。

先说一下这里的逻辑。nctwit有维护两个时间戳,一个是记录在文件当中的最后一次按‘f’键时得到的当前最新消息的时间戳,用localtime表示,是time_t类型的变量;一个是更新时间线之后得到的最新的消息的时间戳,用latesttime表示。那么当我按一下’f’键,nctwit会获取当前latesttime,与从文件中读取到的localtime进行比较,如果当前时间大于localtime,那么就把当前时间戳写入文件,当作localtime。在显示tweets的时候把每个tweet的时间都与localtime进行比较,较新的tweet用红色显示,较旧的用绿色显示。这样就可以用颜色区分已读和未读了。

但是这里有一个地方需要进行转换,因为从twitter api获取的xml中的时间戳是字符串,形式如下“Fri Jul 09 06:49:23 +0000 2010″,而localtime是time_t类型,显然比较time_t类型的数据是方便的,那么就需要把时间字符串转换成time_t类型。nctwit中是用strptime这个函数来转换的。strptime这个函数有三个个参数,第一个是待转换的时间串,第二个是时间串的格式描述,比如与上边那个例子对应的格式描述就是“%a %b %d %H:%M:%S +0000 %Y“,第三个参数是一个指向struct tm类型变量的指针,函数会把转换后的时间存储在这里。具体用法就man吧。

经过debug,发现问题出现在strptime函数总是返回NULL,也就是函数出错,打印出的错误信息竟然是”Operation not permitted”,很是奇怪。在搜索相关错误的时候找到了GNU C library与时间相关的一系列函数说明,其中有一部分就是关于时间转换函数的,于是找到了与strptime功能相似的getdate及其变种getdate_r函数。getdate_r函数在出错的时候会返回错误代码,而在我这里返回的错误代码是7,表示“There is no line in the file that matches the input.“,竟然是格式不匹配。又仔细看了一下格式说明(man strptime能看到),忽然注意到了这么一句“The weekday name according to the current locale“,也就是说是跟current locale相关的,如果当前locale是中文,那么就肯定不能匹配英文的Fri和Jul了。于是在程序开始的地方添加如下代码

setlocale(LC_TIME, "en_US");

把LC_TIME改成英语,再编译运行程序,果然又能用颜色区分已读/未读了。

在自己的台式机和笔记本上我都自己export LC_TIME=en_US了,就是为了在使用date的时候不显示中文,而工作机还没设置,所以出现了这个bug。

铺垫了这么多,bug一句话就解决了……

nctwit开发笔记

经过近20天折腾(2010.01.30 – 2010.02.16),之前一篇文章中的构想,一个基于ncurses的twitter客户端终于完成了,达到了基本可用的程度,代码不到4k行,虽然还很混乱和不完善,不过也算是一个阶段性成果。功能和使用说明如下:

  1. nctwit是一个基于ncurses的终端下的twitter客户端。支持自定义API,支持SOCKS5代理(暂不支持HTTP代理,主要因为本人没有这个需求)。需要libncursesw,libxml2,libcurl支持和相应的开发包。
  2. 支持基本的twitter功能,如更新时间线,发表、回复、回推(旧时retweet,暂不支持官方retweet功能),设置/取消收藏,发direct消息,查看follower和firend,删除消息和direct消息,用颜色区分已读(绿色)消息和未读消息(红色),显示消息时间和来源,显示in reply to信息。不支持多用户,不自动更新时间线。
  3. 具有类似vi和KBS的按键绑定:q退出,j/k(或上下剪头)上下移动查看消息,h/l(或左右剪头)左右移动查看不同时间线,u更新,r回复,t回推,d发direct消息,D删除消息,m设置/取消设置收藏,R更新时间线,f全部已读,0回到开头,$到末尾,N下一页,P上一页,ctrl+l重绘屏幕。
  4. 调用$EDITOR环境变量来编辑消息,如果$EDITOR是空默认使用/usr/bin/vi。(自己用c写ncurses的text field太折磨了……)
  5. 单独的一个主体程序nctwit,使用配置文件$HOME/.nctwit.conf,数据文件夹$HOME/.nctwit/。数据包括home, mention, direct message, favorite, public的timeline的xml文件,和时间戳文件,还有follower, friends的xml文件和时间戳文件。
  6. 配置文件参数:USER=username,PASSWD=passwd,这两个参数是必须的,API=http://your.own.api,SOCKS5_PROXY=your.socks5_proxy.hostname:port,GREETING=greeting message。其中API默认https://twitter.com/,GREETING默认是What are you doing?

写这篇文章主要是为了记录一下开发过程中遇到的困难和问题以及部分解决方案,不能白折腾啊。

首先看一下twitter api的消息,大概了解一下啥是twitter api,它们能干啥,怎么用。具体api说明可以看这里,可以在右侧搜索框搜索特定的API。还参考了dabr的代码

学习如何用libcurl的时候,参考了dabr的代码,这里这里还有这里。从这两篇文章可以学习到如何让libcurl使用socks5代理,12

另外涉及到时间戳问题的时候,需要从xml文件中的时间字符串,比如“Wed Feb 03 05:17:36 +0000 2010″转换成time_t类型的时间戳。可以用strptime解析时间字符串,转换成struct tm结构,然后再用mktime把struct tm转换成time_t,出来的是UTC时间。如果需要utf_offset修正,可以用date +%z在shell下获得,或者使用ftime函数,参考这个

学习使用libxml2解析xml文件的时候可有点费劲了,因为libxml2没有详细的man page,主页上的文档和实例也不是很详细,格式看起来也挺费劲的。最后发现xpath是个好东西,关于xpath可以看这个网址,讲解很简单清楚。用这篇文章中提到的函数xmlNodeGetContent从解析过的xml node数据结构中提取内容。这两篇文章介绍了libxml和xpath的简单使用,比官方文档清楚很多,12

在处理utf8整字处理的时候,这篇文章帮了大忙。主要是说明了utf8编码的规律。


具体来说,UTF-8编码有以下几种格式:

U-00000000 – U-0000007F:  0xxxxxxx
U-00000080 – U-000007FF:  110xxxxx 10xxxxxx
U-00000800 – U-0000FFFF:  1110xxxx 10xxxxxx 10xxxxxx
U-00010000 – U-001FFFFF:  11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
U-00200000 – U-03FFFFFF:  111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
U-04000000 – U-7FFFFFFF:  1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

第 一个字节要么最高位是0(ASCII字节),要么最高两位都是1,最高位之后1的个数决定后面有多少个字节也属于当前字符编码,例如111110xx,最 高位之后还有四个1,表示后面有四个字节也属于当前字符的编码。后面每个字节的最高两位都是10,可以和第一个字节区分开。

另外,这篇文章对字符、编码什么的讲的挺清晰易懂的。还参考了这篇文章

先记这些吧,要出去玩了:)

学习ncurses编程 #1

第一章:NCURSES 库简介

控制终端输出需要用到转义序列(escape sequence),但是在不同的终端上,同样的转义序列的效果是不一样的。能够在不同的终端上输出统一的结果,UNIX 的设计者发明了一种叫做 termcap 的机制。termcap 实际上是一个随同转义序列共同发布的文件。这个文件罗列出当前终端可以正确执行的所有转义序列,使用户输入转移序列的执行结果符合这个文件中的规定。但是,在这种机制发明后的几年中,一种叫做 terminfo的机制逐渐取代 termcap。从此用户不用在编程时翻阅繁琐的 termcap 中的转义序列规定,仅需要通过访问 terminfo 的数据库就可以控制屏幕的输出了。

在使用 terminfo 的情况下,让所有的应用程序访问 terminfo 数据库控制输出(比如发送控制字符,等等……)。不久这些调用代码将会使整个程序变得难以控制和管理。这些问题的出现导致了 CURSES 的诞生。CURSES 的命名是来自一个叫做 “cursor optimization”(光标最优化)的双关语。CURSES 库通过对终端原始控制代码(转义序列)的封装,向用户提供了一个灵活高效的API(应用程序接口)。它提供了一套控制光标,建立窗口,改变前景背景颜色以及处理鼠标操作的函数。使用户在字符终端下编写应用程序时绕过了那些恼人的底层机制。NCURSES是curses的新实现(New Curses),完全兼容cueses。

NCURSES 不仅仅只是封装了底层的终端功能,而且提供了一个相当稳固的工作框架(Framework)用以产生漂亮的界面。它包含了一些创建窗口的函数。而它的姊妹库Menu、Panel 和 Form 则是对 CURSES 基础库的扩展。这些库一般都随同 CURSES 一起发行。我们可以建立一个同时包含多窗口(multiple windows)、菜单(menus)、面板(panels)和表单 (forms)的应用程序。窗口可以被独立管理,例如让它卷动     (scrollability)或者隐藏。菜单(Menus)可以让用户建立命令选项,从而方便执行命令。而窗体(Forms)允许用户建立一些简单的数据输入和显示的窗口。面板(Panels)是 NCURSES 窗口管理功能的扩展,可以用它覆盖或堆积窗口。

第二章:从 Hello World 程序开始

编写ncurses程序,需要在c文件中#include <ncurses.h>,并在编译的时候加上-lncurses库。注意,ncurses.h中已经include stdio.h了。

#include <ncurses.h>

int main()
{
   initscr();                 /* 初始化,进入 NCURSES 模式  */
   printw("Hello World !!!"); /* 在虚拟屏幕上打印 Hello, World! */
   refresh();                 /* 将虚拟屏幕上的内容写到显示器上,并刷新 */
   getch();                   /* 等待用户输入 */
   endwin();                  /* 退出 NCURSES 模式 */
   return 0;
}

其中initscr()函数用于清屏,并进入ncurses模式,给stdscr分配内存。printw 函数把字符串输出到被称作“stdscr”的虚拟坐标(0,0)上。从显示的结果来看,坐标(0,0)在屏幕的左上角上。printw()函数的作用是不断将一些和显示标记相关的数据结构写在虚拟显示器上,并将这些数据写入 stdscr 的缓冲区内。所以,为了显示这些缓冲区中的数据我们必须使用 refresh()函数告诉 curses 系统将缓冲区的内容输出到屏幕上。

第四章:初始化

有一些很有用的初始化函数。

  • raw()和 cbreak()函数:禁用行缓冲。在 raw()函数模式下,这些字符将传送给程序去处理而不作为终端程序处理的信号
  • echo()和 noecho()函数:设置输入是否回显
  • keypad()函数:使用功能键,如F1,方向键等
  • halfdelay()函数:启用半延时模式

一个例子:

#include <ncurses.h>
int main()
{
    int ch;
    initscr();/* 开始 curses 模式     */
    raw();    /* 禁用行缓冲           */
    keypad(stdscr, TRUE);/* 开启功能键响应模式    */
    noecho();            /* 当执行 getch()函数的时候关闭键盘回显 */
    printw("Type any character to see it in boldn");
    ch = getch();
    if(ch == KEY_F(1)) 
        printw("F1 Key pressed");
    else
    { 
        printw("The pressed key is ");
        attron(A_BOLD);
        printw("%c", ch);
        attroff(A_BOLD);
    }
    refresh(); /* 将缓冲区的内容打印到显示器上 */
    getch();   /* 等待用户输入                    */
    endwin();  /* 结束 curses 模式              */
    return 0;
}