Hi all,
During a conversation with ''flexo'' on IRC (not sure of his
real name), he
complained that win32-api was unstable. He wrote his own interface and
maintains that it is very stable and works better with multi-threaded apps.
Please take a look and tell me what you think (below the C code is the Ruby
interface):
/* w32thunk.c */
#include <ruby.h>
#include <windows.h>
#include <stdarg.h>
#include "rubysig.h"
#pragma warning(push,4)
enum
{
CONV_CDECL,
CONV_STDCALL
};
struct worker
{
void *func;
unsigned *argv;
int argc;
int conv;
unsigned ret;
volatile int done;
HANDLE thread;
HANDLE event;
};
#undef printf
#undef vprintf
// #define LOG printf
#undef LOG
void mylog(const char *fmt, ...)
{
RUBY_CRITICAL({
char buf[256];
DWORD written;
va_list va;
va_start(va, fmt);
vsprintf(buf, fmt, va);
va_end(va);
WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), buf, strlen(buf),
&written, NULL);
});
}
#define LOG mylog
#define LOG2 printf
#define LOG
#define LOG2
static unsigned perform_call(unsigned add_esp, INT_PTR func, unsigned argc,
INT_PTR argv)
{
unsigned retv;
__asm
{
mov ebx, func
mov ecx, argc
mov edx, argv
mov esi, add_esp
L_1:
test ecx, ecx
jz L_2
push [edx]
add edx, 4
dec ecx
jmp L_1
L_2:
call ebx
add esp, esi
mov retv, eax
}
LOG2("perform_call() => %ux\n", retv);
return retv;
}
static unsigned val2unsigned(VALUE v)
{
unsigned ret;
VALUE str = rb_big2str(v, 10);
char *pend;
ret = strtoul(STR2CSTR(str), &pend, 10);
if(ret == ULONG_MAX || !pend || *pend != ''\0'')
{
rb_fatal("strtoul(\"%s\", ...) failed", STR2CSTR(str));
}
return ret;
}
static unsigned __stdcall worker_func(struct worker *p)
{
unsigned add_esp;
unsigned i;
LOG2("{%d} starting worker_func\n", GetCurrentThreadId());
LOG2("{%d} test\n", GetCurrentThreadId());
wait_again:
WaitForSingleObject(p->event, INFINITE);
LOG2("{%d} got event, calling %p in conv %d using %d args\n",
GetCurrentThreadId(), p->func, p->conv, p->argc);
if(p->conv == CONV_CDECL)
{
add_esp = p->argc * 4;
}
else if(p->conv == CONV_STDCALL)
{
add_esp = 0;
}
else
{
LOG2("Unknown calling convention %d\n", p->conv);
return 0;
}
LOG2("{%d} add_esp: %d, argc: %d\n", GetCurrentThreadId(), add_esp,
p->argc);
for(i = 0; i < p->argc; i++)
{
LOG2("arg %d: %x\n", i, p->argv[i]);
}
p->ret = perform_call(add_esp, p->func, p->argc, p->argv);
LOG2("{%d} %p retv=%x\n", GetCurrentThreadId(), p, p->ret);
LOG2("{%d}, %p(%p) call done: %x\n", GetCurrentThreadId(), p,
&p->ret, p->ret);
p->done = 1;
goto wait_again;
}
static VALUE start_call(unsigned argc, VALUE *argv)
{
VALUE v_worker, v_lib, v_conv, v_name, v_args;
HMODULE hlib;
struct worker *p;
int i, j = 0;
rb_scan_args(argc, argv, "5", &v_worker, &v_lib, &v_name,
&v_conv,
&v_args);
p = (void *)val2unsigned(v_worker);
LOG("{%d} loading lib ''%s''\n",
GetCurrentThreadId(),
STR2CSTR(v_lib));
RUBY_CRITICAL({
hlib = LoadLibrary(STR2CSTR(v_lib));
});
if(!hlib)
{
rb_fatal("LoadLibrary() failed, GetLastError()=%d",
GetLastError());
}
p->argc = RARRAY(v_args)->len;
p->argv = malloc(sizeof(int) * p->argc);
for(i = RARRAY(v_args)->len - 1; i >= 0; i--)
{
VALUE v_arg = RARRAY(v_args)->ptr[i];
if(TYPE(v_arg) != T_FIXNUM && TYPE(v_arg) != T_BIGNUM)
{
rb_fatal("bad type for arg %d (expected T_FIXNUM or T_BIGNUM)",
i);
}
p->argv[j++] = val2unsigned(v_arg);
LOG("start_call() arg %d: 0x%x\n", i, val2unsigned(v_arg));
}
p->conv = val2unsigned(v_conv);
p->done = 0;
RUBY_CRITICAL({
p->func = (void*)GetProcAddress(hlib, STR2CSTR(v_name));
});
if(!p->func)
{
rb_fatal("GetProcAddress() failed");
}
LOG("{%d} got function ''%s'' at %p, raising
event\n",
GetCurrentThreadId(), STR2CSTR(v_name), p->func);
RUBY_CRITICAL({
SetEvent(p->event);
});
return Qnil;
}
static VALUE create_worker(unsigned argc, VALUE *argv)
{
struct worker *p = malloc(sizeof(struct worker));
rb_scan_args(argc, argv, "0");
RUBY_CRITICAL({
p->event = CreateEvent(NULL, FALSE, FALSE, NULL);
});
if(p->event == NULL)
{
rb_fatal("CreateEvent() failed, GetLastError()=%d",
GetLastError());
}
LOG("Creating worker thread\n");
RUBY_CRITICAL({
p->thread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)worker_func, p, 0, /*CREATE_SUSPENDED,*/ NULL);
});
if(p->thread == NULL)
{
rb_fatal("CreateThread() failed, GetLastError()=%d",
GetLastError());
}
return INT2NUM((int)p);
}
static VALUE run_worker(unsigned argc, VALUE *argv)
{
VALUE v_worker, v_time;
struct worker *p;
unsigned time;
rb_scan_args(argc, argv, "2", &v_worker, &v_time);
p = (void*)val2unsigned(v_worker);
time = val2unsigned(v_time);
#undef Sleep
RUBY_CRITICAL({
unsigned ret;
ret = ResumeThread(p->thread);
if(ret == -1)
{
rb_fatal("ResumeThread() failed, GetLastError()=%d",
GetLastError());
}
else
{
//printf("Resume=%d\n", ret);
}
// printf("yielding for %d ms\n", time);
Sleep(time);
ret = SuspendThread(p->thread);
if(ret == -1)
{
rb_fatal("SuspendThread() failed,
GetLastError()=%d", GetLastError());
}
else
{
//printf("Suspend=%d\n", ret);
}
});
return Qnil;
}
static VALUE get_call_result(unsigned argc, VALUE *argv)
{
VALUE v_worker, v_ret = Qnil;
struct worker *p;
rb_scan_args(argc, argv, "1", &v_worker);
p = (void*)val2unsigned(v_worker);
if(p->done)
{
LOG("getting ret of %p (%p): %d\n", p, &p->ret, p->ret);
v_ret = INT2NUM(p->ret);
}
return v_ret;
}
static VALUE write_memory(unsigned argc, VALUE *argv)
{
VALUE v_ptr, v_str;
void *ptr;
int len;
int bad;
rb_scan_args(argc, argv, "2", &v_ptr, &v_str);
ptr = (void*)val2unsigned(v_ptr);
len = RSTRING(v_str)->len;
RUBY_CRITICAL({
bad = IsBadWritePtr(ptr, len);
});
if(bad)
{
rb_fatal("IsBadWritePtr(%p, %d)", ptr, len);
}
memcpy((void*)val2unsigned(v_ptr), RSTRING(v_str)->ptr,
RSTRING(v_str)->len);
return Qnil;
}
static VALUE read_memory(unsigned argc, VALUE *argv)
{
VALUE v_ptr, v_len;
void *ptr;
int len;
rb_scan_args(argc, argv, "2", &v_ptr, &v_len);
ptr = (void*)val2unsigned(v_ptr);
len = val2unsigned(v_len);
LOG("Reading %d bytes from %p\n", len, ptr);
if(len == 0)
{
int bad;
RUBY_CRITICAL({
bad = IsBadStringPtr(ptr, ~0);
});
if(bad)
{
rb_fatal("IsBadStringPtr(%p, ~0)", ptr);
}
return rb_str_new((void*)val2unsigned(v_ptr),
strlen((void*)val2unsigned(v_ptr)));
}
else
{
int bad;
RUBY_CRITICAL({
bad = IsBadReadPtr(ptr, len);
});
if(bad)
{
rb_fatal("IsBadStringPtr(%p, %d)", ptr, len);
}
return rb_str_new((void*)val2unsigned(v_ptr), len);
}
}
struct callback
{
char thunk[16];
unsigned argc;
unsigned *argv;
unsigned conv;
unsigned called;
unsigned ret;
unsigned disabled;
CRITICAL_SECTION cs;
HANDLE event;
};
__declspec(naked) static unsigned callback(struct callback *p, ...)
{
int i, retv;
va_list va;
int cleanup;
int initial_esp, final_esp;
__asm
{
push ebp
mov ebp,esp
sub esp,0x200 // make
some room
mov initial_esp, ebp
pusha
}
if(p->disabled)
{
goto skip;
}
LOG2("initial_esp: 0x%x\n", initial_esp);
if(p->argc < 0)
{
LOG2("p->argc == %d", p->argc);
abort();
}
LOG2("callback(p=%p, ...) p->argc=%d\n", p, p->argc);
EnterCriticalSection(&p->cs);
p->argv = malloc(p->argc * sizeof(int));
va_start(va, p);
for(i = 0; i < p->argc; i++)
{
p->argv[i] = va_arg(va, int);
LOG2("callback(p=%p, ...) arg %d: %d\n", p, i, p->argv[i]);
}
va_end(va);
p->called = 1;
WaitForSingleObject(p->event, INFINITE);
retv = p->ret;
LOG2("callback(p=%p, ...) ret %d\n", p, retv);
LeaveCriticalSection(&p->cs);
skip:
/**
* Clean up the callback parameter added by the thunk code.
*/
cleanup = 4;
if(p->conv == CONV_STDCALL)
{
cleanup += p->argc * 4;
}
LOG2("callback(p=%p, ...) cleanup=%d\n", p, cleanup);
__asm
{
mov final_esp, ebp
add final_esp, 4
mov edx, cleanup
add final_esp, edx
}
LOG2("final_esp: 0x%x\n", final_esp);
__asm
{
popa
mov edx, cleanup
mov eax, retv
mov esp, ebp
pop ebp
pop ecx
add esp, edx
jmp ecx
}
}
static VALUE create_callback(unsigned argc, VALUE *argv)
{
VALUE v_conv, v_argc;
struct callback *p;
RUBY_CRITICAL({
p = malloc(sizeof(struct callback));
});
rb_scan_args(argc, argv, "2", &v_conv, &v_argc);
if(val2unsigned(v_argc) < 0)
{
rb_fatal("v_argc = %d", val2unsigned(v_argc));
}
if(val2unsigned(v_conv) != CONV_STDCALL &&
val2unsigned(v_conv) != CONV_CDECL)
{
rb_fatal("v_conv = %d", val2unsigned(v_conv));
}
p->called = 0;
p->disabled = 0;
p->argc = val2unsigned(v_argc);
p->conv = val2unsigned(v_conv);
p->thunk[0] = 0xb8;
*(unsigned *)&p->thunk[1] = (int)p; // p
*(unsigned *)&p->thunk[5] = 0x50240487;
p->thunk[9] = 0xe9; // jmp rel32
*(unsigned *)&p->thunk[10] = ((char *)callback) - &p->thunk[14];
RUBY_CRITICAL({
InitializeCriticalSection(&p->cs);
p->event = CreateEvent(NULL, FALSE, FALSE, NULL);
});
return INT2NUM(((int)p));
}
static VALUE exec_callback(unsigned argc, VALUE *argv)
{
VALUE v_callback, v_args;
struct callback *p;
int i;
rb_scan_args(argc, argv, "1", &v_callback);
p = (void *)val2unsigned(v_callback);
if(!p->called)
{
return Qfalse;
}
v_args = rb_ary_new();
for(i = 0; i < p->argc; i++)
{
rb_ary_push(v_args, INT2NUM(p->argv[i]));
}
return v_args;
}
static VALUE disable_callback(unsigned argc, VALUE *argv)
{
VALUE v_callback;
struct callback *p;
rb_scan_args(argc, argv, "1", &v_callback);
p = (void *)val2unsigned(v_callback);
p->disabled = TRUE;
RUBY_CRITICAL({
SetEvent(p->event);
});
return Qnil;
}
static VALUE fini_callback(unsigned argc, VALUE *argv)
{
VALUE v_callback, v_ret;
struct callback *p;
rb_scan_args(argc, argv, "2", &v_callback, &v_ret);
p = (void *)val2unsigned(v_callback);
p->ret = val2unsigned(v_ret);
p->called = FALSE;
RUBY_CRITICAL({
SetEvent(p->event);
});
return Qnil;
}
void Init_w32thunk()
{
VALUE m_w32thunk = rb_define_module("W32Thunk");
rb_define_module_function(m_w32thunk, "_create_worker",
create_worker, -1);
rb_define_module_function(m_w32thunk, "_run_worker", run_worker,
-1);
rb_define_module_function(m_w32thunk, "_start_call", start_call,
-1);
rb_define_module_function(m_w32thunk, "_get_call_result",
get_call_result, -1);
rb_define_module_function(m_w32thunk, "_write_memory", write_memory,
-1);
rb_define_module_function(m_w32thunk, "_read_memory", read_memory,
-1);
rb_define_module_function(m_w32thunk, "_create_callback",
create_callback, -1);
rb_define_module_function(m_w32thunk, "_exec_callback",
exec_callback, -1);
rb_define_module_function(m_w32thunk, "_disable_callback",
disable_callback, -1);
rb_define_module_function(m_w32thunk, "_fini_callback",
fini_callback, -1);
}
# w32thunk.rb
require ''w32thunk/w32thunk.so''
Thread.abort_on_exception = true
class Fixnum
def to_hex
"0x" + self.to_s(16)
end
end
module Kernel32
PROCESS_ALL_ACCESS = 0x1f0fff
def self.method_missing(method, *args)
W32Thunk::call("kernel32", method.to_s, W32Thunk::STDCALL,
*args)
end
end
def my_inspect(y)
if y.is_a? Array
"[#{y.map { |x| my_inspect(x) }.join(", ")}]"
elsif y.is_a? Fixnum
"0x#{y.to_s(16)}"
else
y.inspect
end
end
module W32Thunk
CDECL = 0
STDCALL = 1
@@workers = {}
@@callbacks = []
def W32Thunk.debug=(x)
$w32thunk_debug = x
end
def W32Thunk.dbg(args)
if $w32thunk_debug
Thread.exclusive do
$thread_ids ||= {}
$thread_ids[Thread.current] ||$thread_ids.keys.size
puts "<#{$thread_ids[Thread.current]}>
#{args}"
end
end
end
class Buffer
attr_reader :ptr
attr_reader :len
def Buffer.free_ptr(ptr)
lambda do
W32Thunk.dbg "/// Freeing buffer
#{my_inspect(ptr)}"
Kernel32.LocalFree(ptr)
end
end
def initialize(arg)
init = nil
if arg.is_a? Fixnum
@len = arg
else
raise "Bad type" unless arg.is_a? String
@len = arg.size
init = arg
end
@ptr = Kernel32.LocalAlloc(0x40, len + 1)
W32Thunk.dbg "/// Created buffer #{my_inspect(@ptr)}
(Size: #{len})"
if @ptr.to_i == 0
raise "LocalAlloc() failed to alloc #{len}
bytes: #{Kernel32.GetLastError}"
end
ObjectSpace.define_finalizer(self,
Buffer.free_ptr(@ptr))
if init
dump = ((init.size > 16) ?
"#{init[0..16].inspect} ..." : "#{init.inspect}")
W32Thunk.dbg "Writing #{init.size} bytes to
#{my_inspect(@ptr)} (#{dump})"
W32Thunk::_write_memory(@ptr, init)
end
@ptr
end
def free
Buffer.free_ptr(@ptr).call
ObjectSpace.undefine_finalizer(self)
@ptr = 0
end
def to_s
W32Thunk.dbg "Reading #{@len == 0 ? "<CSTR>" : @len}
bytes from #{my_inspect(@ptr)}"
W32Thunk::_read_memory(@ptr, @len)
end
end
class IntBuf < Buffer
def initialize(n = 0)
super([n].pack("V"))
end
def to_i
to_s.unpack("V")[0]
end
end
class Pointer
def initialize(i)
@i = i
end
def to_i
@i
end
def to_s(len = 0)
W32Thunk.dbg "Reading #{len == 0 ? "<CSTR>" : len}
bytes from #{my_inspect(@i)}"
W32Thunk::_read_memory(@i, len)
end
def inspect
"0x" + @i.to_s(16)
end
end
def self.call(lib, func, conv, *args)
begin
dbg "CALL !!! Preparing #{lib}.dll!#{conv == 0 ?
"<CDECL>" :
"<STDCALL>"}#{func}(#{my_inspect(args)})"
buffers = []
args.map! { |arg|
if arg.is_a? String
dbg "CALL --- Mapping string
#{arg.inspect}"
buf = Buffer.new(arg)
buffers << buf
buf.ptr
elsif arg.is_a? Buffer
dbg "CALL --- Mapping buffer #{arg}"
arg.ptr
elsif arg.is_a? Callback
dbg "CALL --- Mapping callback
#{arg}"
arg.ptr
else
dbg "CALL --- Pushing #{arg}"
arg
end
}
dbg "CALL >>> Calling #{lib}.dll!#{conv == 0 ?
"<CDECL>" :
"<STDCALL>"}#{func}(#{my_inspect(args)})"
worker = @@workers[Thread.current] ||W32Thunk::_create_worker
# puts "--- calling #{func}(#{args.inspect})"
W32Thunk::_start_call(worker, lib, func, conv, args)
loop do
# W32Thunk::_run_worker(worker, 10)
ret = W32Thunk::_get_call_result(worker)
unless ret.nil?
dbg "CALL <<< Returned
#{my_inspect(ret)}"
return ret
end
sleep 0.02
end
end
dbg "CALL --- GC after call to #{func}"
# GC.start
dbg "CALL --- GC done"
end
class Callback
attr_reader :ptr
def Callback.destruct(handle)
lambda do
W32Thunk.dbg "CALLBACK !!! Disabling
#{my_inspect(handle)}"
W32Thunk::_disable_callback(handle)
end
end
def initialize(conv, block)
argc = block.arity
argc = 0 if argc < 0
cb = W32Thunk::_create_callback(conv, argc)
W32Thunk.dbg "CALLBACK +++ Created
#{my_inspect(cb)}"
@thread = Thread.new(cb) do |cb|
loop do
args = W32Thunk::_exec_callback(cb)
if args
W32Thunk.dbg "CALLBACK >>> Invoked
#{my_inspect(cb)} ... (#{my_inspect(args)})"
args.map! { |x|
if x > 0xffff
Pointer.new(x)
else
x
end
}
args = args.first if
args.size == 1
begin
ret block.call(args)
rescue
p $@
puts "Exception in
callback: #{$!}"
# Process.abort
end
ret = 0 if ret.nil?
W32Thunk.dbg "CALLBACK <<<
Returning #{my_inspect(ret)}"
W32Thunk::_fini_callback(cb,
ret)
end
sleep 0.02
# GC.start
end
end
@ptr = cb
ObjectSpace.define_finalizer(self,
Callback.destruct(cb))
end
end
def self.callback(conv = nil, &block)
conv ||= W32Thunk::STDCALL
Callback.new(conv, block)
end
end