24 C API 概述

Lua 和 C 的交互有两种形式:

  • C 拥有控制权,Lua 是库,此时 C 称为应用程序代码
  • Lua 拥有控制权,C 是库,此时 C 称为库代码

应用程序代码和库代码使用同样的 API 来与 Lua 通信,这些 API 称为 C API,遵循 C 的操作模式

Lua 和 C 通信的主要方法是一个无所不在的虚拟栈

第一个示例

一个最原始的解释器程序:

#include <stdio.h> 
#include <string.h>
extern "C" {
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
}

int main(void)
{
	char buff[256];
	int error;
	lua_State *L = luaL_newstate(); // 打开Lua
	luaL_openlibs(L); // 打开标准库
	while (fgets(buff, sizeof(buff), stdin) != NULL) {
		error = luaL_loadbuffer(L, buff, strlen(buff), "line")
			|| lua_pcall(L, 0, 0, 0);
		if (error) {
			fprintf(stderr, "%s", lua_tostring(L, -1));
			lua_pop(L, 1); // 从栈中弹出错误消息
		}
	}
	lua_close(L);
	return 0;
}

lua.h 定义了 Lua 提供的基础函数,包括创建 Lua 环境、调用 Lua 函数等等,lua.h 中定义的所有内容都有 lua_ 前缀

luaxlib.h 定义了辅助库提供的函数,所有定义都有前缀 luaL_。辅助库使用 lua.h 中的 API 编写出较高的抽象层,侧重于解决具体的任务。

Lua 库没有定义任何全局变量,所有的状态都保存在动态结构 lua_State 中,所有的 C API 都要求传入一个指向该结构的指针。luaL_newstate 函数用于创建一个新环境,该环境没有任何预定义的函数,甚至没有 print,luaxlib.h 中定义了打开这些库的函数,luaL_openlibs 可以打开所有的标准库

luaL_loadbuffer 用于编译用户输入的每行内容,如果没有错误,就返回 0,并向栈中压入编译后的程序块

lua_pcall 将程序块从栈中弹出,在保护模式下运行程序块。如果有错误,就向虚拟栈中压入一条错误消息。如果有错,可以在打印后从栈中弹出错误消息

Lua API 使用虚拟栈来在 Lua 和 C 之间交换数据。栈中的每个元素都能保存任何类型的 Lua 值

压入元素

LUA_API void  (lua_pushnil) (lua_State *L);
LUA_API void  (lua_pushboolean) (lua_State *L, int b);
// 默认双精度浮点数
LUA_API void  (lua_pushnumber) (lua_State *L, lua_Number n);
LUA_API void  (lua_pushinteger) (lua_State *L, lua_Integer n);
// 任意字符串
LUA_API void  (lua_pushlstring) (lua_State *L, const char *s, size_t l);
// 零结尾的字符串
LUA_API void  (lua_pushstring) (lua_State *L, const char *s);

压入元素时应该确保栈中有足够的空间。虚拟栈至少有 20 个空闲的槽,如果需要更多空间,可以用 lua_checkstack 来检查(要求增加):

int lua_checkstack (lua_State *L, int extra);

Ensures that there are at least extra free stack slots in the stack. It returns false if it cannot grow the stack to that size. This function never shrinks the stack; if the stack is already larger than the new size, it is left unchanged.

查询元素

例:lua_tostring(L, -1) 会将栈顶的值作为字符串返回。索引 1 表示第一个压入栈的(栈底)元素,-1 表示栈顶元素。

lua_is* 可以检查元素是否为特定类型,例:lua_isboolean(L, index)lua_isnumberlua_isstring 会检查元素是否能被转换为数字或者字符串类型。所以 lua_isstring 对任意数字都返回真

每个类型对应一个常量:

/*
** basic types
*/
#define LUA_TNONE		(-1)

#define LUA_TNIL		0
#define LUA_TBOOLEAN		1
#define LUA_TLIGHTUSERDATA	2
#define LUA_TNUMBER		3
#define LUA_TSTRING		4
#define LUA_TTABLE		5
#define LUA_TFUNCTION		6
#define LUA_TUSERDATA		7
#define LUA_TTHREAD		8

