From: Jan Bernhardt Date: 2003-05-15T06:06:35+09:00 Subject: Embedding/GC/heap corruption problem Hi, I'm working on adding Ruby scripting to the Half-Life game engine, similar to several other projects, like AMXmod (http://amxmod.net). Unfortunately I'm running into problems which seem to be related to garbage collection. I think I have to give a short overview on how the interaction between the game engine and the scripting engine is structured. The Half-Life engine communicates with clients by sending messages. One purpose of the scripting framework is to intercept those messages and run Ruby code in reaction to these events. The game engine calls the C functions MessageBegin, MessageEnd and several other functions to store data inside a message like WriteShort and WriteString. MessageBegin initiates a new messages, afterwards WriteXXX function calls are made to attach data. MessageEnd terminates the message. The C part of the scripting engine catches these function calls and utilizes a global (in C) Ruby variable to store the message's data. Errors (segfaults) occur on execution of the message dispatching code of the Ruby part (MessageRegistry.dispatch_message). It looks like the GC reaps the message's 'args' instance variable - an array containing the message data - even though its referenced by the global message variable. Surprisingly it happens rather seldom, say between every 1000's and 10000's message dispatch. The problem does not occur when I manually do garbage collections on a *very* regular basis. An error case looks like this: | L 05/14/2003 - 22:41:15: [RUBY] Message count: 1039 | L 05/14/2003 - 22:41:15: [RUBY] Message type: StatusValue (107) | L 05/14/2003 - 22:41:15: [RUBY] Message args: [1,1] | L 05/14/2003 - 22:41:15: [RUBY] Message receiver: 3, destination: 1 | L 05/14/2003 - 22:41:15: [RUBY] Message count: 1040 | L 05/14/2003 - 22:41:15: [RUBY] Message type: StatusValue (107) | L 05/14/2003 - 22:41:15: [RUBY] Message args: [2,1] | L 05/14/2003 - 22:41:15: [RUBY] Message receiver: 3, destination: 1 | cstrike/addons/halfruby/kernel/message_dispatch.rb:26: [BUG] Segmentation fault | ruby 1.6.7 (2002-03-19) [i386-linux] Stacktrace case 1: (gdb) bt #0 0x400a4fd1 in kill () from /lib/libc.so.6 #1 0x40036762 in raise () from /lib/libpthread.so.0 #2 0x400a4ca2 in raise () from /lib/libc.so.6 #3 0x400a604d in abort () from /lib/libc.so.6 #4 0x43a67cd9 in rb_bug () at error.c:178 #5 0x43ab4acf in sigsegv () at signal.c:393 #6 0x40039929 in __pthread_sighandler () from /lib/libpthread.so.0 #7 #8 0x43a6b16d in jump_tag_but_local_jump (state=6) at eval.c:1314 #9 0x43a731ff in rb_call0 (klass=1138029288, recv=1138014008, id=9313, argc=0, argv=0xbfffec6c, body=0x43d4bbd4, nosuper=0) at eval.c:4542 #10 0x43a73584 in rb_call (klass=1138029288, recv=1138014008, mid=9313, argc=1, argv=0xbfffec68, scope=1) at eval.c:4606 #11 0x43a73857 in rb_funcall (recv=1138014008, mid=9313, n=1) at ruby.h:571 #12 0x43a5dee5 in MessageEnd() () at messages.cpp:31 #13 0x43772cfe in mm_MessageEnd () from ./cstrike/addons/metamod/dlls/metamod_i386.so Stacktrace case 2: (gdb) bt #0 0x400a4fd1 in kill () from /lib/libc.so.6 #1 0x40036762 in raise () from /lib/libpthread.so.0 #2 0x400a4ca2 in raise () from /lib/libc.so.6 #3 0x400a604d in abort () from /lib/libc.so.6 #4 0x43a67cd9 in rb_bug () at error.c:178 #5 0x43ab4acf in sigsegv () at signal.c:393 #6 0x40039929 in __pthread_sighandler () from /lib/libpthread.so.0 #7 #8 0x00000011 in ?? () #9 0x43a6961d in search_method (klass=1137889328, id=3001, origin=0xbfffd978) at eval.c:247 #10 0x43a69672 in rb_get_method_body (klassp=0xbfffd9c4, idp=0xbfffd9a8, noexp=0xbfffd9ac) at eval.c:268 #11 0x43a734cc in rb_call (klass=1137889328, recv=1137889348, mid=3001, argc=0, argv=0x0, scope=1) at eval.c:4583 #12 0x43a73857 in rb_funcall (recv=1137889348, mid=3001, n=0) at ruby.h:571 #13 0x43ab7cee in rb_obj_as_string (obj=1137889348) at string.c:191 #14 0x43a6efb8 in rb_eval (self=1138014008, n=0x0) at eval.c:2895 #15 0x43a6e587 in rb_eval (self=1138014008, n=0x0) at ruby.h:576 #16 0x43a6cafc in rb_eval (self=1138014008, n=0x0) at eval.c:2028 #17 0x43a6cafc in rb_eval (self=1138014008, n=0x0) at eval.c:2028 #18 0x43a730a9 in rb_call0 (klass=1138029288, recv=1138014008, id=9313, argc=0, argv=0xbffff1d8, body=0x43d4bbd4, nosuper=0) at eval.c:4513 #19 0x43a73584 in rb_call (klass=1138029288, recv=1138014008, mid=9313, argc=1, argv=0xbffff1d4, scope=1) at eval.c:4606 #20 0x43a73857 in rb_funcall (recv=1138014008, mid=9313, n=1) at ruby.h:571 #21 0x43a5dee5 in MessageEnd() () at messages.cpp:31 #22 0x43772cfe in mm_MessageEnd () from ./cstrike/addons/metamod/dlls/metamod_i386.so (Case 1 and 2 differ in changed statement order in dispatch_message in the 'if (@verbose)' block, case 1 crashes on logging the @message_count first, case 2 on logging the @args array first.) I hope you're able to follow my descriptions and understand what's basically going on when reading the attached sources. I wonder if there is some more or less obvious error I'm doing here, since I'm pretty new to Ruby extensions and embedding. I know the description is far from good and feel free to ask any question you have with understanding the problem. If anybody is interested in full sources feel free to ask also. Wow, what a confused post that is. Any hints very appreciated since I'm pretty stuck. - Jan. The relevant code parts follow (I left out some #include's and maybe some unimportant code). messages.cpp ------------ static VALUE message = Qnil; static VALUE args = Qnil; static VALUE message_registry = Qnil; void messages_init() { rb_global_variable(&message); message = rb_eval_string("Message.new"); args = rb_iv_get(message, "@args"); message_registry = rb_eval_string("MessageRegistry.instance"); } void MessageBegin(int dest, int _msg_type, const float *origin, edict_t *edict) { rb_funcall(args, rb_intern("clear"), 0); rb_iv_set(message, "@message_type", INT2FIX(_msg_type)); rb_iv_set(message, "@destination", INT2FIX(dest)); rb_iv_set(message, "@receiver", INT2FIX(ENTINDEX(edict))); RETURN_META(MRES_IGNORED); } void MessageEnd(void) { rb_funcall(message_registry, rb_intern("dispatch_message"), 1, message); RETURN_META(MRES_IGNORED); } void WriteByte(int value) { rb_ary_push(args, INT2FIX(value)); RETURN_META(MRES_IGNORED); } void WriteChar(int value) { rb_ary_push(args, INT2FIX(value)); RETURN_META(MRES_IGNORED); } void WriteShort(int value) { rb_ary_push(args, INT2FIX(value)); RETURN_META(MRES_IGNORED); } void WriteLong(int value) { rb_ary_push(args, INT2NUM(value)); RETURN_META(MRES_IGNORED); } void WriteAngle(float value) { rb_ary_push(args, rb_float_new(value)); RETURN_META(MRES_IGNORED); } void WriteCoord(float value) { rb_ary_push(args, rb_float_new(value)); RETURN_META(MRES_IGNORED); } void WriteString(const char *s) { rb_ary_push(args, rb_str_new2(s)); RETURN_META(MRES_IGNORED); } void WriteEntity(int value) { rb_ary_push(args, INT2NUM(value)); RETURN_META(MRES_IGNORED); } message_dispatch.rb ------------------- class MessageRegistry include Singleton attr_accessor :verbose, :message_count def initialize @registry = {} @message_count = 0 @verbose = true end def register_for(message_name, &block) id = Game.get_message_id(message_name) Game.log "Registering for message id #{id}" if (! @registry.has_key? id) @registry[id] = [] end @registry[id] << block end def dispatch_message(message) @message_count += 1 if (@verbose) Game.log "Message count: #{@message_count}" Game.log "Message type: #{message.message_name} (#{message.message_type})" Game.log "Message args: [#{message.args.join ','}]" Game.log "Message receiver: #{message.receiver}, destination: #{message.destination}" end if (@registry.has_key? message.message_type) @registry[message.message_type].each { |block| block.call(message) } end end end class Message attr_accessor :message_type, :destination, :receiver, :args def initialize @args = [] @message_type = nil @destination = nil @receiver = nil end def message_name Game.get_message_name(@message_type) end end rb_game.cpp ----------- [...] VALUE rb_game_log(VALUE self, VALUE message) { UTIL_LogPrintf("[%s] %s\n", MODULE_TAG, STR2CSTR(rb_obj_as_string(message))); return Qnil; } void rb_game_init() { rb_game_module = rb_define_module("Game"); [...] rb_define_module_function(rb_game_module, "log", RUBY_METHOD_FUNC(rb_game_log), 1); [...] } [...]