KEEP K.I.S.S.

tk's blog

嵌入式C程序资源化的思考

 

这里不考虑那些可以使用库的系统,比如可以使用 gettext 的 linux 系统,这里讨论的是一些很简单的系统,比如 ucos,以及很简单的界面库 ucgui。

一般的做法是将需要翻译的字符串汇集到一个数组中,然后使用函数来引用字符串,为了增加可读性,还会使用枚举。这里有篇文章讲解这种做法:Use Strings to Internationalize C Programs.

这种方法比较直接,优点是占用资源不高,很符合嵌入式的环境。缺点也明显:

  • 手动量比较大。当然如果字符串少的话也没什么问题。添加一个新的字符串要在几处做更改,还需要保证数组的顺序。
  • 不适合自动化生成。自动化并不能给你生成带有意义的枚举名,而且自动化脚本需要修改源文件,这会引发其他问题。
  • 不直观,当维护时,源代码中的字符串看起来就没那么直观了,尤其是接手维护此代码的开发者。

因为用过 Qt ,对于它那种资源化/国际化方式特别喜欢,直观且优雅。后来了解了下 GNU gettext ,大体应该与 Qt 使用的方法类似。

自己也试着思考了下这些工具内部实现的原理。以 Qt 的 tr() 函数举例:

QString str = tr("C语言");

tr() 函数在运行时取出显示用的字符串,它使用的参数就是源代码中使用的字符串本身,这里就是"C语言"tr() 也应该是从一张表中查找翻译语言对应的字符串,没找到就返回它的参数。对于这个表查找应该使用了字符串hash 以提高查找效率。对于表的生成使用了外部工具对源代码进行处理,也就是自动化生成。

下面讲下我准备的思路:

  1. 源代码中需要翻译的字符串用一个宏封装,比如 _TR("some stting")

    #define _TR(str) tr(str)
    

    tr() 就是查找函数,用宏的好处在于灵活性。

  2. 字符串提取可以写个脚本,遍历所有的 C文件,逐行处理提取出所有的字符串,也可以顺带一些位置信息,比如提取自哪个文件哪一行。
  3. tr() 函数查找的表是一个只读数组,数组元素类型定义大概如下:

    enum Language {
        LANG_DEFAULT,
       LANG_ZH_CN,
       LANG_EN,
        // ...
        LANG_LAST
    };
    
    struct UniversalString  {
        int hash; 
       const char * const str[LANG_LAST];
    };
    

    结构定义 struct UniversalString 就是数组元素类型了,hash 的作用是为了加速字符串查找,使用良好的字符串hash函数的话,碰撞率很低,一个 hash 域就足够了,否则可以添加多个存储使用不同 hash 函数产生的值。

  4. 表数组由脚本自动生成,自动产生字符串 hash 值,用原始字符串填充 str[LANG_DEFAULT],这样设计是因为字符串 hash 算法并不是 1对1,可以再进行一次字符串比较,这比直接在整个表中遍历逐个比较效率要高,而且还有个好处,对于翻译人员,有原始字符串更方便(这也可以通过添加注释实现)。如果想避免字符串比较,可以存储2个或多个不同 hash 函数的值,若这些值都相同,就认定字符串相同,这是个“概率保证”,如果 hash 函数足够好,这个认定失败的概率很低。一个示范表定义

    static const struct UniversalString strings[] = {
        {// ...\src\a.c:114 
            15,
            { 
                "C语言",
                "",
                //...
            }
        },
        {// ...\src\a.c:145
            1563,
            {
                "some string",
                "",
                // ...
            }
        },
        // ...
    };
    
  5. 函数 tr() 代码大概如下,其中 find() 就是查找函数,get_lang() 返回的是当前的语言,返回值是枚举 Language 中的取值。

    const char * tr(const char * str)
    {
        const struct UniversalString * ps = find(str);
        if(ps) { // found
            return ps.str[get_lang()];
        } else { // not exist
            return str;
        }
    }
    

    find() 函数就不写出了,这个函数优化的好,整体效率就不错。

  6. 如果是修改代码时候修改了字符串,重新生成时自动化工具应该可以保留一些未改动的字符串的已翻译内容,这是写工具要注意的事情。


参考资料:

  1. Use Strings to Internationalize C Programs.
  2. 使用GetText本地化编程.
  3. 各种字符串Hash函数比较.  

谷歌输入法颜文字扩展 kaos 更新到 2.1.1

好久没写文章,马上也要过除夕了,准备回家中...

以前写过的谷歌颜文字扩展最近重写了,转换器用 python 3 重写,词库也有一些添加。

详情请见 kaos@Github

XP 下 GVim 工作目录问题

用GVim打开文件,工作目录就是文件所在目录。

点击桌面上快捷方式打开GVim,工作目录是桌面,用 :pwd 命令检测。

