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);
[...]
}
[…]