lua_to*(L, index) 函数从栈中获取一个值,如果类型不正确会返回 0 或者 NULL

P213 的例子实现了一个辅助函数,用于打印整个栈的内容

其他栈操作

/*
** basic stack manipulation
*/
// 返回栈中元素个数(栈顶的索引)
LUA_API int   (lua_gettop) (lua_State *L);
// 设置栈顶位置,支持负数索引,以下是一个基于此提供的宏
// define lua_pop(L, n) lua_settop(L, -(n) - 1)
LUA_API void  (lua_settop) (lua_State *L, int idx);
// 指定索引上元素的值,将其压入栈
LUA_API void  (lua_pushvalue) (lua_State *L, int idx);
// 删除指定索引的元素,上面的元素会依次下压
LUA_API void  (lua_remove) (lua_State *L, int idx);
// 在指定的位置插入元素,上面的元素会依次上升
LUA_API void  (lua_insert) (lua_State *L, int idx);
// 弹出栈顶的值,设置到指定索引上
LUA_API void  (lua_replace) (lua_State *L, int idx);
LUA_API int   (lua_checkstack) (lua_State *L, int sz);

LUA_API void  (lua_xmove) (lua_State *from, lua_State *to, int n);

API 提供了以上这些用于普通栈操作的函数,P215 进行了演示

C API 中的错误处理

应用程序代码中的错误处理

如果 Lua 发现错误,会调用紧急函数(Panic Function),用户可以通过函数 lua_atpanic 来设置自己的紧急函数

如果发生内存分配错误,又不想结束程序,可以在紧急函数中调用 longjump 转到之前 setjump 的地方;或者使用 lua_pcall 让代码在保护环境中运行

如果要保护和 Lua 交互的 C 代码,可以用 lua_cpcall,类似于 lua_pcall,但接受 C 函数为参数

库代码中的错误处理

当为 Lua 编写库函数时,只有一种标准的错误处理方法:调用 lua_error,lua_error 会清理 Lua 中所有需要清理的东西,然后跳转回 lua_pcall,附上错误消息

25 扩展应用程序

本章介绍如何用 Lua 配置一个程序

基础

以下代码演示了从 windowData.lua 中读取 Lua 中的全局变量 width 和 height 的值:

#include <stdio.h> 
#include <string.h>
#include <stdlib.h>
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}

void load(lua_State *L, const char *fname, int *w, int *h);
void error(lua_State *L, const char *fmt, ...);

int main(void)
{
	lua_State *L = luaL_newstate(); // 打开Lua
	luaL_openlibs(L); // 打开标准库

	const char filename[] = "windowData.lua";
	int width, height;
	load(L, filename, &width, &height);
	printf("width = %d, height = %d\n", width, height);

	lua_close(L);
	getchar();
	return 0;
}

void load(lua_State *L, const char *fname, int *w, int *h) {
	if (luaL_loadfile(L, fname) || lua_pcall(L, 0, 0, 0))
		error(L, "cannot run config. file: %s", lua_tostring(L, -1));
	lua_getglobal(L, "width");
	lua_getglobal(L, "height");
	if (!lua_isnumber(L, -2))
		error(L, "'width' should be a number\n");
	if (!lua_isnumber(L, -1))
		error(L, "'height' should be a number\n");
	*w = lua_tointeger(L, -2);
	*h = lua_tointeger(L, -1);
}

void error(lua_State *L, const char *fmt, ...) {
	va_list argp;
	va_start(argp, fmt);
	vfprintf(stderr, fmt, argp);
	va_end(argp);
	lua_close(L);
	exit(EXIT_FAILURE);
}

table 操作

-- windowData.lua
width = 800
height = 600
background = {r = 0.3, g = 0.1, b = 0}
BLUE = {r = 0, g = 0, b = 1}
background = BLUE

从 Lua 文件中读取 table:

#include <stdio.h> 
#include <string.h>
#include <stdlib.h>
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}

void load(lua_State *L, const char *fname);
double getField(lua_State *L, const char *key);
void error(lua_State *L, const char *fmt, ...);

int main(void)
{
	lua_State *L = luaL_newstate(); // 打开Lua
	luaL_openlibs(L); // 打开标准库

	const char filename[] = "windowData.lua";
	load(L, filename);

	lua_close(L);
	getchar();
	return 0;
}