把快捷方式加到桌面任务栏的快速启动栏中,“一般”情况下,快速启动的GVim工作目录是 ~ 下,也就是用户目录下。

但是如果你打开过 Windows字体目录(C:\WINDOWS\Fonts),之后再用快速启动打开GVim,工作目录会变成 C:\WINDOWS\Fonts ,多少次都是如此。重启后会恢复正常。

因为GVim安装后默认添加的桌面快捷方式属性中只有目标位置,而起始位置为空,这里面起始位置就会被设定为起始的工作目录(一开始的,如果打开文件,工作目录就会被修改为文件所在目录)。将快捷方式属性中的起始位置设置后,快速启动的GVim的工作目录就是设置的位置。

我的猜测:在快捷方式属性起始位置为空的情况下,在未打开 Windows字体目录前,Windows 会将快速启动栏中程序默认工作目录设置为用户目录,但是在打开 Windows字体目录后,快速启动栏中程序默认的工作目录就变成了 Windows字体目录。不知道算不算一个 bug。

这里当然也学到了一点收获,那就:生成快捷方式的时候,不要留空起始位置属性

虚拟机socket通信丢失头字节奇怪问题一枚

问题描述:在虚拟机里的Linux上用socket TCP (C 编写)连接 Win 主机上 socket(python 编写) 服务器,获取的数据经常少一个字节,而且都是数据的头字节。使用 telnet 连接也是如此。在 Win 主机上用 Socket 调试工具和telnet 都是正常,不会丢失。

硬件环境:虚拟机为 Orcale VM VritualBox 4.1.2,虚拟机上系统为 Debian 6 x86-x64,虚拟机网络类型为 NAT。主机系统为 WinXP SP3 x86。

虚拟机linux下代码:

#include    "unp.h"

int
main(int argc, char **argv)
{
    int    sockfd, n;
    char    recvline[MAXLINE + 1];
    struct sockaddr_in    servaddr;

    if (argc != 2)
        err_quit("usage: a.out <IPaddress>");

    if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
        err_sys("socket error");

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port   = htons(4013);    /* daytime server */
    if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
        err_quit("inet_pton error for %s", argv[1]);

    if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)
        err_sys("connect error");

    while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
        recvline[n] = 0;    /* null terminate */
        printf("%d read from the socket\n", n);
        if (fputs(recvline, stdout) == EOF)
            err_sys("fputs error");
    }
    if (n < 0)
        err_sys("read error");

    exit(0);
}

这代码其实就是《UNIX 网络编程卷1》(UNP)中的第一个程序( unpv13e/intro/daytimetcpcli.c ),用 tcp 连接服务器,读取时间信息的。上述代码做了微小改动。UNP的第三版代码 unpv13e 可以在这里这里下载。

为了测试这个代码我在 Win 主机上用 python 写了一个服务程序:

import socket
import time

host = '192.168.1.68'
port = 4013

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((host, port))
s.listen(5)

while True:
    connection, address = s.accept()
    t = time.strftime("%a %X %x", time.localtime()).encode("ascii")
    c = connection.send(t)
    print("{0:d} sent of {1:d}: ".format(c, len(t)), "\"{0}\"".format(t.decode("ascii")), "TO client {0[0]}:{0[1]}".format(address))
    connection.close()

其中 192.168.1.68 是主机在局域网中的地址,绑定到这个地址上虚拟机中程序也可以访问到服务器。

当在虚拟机中用编译好的程序或者 telnet 访问 Win 上服务器时,经常会输出如下

"ri 16:22:52 12/28/12"

少了头部一个字符 "F" ,读取只是20个字节,但是也有时候正常,但是正常的几率低于 40%(目测)。

而在 Win 主机上打印输出都是正常的,'21 sent of 21:  "Fri 16:54:40 12/28/12" TO client 192.168.1.68:18289' 这样的,的确都是把 21 个字节发送完毕。

在 Win 主机上用 telnet 或者 TCP/UDP socket 调试助手来测试,获取的数据也都正常,为21字节,没有丢失头字节的情况发生。

(⊙﹏⊙) 

感觉可能是虚拟机 NAT 模式的问题或者还是什么,但是只丢失头字节的确是好奇怪。。。。

·-·-·-·-·-·--·-·-· 2012-12-29 分割线 -·-·-·-·-·-·-·-·--·-·-·-·

今天又测试了下,这次通信获取数据大部分情况下是正常的,只有约10%的几率会丢失头字节。在虚拟机里装了tcpdump 工具来看看情况。

两次通信

tisyang@debian:~/Sources/unpv13e/intro$ ./daytimetcpcli 192.168.1.68
20 read from the socket
at 09:14:47 12/29/12

