[Win32utils-devel] Making win32-api better?

Daniel Berger djberg96 at gmail.com
Thu Apr 30 09:33:11 EDT 2009


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



More information about the win32utils-devel mailing list