void load(lua_State *L, const char *fname) {
	if (luaL_loadfile(L, fname) || lua_pcall(L, 0, 0, 0))
		error(L, "cannot run config. file: %s", lua_tostring(L, -1));
	lua_getglobal(L, "background");
	if (!lua_istable(L, -1))
		error(L, "'background' should be a table\n");
	double r = getField(L, "r");
	double g = getField(L, "g");
	double b = getField(L, "b");
	printf("background = (%g, %g, %g)\n", r, g, b);
}

// 假设要读取的table位于栈顶
double getField(lua_State *L, const char *key) {
	double ret;
	lua_pushstring(L, key);
	lua_gettable(L, -2);
    // 以上两行可以写成 lua_getfield(L, -1, key);
	if (!lua_isnumber(L, -1))
		error(L, "invalid component in background color");
	ret = lua_tonumber(L, -1);
	lua_pop(L, 1);
	return ret;
}

void error(lua_State *L, const char *fmt, ...) {
	va_list argp;
	va_start(argp, fmt);
	vfprintf(stderr, fmt, argp);
	va_end(argp);
	lua_close(L);
	exit(EXIT_FAILURE);
}

P222:类似于 lua_gettablelua_getfieldlua_settablelua_setfield 可以设置 table 中的字段

P223:lua_newtable(L) 可以新建一个空的 table 压入栈中,lua_setglobal(L, name) 可以为 Lua 赋予名为 name 的全局变量

调用 Lua 函数

-- windowData.lua
function sumOfSquares(...)
  local ret = 0
  local args = {...}
  for k in pairs(args) do
    ret = ret + k * k
  end
  return ret
end

调用 Lua 函数:

#include <stdio.h> 
#include <string.h>
#include <stdlib.h>
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}
#include <vector>

using std::vector;

double luaSumOfSquares(lua_State *L, const char *fname, const vector<double>& ops);
double getField(lua_State *L, const char *key);
void error(lua_State *L, const char *fmt, ...);

int main(void)
{
	lua_State *L = luaL_newstate(); // 打开Lua
	luaL_openlibs(L); // 打开标准库

	const char filename[] = "windowData.lua";
	vector<double> ops{ 1.0, 2.0, 3.0 };
	double res = luaSumOfSquares(L, filename, ops);
	printf("res = %g\n", res);

	lua_close(L);
	getchar();
	return 0;
}

double luaSumOfSquares(lua_State *L, const char *fname, const vector<double>& ops) {
	if (luaL_loadfile(L, fname) || lua_pcall(L, 0, 0, 0))
		error(L, "cannot run config. file: %s", lua_tostring(L, -1));
	lua_getglobal(L, "sumOfSquares");
	if (!lua_isfunction(L, -1))
		error(L, "'sumOfSquares' should be a function\n");
	lua_checkstack(L, ops.size());
	for (const auto op : ops)
		lua_pushnumber(L, op);
	if (lua_pcall(L, ops.size(), 1, 0) != 0)
		error(L, "error running function 'sumOfSquares'");
	double ret = lua_tonumber(L, -1);
	return ret;
}

void error(lua_State *L, const char *fmt, ...) {
	va_list argp;
	va_start(argp, fmt);
	vfprintf(stderr, fmt, argp);
	va_end(argp);
	lua_close(L);
	exit(EXIT_FAILURE);
}

一个通用的调用函数

#include <stdio.h> 
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}

void error(lua_State *L, const char *fmt, ...);
void call_va(lua_State *L, const char *func, const char *sig, ...);

int main()
{
	lua_State *L = luaL_newstate(); // 打开Lua
	luaL_openlibs(L); // 打开标准库

	const char filename[] = "windowData.lua";
	if (luaL_loadfile(L, filename) || lua_pcall(L, 0, 0, 0))
		error(L, "cannot run config. file: %s", lua_tostring(L, -1));

	double res;
	call_va(L, "sumOfSquares", "ddd>d", 1.0, 2.0, 3.0, &res);
	printf("res = %g\n", res);

	lua_close(L);
	getchar();
	return 0;
}

