require'tempfile'require'open3'moduleFFI# ConstGenerator turns C constants into ruby values.## @example a simple example for stdio# cg = FFI::ConstGenerator.new('stdio') do |gen|# gen.const(:SEEK_SET)# gen.const('SEEK_CUR')# gen.const('seek_end') # this constant does not exist# end # #calculate called automatically at the end of the block## cg['SEEK_SET'] # => 0# cg['SEEK_CUR'] # => 1# cg['seek_end'] # => nil# cg.to_ruby # => "SEEK_SET = 0\nSEEK_CUR = 1\n# seek_end not available"classConstGenerator@options={}attr_reader:constants# Creates a new constant generator that uses +prefix+ as a name, and an# options hash.## The only option is +:required+, which if set to +true+ raises an error if a# constant you have requested was not found.# # @param [#to_s] prefix# @param [Hash] options# @return # @option options [Boolean] :required# @overload initialize(prefix, options)# @overload initialize(prefix, options) { |gen| ... }# @yieldparam [ConstGenerator] gen new generator is passed to the block# When passed a block, {#calculate} is automatically called at the end of# the block, otherwise you must call it yourself.definitialize(prefix=nil,options={})@includes=['stdio.h','stddef.h']@constants={}@prefix=prefix@required=options[:required]@options=optionsifblock_given?thenyieldselfcalculateself.class.options.merge(options)endend# Set class options# These options are merged with {#initialize} options when it is called with a block.# @param [Hash] options# @return [Hash] class optionsdefself.options=(options)@options=optionsend# Get class options.# @return [Hash] class optionsdefself.options@optionsend# @param [String] name# @return constant value (converted if a +converter+ was defined).# Access a constant by name.def[](name)@constants[name].converted_valueend# Request the value for C constant +name+.## @param [#to_s] name C constant name# @param [String] format a printf format string to print the value out# @param [String] cast a C cast for the value# @param ruby_name alternate ruby name for {#to_ruby}## @overload const(name, format=nil, cast='', ruby_name=nil, converter=nil)# +converter+ is a Method or a Proc.# @param [#call] converter convert the value from a string to the appropriate# type for {#to_ruby}.# @overload const(name, format=nil, cast='', ruby_name=nil) { |value| ... }# Use a converter block. This block convert the value from a string to the # appropriate type for {#to_ruby}.# @yieldparam value constant valuedefconst(name,format=nil,cast='',ruby_name=nil,converter=nil,&converter_proc)format||='%d'cast||=''ifconverter_procandconverterthenraiseArgumentError,"Supply only converter or converter block"endconverter=converter_procifconverter.nil?const=Constant.newname,format,cast,ruby_name,converter@constants[name.to_s]=constreturnconstend# Calculate constants values.# @param [Hash] options# @option options [String] :cppflags flags for C compiler# @return [nil]# @raise if a constant is missing and +:required+ was set to +true+ (see {#initialize})defcalculate(options={})binary=File.joinDir.tmpdir,"rb_const_gen_bin_#{Process.pid}"Tempfile.open("#{@prefix}.const_generator")do|f|@includes.eachdo|inc|f.puts"#include <#{inc}>"endf.puts"\nint main(int argc, char **argv)\n{"@constants.each_valuedo|const|f.puts<<-EOF
#ifdef #{const.name}
printf("#{const.name}#{const.format}\\n", #{const.cast}#{const.name});
#endif
EOFendf.puts"\n\treturn 0;\n}"f.flushoutput=`gcc #{options[:cppflags]} -D_DARWIN_USE_64_BIT_INODE -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -x c -Wall -Werror #{f.path} -o #{binary} 2>&1`unless$?.success?thenoutput=output.split("\n").map{|l|"\t#{l}"}.join"\n"raise"Compilation error generating constants #{@prefix}:\n#{output}"endendoutput=`#{binary}`File.unlink(binary+(FFI::Platform.windows??".exe":""))output.each_linedo|line|line=~/^(\S+)\s(.*)$/const=@constants[$1]const.value=$2endmissing_constants=@constants.selectdo|name,constant|constant.value.nil?end.map{|name,|name}if@requiredandnotmissing_constants.empty?thenraise"Missing required constants for #{@prefix}: #{missing_constants.join', '}"endend# Dump constants to +io+.# @param [#puts] io# @return [nil]defdump_constants(io)@constants.eachdo|name,constant|name=[@prefix,name].join'.'if@prefixio.puts"#{name} = #{constant.converted_value}"endend# Outputs values for discovered constants. If the constant's value was# not discovered it is not omitted.# @return [String]defto_ruby@constants.sort_by{|name,|name}.mapdo|name,constant|ifconstant.value.nil?then"# #{name} not available"elseconstant.to_rubyendend.join"\n"end# Add additional C include file(s) to calculate constants from.# @note +stdio.h+ and +stddef.h+ automatically included# @param [List<String>, Array<String>] i include file(s)# @return [Array<String>] array of include filesdefinclude(*i)@includes|=i.flattenendend# This class hold constants for {ConstGenerator}classConstGenerator::Constantattr_reader:name,:format,:castattr_accessor:value# @param [#to_s] name# @param [String] format a printf format string to print the value out# @param [String] cast a C cast for the value# @param ruby_name alternate ruby name for {#to_ruby}# @param [#call] converter convert the value from a string to the appropriate# type for {#to_ruby}.definitialize(name,format,cast,ruby_name=nil,converter=nil)@name=name@format=format@cast=cast@ruby_name=ruby_name@converter=converter@value=nilend# Return constant value (converted if a +converter+ was defined).# @return constant value.defconverted_valueif@converter@converter.call(@value)else@valueendend# get constant ruby name# @return [String]defruby_name@ruby_name||@nameend# Get an evaluable string from constant.# @return [String]defto_ruby"#{ruby_name} = #{converted_value}"endendend