Lua与C的交互

Lua是一个嵌入式的语言,它不仅可以是一个独立运行的程序,也可以是一个用来嵌入其它应用的程序库。

C API是一个C代码与Lua进行交互的函数集,它由以下几部分构成:

1、  读写Lua全局变量的函数;

2、  调用Lua函数的函数;

3、  运行Lua代码片段的函数;

4、  注册C函数后可以在Lua中被调用的函数;

在C和LUA之间交互的关键在于一个虚拟栈(virtual stack),数据交互通过栈进行。操作数据时,首先将数据拷贝到栈上,然后获取数据,栈中的每个数据通过索引值进行定位,索引值为正时表示相对于栈底的偏移索引,索引值为负时表示相对于栈顶的偏移索引。索引值以1或 -1起始值,因此栈顶索引值永远为-1, 栈底索引值永远为1 。 "栈"相当于数据在Lua和C之间的中转地,每种数据都有相应的存取接口 。

另外,还可以使用栈来保存临时变量。栈的使用解决了C和LUA之间两个不协调的问题:

1、  Lua使用自动垃圾收集机制,而C要求显式的分配和释放内存;

2、  Lua使用动态数据类型,而C使用静态类型;

特别注意的是:

1、每当Lua调用C函数时,C函数会使用一个局部栈,这个局部栈与之前的栈,以及其它正在调用的C函数使用的栈都是相互独立的。Lua和C就使用这个局部的栈进行数据交互。

2、当Lua调用C时,栈至少包含LUA_MINSTACK(20)个位置,程序员也可以使用lua_checkstack函数来增加栈的大小。

3、使用伪索引(Pseudo-Indices)来表示一些不在栈中的数据,比如thread环境、C函数环境、registry、C闭包的upvalues。

thread环境(全局变量也在这里),使用伪索引 LUA_GLOBALSINDEX;

运行中的C函数环境,使用伪索引 LUA_ENVIRONINDEX;

Registry,使用伪索引 LUA_REGISTRYINDEX;

C闭包的upvalues,可以使用lua_upvalueindex(n)来访问第n个upvalue;

关于Registry:

在C里面如果函数要保存持久状态,只能依靠全局或static变量,但这样C API库就无法为多个LuaState状态同时提供服务(就类似于带有static变量的C函数是不可重入的)。为此Lua提供了一个名为Registry的预定义table,允许C API往Registry里面存储数据。

关于References:

Reference其实就是在一个指定的表t中保存一次lua的数据对象,Refenence本身其实就是表t的索引子,简称RefIndex,当RefIndex作为Refenence时,t

[RefIndex]其实就是用户要求引用的lua数据对象。当RefIndex被luaL_unref()回收时,t每一个被回收的RefIndex构成一个单向链表: t[Refindex] = Refindex0,

t[Refindex0] = Refindex1, t[Refindex1] = Refindex2 ... t[0] = Refindex。

注意,t[0]始终是指向了空闲链表的头部。每次调用luaL_ref()且回收链表为空时,都会产生一个新的Reference,每次调用luaL_unref()都会销毁一个指定的

Reference存入空闲链表中。

C API接口

类型声明

typedef double lua_Number;

typedef ptrdiff_t lua_Integer;

初始化lua状态机

lua_State* lua_open();

lua_State *lua_newstate (lua_Alloc f, void *ud);

lua_newstate 创建一个新的、独立的Lua状态机,如果因为内存不足导致创建失败,返回NULL。参数f 指定内存分配函数,参数ud是传给f 函数的指针。

lua_open 没有指定内存分配函数的功能,不建议再使用。

注意:lua_State表示的一个Lua程序的执行状态,它代表一个新的线程(注意是指Lua中的thread类型,不是指操作系统中的线程),每个thread拥有独立的数据

栈以及函数调用链,还有独立的调试钩子和错误处理方法。

销毁lua状态机

void lua_close(lua_State *L);

销毁Lua状态机的所有对象,回收分配的内存。

加载lua库

void luaL_openlibs(lua_State *L);

void luaopen_base(lua_State *L);

void luaopen_package(lua_State *L);

void luaopen_string(lua_State *L);

void luaopen_io(lua_State *L);

void luaopen_table(lua_State *L);

void luaopen_math(lua_State *L);