void error(lua_State *L, const char *fmt, ...) {
	va_list argp;
	va_start(argp, fmt);
	vfprintf(stderr, fmt, argp);
	va_end(argp);
	lua_close(L);
	exit(EXIT_FAILURE);
}

void call_va(lua_State *L, const char *func, const char *sig, ...) {
	va_list vl;
	int narg, nres;
	va_start(vl, sig);
	lua_getglobal(L, func);

	// 压入参数
	for (narg = 0; *sig; ++narg) {
		luaL_checkstack(L, 1, "too many arguments");
		switch (*sig++) {
		case 'd':
			lua_pushnumber(L, va_arg(vl, double));
			break;
		case 'i':
			lua_pushinteger(L, va_arg(vl, int));
			break;
		case 's':
			lua_pushstring(L, va_arg(vl, char*));
			break;
		case '>':
			goto endargs;
		default:
			error(L, "invalid option (%c)", *(sig - 1));
		}
	}
	endargs:

	// 完成调用
	nres = strlen(sig);
	if (lua_pcall(L, narg, nres, 0) != 0)
		error(L, "error calling '%s': $s", func, lua_tostring(L, -1));

	// 检索结果
	nres = -nres;
	while(*sig) {
		switch (*sig++) {
		case 'd':
			if (!lua_isnumber(L, nres))
				error(L, "wrong result type");
			*va_arg(vl, double*) = lua_tonumber(L, nres);
			break;
		case 'i':
			if (!lua_isnumber(L, nres))
				error(L, "wrong result type");
			*va_arg(vl, int*) = lua_tointeger(L, nres);
			break;
		case 's':
			if (!lua_isstring(L, nres))
				error(L, "wrong result type");
			*va_arg(vl, const char **) = lua_tostring(L, nres);
			break;
		default:
			error(L, "invalid option (%c)", *(sig - 1));
		}
		++nres;
	}

	va_end(vl);
}

注意无须检查 func 是否为一个函数,lua_pcall 会发现这类错误。

26 从 Lua 调用 C

C 函数

-- windowData.lua
function sumOfSquares(a, b)
  return cpp_sumOfSquares(a, b)
end

以下程序在 Lua 中注册 C 中定义的函数 l_sumOfSquares,并显示 Lua 调用 C 函数的结果

// main.cpp
#include <stdio.h> 
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}

static int l_sumOfSquares(lua_State *L) {
	double a = lua_tonumber(L, 1);
	double b = lua_tonumber(L, 2);
	lua_pushnumber(L, a * a + b * b);
	return 1;
}

void error(lua_State *L, const char *fmt, ...) {
	va_list argp;
	va_start(argp, fmt);
	vfprintf(stderr, fmt, argp);
	va_end(argp);
	lua_close(L);
	exit(EXIT_FAILURE);
}

void call_va(lua_State *L, const char *func, const char *sig, ...) {
	va_list vl;
	int narg, nres;
	va_start(vl, sig);
	lua_getglobal(L, func);

	// 压入参数
	for (narg = 0; *sig; ++narg) {
		luaL_checkstack(L, 1, "too many arguments");
		switch (*sig++) {
		case 'd':
			lua_pushnumber(L, va_arg(vl, double));
			break;
		case 'i':
			lua_pushinteger(L, va_arg(vl, int));
			break;
		case 's':
			lua_pushstring(L, va_arg(vl, char*));
			break;
		case '>':
			goto endargs;
		default:
			error(L, "invalid option (%c)", *(sig - 1));
		}
	}
endargs:

	// 完成调用
	nres = strlen(sig);
	if (lua_pcall(L, narg, nres, 0) != 0)
		error(L, "error calling '%s': $s", func, lua_tostring(L, -1));

	// 检索结果
	nres = -nres;
	while (*sig) {
		switch (*sig++) {
		case 'd':
			if (!lua_isnumber(L, nres))
				error(L, "wrong result type");
			*va_arg(vl, double*) = lua_tonumber(L, nres);
			break;
		case 'i':
			if (!lua_isnumber(L, nres))
				error(L, "wrong result type");
			*va_arg(vl, int*) = lua_tointeger(L, nres);
			break;
		case 's':
			if (!lua_isstring(L, nres))
				error(L, "wrong result type");
			*va_arg(vl, const char **) = lua_tostring(L, nres);
			break;
		default:
			error(L, "invalid option (%c)", *(sig - 1));
		}
		++nres;
	}

	va_end(vl);
}

