Envision, Create, Share

Welcome to HBGames, a leading amateur game development forum and Discord server. All are welcome, and amongst our ranks you will find experts in their field from all aspects of video game design and development.

Ruby pack/unpack binary

I am having a hard time with this one.

So I am using the old Win32API in ruby and I have a DLL function GetStruct that returns a pointer to a structure, my ruby looks a bit like this;

Ruby:
module XNE

  

  @getStruct        = Win32API.new( 'XNE100a', 'XNE_GetStruct', 'V', 'P' )

 

  def self.GetStruct

    return @getStruct.call

  end

end

The structure in C++ looks like this:
C++:
typedef int integer_t;      // I

typedef long long_t;        // L

typedef void void_t;        // V

typedef void * pointer_t;   // P

 

typedef struct struct_s {

    integer_t   integer;

    long_t      longInteger;

    pointer_t   string;

} struct_t;

I call this code:
Ruby:
test = XNE::GetStruct()

a, b, c = test.unpack('ILP')

print a, b, c

a b and c are all nil, but the code runs perfectly fine?
I tried returning the structure itself instead of a pointer to it, but the whole thing crashes.

I am testing this in RPG Maker VX.

The full C++ source of my DLL function:
C++:
typedef int integer_t;      // I

typedef long long_t;        // L

typedef void void_t;        // V

typedef void * pointer_t;   // P

 

typedef struct struct_s {

    integer_t   integer;

    long_t      longInteger;

    pointer_t   string;

} struct_t;

 

__declspec( dllexport ) pointer_t   XNE_GetStruct( void_t );

 

pointer_t XNE_GetStruct( void_t ) {

    struct_t * const result = <span style="color: #0000dd;">new struct_t;

    result->integer = <span style="color: #0000dd;">54;

    result->longInteger = <span style="color: #0000dd;">108;

    result->string = <span style="color: #666666;">"Winner";

 

    return result;

}

 


What am I misunderstanding here and what do I do????

EDIT: I kind of understand now that I need to provide a buffer via ruby to store the struct data, can anyone help me out here?

EDIT2: So I have it being able to return integers from the structure perfectly fine, but I have trouble with that C character pointer (The text "Winner") how do I go about doing this? With the Win32API I can usually use the pointer "P" return type and my c strings will cast into ruby strings nicely, but I can't figure it out here
 
Lemme give you a bit from a project I started a while back that was decoding the Terraria world header:

