文章目录
  1. 1. 动机
  2. 2. 实现
  3. 3. 总结

Lua是一个轻量级的脚本语言,其设计目标是容易嵌入其他编程语言中去,不依赖任何第3方库使用标准C写成,因此具有很好的可移植性,并且其性能在脚本类语言中也是首屈一指的。lua自身的标准库只提供了一些非常基本且有限的功能,因此单纯使用lua能做的事情并不多,但是lua被设计成很容易由C来扩展它的功能。

动机

在Windows下lua是不具备直接调用Windows API的能力,因此如果想在lua中使用Windows API就需要先在C或C++中对Windows API进行一次封装,然后lua才能够去调用。由于Windows API数量巨多,对其进行逐个封装的工作量也是不小的。作为程序员对这种重复性的劳动总是比较厌烦的,因此就想写一个库支持从lua中直接调用,一劳永逸。

用过.Net的都知道.Net中有一个叫P/Invoke的技术,允许托管代码调用dll中的Native函数,这个库也是受此启发而来的,我将其称为NInvoke演示程序含example)。

实现

NInvoke的实现包含了

  • 一堆C内建(built-in)基本类型的封装(如ctype_intctype_long等)
    Windows API是基于C的(其各种稀奇古怪的参数和返回值都是对C的基本类型进行typedef),C的类型系统中单单整型就有好几种,而lua中只有booleannumber这两种数值类型(前者双值,后者默认是double)因此需要对这些基本类型进行封装,这样在可以在lua调用函数的时候进行类型检查,避免由于调用时的粗心导致程序运行不正确。

  • 一个指针封装类ctype_pointer
    Windows API中的HANDLEHWND等参数或返回值都是指针类型的,此外Windows API也常使用指针作为参数来返回数据。在NInvoke中允许所有的数据类型获取它的指针封装类的对象,以及将指针对象解引用成任意的C内建类型的封装对象。这包括了取得指针的指针,指针解引用成指针(即对多级指针的支持)。

  • 一个代理函数封装类native_function_wrapper
    由于lua无法直接调用Windows API因此需要一个代理函数对调用动作进行转发。主要工作包括从lua中获取参数,调用真正的目标函数,获取函数的返回值,返回返回值给lua。此外在NInvoke中要求lua在获取这个代理函数前需要先将目标函数的签名都要先告诉函数加载器,然后才能获得这个代理类,函数签名包扩调用约定(x64下会忽略调用约定)、参数的类型、返回值的类型等,这样这个代理函数就可以在lua调用的时候对参数进行检查,因此只要确保一开始函数签名提供的是正确的,就能确保调用是正确的。

  • 一个缓冲区发生器byte_array
    Windows API中有一些是要求提供一个缓冲区,然后传入缓冲区的指针供其读写。byte_array可以创建任意非负长度(含0)的缓冲区,允许按字节对缓冲区进行读写,可以同lua的string类型进行转换,转换时可以指定byte_array的文本编码(目前支持utf8和utf16le以及Windows系统默认的字符集ansi,未指定时视为utf8),lua的string总是视为utf8。事实上这个byte_array还常用来作为结构体(通过构造一个与结构体相同大小长度的数组)

  • 一个带边界检查的强类型的数组读写器array_view
    前面提到常将byte_array作为结构体来使用,而结构体内部可以有多种数据类型,byte_array被设计成只能按字节对缓冲区进行修改,对长度大于1的数据(如int或long long)修改会比较不方便(比如要考虑字节序等问题)。array_view是一个能够将一个内存块视为任意类型的长度为正数(不含0)的数组进行读写的类。此外在NInvoke中所有C类型封装对象都是只读(read-only)的,其值在对象创建后在其生命周期内通常会一直保持不变,唯一的例外是允许通过array_view对其进行修改,在这种情况下会将其视为长度为1的数组。

  • 一些C标准库常用函数如strlenwcslen等的封装
    某些Windows函数通过缓冲区返回一个字符串可能不会告诉调用者这个字符串的长度,因此需要用strlen或wcslen来判断字符串的长度。将这些常用的C函数进行封装可以方便在lua中使用。

尚未实现的有浮点类参数和返回值回调函数。对于前者是由于浮点类型在调用时传递的方式和整数类型不太一样,并且Windows API似乎极少(没有?)参数或返回值为浮点数的。而对于后者主要的问题在于lua被设计成单线程的语言,对于回调函数被另一个线程调用后与当前线程并发执行的安全问题似乎无解?典型的如CreateThread的线程过程函数。

在NInvoke的开发实现中否决了自己一开始设想的在C中提供对结构体的封装,原因是后来发现上面提供的这些基础设施已经足够在lua中实现对结构体的封装,所以计划之后在lua层实现这个功能。

总结

博主对lua的了解属于入门水平(目前《Programming in Lua》这本书也还没看完),这个库更多的是以C的角度来考虑(实现用的C++),所以典型的如byte_array和array_view的下标都是以0开始的(lua以1开始),目前NInvoke基本上可以调用任何不需要回调函数的Windows API了。有了带NInvoke的lua可以爆bat(Windows的批处理)X条街,决定以后就直接用lua来写批处理了。

附图一张:

文章目录
  1. 1. 动机
  2. 2. 实现
  3. 3. 总结