2012-08-10

Lua output redirection

While ago I was working on Lua based implementation of the scripting library. One of the requirements was a total control of output and error messages, to allow it's automatic handling. One of the issues I've faced is that some output was going to stdout even if there is redefined print Lua function.

I thought about two quick solutions:
- redirect standard streams;
- redefine printf and it's adjacent functions.

Since standard streams, both stdout and stderr, in my case were already redefined (for remote debugging purposes), I didn't had much to choose from.

Looking into Lua 5.1 sources, I've found that stdout was simply hard-coded in some places. During up-streaming this patch to Lua 5.2 surprisingly most of the hard-coded stream names went away, and it looks like it would be possible to use redefined print without losing anything. I haven't tested this though. Following just works for Lua 5.1.x and above.

Patching Lua


Original patch was implemented for Lua 5.1, but yesterday at I was asked to upstream it to 5.2 too.
Patches are available here: 5.1.5 and 5.2.1.

Step into Lua source top folder, apply the patch:

Lua 5.1.5:
patch -p1 < ~/download/lua-5.1.5-output-redirect.patch 
Lua 5.2.1:
patch -p1 < ~/download/lua-5.2.1-output-redirect.patch 

Building patched Lua library

I'm too lazy to hack the Makefile and provide a clean solution, but building is straight-forward anyway. Check the Makefile in src/ folder for make parameters for your platform, and pass LUA_REDIRECT define from the command line.

Lua 5.1.5 for Linux:
cd src
make a LUA_A="liblua-5.1.5-redirect.a" MYCFLAGS="-DLUA_USE_LINUX -DLUA_REDIRECT"  MYLIBS="-Wl,-E -ldl -lreadline -lhistory -lncurses"
Lua 5.2.1 for Linux:
cd src
make a LUA_A="liblua-5.2.1-redirect.a" SYSCFLAGS="-DLUA_USE_LINUX -DLUA_REDIRECT" SYSLIBS="-Wl,-E -ldl -lreadline -lncurses"
Some clarifications:
- Building `luac` or `lua` binaries will lead to unresolved externals. `make a` will build only the library, which is what we want.
- LUA_A="liblua-5.x.y-redirect.a" tells the Makefile to save our library to a non-default (`liblua.a`) name. This is in case if you want to build `lua` or `luac` binaries from the same sources. Sure, you can choose whatever name you want, but I prefer to have unique names and there is a hidden reason for that, which is out of scope of this topic.

Checking assembled Lua library

Check if hooks are present, and Lua does not link to `printf` and other redefined functions:
nm liblua-5.x.y-redirect.a | grep printf
nm liblua-5.x.y-redirect.a | grep fputs
nm liblua-5.x.y-redirect.a | grep fwrite
All listed symbols should be prefixed with lua_, i.e.:
#> nm liblua-redirect.a | grep printf

0000000000000008 C lua_fprintf
0000000000000008 C lua_printf
0000000000000008 C lua_fprintf
0000000000000008 C lua_printf
...

Testing Lua redirect

To nicely test Lua output redirect, it is required to re-implement standard output functions. Sure, the easiest way would be just to hook Lua back to standard functions:
lua_fprintf = fprintf;
lua_fputs   = fputs;
lua_fwrite  = fwrite;
lua_printf  = printf;
But we want our output into the buffer! The resulting test program is below (download here):
// NB! luaredir.h and other Lua includes should go *before*
//     standard library headers, otherwise luaredir.h will
//     redefine printf/fprintf/fputs/fwrite functions, and
//     they will become unusable in a given file.

extern "C" {
#include "luaredir.h"
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}

// following includes should be placed in brackets rather than quotes,
// intentially replaced due to blog highlighter issues
#include "cstdio"
#include "cstring"
#include "iostream"
#include "string"

using namespace std;

// lua standard stream buffers
string stdout_buf;
string stderr_buf;

// fprintf hook for Lua output redirection
int redirect_fprintf(FILE* stream, const char* msg, ...) {
 char buffer[1024];

 va_list args;
 va_start(args, msg);
 vsprintf(buffer, msg, args);
 va_end(args);

 if (stream == stdout)
  stdout_buf.append(buffer);
 else
  stderr_buf.append(buffer);

 return strlen(buffer);
}

// fputs hook for Lua output redirection
int redirect_fputs(const char* msg, FILE* stream) {
 return redirect_fprintf(stream, msg);
}

// fwrite hook for Lua output redirection
int redirect_fwrite(const void* ptr, size_t size, size_t count, FILE* stream) {
 char buffer[1024];
 memset(buffer, 0, sizeof(buffer));
 memcpy(buffer, ptr, size * count);

 if (stream == stdout)
  stdout_buf.append(buffer);
 else
  stderr_buf.append(buffer);

 return count;
}

// printf hook for Lua output redirection
int redirect_printf(const char* msg, ...) {
 char buffer[1024];

 va_list args;
 va_start(args, msg);
 vsprintf(buffer, msg, args);
 va_end(args);

 stdout_buf.append(buffer);

 return strlen(buffer);
}

// actual code
int main() {
 int lua_error;

 // connect redirection hooks
 lua_fprintf = redirect_fprintf;
 lua_fputs   = redirect_fputs;
 lua_fwrite  = redirect_fwrite;
 lua_printf  = redirect_printf;

 // initialize Lua
 lua_State *luaVM = luaL_newstate();
 luaL_openlibs(luaVM);
 if (luaVM == NULL) {
  printf("Error initializing lua!\n");
  return -1;
 }

 // execute Lua test code, and print it's precious output
 lua_error = luaL_dostring(luaVM, "print(\"hello world!\")");
 printf("===== Test 1 output =====\n");
 printf("Lua stdout buffer:\n---\n%s\n---\n", stdout_buf.c_str());
 printf("Lua stderr buffer:\n---\n%s\n---\n", stderr_buf.c_str());
 printf("Lua error message:\n---\n%s\n---\n", lua_tostring(luaVM, -1));
 stdout_buf.clear();
 stderr_buf.clear();

 // execute Lua test code, and print it's precious output
 lua_error = luaL_dostring(luaVM, "bad_function()");
 printf("===== Test 2 output =====\n");
 printf("Lua stdout buffer:\n---\n%s\n---\n", stdout_buf.c_str());
 printf("Lua stderr buffer:\n---\n%s\n---\n", stderr_buf.c_str());
 printf("Lua error message:\n---\n%s\n---\n", lua_tostring(luaVM, -1));
 stdout_buf.clear();
 stderr_buf.clear();

 // cleanup and quit
 lua_close(luaVM);
 return 0;
}
This test program was built using following command line:
g++ -DLUA_REDIRECT -I/path/to/lua-5.x.y/src/ -L. lua-redirect-test.cpp -llua-5.x.y-redirect -ldl -o lua-redirect-test
Test program output:
===== Test 1 output =====
Lua stdout buffer:
---
hello world!

---
Lua stderr buffer:
---

---
Lua error message:
---
(null)
---
===== Test 2 output =====
Lua stdout buffer:
---

---
Lua stderr buffer:
---

---
Lua error message:
---
[string "bad_function()"]:1: attempt to call global 'bad_function' (a nil value)
---

No comments:

Post a Comment