Code:
 

  #--------------------------------------------------------------------------

  # * Read boolean

  #     file : input file, opened for reading

  #--------------------------------------------------------------------------

  def read_boolean(file)

    if (!validate_file_read(file))

      return nil

    end

 

    byte = read_unsigned_byte(file)

 

    if(!validate_unsigned_byte(byte))

      return nil

    end

 

    return (byte != 0)

  end

 

  #--------------------------------------------------------------------------

  # * Read Pascal String

  #     file : input file, opened for reading

  #--------------------------------------------------------------------------

  def read_pstring(file)

    if (!validate_file_read(file))

      return nil

    end

 

    length = read_unsigned_byte(file)

 

    if (length <= 0)

      return nil

    end

 

    binary_data = file.read(length)

    result = binary_data.unpack("M*")[0]

 

    if(!validate_string(result))

      return nil

    end

 

    return result

  end

 

  #--------------------------------------------------------------------------

  # * Read Rectangle

  #     file : input file, opened for reading

  #--------------------------------------------------------------------------

  def read_rect(file)

    if (!validate_file_read(file))

      return nil

    end

 

    x      = read_unsigned_int(file)

    width  = read_unsigned_int(file)

    y      = read_unsigned_int(file)

    height = read_unsigned_int(file)

 

    if (!validate_unsigned_int(x))

      return nil

    elsif (!validate_unsigned_int(width))

      return nil

    elsif (!validate_unsigned_int(y))

      return nil

    elsif (!validate_unsigned_int(height))

      return nil

    end

 

    return Rect.new(x, y, width, height) 

  end

 

  #--------------------------------------------------------------------------

  # * Read signed int

  #     file : input file, opened for reading

  #--------------------------------------------------------------------------

  def read_signed_int(file)

    if (!validate_file_read(file))

      return nil

    end

 

    binary_data = file.read(4)

    result = binary_data.unpack("l*")[0]

 

    if(!validate_signed_int(result))

      return nil

    end

 

    return result

  end

 

  #--------------------------------------------------------------------------

  # * Read unsigned byte

  #     file : input file, opened for reading

  #--------------------------------------------------------------------------

  def read_unsigned_byte(file)

    if (!validate_file_read(file))

      return nil

    end

 

    binary_data = file.read(1)

    result = binary_data.unpack("C")[0]

 

    if(!validate_unsigned_byte(result))

      return nil

    end

 

    return result

  end

 

  #--------------------------------------------------------------------------

  # * Read unsigned double

  #     file : input file, opened for reading

  #--------------------------------------------------------------------------

  def read_unsigned_double(file)

    if (!validate_file_read(file))

      return nil

    end

 

    binary_data = file.read(8)

    result = binary_data.unpack("E*")[0]

 

    if(!validate_unsigned_double(result))

      return nil

    end

 

    return result

  end

 

  #--------------------------------------------------------------------------

  # * Read unsigned_int

  #     file : input file, opened for reading

  #--------------------------------------------------------------------------

  def read_unsigned_int(file)

    if (!validate_file_read(file))

      return nil

    end

 

    binary_data = file.read(4)

    result = binary_data.unpack("V*")[0]

 

    if(!validate_unsigned_int(result))

      return nil

    end

 

    return result

  end

 

  #--------------------------------------------------------------------------

  # * Write boolean

  #     file  : output file, opened for reading

  #     value : value to write

  #--------------------------------------------------------------------------

  def write_boolean(file, value)

    if (!validate_file_write(file))

      return false

    elsif (!validate_boolean(value))

      return false

    end

 

    return write_unsigned_byte(file, (value ? 1 : 0))

  end

 

  #--------------------------------------------------------------------------

  # * Write Pascal String

  #     file  : output file, opened for reading

  #     value : value to write

  #--------------------------------------------------------------------------

  def write_pstring(file, value)

    if (!validate_file_write(file))

      return false

    elsif (!validate_string(value))

      return false

    end

 

    array = Array.new(1, value)

    binary_data = array.pack("M*")

 

    if (!write_unsigned_byte(file, binary_data.length))

      return false

    end

 

    if(binary_data.length == 0)

      return true

    end

 

    file.write(binary_data)

 

    return true

  end

 

  #--------------------------------------------------------------------------

  # * Write Rectangle

  #     file  : output file, opened for reading

  #     value : value to write

  #--------------------------------------------------------------------------

  def write_rect(file, value)

    if (!validate_file_write(file))

      return false

    elsif (!validate_rect(value))

      return false

    end

 

    if (!write_unsigned_int(file, value.x))

      return false

    elsif (!write_unsigned_int(file, value.width))

      return false

    elsif (!write_unsigned_int(file, value.y))

      return false

    elsif (!write_unsigned_int(file, value.height))

      return false

    end

 

    return true

  end

 

  #--------------------------------------------------------------------------

  # * Write signed int

  #     file  : output file, opened for reading

  #     value : value to write

  #--------------------------------------------------------------------------

  def write_signed_int(file, value)

    if (!validate_file_write(file))

      return false

    elsif (!validate_signed_int(value))

      return false

    end

 

    array = Array.new(1, value)

    binary_data = array.pack("l")

    file.write(binary_data)

 

    return true

  end

 

  #--------------------------------------------------------------------------

  # * Write unsigned byte

  #     file  : output file, opened for reading

  #     value : value to write

  #--------------------------------------------------------------------------

  def write_unsigned_byte(file, value)

    if (!validate_file_write(file))

      return false

    elsif (!validate_unsigned_byte(value))

      return false

    end

 

    array = Array.new(1, value)

    binary_data = array.pack("C")

    file.write(binary_data)

 

    return true

  end

 

  #--------------------------------------------------------------------------

  # * Write unsigned double

  #     file  : output file, opened for reading

  #     value : value to write

  #--------------------------------------------------------------------------

  def write_unsigned_double(file, value)

    if (!validate_file_write(file))

      return false

    elsif (!validate_unsigned_double(value))

      return false

    end

 

    array = Array.new(1, value)

    binary_data = array.pack("E")

    file.write(binary_data)

 

    return true

  end

 

  #--------------------------------------------------------------------------

  # * Write unsigned_int

  #     file  : output file, opened for reading

  #     value : value to write

  #--------------------------------------------------------------------------

  def write_unsigned_int(file, value)

    if (!validate_file_write(file))

      return false

    elsif (!validate_unsigned_int(value))

      return false

    end

 

    array = Array.new(1, value)

    binary_data = array.pack("V")

    file.write(binary_data)

 

    return true

  end