tisyang@debian:~/Sources/unpv13e/intro$ ./daytimetcpcli 192.168.1.68
21 read from the socket
Sat 09:14:51 12/29/12

说明第一次通信丢失了头字节 'S',第二次通信正常。在此同时,我已经运行 tcpdump 来截获和主机 192.168.1.68 的通信包,命令和输出如下

tisyang@debian:~$ sudo tcpdump  host 192.168.1.68
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes

09:14:46.337897 IP 10.0.2.15.43314 > 192.168.1.68.4013: Flags [S], seq 3451477296, win 14600, options [mss 1460,sackOK,TS val 398218 ecr 0,nop,wscale 6], length 0
09:14:46.341664 IP 192.168.1.68.4013 > 10.0.2.15.43314: Flags [S.], seq 46272001, ack 3451477297, win 65535, options [mss 1460], length 0
09:14:46.341712 IP 10.0.2.15.43314 > 192.168.1.68.4013: Flags [.], ack 1, win 14600, length 0
09:14:46.341747 IP 192.168.1.68.4013 > 10.0.2.15.43314: Flags [S.], seq 46272001, ack 3451477297, win 65535, options [mss 1460], length 0
09:14:46.341837 IP 10.0.2.15.43314 > 192.168.1.68.4013: Flags [.], ack 1, win 14600, length 0
09:14:46.341865 IP 192.168.1.68.4013 > 10.0.2.15.43314: Flags [FP.], seq 1:21, ack 1, win 65535, length 20
09:14:46.342276 IP 10.0.2.15.43314 > 192.168.1.68.4013: Flags [F.], seq 1, ack 22, win 14600, length 0
09:14:46.342360 IP 192.168.1.68.4013 > 10.0.2.15.43314: Flags [.], ack 2, win 65535, length 0


09:14:49.632861 IP 10.0.2.15.43315 > 192.168.1.68.4013: Flags [S], seq 3496530236, win 14600, options [mss 1460,sackOK,TS val 399041 ecr 0,nop,wscale 6], length 0
09:14:49.633743 IP 192.168.1.68.4013 > 10.0.2.15.43315: Flags [S.], seq 46720001, ack 3496530237, win 65535, options [mss 1460], length 0
09:14:49.633771 IP 10.0.2.15.43315 > 192.168.1.68.4013: Flags [.], ack 1, win 14600, length 0
09:14:49.633983 IP 192.168.1.68.4013 > 10.0.2.15.43315: Flags [P.], seq 1:22, ack 1, win 65535, length 21
09:14:49.634048 IP 192.168.1.68.4013 > 10.0.2.15.43315: Flags [F.], seq 22, ack 1, win 65535, length 0
09:14:49.634245 IP 10.0.2.15.43315 > 192.168.1.68.4013: Flags [.], ack 22, win 14600, length 0
09:14:49.634749 IP 10.0.2.15.43315 > 192.168.1.68.4013: Flags [F.], seq 1, ack 23, win 14600, length 0
09:14:49.634815 IP 192.168.1.68.4013 > 10.0.2.15.43315: Flags [.], ack 2, win 65535, length 0

输出中的分段是我添加的,为了区分第一次通信和第二次通信。显然,第一次通信中就只获取到了 20 个字节(length 20),而第二次正常(length 21)。

TCP 连接要经过3次握手,而关闭需要4次握手,从第二次通信就可以看出来。

而在第一次通信中,3次握手tcp建立后,服务器很奇怪的发来一个 SYN 包,而且 FIN 包(用于连接终止)和 PUSH 包(传送数据)是在同一个包内。而在第二次通信里,没有这种情况。

 

PPP 数据帧中字符转义设计

一个比较成熟的字符转义序列设计,记录在此备用。可以用来作为通信协议设计参考(重复造轮子?)。

  1. PPP 数据帧都以标志字符 0x7e 开始和结束。
  2. 由于标志字符的值是 0x7e,因此当该字符出现在信息字段中时,需要对它进行转义。特殊字符 0x7d 用作转义字符。转义算法为当 0x7d 出现在数据中时,紧接着的字符的第 6 个比特要取其补码,即与 0x20 做异或运算。
    1) 当遇到字符 0x7e 时,需连续编码两个字符:0x7d 和 0x5e,以实现标志字符转义(0x7e ^ 0x20 = 0x5e)。
    2) 当遇到转义字符 0x7d 时,需连续编码两个字符:0x7d 和 0x5d,以实现转义字符的转义(0x7d ^ 0x20 = 0x5d)。
    3) 默认情况下,如果字符的值小于 0x20 (ASCII 控制字符),一般都要进行转义。例如字符 0x01 会被转义为两个连续字符 0x7d 和 0x21。

当然我也在怀疑是否用的上.....

参考资料:

TCP/IP详解卷1:协议, 机械工业出版社, 2000年.