KEEP K.I.S.S.

tk's blog

类型转换及 Type Punning

 

将一个 float 类型的值转换为网络字节序,该如何做?

除了手动编码转换外,我们知道 htonl 是将一个32bit无符号整数转换为网络字节序,而 float 也是32bit,那么只要能将一个 float 类型的值转换为bit位相同的 uint32 值,之后再同样转换回 float 就应该可以了。

一般我们会想到利用指针强制类型转换来迫使编译器重新解释指针指向的内存:

float hton_f(float value)
{
    int tmp = htonl(*(unsigned int *)&value);
    return *(float *)&tmp;
}

指针存储的是内存中的地址,地址上的内容由指针类型来解释。这种强制类型转换的技巧叫做 Type punning . 事实上,这种方法存在隐患。

Type punning
A form of pointer aliasing where two pointers and refer to the same location in memory but represent that location as different types. The compiler will treat both "puns" as unrelated pointers. Type punning has the potential to cause dependency problems for any data accessed through both pointers.

多数时候,Type punning 并不会引起任何问题。虽然在 C 标准中它属于依赖实现的实现,但通常可以正常工作。[参考*]

当打开 GCC 的编译选项 -fstrict_aliasing (优化选项 -O2 等会默认打开这个选项)时,则有可能出现问题。编译器认定两种不同类型(signed/unsigned不影响,还有 char *void * 是例外)的指针不会指向同一内存地址,它们之间也没有依赖关系,这种认定下编译器可以对程序进行优化,比如会将不同类型的指针进行操作的代码的顺序(编译的机器码)打乱。而如果这两种类型指针事实上指向同一位置,那么程序代码的执行结果可能会出乎意料(执行顺序跟代码中的顺序不一致)。这种规则也称之为 strict aliasing rule .

一个可以使用的解决方法是使用 union 来进行转换:

float hton_f(float value)
{
    union tagFTOI { 
        float f;
        unsigned int i;
    };
    ((union tagFTOI *)&value)->i = htonl(((union tagFTOI *)&value)->i);               
    return ((union tagFTOI *)&value)->f;
}

除了这种指针写法外,还可以用值方法,就是临时定义一个 union 对象,然后进行赋值转换。

根据 C 标准,任何使用 type punning 的行为依赖编译器实现。那么根据“标准”来看,使用 union 也不是一定能解决问题。标准中规定,设置了 union 中的某个域的值,那么就应该在相同的域读回。

但是大多数编译器都支持使用 union 来进行 type punning 而不破坏 strict aliasing rule . 比如 GCC,GCC 文档中写道:

The practice of reading from a different union member than the one most recently written to (called “type-punning”) is common. Even with -fstrict-aliasing, type-punning is allowed, provided the memory is accessed through the union type.

strict aliasing rule 有两个例外,char *void * 指针,举个栗子,用代码实现 float 的绝对值函数:[来源*]

float funky_float_abs (float *a)
{
    float temp_float = *a;
    // valid, because it's a char pointer. These are special.
    unsigned char * temp = (unsigned char *) a;
    temp[3] &= 0x7f;
    return temp_float;
}

float funky_float_abs (float *a)
{
    int temp_int i;
    float temp_float result;
    // Memcpy takes void pointers, so it will force aliasing as well.
    memcpy (&i, a, sizeof (int));
    i &= 0x7fffffff;
    memcpy (&result, &i, sizeof (int));
    return result;
}

上面两个函数都可以正常工作而不违反 strict aliasing rule 规则。至于函数怎么实现 float 的绝对值,可以参考 float 通常是如何在计算机上存储的。


参考资料

  1. Type punning.
  2. Type punning isn't funny: Using pointers to recast in C is bad.
  3. "What are the common undefined/unspecified behavior for C that you run into?.
  4. Type-punning and strict-aliasing

嵌入式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函数比较.  

使用了互斥量访问全局变量还需要 volatile 吗

今天在看 pthread ,pthread_cond_wait 是个很有趣的函数,至于标题所提出的问题也是困扰我很久的一个“小问题”。

因为“虽然”互斥量能保证多个线程访问全局变量的一致性,但是由于编译器优化和 cache 机制,多个线程对于内存中变量操作也许就没有“真正”反馈到内存里(也许仅仅改变了寄存器中的缓存值),但是这个说法又与互斥量的目的有矛盾(既然都要保证一致性,为什么又“出现”这种内存中“不一致”的状况?)

于是就找资料,终于找到一个很完备的解释:多处理器环境和线程同步的高级话题(白杨) 涉及到 内存屏障 (memory barrier)技术。

至于 pthread_cond_wait 和 pthread_cond_t 的使用,也是有很多需要理解和注意的细节,在这里可以解惑:
http://www.cppblog.com/izualzhy/archive/2011/11/14/160120.html

pthread 的使用教程这里有:http://www.yolinux.com/TUTORIALS/LinuxTutorialPosixThreads.html

不过都是英文,可能在一些细节解释上理解会有难度,不过有搜索引擎还是很容易的,另外也可以参考 《Unix 高级环境编程》也就是所谓的“APUE”。

越来越喜欢 C 的风格和思想了,算是回归吧。

do ... while(0) 的使用

今天稍微看了下 Ruby 的 array#pack 和 string#unpack 的实现,额,相当复杂(因为 lua 标准库里没有这样的函数实现,所以看 Ruby 的)。然后在其中发现了很多宏定义使用了

#define MACRO(x) do {\
statements; \
} while(0)

这样的形式,蓝后就去百度谷歌看了看,这里就贴链接,不重复解释了,反正是妙用。

do...while(0)的妙用                     http://www.cnblogs.com/flying_bat/archive/2008/01/18/1044693.html

do { … } while (0) what is it good for?       http://stackoverflow.com/questions/257418/do-while-0-what-is-it-good-for

setjmp/longjmp 使用注意

  1. 不应从调用 setjmp() 的过程中返回。
    如果setjmp所在的函数已经调用返回了,那么longjmp 使用该处 setjmp 所填写的对应 jmp_buf 缓冲区将不再有效。这是因为longjmp 所要返回的"栈帧"(stack frame) 已经不再存在了,程序返回到一个不再存在的执行点,很可能覆盖或者弄坏程序栈。
    上篇文章叙述过这个,也可以参见:setjmp 的正确使用
     
  2. 保证局部变量在 longjmp 过程中一直保持它的值的唯一可靠方法是把它声明为 volatile (这使用于那些值在 setjmp 执行和longjmp 返回之间会改变的变量) 详细可以参见:setjmp 和 longjmp,以及对变量的影响 

    简单来说如下:
    1. 优化不影响 global, static, volatile 变量。
    2. 标出来的那句话意思是,保存在内存中的变量,longjmp返回后,保持了longjmp时的值。
        而在 cpu 中的值将会退回到setjmp时的值。
    3. 没有优化时global, static, volatile, auto, register都存在内存中。
        返回的时候,这些值仍然是longjmp时候的值。
        但在有优化的时候, auto, register的值是在寄存器的。
        所以返回的时候,这两个值是在setjmp时候的值。
     
  3. 编程的惯用法: if( setjmp(x) ){/* handle longjmp(x) */}。
     
  4. 仅在特定范围内引用 setjmp 。