luaL_openlibs 在给定的Lua状态机中打开所有的标准Lua库;

编译/加载 lua代码

int luaL_dofile(lua_State *L, char *lua_script);

int luaL_dostring (lua_State *L, const char *str);

int lua_load (lua_State *L, lua_Reader reader, void *data,const char *chunkname);

int luaL_loadbuffer (lua_State *L, const char *buff, size_t sz, const char *name);

int luaL_loadfile (lua_State *L, const char *filename);

int luaL_loadstring (lua_State *L, const char *s);

luaL_dofile 加载并执行给定lua文件,成功返回0,错误返回1;

luaL_dostring 加载并执行给定string,成功返回0,错误返回1;

lua_load 加载一段chunk(但并不执行它),并将编译后的代码作为一个函数压入堆栈,如果发生错误,则将错误消息压入堆栈;

luaL_loadbuffer 从一个buffer中加载chunk;

luaL_loadfile从文件加载chunk;

luaL_loadstring从字符串加载chunk;

函数参数检查

void luaL_checkany (lua_State *L, int narg);

int luaL_checkint (lua_State *L, int narg);

lua_Integer luaL_checkinteger (lua_State *L, int narg);

long luaL_checklong (lua_State *L, int narg);

const char *luaL_checklstring (lua_State *L, int narg, size_t *l);

lua_Number luaL_checknumber (lua_State *L, int narg);

int luaL_checkoption (lua_State *L, int narg, const char *def, const char *const lst[]);

const char *luaL_checkstring (lua_State *L, int narg);

void luaL_checktype (lua_State *L, int narg, int t);

void *luaL_checkudata (lua_State *L, int narg, const char *tname);

luaL_checkany 检查函数有多少个(由narg指定)参数;

luaL_checktype 检查函数第narg个参数是否为指定类型;

luaL_checkudata 检查函数第narg个参数是否为name类型的userdata;

luaL_checkint/luaL_checkinterger 等接口非常类似:

以luaL_checkint为例,它是检查函数的第narg个参数类型是否number型,并且返回这个number型参数;

luaL_checkoption 检查函数第narg个参数是否位字符串类型,并且在lst[](字符串数组)中搜索这个字符串,最后返回匹配的数组下标,如未能匹配,引发一个

错误。如果参数def非空,当narg参数不存在或为nil时,就是要def作为搜索串。这个函数的作用是将字符串映射为C的enum类型。

table操作

void lua_createtable (lua_State *L, int narr, int nrec);

void lua_newtable (lua_State *L);

void lua_settable (lua_State *L, int index);

void lua_gettable (lua_State *L, int index);

lua_createtable 创建一个空table并压入堆栈,它会为narr个数组风格元素,和nrec个记录风格元素预分配内存空间。

lua_newtable 创建一个空table,并压入stack,等价于 lua_createtable(L, 0, 0);

lua_settable 相当于t[k]=v 操作,其中值t由参数index指定,v是栈顶,k是栈顶下一个元素;这个函数会将key和value都弹出栈;

lua_gettable 将t[k]压入堆栈,由参数index指定操作的table,k是栈顶元素。这个函数会弹出栈顶的key,并由t[k]代替;

metatable操作

int luaL_newmetatable (lua_State *L, const char *tname);

void luaL_getmetatable (lua_State *L, const char *tname);

int lua_getmetatable (lua_State *L, int index);

int lua_setmetatable (lua_State *L, int index);

int luaL_getmetafield (lua_State *L, int obj, const char *e);

luaL_newmetatable 在Registry中创建一个key为tname的metatable,并返回1;如果Registry 已经有tname这个key,则返回0;这两种情况都会将metatable压入

堆栈;

luaL_getmetatable 将Registry中key为tname的metatable压入堆栈;

luaL_getmetatable 将index处的元表压入堆栈;

lua_setmetatable 弹出栈顶,并将它作为由index指定元素的元表;

luaL_getmetafield 将索引obj处的元素的元表的e字段压入堆栈;

stack操作

void lua_pushboolean (lua_State *L, int b);

void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n);

void lua_pushcfunction (lua_State *L, lua_CFunction f);

const char *lua_pushfstring (lua_State *L, const char *fmt, ...);

void lua_pushinteger (lua_State *L, lua_Integer n);

