Created
October 5, 2012 21:12
-
-
Save jamesu/3842450 to your computer and use it in GitHub Desktop.
Generates bindings from a Torque3D engineAPI.xml export
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# 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