标签 Golang 下的文章

Go defer的C实现

Go语言中引入了一个新的关键字defer,个人认为这个语法关键字让异常处理也变得得心应手许多,对改善代码的可读性和可维护性大有裨益,是典型的语法棒棒糖^_^。

像下面这种代码(伪代码):

void foo() {
    apply resource1;

    retv = action1;
    if not success
        release resource1

    apply resource2;

    retv = action2;
    if not success
        release resource1
        release resource2
}

有了defer后,代码就变得优美多了。

void foo_with_defer() {
    apply resource1;
    defer (release_resource1)

    retv = action1;
    if not success
        return

    apply resource2;
    defer (release_resource2)

    retv = action2;
    if not success
        return
}

如果能在C语言中实现defer这样的语法糖,那该多棒!是否可行呢?经过一段时间钻研,找到一个不那么美的实现方法,约束也很多,也不甚严谨, 谈不上什么可移植性,切不可用到产品环境,权当一种探讨罢了。

Go中defer的语义大致是这样的:
* 在使用defer的函数退出前,defer后面的函数将会被执行;
* 如果一个函数内有多个defer,那么defer按后进先出(LIFO)的顺行执行;
* 即使发生Panic,defer依然可以得到执行

最后一个比较难于模拟,这里仅先尝试前两个语义。下面从设计思路说起。

* “借东风”

要想模拟defer,首先要考虑的一点那就是defer后的语句是在函数return之前执行的。在标准C中,我们无任何举措可以实现这些。要在 C中实现defer,势必要借用一些编译器扩展特性,比如Gcc的扩展。这里实验所使用的编译器是Gcc(4.6.3 (Ubuntu 12.04))。Gcc扩展支持-finstrument-functions编译选项,该选项可以在函数执行前后插入一段运行代码。在之前写过的一篇名 为“为函数添加enter和exit级trace”的文章中对此有较为详细的说明,这里我们还要用到这个扩展特性。

* 偷天换日

如果完全模仿Go的语法,在C中使用defer,大致是这样一种形式:

void foo(void) {
    FILE * fp = NULL;
    fp = fopen("foo.txt", "r");
    if (!fp) return;
    defer(fclose(fp));
   
    /* use fp */
    … …
    return;
}

但C毕竟是C,一门静态的编译型语言,我们如何将fclose(fp)这个信息传递给编译器自动插入的代码中呢?在C语言中,几乎没有手段获得函 数的元信息以及运行时参数信息,并再通过这些信息重新调用和执行该函数。我们得“想招”将这些信息存储起来。

大家知道C语言中的函数,比如这里的fclose,其实是一个函数起始地址;如果我们知道函数地址或又叫函数指针,再加上函数的参数,我们就可以 拼凑在一起执行该函数了。但理论上来说,函数指针也是有类型的,比如:

typedef int (*FUNC_POINTER)(int, int);

这个函数指针类型可以用来执行诸如:int foo(int a, int b)这样的函数,比如:

FUNC_POINTER fp = foo;
fp(1, 2);

但defer后面执行的函数千差万别,我们如何能够得知函数对应的函数指针类型呢?用void*存储?比如:

void *p = foo;
p(1, 2);

编译器会给你一个严重错误!p不是函数指针,不能这么用。那我们如何能让编译器知道这个指针是一个可调用的函数指针呢?我们试试来定义一个“通用 的函数指针”:

typedef void (*defer_func)();

没有返回值,没有参数,这样的函数指针能否执行foo这样的函数呢?答案是可以的,但不是那么完美。至少你不会得到返回值。这么做有两点考虑:
a) 至少可以让编译器知道这是一个函数指针,可以被用来执行函数。
b) 通常我们并不关心defer后面函数的返回值。
c) 参数列表的不同至少目前可以逃过编译器的错误检查,至多给个Warning。

函数指针的问题暂时算是有着落了,那参数怎么办?也就是说defer(fclose(fp))中的fp如何存储下来呢?如果在C中真的使用 defer(fclose(p))这种形式的语法,那么我是砸破脑袋也想不出啥招了!因此我们应该重新设计一下C中的defer应该如何使用?我 们用下面的语法来替代:

defer(fclose, 1, p);

fclose是函数起始地址,1是参数个数,p则是传给fclose的参数。这样fclose和p都可以单独分离出来存储了。但是还是那句 话:defer后面可以执行的函数千万种,哪能穷尽?怎么才能表示成一种通用的方式存储参数呢?回想一下自己在编码过程中用于释放资源的那几类函 数,无非就是关闭文件、关闭文件描述符(包括socket)、释放内存等,这些函数传递的参数不是指针就是整型数,少有传浮点类型或将一个自定义 结构体以传值的方式传入的。我们不妨再次尝试一次“偷天换日” – 用void*存储整型参数或任意指针类型参数。当然其约束就像刚才所说的那些。不过对付大多数资源释放函数而言,应该是足够的了。至于将参数个数也作为一 个固定参数放入defer中,也是鉴于目前无法通过操作可变个数参数列表相关宏来获得参数数量。

