TimothyQiu's Blog

keep it simple stupid

Lua 学习笔记:面向对象

没错,Lua 中只存在表(Table)这么唯一一种数据结构,但依旧可以玩出面向对象的概念。

添加成员函数

好吧,如果熟悉 C++ 还是很好理解类似的演化过程的:如果说 struct 里可以添加函数是从 C 过渡到 C++ 的第一认识的话,为 Table 添加函数也可以算是认识 Lua 是如何面向对象的第一步吧。

player = { health = 200 }  --> 一个普通的 player 表,这里看作是一个对象
function takeDamage(self, amount)
  self.health = self.health - amount
end

takeDamage(player, 20)  --> 调用

如何将独立的 takeDamage 塞进 player 中咧?答案是直接定义进去:

player = { health = 200 }
function player.takeDamage(self, amount)
  self.health = self.health - amount
end

player.takeDamage(player, 20)  --> 调用

这样就相当于在 player 表中添加了一个叫做 takeDamage 的字段,和下面的代码是一样的:

player = {
  health = 200,
  takeDamage = function(self, amount)  --> Lua 中的函数是 first-class value
    self.health = self.health - amount
  end
}

player.takeDamage(player, 20)  --> 调用

调用时的 player.takeDamage(player, 20) 稍显不和谐(据说用术语叫做 DRY),于是就要出动「冒号操作符」这个专门为此而生的语法糖了:

player:takeDamage(20)              --> 等同于 player.takeDamage(player, 20)
function player:takeDamage(amount) --> 等同于 function player.takeDamage(self, amount)

从对象到类

类的意义在于提取一类对象的共同点从而实现量产(我瞎扯的 >_<)。同样木有 Class 概念的 Javascript 使用 prototype 实现面向对象,Lua 则通过 Metatable 实现与 prototype 类似的功能。

Player = {}

function Player:create(o)    --> 参数 o 可以暂时不管
  o = o or { health = 200 }  --> Lua 的 or 与一般的 || 不同,如果非 nil 则返回该非 nil 值
  setmetatable(o, self)
  self.__index = self
  return o
end

function Player:takeDamage(amount)
  self.health = self.health - amount
end

playerA = Player:create()  --> 参数 o 为 nil
playerB = Player:create()

playerA:takeDamage(20)
playerB:takeDamage(40)

顾名思义 Metatable 也是一个 Table,可以通过在其中存放一些函数(称作 metamethod)从而修改一些默认的求值行为(如何显示为字符串、如何相加、如何连接、如何进行索引)。Metatable 的 __index 域设置了「如何进行索引」的方法。例如调用 foo.bar 时,如果在 foo 中没有找到名为 bar 的域时,则会调用 Metatable:__index(foo, bar)。于是:

playerA:takeDamage(20)

因为在 playerA 中并不存在 takeDamge 函数,于是求助于 Metatable:

getmetatable(playerA).__index.takeDamage(playerA, 20)

带入 Metatable 后:

Player.__index.takeDamage(playerA, 20)

因为 Player__indexcreate 时被指定为 self,所以最终变为:

Player.takeDamage(playerA, 20)

于是 takeDamageself 得到了正确的对象 playerA

继承

继承是面向对象的一大特性,明白了如何创建「类」,那么继承也就比较明了了,还记得大明湖畔的参数 o 么?

RMBPlayer = Player:create()
function RMBPlayer:broadcast(message)  --> 为子类添加新的方法
  print(message)
end
function RMBPlayer:takeDamage(amount)  --> 子类重载父类方法
  self.health = self.health - amount / (self.money / 100)
end

vip = RMBPlayer:create { money = 200 } --> 子类添加新成员(单个 Table 作为参数可以省略括号)

vip:takeDamage(20)
vip:broadcast("F*ck")

以上便是 Lua 中实现面向对象的基本方法。

MinGW 下编译 libcurl

libcurl 是鼎鼎大名的开源客户端 URL 传输库,支持 FTP、HTTP 以及其它很多乱七八糟的协议。在各种语言上的实现也很多:C、C++、Lua、Java、Pascal、Perl、PHP、Python、Ruby、Visual Basic……。这里说最常用的 C 语言实现,环境是 Windows 7 + MinGW32。

