Skip to content

Instantly share code, notes, and snippets.

@jamesu
Created October 5, 2012 21:12
Show Gist options
  • Save jamesu/3842450 to your computer and use it in GitHub Desktop.
Save jamesu/3842450 to your computer and use it in GitHub Desktop.
Generates bindings from a Torque3D engineAPI.xml export
#
# Generates engineAPI bindings for Torque3D taken from engineAPI.xml
# Outputs to ext_bindings.h and ext_bindings.cpp
#
# NOTE: Currently the generated code is not meant to be usable,
# and is more of a proof-of-concept. There is also a lot of
# unnecessary duplication.
#
# To export the engineAPI console.xml:
#
# exportEngineAPIToXML().saveFile("engineAPI.xml");
#
#
# (C) 2012 James S Urquhart
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
require 'rubygems'
require 'nokogiri'
# Generated lists
$global_function_defs = []
$global_function_vars = []
$class_defs = []
$klass_data = {}
$forward_cdecls = []
$function_pointer_vars = []
$function_pointer_assigns = []
$engine_type_vars = []
$engine_typeinfo_vars = []
$engine_type_assigns = []
# Mapping of basic type -> typedef
$basic_types = {
'void' => '',
'float' => '',
'double' => '',
'bool' => '',
'uint' => 'unsigned int',
'byte' => 'char',
'ubyte' => 'unsigned char',
'int' => '',
'bool' => '',
}
$basic_typemaps = {
'void' => 'void',
'ptr' => 'void*',
'float' => 'float',
'double' => 'double',
'bool' => 'bool',
'uint' => 'uint',
'byte' => 'byte',
'ubyte' => 'ubyte',
'int' => 'int',
'string' => 'TorqueString'
}
$type_sizes = {
'void' => 4,
'float' => 4,
'double' => 8,
'bool' => 1,
'uint' => 4,
'ubyte' => 1,
'byte' => 1,
'int' => 4,
}
# List of fields to enumerate once we've figured out all the types
# [klass, size]
$fields_to_enumerate = []
# List of classes to enumerate once we've figured out all the types
$klasses_to_enumerate = []
# List of functions to enumerate once we've figured out all the types
# [klass, scope, methods]
$functions_to_enumerate = []
# Current namespace scope
$engine_scope = []
# Generates bindings for an enum
def bind_enum(node)
type_name = ($engine_scope + [node['name']]).join('::')
enum_list = ["typedef enum {"]
enum_list << node.xpath('enums/*').map do |enum|
" #{enum['name']} = #{enum['value']}"
end.join(",\n")
enum_list << "} EXP#{node['name']};\n"
$basic_typemaps[type_name] = "EXP#{node['name']}"
$engine_type_vars << enum_list.join("\n")
end
# Prepares to generate bindings for a struct
def bind_struct(node)
# Similar to class except we definitely have fields
bind_class(node)
end
# Prepares to generate bindings for a global console function
def bind_function(node)
$functions_to_enumerate << [nil, $engine_scope.clone, [node], []]
end
# Prepares to generate bindings for an EngineExportScope
def bind_scope(node)
scope_nest = " " * ($engine_scope.length * 2)
unless node['name'].empty?
puts "#{scope_nest}Entering scope #{node['name']}"
$engine_scope << node['name']
scope_nest = " " * ($engine_scope.length * 2)
end
node.xpath('exports/*').each do |export|
puts "#{scope_nest}Adding node #{export.node_name} (#{export['name']})"
case export.node_name
when 'EngineExportScope'
bind_scope(export)
when 'EngineStructType'
bind_struct(export)
when 'EngineClassType'
bind_class(export)
when 'EngineEnumType'
bind_enum(export)
when 'EngineFunction'
bind_function(export)
end
end
unless node['name'].empty?
$engine_scope.pop unless node['name'].empty?
scope_nest = " " * ($engine_scope.length * 2)
puts "#{scope_nest}Exiting scope #{$engine_scope[$engine_scope.length-1]}"
end
end
# Generates functions for class methods or scope functions
def get_method_list(name, methods, is_klass=true, is_global=false)
function_list = []
methods.each do |method|
args = []
arg_names = []
next if method['isCallback'].to_i == 1 # not sure how to handle these yet
static_function = true
return_type = $basic_typemaps[method['returnType']] || "UNK#{method['returnType']}"
method.xpath('arguments/EngineFunctionArgument').each do |arg|
if arg['name'] == 'this' && is_klass
static_function = false
next # no need to emit this
end
arg_type = $basic_typemaps[arg['type']] || "UNK#{arg['type']}"
args << "#{arg_type} #{arg['name']}"
arg_names << arg['name']
arg.type
end
if method['isVariadic'].to_i == 1
args << '...'
end
# Output the correct code to define the function
if is_global
# Global functions go in the EXPConsoleFunctions namespace
$engine_type_vars << "typedef #{return_type}(*EXP#{method['symbol']})(#{args.join(',')});"
function_list << "extern EXP#{method['symbol']} ptr#{method['name']};"
# Setup function pointer
func_ptr = "EXPConsoleFunctions::ptr#{method['name']}"
$global_function_vars << "EXP#{method['symbol']} ptr#{method['name']};"
$function_pointer_assigns << "#{func_ptr} = (EXP#{method['symbol']}}getTorqueProc(\"#{method['symbol']}\");"
# Wrapper to call engine function
function_list << "inline #{return_type} #{method['name']}(#{args.join(',')}) { #{return_type == 'void' ? '' : 'return '} #{func_ptr}(#{arg_names.join(',')}); }"
elsif static_function
# Engine function type
$engine_type_vars << "typedef #{return_type}(*EXP#{method['symbol']})(#{args.join(',')});"
# Engine function pointer
function_list << "static EXP#{method['symbol']} ptr#{method['name']};"
# Setup function pointer
func_ptr = "EXP#{name}::ptr#{method['name']}"
$function_pointer_vars << "EXP#{method['symbol']} #{func_ptr};"
$function_pointer_assigns << "#{func_ptr} = (EXP#{method['symbol']}}getTorqueProc(\"#{method['symbol']}\");"
# Wrapper to call engine function
function_list << "static inline #{return_type} #{method['name']}(#{args.join(',')}) { #{return_type == 'void' ? '' : 'return '} #{func_ptr}(#{arg_names.join(',')}); }"
else
# Engine function type
$engine_type_vars << "typedef #{return_type}(EXP#{name}::*EXP#{method['symbol']})(#{args.join(',')});"
# Setup function pointer
full_args = ["EXP#{name}* this"] + args
function_list << "static EXP#{method['symbol']} ptr#{method['name']};"
# Grab proc address
func_ptr = "EXP#{name}::ptr#{method['name']}"
$function_pointer_vars << "EXP#{method['symbol']} #{func_ptr};"
$function_pointer_assigns << "#{func_ptr} = (EXP#{method['symbol']})getTorqueProc(\"#{method['symbol']}\");"
# Wrapper to call engine function
function_list << "inline #{return_type} #{method['name']}(#{args.join(',')}) { #{return_type == 'void' ? '' : 'return '} ((this).*(#{func_ptr}))(#{arg_names.join(',')}); }"
end
end
function_list
end
# Gets the field list in a class or struct
def get_field_list(fields, size, parent_fields=[])
last_ptr = 0
# pre-process field list
field_entries = (fields+parent_fields).map do |field|
{
'offset' => field['offset'].to_i,
'name' => field['name'],
'type' => field['type'],
'indexedSize' => field['indexedSize'].to_i
}
end.sort_by{|f|f['offset']}
# Generate a list of fields, filling in the blanks with data
fields_list = []
field_entries.each do |field|
offset = field['offset']
if offset > last_ptr
gap_size = offset - last_ptr
fields_list << "char unkField#{fields_list.length}[#{gap_size}];"
end
# Enumerate field
# TODO: does indexedSize indicate an array?
field_type = $basic_typemaps[field['type']] || "UNK#{field['type']}"
fields_list << "#{field_type} #{field['name']};"
last_ptr += $type_sizes[field['type']] || 1
end
if size - last_ptr > 0
fields_list << "char unkField#{fields_list.length}[#{size - last_ptr}];"
end
fields_list
end
# Prepares to generate a binding for a class or struct
def bind_class(node)
name = node['name']
scope = $engine_scope.clone
type_name = ($engine_scope + [node['name']]).join('::')
methods = node.xpath('exports/*')
fields = node.xpath('fields/*')
function_list = []
# Add constructor and destructor if we can construct this
if node['constructable'].to_i
function_list << "EXP#{name}() { sTypeInfo->constructInstance(this); }"
function_list << "~EXP#{name}() { sTypeInfo->destructInstance(this); }"
end
klass = {
'name' => name,
'type_name' => type_name,
'scope' => scope,
'size' => node['size'].to_i,
'parent' => node['superType']||nil,
'function_types' => [],
'function_list' => function_list,
'fields_list' => nil,
'fields_node' => fields.map{|n|n},
'methods_node' => methods
}
$type_sizes[klass['type_name']] = node['size'].to_i
$engine_type_assigns << "EXP#{name}::sTypeInfo = EngineTypeInfo::getTypeInfoByName(\"#{name}\");"
$engine_typeinfo_vars << "EngineTypeInfo *EXP#{name}::sTypeInfo;"
$klasses_to_enumerate << klass
$klass_data[klass['type_name']] = klass
$basic_typemaps[klass['type_name']] = node.name == 'EngineClassType' ? "EXP#{name}*" : "EXP#{name}"
end
# Enumerates all fields, classes and functions once we've run over the type information
def bind_all_classes()
# Enumerate fields, though collect the parent type list first
$klasses_to_enumerate.each do |klass|
parent_fields = []
parent_methods = []
parent_klass = klass['parent']
while parent_klass != nil
parent_fields += $klass_data[parent_klass]['fields_node'].map{|n|n}
parent_methods += $klass_data[parent_klass]['methods_node'].map{|n|n}
parent_klass = $klass_data[parent_klass]['parent']
end
# Enumerate class methods including parent methods
$functions_to_enumerate << [klass, klass['scope'], klass['methods_node'], parent_methods]
klass['fields_list'] = get_field_list(klass['fields_node'], klass['size'], parent_fields)
end
# Populate global and class functions
$functions_to_enumerate.each do |func|
klass, scope, methods, parent_methods = func
if klass.nil?
# Global scope function
funcs = get_method_list(scope.join('_'), methods, false, true)
$global_function_defs += funcs
else
# Class method
#puts "Enumerating #{func.inspect}"
funcs = get_method_list(klass['name'], (parent_methods||[]) + methods)
klass['function_list'] += funcs
end
end
$klasses_to_enumerate.each do |klass|
name = klass['name']
fields_list = klass['fields_list']
size = klass['size']
function_list = klass['function_list']
class_str = <<-EOS
class EXP#{name} {
public:
union {
struct {
#{fields_list.join("\n ")}
};
char data[#{size}];
};
#{function_list.join("\n ")}
static EngineTypeInfo *sTypeInfo;
};
EOS
$class_defs << class_str
end
end
# Outputs the ext_bindings.h, ext_bindings.cpp files
def output_binding_files()
File.open('ext_bindings.h', 'w') do |f|
basic_typelist = $basic_types.map do |k,v|
v.empty? ? nil : "typedef #{v} #{k};"
end.compact
f.write(<<-EOS
// Automatically generated engineAPI binding code
// Generated at #{Time.now.to_s}
class TorqueString;
// Typedefs for basic types
#{basic_typelist.join("\n")}
#{$engine_type_vars.join("\n")}
// Types
#{$class_defs.join("\n\n")}
// Global functions
namespace EXPConsoleFunctions
{
#{$global_function_defs.join("\n")}
}
void extInitEngineBindings(); // Initializes bindings
EOS
)
end
File.open('ext_bindings.cpp', 'w') do |f|
f.write(<<-EOS
// Automatically generated engineAPI binding code
// Generated at #{Time.now.to_s}
// Function pointers
#{$function_pointer_vars.join("\n")}
#{$engine_typeinfo_vars.join("\n")}
// Global functions
namespace EXPConsoleFunctions
{
#{$global_function_vars.join("\n")}
}
void extInitEngineBindings()
{
// Engine types
#{$engine_type_assigns.join("\n ")}
// Function pointers
#{$function_pointer_assigns.join("\n ")}
}
EOS
)
end
end
# Entrypoint
$document = Nokogiri::XML
doc = File.open('engineAPI.xml').read
# Parse document
$document.parse(doc).xpath('EngineExportScope').each do |scope|
bind_scope(scope)
end
# Bind and write the bindings!
bind_all_classes()
output_binding_files()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment