Lua Workshop 2008, Washington, DC, July 2008
What Application?
Serotek Corporation's System Access product:
- A screen reader for blind users
- A basic screen magnifier for low-vision users
- A simplified web browsing UI that integrates with Serotek's subscription-based services
Available packages:
- Typical installable application
- USB thumb drive configuration
- System Access to Go: visit a web page with spoken prompts, download, and run immediately
Why a Native Windows Application?
- Accessibility tools use many OS-specific facilities.
- Cross-platform abstractions, especially GUI toolkits, significantly increase code size.
- Need to minimize download size for SA to Go.
Why Lua?
- We minimize C/C++ development to increase productivity and reliability.
- I generally prefer Python, but the large Python core and standard library make it difficult to produce a small package.
- Lua is one of the most lightweight dynamic languages, especially in code size.
- Lua is also a mature, powerful languages.
- Lua has a well-designed C API; this project needs many Windows API bindings.
- I wasn't comfortable using MS JScript for a project of this scale with so many C API bindings.
Two Stages of Lua Adoption
Combination of Lua and Python
- Started in early 2005
- Used Lua in a DLL that's injected into running applications; dirty, but efficient
- Used Lua for small auxillary programs
- Final stage of update installation procedure
- Utility to send logs to Serotek
- Launchers for thumb drive and SA to Go
Rewrite of Python code in Lua
- Started in late 2007, to be released very soon
- Will dramatically reduce startup time of SA to Go
Demo
Summary of Challenges
- Sheer number of Windows API bindings
- COM interface bindings in both directions for interfaces not based on COM Automation
- Thorough Unicode support, even in the io, os, and lfs libraries
- Problem with combination of coroutines and C-to-Lua callbacks
Introducing LuaWin
- My collection of Lua Windows API bindings developed for Serotek products
- Emphasis on accessibility, UI inspection and manipulation, and Internet access
- Currently not well organized; builds one DLL and a static-library version
- https://code.launchpad.net/luawin
LuaWin Conventions and Guidelines
- LuaWin framework functions start with "luawin_" and follow Lua C API naming style.
- LuaWin Lua modules are mostly named after Windows header files, not after Windows DLLs.
- LuaWin provides macros to check and push standard Windows data types (DWORD, LRESULT, etc.); may ease porting to 64-bit Windows.
- LuaWin provides userdata with metatables for handles, except for window handles (HWND).
- LuaWin is written mostly in C, like Lua itself.
Sample Windows API Function Binding
GetWindowThreadProcessId
static int wrap_GetWindowThreadProcessId(lua_State *L) {
HWND hWnd;
DWORD rv;
DWORD dwProcessId;
lua_settop(L, 1);
hWnd = luawin_checkhwnd(L, 1);
rv = GetWindowThreadProcessId(hWnd, &dwProcessId);
if (rv == 0)
luawin_error(L);
luawin_pushdword(L, rv);
luawin_pushdword(L, dwProcessId);
return 2;
}
Strings
Goal: Ease Unicode support
- All Lua strings are in UTF-8.
- LuaWin C Api provides functions to convert between UTF-8 and wide characters (UTF-16), and between UTF-8 and ACP.
- Using the standard UNICODE define, LuaWin provides macros to convert between UTF-8 and TCHAR.
Sample Windows API Function Binding with Strings
FindWindow
static int wrap_FindWindow(lua_State *L) {
LPCTSTR lpClassName;
LPCTSTR lpWindowName;
HWND hWnd;
lua_settop(L, 2);
lpClassName = luawin_opttstring(L, 1, NULL);
lpWindowName = luawin_opttstring(L, 2, NULL);
hWnd = FindWindow(lpClassName, lpWindowName);
if (hWnd == 0) {
lua_pushnil(L);
luawin_pusherror(L);
return 2;
} else {
luawin_pushhwnd(L, hWnd);
return 1;
}
}
LuaWin COM Interface Bindings: Lua to COM
- Lua userdatum for each COM interface pointer
- QueryInterface is exposed to Lua
- Metatable hierarchy reflecting COM interface hierarchy
- Recursive metatable check for userdata arguments
- C functions to register, check, and push COM interfaces, using IIDs as metatable names
- Conversion between Lua types and COM variants
- Uses Lua GC to manage reference counts as well as temporary BSTRs and variants
- Conversion between LuaWin IDispatch wrapper and LuaCOM object
Example Usage of COM Interfaces from Lua
Microsoft Active Accessibility in Internet Explorer
local gti = winuser.GetGUIThreadInfo()
local hwnd = gti.hwndFocus
local acc =
oleacc.AccessibleObjectFromWindow(hwnd, winuser.OBJID_CLIENT,
oleacc.IID_IAccessible)
print(acc:get_accName(0))
local sp = acc:QueryInterface(servprov.IID_IServiceProvider)
local windowLW = sp:QueryService(mshtml.IID_IHTMLWindow2,
oleauto.IID_IDispatch)
local window = combridge.toluacom(windowLW)
-- now work with the IE DOM...
Sample COM Interface Method Binding
IAccessible::get_accRole
static int wrap_IAccessible_get_accRole(lua_State *L) {
IAccessible *self;
VARIANT *pvarChild;
VARIANT *pvarRole;
HRESULT hr;
lua_settop(L, 2);
self = checkIAccessible(L, 1);
pvarChild = luawin_newvariant(L);
luawin_checkvariant(L, 2, pvarChild);
pvarRole = luawin_newvariant(L);
hr = self->lpVtbl->get_accRole(self, *pvarChild, pvarRole);
if (hr == E_FAIL || !luawin_checkhr(L, hr))
return 0;
luawin_pushvariant(L, pvarRole);
return 1;
}
LuaWin COM Gateways: COM to Lua
- Term "gateway" taken from PyWin32
- C++ class for each COM interface for which a gateway is implemented
- Lua table of methods with a self argument
- C++ gateway object may outlive Lua state if references are held long enough; C++ gateway methods return E_FAIL after the Lua state goes away.
- Heavy use of macros and varargs to reduce boilerplate code while still handling Lua errors correctly
Sample Lua Implementation of a COM Method
IDocHostUIHandler::GetExternal
function UIHandler:GetExternal()
local external, ifName = self.parent:getExternal()
if not external then
return false
end
local lcObj =
luacom.ImplInterfaceFromTypelib(external, getResourceDLLPath(),
ifName)
return true, fromluacom(lcObj):QueryInterface(IID_IDispatch)
end
Sample COM Gateway Method
IBindStatusCallback::OnProgress
LUAWIN_GATEWAY_DEFINE_METHOD(IBindStatusCallback, OnProgress,
(ULONG ulProgress, ULONG ulProgressMax, ULONG ulStatusCode,
LPCWSTR szStatusText), ulProgress) {
LUAWIN_GATEWAY_METHOD_ARG(ULONG, ulProgress);
LUAWIN_GATEWAY_METHOD_ARG(ULONG, ulProgressMax);
LUAWIN_GATEWAY_METHOD_ARG(ULONG, ulStatusCode);
LUAWIN_GATEWAY_METHOD_ARG(LPCWSTR, szStatusText);
luawin_pushulong(L, ulProgress);
luawin_pushulong(L, ulProgressMax);
luawin_pushulong(L, ulStatusCode);
luawin_pushwstring(L, szStatusText);
LUAWIN_GATEWAY_METHOD_CALL(0);
} LUAWIN_GATEWAY_METHOD_END
How Does it Really Work?
Refer to include/luawin_gateway.h in LuaWin
Unicode and Lua Standard Libraries: The Problem
- We need thorough Unicode support. In Lua, this is best done by using UTF-8 for all Lua strings.
- To use Unicode with the Windows API and Microsoft CRT library, one must use wide-character versions of functions.
- The Lua standard library and LuaFileSystem don't use any wide-character functions.
- How do I achieve thorough Unicode support without excessively patching Lua and LuaFileSystem source?
-
Unicode and Lua Standard Libraries: My Solution
The utf8api library
- Contains proxy implementations of non-wide-character CRT functions
- Convert string arguments from UTF-8 to wide characters
- Call wide-character function
- Convert string results from wide characters to UTF-8
- Also done for a few Windows API functions called in loadlib.c
- When building Lua and LFS, linker searches this library before CRT
- Only works when dynamically linking with MS CRT
- Source not yet available
Coroutines and C-to-Lua Callbacks: The Problem
- Coroutine calls naive C function which stores lua_State pointer to call back into Lua later
- C function's lua_State pointer is for the coroutine, not the main Lua thread (i.e. the pointer returned by lua_newstate)
- Coroutine dies
- lua_State pointer is invalid
Coroutines and C-to-Lua Callbacks: My Solution
New Lua C API function: lua_getmainthread
LUA_API lua_State *lua_getmainthread (lua_State *L) {
lua_State *L1;
lua_lock(L);
L1 = G(L)->mainthread;
lua_unlock(L);
return L1;
}
Using the new function
C function in previous scenario should call lua_getmainthread and store the returned pointer
Lua versus Python: Python's Strengths
- Large standard library -- but also a weakness; as discussed earlier, difficult to minimize download size
- Large number of open-source libraries -- but again, even small increases in package size add up
- Standard class system; with Lua, anyone who wants a class system rolls their own
- More conveniences for working with strings, lists, and dictionaries
- Built-in Unicode support (but Unicode support in PyWin32 seems haphazard)
- ctypes FFI module; has more features than alien module for Lua
- "continue" statement
- I could go on, but...
Lua versus Python: Lua's Strengths
- Small, simple core -- This is such a rare quality in software that it's worth giving up some conveniences.
- Designed to be embedded -- Remember, we first used Lua in a DLL that's injected into other running applications.
- Much more pleasant C API
- Built-in coroutines
- Much simpler to build from source, especially with a custom build environment or extra compiler/linker options
- Option to have multiple Lua states in one process, e.g. one state per OS thread
- Closures can set variables in enclosing scope
- Probably more that I haven't discovered or didn't think of at the moment
Conclusion
- Lua is one of the best dynamic languages for bloat-free desktop applications, especially on Windows.
- Developing a complex native Windows application in Lua has been challenging, but Lua has proven to be more than flexible enough.
- Take a look at LuaWin; community review and feedback always helps.
Q&A