void lua_pushlightuserdata (lua_State *L, void *p);

void lua_pushliteral (lua_State *L, const char *s);

void lua_pushlstring (lua_State *L, const char *s, size_t len);

void lua_pushnil (lua_State *L);

void lua_pushstring (lua_State *L, const char *s);

lua_pushfstring 将一个格式化字符串压入堆栈,并返回指向这个字符串的指针;

lua_pushlstring 将一个指定大小的字符串压入堆栈;

lua_pushvalue 将栈中指定索引的元素复制一份到栈顶;

值得注意的是,向栈中压入一个元素时,应该确保栈中具有足够的空间,可以调用lua_checkstack来检测是否有足够的空间。

实质上这些API是把C语言里面的值封装成Lua类型的值压入栈中的,对于那些需要垃圾回收的元素(如string、full userdata),在压入栈时,都会在Lua(也就是Lua虚拟机中)生成一个副本,从此不会再依赖于原来的C值。例如lua_pushstring 向栈中压入一个以'\0'结尾的字符串,在C中调用这个函数后,可以任意修改或释放这个字符串,也不会出现问题。

void lua_pop(lua_State *L, int n);

int lua_gettop (lua_State *L);

void lua_concat (lua_State *L, int n);

void lua_getfield (lua_State *L, int index, const char *k);

void lua_setfield (lua_State *L, int index, const char *k);

void lua_getglobal(lua_State *L, char *name);

void lua_setglobal (lua_State *L, const char *name);

void lua_insert (lua_State *L, int index);

void lua_remove (lua_State *L, int index);

void lua_replace (lua_State *L, int index);

int lua_next (lua_State *L, int index);

size_t lua_objlen (lua_State *L, int index);

void luaL_checkstack (lua_State *L, int sz, const char *msg);

lua_pop 从栈中弹出n个元素;

lua_gettop 返回栈顶元素的索引(也即元素个数);

lua_concat 将栈顶开始的n个元素连接起来,并将它们出栈,然后将结果入栈;

lua_getfield 将t[k]压入堆栈,t由参数index指定在栈中的位置;

lua_setfield 相当于t[k]=v,t由参数index指定在栈中的位置,v是栈顶元素,改函数会将栈顶的value出栈;

lua_getglobal(L,s) 等价于 lua_getfield(L, LUA_GLOBALSINDEX, s),注意:栈中LUA_GLOBALSINDEX索引位置处是当前Lua状态机的全局变量环境。

lua_setglobal(L,s) 等价于 lua_setfield(L, LUA_GLOBALSINDEX, s);

lua_insert 移动栈顶元素到index指定的位置;

lua_remove 移除index处的元素,index之上的元素均下移一个位置;

lua_replace 将栈顶元素移到指定位置,并取代原来的元素,原先的栈顶元素弹出;

lua_next 弹出一个key,然后将t[key]入栈,t是参数index处的table;在利用lua_next遍历栈中的table时,对key使用lua_tolstring尤其需要注意,除非知道

key都是string类型。

lua_objlen 返回index处元素的长度,对string,返回字符串长度;对table,返回"#"运算符的结果;对userdata,返回内存大小;其它类型返回0;

luaL_checkstack 增加栈大小(新增sz个元素的空间),如果grow失败,引发一个错误,msg参数传递错误消息。

int lua_isboolean (lua_State *L, int index);

int lua_iscfunction (lua_State *L, int index);

int lua_isfunction (lua_State *L, int index);

int lua_islightuserdata (lua_State *L, int index);

int lua_isnil (lua_State *L, int index);

int lua_isnone (lua_State *L, int index);

int lua_isnoneornil (lua_State *L, int index);

int lua_isnumber (lua_State *L, int index);

int lua_isstring (lua_State *L, int index);

int lua_istable (lua_State *L, int index);

int lua_isthread (lua_State *L, int index);

int lua_isuserdata (lua_State *L, int index);

这些都是栈中指定元素类型检查的接口;

函数调用

void lua_call (lua_State *L, int nargs, int nresults);

int lua_pcall (lua_State *L, int nargs, int nresults, int errfunc);

int lua_cpcall (lua_State *L, lua_CFunction func, void *ud);