下载 libcurl 源代码

libcurl 可以在官网 http://curl.haxx.se/ 获得。Download 页有源代码和为各平台预编译的二进制文件(curl 程序)和开发包(include + lib + doc)下载。不过我想要的 MinGW32 开发包的链接失效了 = = 就下载个源代码包自己编译吧。

在 Download 页的 Source Archives 栏里有最新的 curl 7.27.0 版本源代码压缩包,下载 curl-7.27.0.zip 文件。

编译 libcurl 库

  1. 解压 curl-7.27.0.zip 文件
  2. 通过命令提示符进入 curl-7.27.0 文件夹
  3. 输入 mingw32-make mingw32 进行生成(这里我只需要普通的功能,于是没有加附加的选项)

编译完成后,在 lib 文件夹中会有我们需要的三个文件。

接下来,可以进入 docs/examples 文件夹,测试编译一些示例程序。可以直接用 Makefile.m32 文件,也可以手动一个个用 gcc 编译。

动态链接:

gcc -I../../include -L../../lib simple.c -lcurldll 

静态链接:

gcc -I../../include -L../../lib simple.c -DCURL_STATICLIB -lcurl -lws2_32 -lwldap32 

动态链接编译出的程序运行时依赖 libcurl.dll。静态链接参数中的 ws2_32 是 Windows Socket 2 库,wldap32 是微软的 Lightweight Directory Access Protocol API 库。

一般使用流程

基本的 curl 程序主要分四步:

  1. curl_easy_init 创建 CURL 对象
  2. curl_easy_setopt 设置操作选项
  3. curl_easy_perform 进行操作
  4. curl_easy_cleanup 销毁 CURL 对象

最简单的示例可以看 doc/examples 下的 simple.c 文件,演示如何将 curl 主页内容输出到 stdout。

Markdown 的链接

换到 Typecho 的其中一个原因就是简洁,以至于撰写界面完全没有编辑器,就是一个 <textarea> 放在那里。当然,如果仅仅如此的话,为了排下版就得手动写 HTML 写得眼花缭乱真有点扛不住,于是就找了个 Markdown 插件来用。

由于之前略懂过一下 Markdown,隐约记得插链接的完整语法是相对来说比较复杂的:

[链接文字](URL "可选的提示文本")

举个例子应用到文章中可能就变成了:

自从2011年5月改版以来,[B站](http://bilibili.tv "Bilibili")采用了和[A站](http://acfun.tv "AcFun")不同的视频推荐方式。

至少在编辑界面看着还是略瞎的。于是,仔细看了 Markdown Readme 后发现上面这个语法是内联式(inline)插入,Markdown 中插入链接还另有一个引用式(reference)链接:

[链接文字][可选的标识]
[可选的标识或链接文字]: URL "可选的提示文本"

改写一下之前的例子就是:

自从2011年5月改版以来,[B站][]采用了和[A站][]不同的视频推荐方式。
[A站]: http://acfun.tv "AcFun"
[B站]: http://bilibili.tv "Bilibili"

或者

自从2011年5月改版以来,[B站][1]采用了和[A站][2]不同的视频推荐方式。
[1]: http://acfun.tv "AcFun"
[2]: http://bilibili.tv "Bilibili"

因为链接正文和链接定义两部分可以分开,类似于参考文献的格式,所以即便是纯文本方式下看着也很舒服。实际上,Markdown 的 Readme 里也是这么说的:

The point of reference-style links is not that they’re easier to write. The point is that with reference-style links, your document source is vastly more readable.

切换到 Typecho

呃~换到 Typecho 了 >.<

之前的 WordPress 因为 PDO (SQLite) For WordPress 插件的关系,最近一次升级后无法升级数据库结构,完全没法进后台了。折腾许久无果之后,趁国庆把整个博客转移到 Typecho 上。

鉴于原先的 WordPress 用了 PDO(SQLite) 插件,而且文章也不多,所以我还是手动迁移了数据。过程中最头大的还是 Typecho 的日期时间存放的是 Unix 时间,而 WordPress 使用的是纯文本格式。

呃……写完发现好像也没什么特别的呢……WordPress 之前有三篇草稿状态的文章,基本上也都是开始写了才发现没什么特别的,就搁置着,汗 = =

程序崩溃的善后工作

说来,写 C/C++ 的程序,由于指针的存在,程序崩溃什么的也就没什么大惊小怪的了。人非圣贤,孰能无过嘛,而且个人觉得程序崩溃比出现错误的结果好调试多了:在 Visual Studio 里 Debug 版本 F5 调试运行直接可以断在崩溃的地方,方便调试。但 Release 版本就没这么幸运了 :(

如果说单纯是是调试 Release 版本,我只用过《游戏之旅》中介绍的勾选 Linker 选项中的 Generate Map File,然后通过崩溃提示信息中提供的 EIP 查这个 Map File 找到崩在哪个函数里,兴致高一点的根据反汇编一步步走下去兴许还能知道是崩在哪句上 :)

不过说到最终交付出去的程序,面对可能存在的各种未知问题,还是生成 Dump 文件,把崩溃那一刻的信息写进文件以供日后分析比较靠谱。

捕捉未捕获的异常

好吧,Windows API 提供了 SetUnhandledExceptionFilter 函数来设置在发生未捕获的异常时调用的回调函数(仅在程序不处于调试运行时调用)。例如设置 CrashCallback 函数:

#include <windows.h>

LONG WINAPI CrashCallback(EXCEPTION_POINTERS *exceptionInfo)
{
    // 崩溃处理
    return EXCEPTION_EXECUTE_HANDLER;
}

int main()
{
    SetUnhandledExceptionFilter(CrashCallback);
    // 程序段
}

回调时传入的参数 exceptionInfo 保存了关于该异常的详细信息,不过 Dump 的输出可以不需要亲自干预太多。

生成 Dump 文件

利用上面回调中给出的 EXCEPTION_POINTERS 结构指针提供的信息,MiniDumpWriteDump 函数即可按要求输出一个 Dump 文件内容,其原型:

BOOL WINAPI MiniDumpWriteDump(
    HANDLE hProcess,         // Dump 目标进程句柄
    DWORD ProcessId,         // Dump 目标进行 ID
    HANDLE hFile,            // 输出文件句柄
    MINIDUMP_TYPE DumpType,  // 输出类型,决定输出哪些内容
    MINIDUMP_EXCEPTION_INFORMATION *ExceptionParam,
    MINIDUMP_USER_STREAM_INFORMATION *UserStreamParam,
    MINIDUMP_CALLBACK_INFORMATION *CallbackParam
);

输出 Dump 文件的内容根据 DumpType 参数变化,详见 MSDN 关于 MINIDUMP_TYPE 的条目。这里举例输出一个最小的 Dump 文件:

HANDLE hFile = CreateFile(TEXT("filename.dmp"), GENERIC_READ | GENERIC_WRITE,
    0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

if ((hFile != NULL) && (hFile != INVALID_HANDLE_VALUE)) {
    MINIDUMP_EXCEPTION_INFORMATION mdei;

    mdei.ThreadId          = GetCurrentThreadId();
    mdei.ExceptionPointers = exceptionInfo;
    mdei.ClientPointers    = FALSE;

    MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),
                      hFile, MiniDumpNormal, &mdei, NULL, NULL);
    CloseHandle(hFile);
}

需要注意的是,MiniDumpWriteDump 等声明在 DbgHelp.h 中,需要链接 DbgHelp.lib。当然也可以自行从 DbgHelp.dllLoadLibrary 之。

Dump 文件的使用

Dump 文件是可以用 WinDbg 打开的,不过因为手头没有这东西所以没有试过 =3=

Dump 文件也可以用 Visual Studio 打开,而且(似乎)方便一些:把 dmp 文件、exe 文件、pdb 文件放在同一目录中,然后用 Visual C++ 打开 dmp 文件即出现 Minidump File Summary 页面。可以查看异常信息,或者使用右侧的调试按钮开始调试运行并直接断在崩溃处。

以上,就是好久以来的流水帐……