最后一个问题。由于被defer的函数的参数个数不定。defer无法将可变个数参数重组后传给被defer的函数。因此目前暂只能通过一种“丑陋”的方式来实现。样例中最多只支持两个参数的被defer函数。

* 样例

首先看看我们的examples的主函数文件main.c。

#include <stdio.h>
#include <stdlib.h>
#include "defer.h"

int bar(int a, char *s) {
    printf("a = [%d], s = [%s]\n", a, s);
}

int main() {
    FILE *fp = NULL;
    fp = fopen("main.c", "r");
    if (!fp) return;
    defer(fclose, 1, fp);

    int *p = malloc(sizeof(*p));
    if (!p) return;
    defer(free, 1, p);

    defer(bar, 2, 13, "hello");
    return 0;
}

从这里我们可以看到defer的用法,但这不是重点,重点是实现。

有了上面的一些设计思路的阐述,下面的代码也就不难理解了。核心是defer.c。
/* defer.h */
typedef void (*defer_func)();

struct zero_params_func_ctx {
    defer_func df;
};

struct one_params_func_ctx {
    defer_func df;
    void *p1;
};

struct two_params_func_ctx {
    defer_func df;
    void *p1;
    void *p2;
};

struct defer_func_ctx {
    int params_count;
    union {
        struct zero_params_func_ctx zp;
        struct one_params_func_ctx op;
        struct two_params_func_ctx tp;
    } ctx;
};

void stack_push(struct defer_func_ctx *ctx);
struct defer_func_ctx* stack_pop();
int stack_top();

/* defer.c */
struct defer_func_ctx ctx_stack[10];
int top_of_stack = 0; /* stack top from 1 to 10 */

void stack_push(struct defer_func_ctx *ctx) {
    if (top_of_stack >= 10) {
        return;
    }

    ctx_stack[top_of_stack] = *ctx;
    top_of_stack++;
}

struct defer_func_ctx* stack_pop() {
    if (top_of_stack == 0) {
        return NULL;
    }

    top_of_stack–;
    return &ctx_stack[top_of_stack];
}

int stack_top() {
    return top_of_stack;
}

void defer(defer_func fp, int arg_count, …) {
    va_list ap;
    va_start(ap, arg_count);

    struct defer_func_ctx ctx;
    memset(&ctx, 0, sizeof(ctx));
    ctx.params_count = arg_count;

    if (arg_count == 0) {
        ctx.ctx.zp.df = fp;

    } else if (arg_count == 1) {
        ctx.ctx.op.df = fp;
        ctx.ctx.op.p1 = va_arg(ap, void*);

    } else if (arg_count == 2) {
        ctx.ctx.tp.df = fp;
        ctx.ctx.tp.p1 = va_arg(ap, void*);
        ctx.ctx.tp.p2 = va_arg(ap, void*);
        ctx.ctx.tp.df(ctx.ctx.tp.p1, ctx.ctx.tp.p2);
    }

    va_end(ap);
    stack_push(&ctx);
}

多个defer的FIFO调用顺序用一个固定大小的stack来实现。这里只是为了演示,所以stack实现的简单和固定些。

组装后的函数在funcexit.c中执行:

extern struct defer_func_ctx ctx_stack[10];

__attribute__((no_instrument_function))
void __cyg_profile_func_exit(void *this_fn, void *call_site) {
    struct defer_func_ctx *ctx = NULL;

    while ((ctx = stack_pop()) != NULL) {
        if (ctx->params_count == 0) {
            ctx->ctx.zp.df();
        } else if (ctx->params_count == 1) {
            ctx->ctx.op.df(ctx->ctx.op.p1);
        } else if (ctx->params_count == 2) {
            ctx->ctx.tp.df(ctx->ctx.tp.p1, ctx->ctx.tp.p2);
        }
    }
}

最后我们将defer.c、funcexit.c编译成一个.so文件:

gcc -g -fPIC -shared -o libcdefer.so funcexit.c defer.c

而编译main.c的方法如下:

gcc -g main.c -o main -finstrument-functions -I ../lib -L ../lib -lcdefer

一切OK后,先将libcdefer.so放在main同级目录下,执行main即可。

$> ./main
a = [13], s = [hello]

具体代码已经传至这里(trunk/cdefer),需要的童鞋可自行下载。 

升级到Ubuntu 12.04LTS

Ubuntu 10.04 LTS已经伴随我两年了,经过我这么长时间的折腾,Ubuntu早已不堪重负^_^。在未升级前,Ubuntu 10.04已经表现出诸多问题:

- 在家中连接无线路由器时间漫长,且经常掉线;
- 在公司用有线网络经常掉线;
- 由于反复安装软件,系统中残留较多垃圾数据;
- Ubuntu 10.04官方源中的软件版本都有些低,很多软件手工安装高版本比较费力;