lua_call 调用函数,参数nargs指定函数参数个数,参数nresults指定返回值个数。首先,被调函数必须在栈中;其次,函数参数必须是按从左往右的顺序入栈的;函数调用时,所有函数参数都会弹出堆栈。函数返回时,其返回值入栈(第一个返回最最先入栈)。

lua_pcall 以保护模式调用函数,如果发生错误,捕捉它,并将错误消息压入栈,然后返回错误码。

lua_cpcall 以保护模式调用C函数func,参数ud指针指向一个用户自定义数据。

错误处理

int luaL_error (lua_State *L, const char *fmt, ...);

引发一个错误。

类型转换

int lua_toboolean (lua_State *L, int index);

lua_CFunction lua_tocfunction (lua_State *L, int index);

lua_Integer lua_tointeger (lua_State *L, int index);

const char *lua_tolstring (lua_State *L, int index, size_t *len);

lua_Number lua_tonumber (lua_State *L, int index);

const void *lua_topointer (lua_State *L, int index);

const char *lua_tostring (lua_State *L, int index);

lua_State *lua_tothread (lua_State *L, int index);

void *lua_touserdata (lua_State *L, int index);

lua_toboolean 将给定index索引处的元素转换为bool类型(0或1);

lua_tocfunction 将给定index索引处的元素转换为C函数;

lua_tointeger 将给定index索引处的元素转换为int类型;

lua_tolstring 将给定index索引处的元素转换为char*类型,如果len不为空,同时还设置len为字符串长度;该函数返回的指针,指向的是Lua虚拟机内部的字符

串,这个字符串是以'\0'结尾的,但字符串中间也可能包含值为0的字符。

lua_tostring 等价于参数len=NULL时的lua_tolstring;

lua_tonumber 将给定index索引处的元素转换为double类型;

lua_topointer 将给定index索引处的元素转换为void*类型;

lua_tothread 将给定index索引处的元素转换为lua_State*类型(即一个thread);

lua_touserdata 返回给定index索引处的userdata对应的内存地址;

thread 操作

lua_State *lua_newthread (lua_State *L);

int lua_resume (lua_State *L, int narg);

int lua_yield (lua_State *L, int nresults);

lua_newthread 创建一个新的thread,然后压入堆栈,并返回一个lua_State*指针表示创建的新thread。

新创建的thread与当前thread共享一个全局环境。没有销毁thread的显式调用,它由垃圾收集器负责回收。

C 调用 Lua代码

一个简单的例子:

// test.c

#include        <stdio.h>

#include        "lua.h"

#include        "lualib.h"

#include        "lauxlib.h"

/*the lua interpreter*/

lua_State* L;

int luaadd(int x, int y)

{

int sum;

lua_getglobal(L,"add");

lua_pushnumber(L, x);

lua_pushnumber(L, y);

lua_call(L, 2, 1);

sum = (int)lua_tonumber(L, -1);

lua_pop(L,1);

return sum;

}

int main(int argc, char *argv[])

{

int sum;

L = lua_open();

luaL_openlibs(L);

luaL_dofile(L, "add.lua");

sum = luaadd(10, 15);

printf("The sum is %d \n",sum);

lua_close(L);

return 0;

}

注意:在C代码里面我们要引入三个头文件lua.h,lauxlib.h和lualib.h:

lua.h中定义的是最基础的API;

lauxlib.h中的函数都以luaL_开头,他们是比基础API更抽象的函数;

lualib.h中定义了打开标准类库的API,比如luaL_openlibs(L)。

程序开始用luaL_open()函数创建一个lua_State。lua_State中保存了Lua运行时的所有的状态信息(比如变量的值等),并且所有的Lua的C的API都有一个lua_newstate指针的参数。luaL_open函数会创建一个全新的Lua运行时状态,其中没有任何预先定义好的函数(包括最基本的print函数)。如果需要试用标准类库的话,只要调用luaL_openlibs(L)函数就打开标准类库就可以了。标准类库被分别封装在不同的包中,当你需要使用的时候再引入到代码中,这样做的好处是可

以使Lua尽可能的小(嵌入式语言必须要小),从而可以方便嵌入到其他语言中去。当Lua运行时状态和标准类库都准备完成后,就可以调用luaL_dofile

(L,"test.lua")函数来执行Lua脚本。运行结束后,需要调用lua_close(L)来关闭Lua运行时状态。

被调用的test.lua文件:

-- test.lua

function add(x,y)

return x + y

end

编译命令,实际命令需要根据自己的lua环境调整

gcc test.c -o test -llua-5.1 -I /usr/local/include/

执行./test命令的输出:

The sum is 25

另外一个操作table的例子:

int main()

{

lua_State *L = luaL_newstate();

luaL_openlibs(L);

lua_newtable(L);

lua_pushstring(L, "i am key");

lua_pushstring(L, "i am value");

lua_settable(L, -3);

lua_pushstring(L, "i am key");

lua_gettable(L, -2);

const char *str = lua_tostring(L, -1);

printf("%s", str);

lua_close(L);

return 0;

}

Lua 调用 C 函数

当Lua调用C函数时,也使用了一个与C语言调用Lua时相同的栈。C函数从栈中获取函数参数,并将结果压入栈中。为了在栈中将函数结果与其他值区分开,C函数还

应返回其压入栈中的结果个数。

栈不是一个全局性的结构,每个函数都有自己的局部私有栈。当Lua调用一个C函数时,第一个参数总是这个局部栈的索引1。

对于可被Lua调用的C函数而言,其接口必须遵循Lua要求的形式,即

typedef int (*lua_CFunction)(lua_State* L);

接收一个参数Lua_State*,即Lua的状态,返回值表示压入栈中的结果个数。

把要调用的C 函数注册到lua状态机中:

void lua_register (lua_State *L, const char *name, lua_CFunction f);

lua_register 是一个宏:#define lua_register(L,n,f) (lua_pushcfunction(L, f), lua_setglobal(L, n))

其中,参数name是lua中的函数名,f 是C中的函数。

从宏定义可以看出,这个函数的作用是把C函数压入堆栈,并在全局环境中设置Lua函数名;

Lua在require模块的时候,除了搜索 "*.lua" 文件,也会搜索 "*.so" 文件,也就是说,Lua支持加载C/C++语言编译的动态库文件。

// foo.c

#include        "lua.h"

#include        "lualib.h"

#include        "lauxlib.h"

static int add(lua_State *L)

{

int n = lua_gettop(L);      /* number of arguments */

lua_Number sum = 0;

int i;

for (i = 1; i <= n; i++) {

if (!lua_isnumber(L, i)) {

lua_pushstring(L, "incorrect argument");

lua_error(L);

}

sum += lua_tonumber(L, i);

}

lua_pushnumber(L, sum/n);    /* first result */

lua_pushnumber(L, sum);      /* second result */

return 2;                    /* number of results */

}

int luaopen_foo(lua_State *L)

{

lua_register(L, "add", add);

return 1;

}

注意:luaopen_MODULE 函数的后缀是有规则的,必须是模块名称,而lua_register的第二个参数是供Lua代码调用的函数名称,第三个参数是当前C函数;

OK,现在把C代码编译成动态库:

gcc foo.c -shared -fPIC -o foo.so  -llua-5.1 -I /usr/local/include/

然后在lua代码里面可以加载该模块:

require("foo")

这条命令会自动加载foo.so库,并调用其中的 luaopen_foo 函数,然后执行里面的函数注册代码,这样接下来就能直接使用那些注册到Lua状态机里面的C函数了。

print(add(14,25,15))

输出结果:

18      54