int main()
{
	lua_State *L = luaL_newstate(); // 打开Lua
	luaL_openlibs(L); // 打开标准库

	// 注册C函数
	lua_pushcfunction(L, l_sumOfSquares);
	lua_setglobal(L, "cpp_sumOfSquares");

	// C调用Lua
	const char filename[] = "windowData.lua";
	if (luaL_loadfile(L, filename) || lua_pcall(L, 0, 0, 0))
		error(L, "cannot run config. file: %s", lua_tostring(L, -1));

	// 显示Lua调用C的结果
	double res;
	call_va(L, "sumOfSquares", "dd>d", 1.0, 2.0, &res);
	printf("res = %g\n", res);

	lua_close(L);
	getchar();
	return 0;
}

C 模块

Lua 模块是一个程序块,其中定义了一些 Lua 函数,通常被存储为 table 条目。而一个为 Lua 编写的 C 模块可以模仿这种行为。

通常 C 模块只有一个公共(外部)函数,其他所有函数都是私有的,在 C 中声明为 static

以下为手动编写 dll 库,并在 Lua 中调用 dll 的方法

生成 dll

在 VS2017 中新建 Win32 控制台程序,并在向导中选择 dll

// myMathLib.cpp : 定义 DLL 应用程序的导出函数。
//
#include "stdafx.h"
#include <Windows.h>

#include <stdio.h> 
#include <string.h>
#include <stdlib.h>
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}

static void error(lua_State *L, const char *fmt, ...) {
	va_list argp;
	va_start(argp, fmt);
	vfprintf(stderr, fmt, argp);
	va_end(argp);
	lua_close(L);
	exit(EXIT_FAILURE);
}

static int l_add(lua_State *L) {
	double a = lua_tonumber(L, 1);
	double b = lua_tonumber(L, 2);
	lua_pushnumber(L, a + b);
	return 1;
}

static int l_sub(lua_State *L) {
	double a = lua_tonumber(L, 1);
	double b = lua_tonumber(L, 2);
	lua_pushnumber(L, a - b);
	return 1;
}

static int l_mul(lua_State *L) {
	double a = lua_tonumber(L, 1);
	double b = lua_tonumber(L, 2);
	lua_pushnumber(L, a * b);
	return 1;
}

static int l_div(lua_State *L) {
	double a = lua_tonumber(L, 1);
	double b = lua_tonumber(L, 2);
	if (b == 0.0)
		error(L, "cannot divide zero");
	lua_pushnumber(L, a / b);
	return 1;
}

static const struct luaL_Reg myMathLib[] = {
	{ "add", l_add },
	{ "sub", l_sub },
	{ "mul", l_mul },
	{ "div", l_div },
	{ NULL, NULL }
};

// 因为用cpp编写dll,要加上extern "C"
// __declspec(dllexport)用于标识dll的导出
// luaopen_后面的函数名要和上面的结构名大小写完全一致
extern "C" int __declspec(dllexport)
luaopen_myMathLib(lua_State *L) {
	luaL_register(L, "myMathLib", myMathLib);
	return 1;
}

在解决方案上右键 -> 重新生成,在文件夹中(我在 Debug 模式下生成就在 Debug 文件夹下面寻找)找到 myMathLib.dll,复制粘贴到 Lua 调用的路径下(任意路径皆可,我放到了 Lua 源文件的文件夹)

Lua 调用 dll

-- 把dll存放的位置添加到cpath路径
package.cpath = "C:\\Users\\Administrator\\Desktop\\LuaTests\\?.dll;" .. package.cpath
-- 导入dll
myMathLib = require "myMathLib"
-- 测试
for k, v in pairs(myMathLib) do
  print(k, v)
end
print(myMathLib.add(1, 3))
print(myMathLib.sub(1, 3))
print(myMathLib.mul(1, 3))
print(myMathLib.div(1, 3))
print(myMathLib.div(1, 0))