另外原先与Ubuntu 10.04共存的Windows 7系统已经早在大半年前就罢工了,无法引导进入,原因不明,我也懒得去fix,平时根本也用不到Windows系统。因此这次升级系统还有另外一个目的, 那就是将Windows 7的残余数据彻底清除出我的本本。

虽然Ubuntu最新版本是刚刚发布不久的12.10,但本着只用LTS版的原则,这次打算升级12.04 LTS,目前的最新版本是12.04.1。

原以为我的老旧的ThinkPad X60可以安装64位的12.04,但在安装时引导程序提示X60的CPU不是X86-64类型的,而是一颗双核的i686 CPU。恼火啊!下载和刻录一个iso容易吗,尤其在公司这个代理网络里!无奈只能重新折腾,重新下载和刻录32位的Ubuntu 12.04.1。

安装方法这里不赘述了。这次在安装时我使用了安装界面上可选的自定义安装分区的方法将12.04安装到了原Windows 7的分区中了,但安装结束重启后,Grub2的引导初始页面居然依旧显示以前的系统菜单,并且菜单中并没有我新装的12.04菜单项。重新安装,这次格掉 了原Ubuntu 10.04的安装分区。经过漫长等待后重启机器,映入眼帘的是"grub rescue>",引导再次失败,显而易见,Grub2依旧没有找到正确的引导分区。

Google了一把,原来是我对Grub2的引导原理理解还不够,Grub2是两阶段引导。直接格式化原有分区并安装新系统并未重新刷新 MBR(主引导记录)中的第二阶段引导分区的id,因此机器启动后,MBR依旧按原有的配置去寻找那个分区ID,但装有Ubuntu的分区ID已 经发生了变化,原引导分区被重新格式化并且无系统,因此Grub2无法找到分区,无法开启第二阶段引导。

无奈只能使用livecd,进入terminal,执行如下命令(ubuntu 12.04安装在sda1):
> sudo mount /dev/sda1 /mnt
> sudo grub-install –boot-directory=/mnt/boot  /dev/sda

再次重启后,系统引导正常,终于可以进入12.04了。网上说利用grub rescue命令也可以刷新MBR记录,不过我没能试验成功。

不同Ubuntu的配置过程大同小异,我早已轻车熟路了:

- 添两个源:搜狐和网易的ubuntu 12.04的源,然后更新软件包列表;
- 打开更新管理器,设置首选软件源;
- 打开“语言支持”,下载和更新语言包;
- 安装Google Chrome、Vim、iptux、rdesktop、Filezilla、subversion、htop、git、golang、apache2、 parcellite等工具;
- Thunderbird配置恢复(Ubuntu 12.04已经将thunderbird作为默认mail客户端);
- 恢复用户配置,包括.bashrc、模板、vim配置和插件等;
- 恢复hosts、apache2等配置;

Ubuntu演进到今天,对中文的支持已经很好了。默认情况下的iBus拼音已经很好用了。更新完语言包后,输入法变成SunPinyin,用起 来的确比小企鹅输入法智能多了。

Ubuntu默认的桌面环境是自行开发的Unity,至少目前感觉还行,其Dash程序启动器比较好用,基本可以替代原先在Gnome下用的 launchy。不过对于我用的X60 12寸普通屏幕(非宽屏)来讲,左边的Dock启动栏显然占据了应用本已不大的界面空间。

Ubuntu 12.04配置与应用安装时遇到了两个问题,这里做个分享和备忘:

1、ext3分区自动挂载以及权限问题

这次安装时,原安装ubuntu 10.04的分区被重新格式化了,但并未挂载目录。系统启动后,该分区未被自动挂载,只能手动挂载。于是尝试通过修改/etc/fstab自动挂载该ext3分区。

root下建立/home1目录,在/etc/fstab中添加一行,将该分区自动挂载到/home1:

# / was on /dev/sda3 during installation
UUID=1ed84fc1-5ba2-4e82-94f5-c3e4f5654036 /home1          ext3    defaults,errors=remount-ro 0       0

重启后,该分区如预期一样被自动挂载。但有出现了新问题,该分区下无法用普通用户权限创建文件,也就是没有写权限。反复改了几次fstab中的挂载参数, 都无法解决。后想到既然分区已经挂载到了/home1目录,那修改/home1目录的权限是否可以解决这个问题呢?于是sudo chmod 777 /home1。命令执行完后重启。新分区自动挂载,并可写了。

2、恢复iptux默认配置

部门都用飞秋作为内部IM工具。Linux下的feiq协议兼容工具是iptux。Ubuntu 12.04下用apt-get就可以正确安装iptux,运行也一切OK。但我在配置iptux时,无意中选择了“启动后主面板自动隐藏”,导致始终无法 看到iptux主界面,也就无法发送消息。于是开始尝试恢复iptux的默认配置。

直接上方法:
- 后台杀掉iptux;
- cd ~/.gconf/apps/iptux
- 删除iptux配置文件
- 执行gconftool-2 –recursive-unset /apps/iptux

注意如果不用上面方法,即便是卸载再重装iptux也是无济于事的。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats