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.

Module C

Module C
By: Zeus81


Introduction
This script almost does nothing, it's a script for scripters, so if you're not you can pass your way, it won't interest you.
This module allows you to create/read/alter C typed data directly in Ruby and it's easier to use than Strings with pack/unpack, everything being done automatically.
If you intend to do a big script that use complicated structures or need to communicate in both ways with a dll then that's for you.


Script

Ruby:
module C # v 5.1 by Zeus81

  RtlMoveMemory = Win32API.new('kernel32', 'RtlMoveMemory', 'iii', '')

  def self.memcpy(destination, source, size)

    RtlMoveMemory.call(

        destination.is_a?(String) ? string_address(destination) : destination,

        source.is_a?(String) ? string_address(source) : source, size)

    destination

  rescue raise($!, $!.message, caller)

  end

  

  def self.string_address(string) [string].pack('p').unpack('L')[0]

  rescue raise($!, $!.message, caller)

  end

  

  @anonymous_class_count = 0

  def self.name_anonymous_class(type)

    if type.name.empty?

      type.superclass.name =~ /.*::(.*)/

      const_set("Anonymous#{$1}_%02X" % @anonymous_class_count+=1, type)

    end

  rescue raise($!, $!.message, caller)

  end

  

  def self.make_accessors(klass, data, child=nil)

    data.each_with_index do |d,i|

      if d[2] != nil

        a = d[2] == :[] ? 'i,' : ''

        b = d[2] == :[] ? '[i]' : child ? ".#{d[2]}" : "[#{i}]"

        b = "[#{child}]#{b}" if child

        c = b + (child ? '=' : '.set')

        b << '.get' unless child or d[0] < CData

        klass << "

        define_method(:#{d[2]}) {|#{a[0,1]}| @children#{b}}

        define_method(:#{d[2]}=) {|#{a}v| @children#{c}(v)}"

        break if d[2] == :[]

      else make_accessors(klass, d[0].data, child || i)

      end

    end

  rescue raise($!, $!.message, caller)

  end

  

  class CType

    def self.format() self::FORMAT end

    def format()       type.format end

    def self.size()   self::SIZE   end

    def size()         type.size   end

    def initialize(*v, &b)

      @address = b ? b.call.to_int : C.string_address(@string=("\0"*size).freeze)

      @children = data.map {|t,o| t.new {to_int+o}} if type < C::CData

      set(*v) unless v.empty?

    rescue raise($!, $!.message, caller)

    end

    def get()  unpack

    rescue raise($!, $!.message, caller)

    end

    def set(v) pack(v)

    rescue raise($!, $!.message, caller)

    end

    def unpack() to_str.unpack(format)[0].freeze

    rescue raise($!, $!.message, caller)

    end

    def pack(v)  C.memcpy(self, v.is_a?(type) ? v : [v].pack(format), size)

    rescue raise($!, $!.message, caller)

    end

    def to_int() @address

    rescue raise($!, $!.message, caller)

    end

    def to_str() @string or C.memcpy(("\0"*size).freeze, self, size)

    rescue raise($!, $!.message, caller)

    end

  end

  

  class CData < CType

    def self.data() self::DATA end

    def data()       type.data end

    def get() @children.map {|c| c.get}.freeze

    rescue raise($!, $!.message, caller)

    end

    def set(*v)

      if    v[0].is_a?(type); super(v[0])

      elsif v[0].is_a?(Hash)

        v[0].each {|n,v| n.is_a?(Integer) ? @children[n].set(*v) : send("#{n}=",v)}

      else v.each_with_index {|v,i| @children[i].set(*v)}

      end

    rescue raise($!, $!.message, caller)

    end

    def [](i)    data[i][0] < CData ? @children[i] : @children[i].get

    rescue raise($!, $!.message, caller)

    end

    def []=(i,v) @children[i].set(*v)

    rescue raise($!, $!.message, caller)

    end

  end

  

  CStruct, CUnion, CArray = Class.new(CData), Class.new(CData), Class.new(CData)

  

  def self.Class(type, klass)

    raise(TypeError,"Type expected, got #{type.type}") unless type.is_a?(Class) and

                                                              type <= CType

    Class.new(type) {class_eval(klass, __FILE__, __LINE__)}

  rescue raise($!, $!.message, caller)

  end

  

  def self.Type(format, size)

    raise(TypeError,"String expected, got #{format.type}") unless format.is_a?(String)

    raise(TypeError,"Integer expected, got #{size.type}") unless size.is_a?(Integer)

    Class(CType, "SIZE, FORMAT = #{size}, '#{format}'.freeze")

  rescue raise($!, $!.message, caller)

  end

  

  def self.Enum(type, *variables)

    h, v = {}, -1

    variables.each_with_index do |n,i|

      next unless n.is_a?(Symbol)

      h[n] = v = (next_v=variables[i+1]).is_a?(Symbol) ? v.succ : next_v

    end

    Class(type, "DATA = #{h.inspect}.freeze

    def self.method_missing(sym, *args) DATA[sym] or super

    rescue raise($!, $!.message, caller)

    end")

  rescue raise($!, $!.message, caller)

  end

  

  def self.Data(type, variables, array_size=nil)

    size, data, t = 0, [], nil

    variables.each_with_index do |n,i|

      if n.is_a?(Class) and n < CType

        t, n = n, nil

        next if variables[i+1].is_a?(Symbol)

      end

      next unless t != nil and (n==nil or n.is_a?(Symbol))

      name_anonymous_class(t)

      if    type == CStruct

        data << [t,size,n]

        size = size+t.size

      elsif type == CUnion

        data << [t,0,n]

        size = t.size if t.size > size

      elsif type == CArray

        data.replace(Array.new(array_size) {|j| [t,j*t.size,n]})

        size = t.size*array_size

      end

    end

    klass = "SIZE, FORMAT, DATA = #{size}, 'a#{size}'.freeze, #{data.inspect}.freeze"

    make_accessors(klass, data) unless type == CArray

    Class(type, klass)

  rescue raise($!, $!.message, caller)

  end

  

  def self.Struct(*variables) Data(CStruct, variables)

  rescue raise($!, $!.message, caller)

  end

  

  def self.Union (*variables) Data(CUnion , variables)

  rescue raise($!, $!.message, caller)

  end

  

  def self.Array (type, *dimensions)

    dimensions.reverse_each {|s| type=Data(CArray , [type,:[]], s)}

    type

  rescue raise($!, $!.message, caller)

  end

  

  CHAR      =             Type('c',1)

  UCHAR     = BYTE      = Type('C',1)

  SHORT     =             Type('s',2)

  USHORT    = WORD      = Type('S',2)

  INT       =             Type('i',4)

  UINT      =             Type('I',4)

  LONG      =             Type('l',4)

  ULONG     = DWORD     = Type('L',4)

  LONGLONG  =             Type('q',8)

  ULONGLONG = DWORDLONG = Type('Q',8)

  FLOAT     =             Type('f',4)

  DOUBLE    =             Type('d',8)

  BOOLEAN   =             Type('C',1)

  BOOL      =             Type('i',4)

  POINTER   =             Type('L',4)

  class BOOLEAN

    def unpack() super==0 ? false : true

    rescue raise($!, $!.message, caller)

    end

    def pack(v)  super(!v || v==0 ? 0 : 1)

    rescue raise($!, $!.message, caller)

    end

  end

  class BOOL

    def unpack() super==0 ? false : true

    rescue raise($!, $!.message, caller)

    end

    def pack(v)  super(!v || v==0 ? 0 : 1)

    rescue raise($!, $!.message, caller)

    end

  end

  class POINTER

    def unpack() @pointer

    rescue raise($!, $!.message, caller)

    end

    def pack(v)  super(@pointer=v ? v.to_int : 0)

    rescue raise($!, $!.message, caller)

    end

  end

end
[/i]
Instructions

Type:
There are 15 predefined data types and there is no need for more.
CHAR = C signed char
UCHAR = BYTE = C unsigned char
SHORT = C signed short
USHORT = WORD = C unsigned short
INT = C signed int
UINT = C unsigned int
LONG = C signed long
ULONG = DWORD = C unsigned long
LONGLONG = C signed long long
ULONGLONG = DWORDLONG = C unsigned long long
FLOAT = C float
DOUBLE = C double
BOOLEAN which is an UCHAR but that can handle Ruby's true & false.
BOOL which is an INT but that can handle Ruby's true & false.
POINTER which can point on any object given by the C module.

Enum:
C.Enum(type, [name, [value, [name, [value...]]]])
It's not indispensable, it's just to simplify the creation of constants.
type is the Type the Enum will have in case we use it in a Struct (usualy it's INT).
The names must be Symbols.
The values must be Integers.
If a value is omitted it'll take the previous one + 1, or 0 if it's the first.
Color = C.Enum(C::INT, :red  , 0,
:green, 1,
:blue , 2)

equals:
Color = C.Enum(C::INT, :red  ,
:green,
:blue )

and to read a value we do:
Color.green


Array:
C.Array(type, [dimension1, [dimension2 ...]])
type is the Type all the elements of the array will have.
We can specify as much dimensions as we want with Integers.
I4 = C.Array(C::INT, 4)
tab = I4.new

I4 is the class that will allow me to create a 4 int array.
Generally for arrays we'll use anonymous classes by directly doing:
tab = C.Array(C::INT, 4).new
By default an array is initialized with everything to 0, there is many methods to change values.
Immediately during instantiation by passing parameters in order:
tab = I4.new(60, 61, 62, ...)
in disorder:
tab = I4.new(2=>62, 0=>60, ...)
or with an object of the same class:
tab2 = I4.new(tab1)
Once the object is created we can modify values with the set method (which works like new) or with the operator []= :
tab[0] = 60
To read values we use [] :
tab[0] # => 60
or get method which returns a Ruby array of all values:
tab.get # => [60, 61, 62, 63]
Multidimensional Arrays are in fact Arrays of Arrays:
C.Array(C::INT, 4, 3)
equals:
C.Array(C.Array(C::INT, 3), 4)
And so we can change values in several ways:
tab = C.Array(C::INT, 4, 3).new
tab[0][2] = 1
tab[1].set(2, 3, 4)
tab[2] = [5, 6, 7]
tab.set(3=>[8, 9])
tab.get # => [[0, 0, 1], [2, 3, 4], [5, 6, 7], [8, 9, 0]]

I know, I explained very poorly, but normally it should be fairly logical.


Struct:
C.Struct([type, [name, [type, [name, ...]]]])
type can be an INT, CHAR, etc. ... but also Struct, Union, Enum Array.
Names should be passed as symbols.
If a type is omitted the variable will have the previous type.
If a name is omitted the variable will be anonymous.
If the anonymous variable is a Struct, Union or Array, its functions will be inherited (as in C).

Basically a Struct is like an Array except that each element has a different type and name.
Fruit = C.Struct(C::FLOAT, :diameter,
C::BOOL , :seeds)
As for an Array there are several ways to configure a Struct:
apple = Fruit.new
apple.set(5.8, true)
apple.set:)diameter=>5.8, :seeds=>true)
apple.set(0=>5.8, 1=>true)
apple[0] = 5.8 # Beware if the Struct contains an anonymous Array, this is not possible.
apple.seeds = true
apple1.set(apple2)

Even with a Struct get still returns a Ruby array of values:
apple.get # => [5.8, true]
Please note, if a Struct contains another Struct you must group the data as for Multidimensional Arrays.
Tree = C.Struct(C::INT, :height,
Fruit , :fruit,
Color , :color)
Tree.new(9, 5.8, true, Color.green) # => error
Tree.new(9, [5.8, true], Color.green) # => ok
Tree.new(9, apple, Color.green) # => ok




Union:
It works like Struct except a few details.
Example of an Union with anonymous Struct and Array:
MATRIX = C.Union(C.Struct(FLOAT, :_11, :_12, :_13, :_14,
:_21, :_22, :_23, :_24,
:_31, :_32, :_33, :_34,
:_41, :_42, :_43, :_44),
C.Array(FLOAT,4,4))
m = MATRIX.new(0=>[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16])
# You must choose according to which format of the Union you will modify the data,
# since both are anonymous I do with the id, here 0 is the Struct, for the Array I should have done:
# m = MATRIX.new(1=>[[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]])
m._43 == m[3][2] # => true
# Because the elements are anonymous their functions are inherited
# m[3] don't call the element 3 of m but the row 3 of the Array.
m.get # => [ [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16],
# [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]]
# ]
# the get of an Union returns an array containing all its formats.




Pointer:
A short piece about the pointers because it's not obvious.
A = C.Struct(C::POINTER, :ptr)
a = A.new
b = C::CHAR.new(7) # char*
a.ptr = b
a.ptr.get # => 7
b.set(45)
a.ptr.get # => 45
a.ptr.set(-16)
b.get # => -16

I think that it is clear enough.
Another example of what would correspond to a double indirection:
a.ptr = C::POINTER.new(C::INT.new(128)) # int**
a.ptr.get.get # => 128

And as said before, a pointer can point to any object of the C module:
a.ptr = a
a.ptr.get # => a

Another thing about pointers, if you retrieve a pointer via an API:
ptr = Win32API.new('dll', 'GetPointer', '', 'i').call
We can read / change the value to which it points, whatever the type is, by passing the address into a block of a new object.
For example if it's a float:
f = C::FLOAT.new {ptr}
f.get # => retrieves the value directly from memory
f.set(0.1) # change it

And it also works with Struct/Array/Union:
MyStruct = C.Struct(...)
s = MyStruct.new {ptr}

Of course MyStruct must be an exact replica of the C struct to which it points.
However, beware to memory allocations, if I get the pointer to a data of the dll which was released there may be problems.

Simple and concrete example of use, if I want to reproduce this structure and use this Api I do:
POINT = C.Struct(C::LONG, :x, :y)
GetCursorPos = Win32API.new('user32', 'GetCursorPos', 'i', 'i') # we put 'i' for the struct and not 'p ' to avoid copies in some cases
$cursor = POINT.new
GetCursorPos.call($cursor) # we pass the object directly.
$cursor.x # returns the x position of the cursor

Well of course in this example it's not really intredasting to use my script, but that's for those who want to do things far more complicated.
 
I haven't actually tried it, but I must say, this looks like it would be very useful for any scripter who wants to work in advanced Win32API. I wouldn't use it myself, but that's mostly because I'm quite used to doing it the old fashioned way, and it cuts down on required scripts if I continue doing what I already know how to do.

That said, I highly recommend this script to anyone who wants to do more than just the basics in Win32API, if only because it saves me from trying to explain how to deal with passing datatypes you can't pass.
 
I like this Struct thing. I often make Hashes which get dumped into objects to simulate the data structure of RMXP. By creating what are essentially arrays but with .valuenames it would definitely cut down on loading time. Cheers.
 

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