#!/usr/bin/env ruby
LICENSE = <
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
GPLv3
require "optparse"
require "ostruct"
# Static templates:
TPL_FILENAME = 'CLibraryLoader#{options.name}.hpp'
TPL_INCLUDE = '#include <#{include_file}>'
TPL_STRUCT_COMMENT = ' // generated by dlib_generator.rb for #{header_file}'
TPL_STRUCT_POINTER = ' #{@return} (*#{@name})(#{@argument_list});'
TPL_LOAD_COMMENT = ' // generated by dlib_generator.rb for #{header_file}'
TPL_LOAD_SYMBOL = ' m_symbols->#{@name} = (#{@return} (*)(#{@argument_list})) LoadProcedure(\"#{@name}\");'
TPL_DEFINE_COMMENT = '// generated by dlib_generator.rb for #{header_file}'
TPL_DEFINE = '#define #{@name}(#{@argument_num_list}) (DLIB_#{@@library_name.upcase}.GetSymbols()->#{@name})(#{@argument_num_list})'
# Used to parse configuration file. The fileformat is very simple as you can see
# it just takes the keys as arguments
class GeneratorConfigParser
def initialize(filename)
if not File.exists? filename # or not a file
puts "ERROR: File not found: #{filename}"
exit
end
@config = {}
IO.readlines(filename).each do |line|
line.strip.match /^([#])?([^ ]+)( = )?(.*)?$/
next if $1 == '#' or $2 == '#'
if $2 and not $2.empty?
key = $2
value = $4.gsub(/^\"(.*)\"$/, "\\1") if $4
@config[key] = value
end
end
end # end initialize
def createArguments()
arguments = []
@config.each_pair do |key, value|
arguments << "--"+key
arguments << value if value and not value.empty?
end
return arguments
end
end
# Used to parse command-line parameters
class GeneratorOptionParser
def self.parse(args)
# setup default values for options
options = OpenStruct.new
# generator options
options.name = nil
options.template = "dlib_generator.tpl"
options.config = nil
options.write = nil
options.filename = nil # "virtual" is created on-the-fly from template
options.define = false
options.include = nil
# header options
options.header_file = nil
options.header_path = nil
options.file_pattern = "\\.h$"
options.prototype_pattern = nil
# binary options
options.library_file = nil
options.symbol_prefix = ""
# miscellaneous
options.verbose = false
# create option parser
opts = OptionParser.new do |opts|
opts.banner = "Usage: #{$0} [OPTIONS]\n" +
"Parse shared library binary and headers to generate code for a dynamic loader."
opts.separator ""
opts.separator "You need at least specify a name, a header file (or directory) and a prototype"
opts.separator "pattern. The binary options are optional, but can help to improve the result."
opts.separator ""
# generator options
opts.separator "Generator Options:"
opts.on("-n", "--name NAME",
"The name used to identify the library") do |name|
options.name = name
end
opts.on("-t", "--template FILE",
"Use FILE as generator template ",
"(Default: #{options.template})") do |template|
options.template = template
end
opts.on("-c", "--config FILE",
"Use FILE for configuration, overwrites other options") do |config|
options.config = config
end
opts.on("-w", "--write [FILE]",
"Write generated code to FILE, in addition to stdout",
"(Default file: #{TPL_FILENAME})",
"Need to be specified after name!") do |write|
options.write = true
if not write or write.empty?
if not options.name
puts "ERROR: you need to specify name before write."
exit
end
options.filename = eval('"'+TPL_FILENAME+'"')
else
options.filename = write
end
end
opts.on("-d", "--define",
"Create a C macro for each procedure") do
options.define = true
end
opts.on("-i", "--include a.hpp,b.hpp", Array,
"List include header files.") do |include|
options.include = include
end
opts.separator ""
# header options
opts.separator "Library Header Options:"
opts.on("-f", "--header_file FILE",
"Use FILE or PATH for header declarations") do |header_file|
options.header_file = header_file
end
opts.on("-p", "--header_path PATH") do |header_path|
options.header_path = header_path
end
opts.on("-r", "--file_pattern PATTERN",
"Parse only files in header PATH that matches",
"PATTERN (Default: #{options.file_pattern})") do |file_pattern|
options.file_pattern = file_pattern
end
opts.on("-R", "--prototype_pattern PATTERN",
"Use PATTERN to parse procedures, assumes three",
"groups: Return, procedure name and arguments") do |prototype_pattern|
options.prototype_pattern = prototype_pattern
end
opts.separator ""
# binary options
opts.separator "Library Binary Options:"
opts.on("-l", "--library FILE",
"Shared library FILE parsed with nm for symbols") do |library_file|
options.library_file = library_file
end
opts.on("-s", "--symbol_prefix PREFIX",
"Use symbol PREFIX to extract procedure names",
"from shared library") do |symbol_prefix|
options.symbol_prefix = symbol_prefix
end
opts.separator ""
# miscellaneous options
opts.separator "Miscellaneous:"
opts.on("-v", "--verbose", "Activates console messages") do |verbose|
options.verbose = true
end
opts.on("-h", "--help", "Show this message") do
puts opts
exit
end
opts.separator ""
end # end OptionParser
# save args for later merge with configuration args
arg_copy = args.dup
# parse arguments with created option parser
opts.parse!(args)
# if configuration file was defined, parse it as arguments again
if options.config
config = GeneratorConfigParser.new(options.config)
opts.parse!(config.createArguments() + arg_copy)
end
# check for required options:
usage = false
if not options.name
puts "ERROR: You need to specify a name."
usage = true
end
if not options.header_file and not options.header_path
puts "ERROR: You need to specify a header file or directory."
usage = true
end
if not options.prototype_pattern
puts "ERROR: You need to specify a prototype pattern."
usage = true
end
if usage
puts
puts opts
exit
end
# return resulting option structure
return options
end # end static parse method
end # end GeneratorOptionParser
# Procedure function declaration/prototype class
class Procedure
@@library_name = ""
def initialize(name, return_type, arguments)
@name = name
@return = return_type
@arguments = arguments
@argument_list = arguments.join(', ')
@arguments_num = []
@arguments.each_index do |num|
@arguments_num << "v#{num}"
end
@argument_num_list = @arguments_num.join(',')
end
def createPointer()
return eval('"'+TPL_STRUCT_POINTER+'"')
end
def createLoadSymbol()
return eval('"'+TPL_LOAD_SYMBOL+'"')
end
def createDefine()
return eval('"'+TPL_DEFINE+'"')
end
def self.setLibraryName(name)
@@library_name = name
end
end # end procedure
# Performs the actual parsing of library headers and binary
class LibraryParser
@@verbose = false # print some additional informations
def initialize()
@headers_content = {}
@library_symbols = []
@procedures = {}
end
# read the contents of a single header file
def readHeaderFile(header_file)
return if @headers_content.has_key? header_file or not File.exists? header_file
puts "Read header file #{header_file}" if @@verbose
@headers_content[header_file] = _readFileContents(header_file)
end
# lists all files in the header path and read every single header
# that matches prefix (if any)
def readHeaderPath(header_path, file_pattern = ".*")
puts "Read header path #{header_path}" if @@verbose
Dir.new(header_path).each do |header_file|
next if not header_file.match(Regexp.new(file_pattern))
readHeaderFile(header_path + "/" + header_file)
end
end
# read symbols of binary header with nm into @library_symbols array
# if array is not empty during parsing process, it will be checked
# against for each symbol name.
def readSymbols(library_file, symbol_prefix)
puts "Read symbols from shared library binary #{library_file}" if @@verbose
nm = `nm -D --defined-only #{library_file}`
nm.scan(/ T (#{Regexp.escape(symbol_prefix)}.*)$/).each do |symbol|
puts "Found symbol name: #{symbol[0]}" if @@verbose
@library_symbols << symbol[0]
end
puts "(found #{@library_symbols.length} symbols)" if @@verbose
end
# parse procedure declarations/prototypes from headers with the specified
# regular expression, the pattern assumes three match groups: return type,
# name and arguments. Found procedures are stored in Procedure instances
# within the @procedures hash.
def parseProcedures(prototype_pattern)
@headers_content.each_pair do |header_file, header_contents|
puts "\nParse header file #{header_file} for procedures (#{header_contents.length})" if @@verbose
next if @procedures.has_key? header_file
@procedures[header_file] = []
header_contents.scan(Regexp.new(prototype_pattern, Regexp::MULTILINE)) do |match|
return_type = match[0].strip
name = match[1].strip
arguments = match[2].strip.split(',').map{ |argument| argument.strip }
# if library symbols is not empty, ignore every procedure not within
if not @library_symbols.empty?
if @library_symbols.include?(name)
@library_symbols.delete(name)
else
next
end
end
# create new Procedure object and append to procedures hash
@procedures[header_file] << Procedure.new(name, return_type, arguments)
next if not @@verbose
puts "\t Filename : " + header_file
puts "\t Name : " + name.inspect
puts "\t Return : " + return_type.inspect
puts "\tArguments : " + arguments.inspect
puts
end
end
return @procedures
end # end parseProcedures
def getRemainingSymbols()
return @library_symbols.join(", ")
end
def getProcedures()
@procedures
end
# a private method for reading a (text) file
def _readFileContents(filename)
if not File.exists? filename # or not a file
puts "ERROR: File not found: #{filename}"
exit
end
IO.readlines(filename).join
end
def self.setVerboseMode(verbose)
@@verbose = verbose
end
end # end LibraryParser
# Populates template variables and blocks as instance attributes, reads
# the template file and creates the resulting class.
class LibraryTemplate
@@verbose = false
def initialize(name, filename, template, procedures)
@name = name
@filename = filename
@template = _readTemplate(template)
@procedures = procedures
if @@verbose
print "LibraryTemplate created,"
puts " name=#{name} filename=#{filename} template=#{template}"
puts
end
# create blocks
@block_include = ""
@block_define = ""
_createBlockSymbolsStruct()
_createBlockLoadSymbols()
end
def createBlockInclude(include)
return if not include or include.empty?
include.each do |include_file|
@block_include += eval('"'+TPL_INCLUDE+'\n"')
end
end
def _createBlockSymbolsStruct()
@block_symbols_struct = ""
@procedures.each_pair do |header_file, procedures|
next if procedures.empty?
@block_symbols_struct += eval('"'+TPL_STRUCT_COMMENT+'\n"')
# iterate over procedures:
procedures.each do |procedure|
@block_symbols_struct += procedure.createPointer() + "\n"
end
@block_symbols_struct += "\n"
end
end
def _createBlockLoadSymbols()
@block_load_symbols = ""
@procedures.each_pair do |header_file, procedures|
next if procedures.empty?
@block_load_symbols += eval('"'+TPL_LOAD_COMMENT+'\n"')
# iterate over procedures:
procedures.each do |procedure|
@block_load_symbols += procedure.createLoadSymbol() + "\n"
end
@block_load_symbols += "\n"
end
end
def createBlockDefine()
@block_define = ""
@procedures.each_pair do |header_file, procedures|
next if procedures.empty?
@block_define += eval('"'+TPL_DEFINE_COMMENT+'\n"')
# iterate over procedures:
procedures.each do |procedure|
@block_define += procedure.createDefine() + "\n"
end
@block_define += "\n"
end
end
def evalTemplate()
return eval("<