This was how I was reading and writing the binary data from Terraria. The same rules apply to grabbing stuff from a dll via Win32API, and you can reverse the process to send a struct back to the dll. (Which is fun, once you get it to work.) For reference, check out String.unpack and Array.pack. Those give a full description of the available packing characters and what they'll assume you're handing them, and what you're converting them to and from.

Edit: You might have trouble with Strings, Ruby is weird about String encoding. Here's a bit from my keyboard module, regarding that:

Code:
 

    # create blank input array

    @keyboard_state = '0' * 256

    # Call current keyboard state

    @getKeyboardState.call(@keyboard_state)

 

    # Lots of stuff went here

 

    # Create a character buffer

    buffer = '0' * 10

    # Translate the current key to Unicode

    success = @toUnicodeEx.call(key, scan_code, @keyboard_state, buffer, 5, 0,

      @layout)

 

    # Other stuff validating input regarding dead keys went here

 

    # Translate the characters to Ruby encoding

    characters = buffer.unpack('C*').pack('U*')
Basically, that converted the output from the Win32API method ToUnicodeEx to the data I needed in a format I could use. Something to note is that, while I was working on this, I discovered that Windows operating systems from before Vista will actually fill any extra space in a buffer you supply them with garbage data, rather than only changing what they need. I can't remember how or if I managed to deal with that, but it was also before XP SP3 was released, so it MAY have changed.

Edit 2: Regarding the Strings again, I would recommend returning it in a similar fashion to how I dealt with Pascal Strings, i.e. have an int value listing the string's length, to use for decoding. It's fairly common in the native Windows API whenever there are dynamic elements in a struct.
 
Thanks dude, I got what I wanted working.

XNE::GetStruct
Ruby:
  def self.GetStruct

    buffer = '0' * 88

    @getStruct.call( buffer )

    arr = buffer.unpack( 'LLC*' )

    

    string = Array.new( arr )

    string.shift

    string.shift

    arr[2] = string.pack( 'U*' )

    

    return arr

  end
The 88 is the same as sizeof( struct_t ) in the C code, I will most likely modify this so it gets the sizeof( struct_t ) during intialisation.

Test snippet
Ruby:
test = XNE::GetStruct()

a = test[0]

b = test[1]

c = test[2]

print a

print b

print c
Out puts 54, 108, Winner as expected

C stuff
C:
<div class="c" id="{CB}" style="font-family: monospace;"><ol><span style="color: #993333;">typedef <span style="color: #993333;">int integer_t;      // I

<span style="color: #993333;">typedef <span style="color: #993333;">long long_t;        // L

<span style="color: #993333;">typedef <span style="color: #993333;">void void_t;        // V

<span style="color: #993333;">typedef <span style="color: #993333;">void * pointer_t;   // P

 

<span style="color: #993333;">typedef <span style="color: #993333;">struct struct_s {

    integer_t   integer;

    long_t      longInteger;

    <span style="color: #993333;">char        <span style="color: #993333;">string[<span style="color: #cc66cc;">80];

} struct_t;

 

__declspec( dllexport ) void_t  XNE_GetStruct( pointer_t _struct );

 

void_t XNE_GetStruct( pointer_t _struct ) {

    struct_t * <span style="color: #993333;">const result = ( struct_t * )_struct;

    result->integer = <span style="color: #cc66cc;">54;

    result->longInteger = <span style="color: #cc66cc;">108;

    strcpy( result->string, "Winner" );

}
Biggest difference here is that the string "Winner" is now stored along side the two numbers, rather than as a "nested" pointer (Which is how Ruby would see it)

And all the memory "management" is handled by Ruby! (And it's goddamn garbage collector)

EDIT: On top of this I should be able to write a lovely wrapper to automatically align the strings among the rest of the data, in addition to this the tokens can be defined in the C code and grabbed from RGSS, so we can detail the structs where the structs are defined (The DLL) and just have RGSS read that and store them into Ruby arrays, with the ability for it to go the other way around.
 

Thank you for viewing

HBGames is a leading amateur video game development forum and Discord server open to all ability levels. Feel free to have a nosey around!

Discord

Join our growing and active Discord server to discuss all aspects of game making in a relaxed environment. Join Us

Content

  • Our Games
  • Games in Development
  • Emoji by Twemoji.
    Top