lua与c的交互(函数专用)的更多相关文章

  1. Lua与C的交互

    Lua 与 C 的交互 Lua是一个嵌入式的语言,它不仅可以是一个独立运行的程序,也可以是一个用来嵌入其它应用的程序库. C API是一个C代码与Lua进行交互的函数集,它由以下几部分构成: 1.  ...

  2. cocos2d-x lua与c++简单交互

    cocos2d-x lua与c++简单交互 version: cocos2d-x 3.6 本文讲述lua与c++的一些简单交互: lua通过消息方式调用c++无参接口 c++调用lua带参接口 1.通 ...

  3. lua与C/C++交互

    Lua设计小巧很容易与C/C++进行交互,下面我们具体讲解C/C++中如何调用lua,而lua中又如何调用C代码. 首先lua和C交互的一个重要的数据结构lua_State,它是进行数据交换的堆栈,按 ...

  4. Lua与C/C++交互问题

    初学lua,遇到注册C/C++交互函数问题 在lua与C/C++交互时,C/C++的注册Lua函数若是一个有返回类型(压栈)而不是获取类型的时候应该返回1而不是返回0,否则会出现在Lua中值为nil( ...

  5. 简述C/C++调用lua中实现的自定义函数

    1.首先说下目的,为什么要这么做 ? 正式项目中,希望主程序尽量不做修改,于是使用C/C++完成功能的主干(即不需要经常变动的部分)用lua这类轻量级的解释性语言实现一些存在不确定性的功能逻辑:所以, ...

  6. lua与C/C++交互概要

    转 http://blog.csdn.net/wildfireli/article/details/22307635 Lua生来就是为了和C交互的,因此使用C扩展Lua或者将Lua嵌入到C当中都是非常 ...

  7. 通过lua栈了解lua与c的交互

    lua是如何执行的 其中分析.执行部分都是c语言实现的. lua与c的关系 lua的虚拟机是用c语言实现的,换句话说一段lua指令最终在执行时都是当作c语言来执行的,lua的global表,函数调用栈 ...

  8. Lua脚本和C++交互(一)

    现在,越来越多的C++服务器和客户端融入了脚本的支持,尤其在网游领域,脚本语言已经渗透到了方方面面,比如你可以在你的客户端增加一个脚本,这个脚本将会帮你在界面上显示新的数据,亦或帮你完成某些任务,亦或 ...

  9. Lua基本语法-lua与C#的交互(相当简单详细的例子)

    lua脚本 与 C#的交互 本文提供全流程,中文翻译.Chinar坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) 1 Lua And C# -- ...

随机推荐

  1. mysql的count和sum使用条件表达式

    count函数条件不为null的时候显示结果.即使为false也也会显示结果. 可以是使用if条件或者case when语句.如果条件不为null即需要的结果. 使用count()函数实现条件统计的基 ...

  2. antd源码分析之——对话框(modal)

    目录 一.组件结构 1.antd代码结构 2.rc-ant代码结构 3.组件结构 二.antd组件调用关系及功能详解 1.Model.tsx 2.confirm 三.rc-dialog详解 1.e.t ...

  3. Linux 查看网卡速率及版本

    查看网卡速率:ethtool 网卡名  如ethtool eth0 查看网卡驱动版本号:ethtool -i 网卡名   如ethtool -i eth0 示例: [root@nt3 ~]# etht ...

  4. html提交表单,php在后台获取表单内容的方法_例1

    html代码:   <html>   <head>   <meta http-equiv="Content-Type" content="t ...

  5. 忘记mysql或mariadb数据库的密码之解决方案

    一.实验环境 CentOS Linux release 7.5.1804 (Core) mysql  Ver 15.1 Distrib 5.5.56-MariaDB, for Linux (x86_6 ...

  6. mysql 松散索引与紧凑索引扫描(引入数据结构)

    这一篇文章本来应该是放在 mysql 高性能日记中的,并且其优化程度并不高,但考虑到其特殊性和原理(索引结构也在这里稍微讲一下) 一,mysql 索引结构 (B.B+树) 要问到 mysql 的索引用 ...

  7. centos7.2 apollo1.7.1的搭建

    1.准备工作 第一步:linux系统中配置好java环境安装参考地址:http://www.oracle.com/technetwork/java/javase/downloads/jdk8-down ...

  8. DS18b20温度传感器基础使用

    认识管脚 认识唯一标示的64位地址序列号 寄存器数据译码成温度值(下面只针对12位转化的,还有9..10等其他位的转化方式,不同位的转化,其精度也不同) 传感器存储器 配置寄存器使用说明 DS18b2 ...

  9. C++中非数nan的定义与范例

    /* * C++中非数的定义 NaN :阶码的每个二进制位全为1  并且  尾数不为0: 无穷:阶码的每个二进制位全为1  并且  尾数为0:符号位为0,是正无穷, 符号位为1是负无穷.所以NaN.正 ...

  10. 在VM虚拟机Windows Server r2上部署安装Microsoft Dynamics CRM 2016 步骤详解(一)

    应公司需求,最近在学微软的Dynamics CRM.在搭建环境的过程中也遇到了一些雷坑,在这里分享一下安装部署过程当中所遇到的一些问题, 安装Microsoft Dynamics CRM 2016的几 ...