metasploit-framework/lib/metasm/samples/dynamic_ruby.rb

1930 lines
59 KiB
Ruby

# This file is part of Metasm, the Ruby assembly manipulation suite
# Copyright (C) 2006-2009 Yoann GUILLOT
#
# Licence is LGPL, see LICENCE in the top-level directory
# This sample hacks in the ruby interpreter to allow dynamic loading of shellcodes as object methods
# Also it allows raw modifications to the ruby interpreter memory, for all kind of purposes
# Includes methods to dump the ruby parser AST from the interpreter memory
# elf/linux/x86 only
require 'metasm'
module Metasm
class RubyHack < DynLdr
# basic C defs for ruby AST - ruby1.8 only !
RUBY_INTERN_NODE = <<EOS
struct node {
long flags;
char *file;
long a1;
long a2;
long a3;
};
#define FL_USHIFT 11
#define nd_type(n) ((((struct node*)n)->flags >> FL_USHIFT) & 0xff)
EOS
NODETYPE = [
:method, :fbody, :cfunc, :scope, :block,
:if, :case, :when, :opt_n, :while,
:until, :iter, :for, :break, :next,
:redo, :retry, :begin, :rescue, :resbody,
:ensure, :and, :or, :not, :masgn,
:lasgn, :dasgn, :dasgn_curr, :gasgn, :iasgn,
:cdecl, :cvasgn, :cvdecl, :op_asgn1, :op_asgn2,
:op_asgn_and, :op_asgn_or, :call, :fcall, :vcall,
:super, :zsuper, :array, :zarray, :hash,
:return, :yield, :lvar, :dvar, :gvar, # 50
:ivar, :const, :cvar, :nth_ref, :back_ref,
:match, :match2, :match3, :lit, :str,
:dstr, :xstr, :dxstr, :evstr, :dregx,
:dregx_once, :args, :argscat, :argspush, :splat,
:to_ary, :svalue, :block_arg, :block_pass, :defn,
:defs, :alias, :valias, :undef, :class,
:module, :sclass, :colon2, :colon3, :cref,
:dot2, :dot3, :flip2, :flip3, :attrset,
:self, :nil, :true, :false, :defined,
:newline, :postexe, :alloca, :dmethod, :bmethod, # 100
:memo, :ifunc, :dsym, :attrasgn, :last
]
new_api_c 'void rb_define_method(uintptr_t, char *, uintptr_t (*)(), int)'
new_api_c 'void *rb_method_node(uintptr_t, unsigned id)'
class << self
def set_class_method_raw(klass, meth, code, nparams)
memory_perm(str_ptr(code), code.length, 'rwx')
rb_define_method(rb_obj_to_value(klass), meth, code, nparams)
end
def get_method_node_ptr(klass, meth)
raise "#{klass.inspect} is not a class" if not klass.kind_of? Module
rb_method_node(rb_obj_to_value(klass), meth.to_sym.to_i)
end
# sets up rawopcodes as the method implementation for class klass
# rawopcodes must implement the expected ABI or things will break horribly
# this method is VERY UNSAFE, and breaks everything put in place by the ruby interpreter
# use with EXTREME CAUTION
# nargs arglist
# -2 self, arg_ary
# -1 argc, VALUE*argv, self
# >=0 self, arg0, arg1..
def set_method_binary(klass, methodname, raw, nargs=nil)
nargs ||= klass.instance_method(methodname).arity
if raw.kind_of? EncodedData
baseaddr = str_ptr(raw.data)
bd = raw.binding(baseaddr)
raw.reloc_externals.uniq.each { |ext| bd[ext] = sym_addr(0, ext) or raise "unknown symbol #{ext}" }
raw.fixup(bd)
raw = raw.data
end
(@@prevent_gc ||= {})[[klass, methodname]] = raw
set_class_method_raw(klass, methodname.to_s, raw, nargs)
end
# same as load_binary_method but with an object and not a class
def set_singleton_method_binary(obj, *a)
set_method_binary((class << obj ; self ; end), *a)
end
def read_method_ast(klass, meth)
read_node get_method_node_ptr(klass, meth)
end
def read_singleton_method_ast(klass, meth)
klass = (class << klass ; self ; end)
read_method_ast(klass, meth)
end
def read_node(ptr, cur=nil)
return if ptr == 0 or ptr == 4
type = NODETYPE[(memory_read_int(ptr) >> 11) & 0xff]
v1 = memory_read_int(ptr+8)
v2 = memory_read_int(ptr+12)
v3 = memory_read_int(ptr+16)
case type
when :block, :array, :hash
cur = nil if cur and cur[0] != type
cur ||= [type]
cur << read_node(v1)
n = read_node(v3, cur)
raise "block->next = #{n.inspect}" if n and n[0] != type
cur
when :newline
read_node(v3) # debug/trace usage only
when :if
[type, read_node(v1), read_node(v2), read_node(v3)]
when :cfunc
v2 = {0xffffffff => -1, 0xfffffffe => -2, 0xffffffffffffffff => -1, 0xfffffffffffffffe => -2}[v2] || v2
[type, {:fptr => v1, # c func pointer
:arity => v2}]
when :scope
[type, {:localnr => (v1 != 0 && v1 != 4 ? memory_read_int(v1) : 0), # nr of local vars (+2 for $_/$~)
:cref => read_node(v2)[1..-1]}, # node, starting point for const/@@var resolution
read_node(v3)]
when :cref
cur = nil if cur and cur[0] != type
cur ||= [type]
cur << rb_value_to_obj(v1) if v1 != 0 and v1 != 4
n = read_node(v3, cur)
raise "block->next = #{n.inspect}" if n and n[0] != type
cur
when :call, :fcall, :vcall
[type, read_node(v1), v2.id2name, read_node(v3)]
when :dstr
ret = [type, [:str, rb_value_to_obj(v1)]]
if args = read_node(v3)
raise "#{ret.inspect} with args != array: #{args.inspect}" if args[0] != :array
ret.concat args[1..-1]
end
ret
when :zarray
[:array]
when :lasgn
[type, v3, read_node(v2)]
when :iasgn, :dasgn, :dasgn_curr, :gasgn, :cvasgn
[type, v1.id2name, read_node(v2)]
when :masgn
# multiple assignment: a, b = 42 / lambda { |x, y| }.call(1, 2)
# v3 = remainder storage (a, b, *c = ary => v3=c)
[type, read_node(v1), read_node(v2), read_node(v3)]
when :attrasgn
[type, ((v1 == 1) ? :self : read_node(v1)), v2.id2name, read_node(v3)]
when :lvar
[type, v3]
when :ivar, :dvar, :gvar, :cvar, :const, :attrset
[type, v1.id2name]
when :str
# cannot use _id2ref here, probably the parser does not use standard alloced objects
s = memory_read(memory_read_int(v1+12), memory_read_int(v1+16))
[type, s]
when :lit
[type, rb_value_to_obj(v1)]
when :args # specialcased by rb_call0, invalid in rb_eval
cnt = v3 # nr of required args, copied directly to local_vars
opt = read_node(v1) # :block to execute for each missing arg / with N optargs specified, skip N 1st statements
rest = read_node(v2) # catchall arg in def foo(rq1, rq2, *rest)
[type, cnt, opt, rest]
when :and, :or
[type, read_node(v1), read_node(v2)] # shortcircuit
when :not
[type, read_node(v2)]
when :nil, :true, :false, :self
[type]
when :redo, :retry
[type]
when :case
# [:case, var_test, [:when, cnd, action, [:when, cnd2, action2, else]]]
# => [:case, var_test, [:when, cnd, action], [:when, cnd2, action], else]
cs = [type, read_node(v1), read_node(v2)]
cs << cs[-1].pop while cs[-1][0] == :when and cs[-1][3]
cs
when :when
# [:when, [:array, [test]], then, else]
[type, read_node(v1), read_node(v2), read_node(v3)]
when :iter
# save a block for the following funcall
args = read_node(v1) # assignments with nil, not realized, just to store the arg list (multi args -> :masgn)
body = read_node(v2) # the body statements (multi -> :block)
subj = read_node(v3) # the stuff which is passed the block, probably a :call
[type, args, body, subj]
when :while, :until
[type, read_node(v1), read_node(v2), v3]
when :return, :break, :next, :defined
[type, read_node(v1)]
when :to_ary
[type, read_node(v1)]
when :colon2
[type, read_node(v1), v2.id2name]
when :colon3 # ::Stuff
[type, v2.id2name]
when :method
[type, v1, read_node(v2), v3]
when :alias
[type, v1, v2, v3] # ?
when :evstr
[type, read_node(v2)]
when :dot2, :dot3
[type, read_node(v1), read_node(v2)]
when :splat
[type, read_node(v1)]
when :argscat
[type, read_node(v1), read_node(v2), v3]
when :block_pass
# [args, block, receiver]: foo(bar, &baz) => [:bpass, [:array, bar], [:lvar, baz], [:call, 'foo', bar]] (args in v1&v3!)
[type, read_node(v1), read_node(v2), read_node(v3)]
when :block_arg
[type, v1.id2name, v2, v3]
when :ensure
[type, read_node(v1), v2, read_node(v3)]
else
puts "unhandled #{type.inspect}"
[type, v1, v2, v3]
end
end
end # class << self
end
# a ruby2c C generator for use in the current ruby interpreter
# generates C suitable for shellcode compilation & insertion in the current interpreter
# has hardcoded addresses etc
class RubyLiveCompiler
attr_accessor :cp
RUBY_H = <<EOS
#{DynLdr::RUBY_H}
VALUE rb_ivar_get(VALUE, unsigned);
VALUE rb_ivar_set(VALUE, unsigned, VALUE);
VALUE rb_ivar_defined(VALUE, unsigned);
VALUE rb_cvar_get(VALUE, unsigned);
VALUE rb_cvar_set(VALUE, unsigned, VALUE, int);
VALUE rb_gv_get(const char*);
VALUE rb_gv_set(const char*, VALUE);
VALUE rb_ary_new(void);
VALUE rb_ary_new4(long, VALUE*);
VALUE rb_ary_push(VALUE, VALUE);
VALUE rb_ary_pop(VALUE);
VALUE rb_ary_shift(VALUE);
VALUE rb_hash_new(void);
VALUE rb_hash_aset(VALUE, VALUE, VALUE);
VALUE rb_str_new(const char*, long);
VALUE rb_str_new2(const char*);
VALUE rb_str_cat2(VALUE, const char*);
VALUE rb_str_concat(VALUE, VALUE);
VALUE rb_str_append(VALUE, VALUE);
VALUE rb_obj_as_string(VALUE);
VALUE rb_range_new(VALUE, VALUE, int exclude_end);
VALUE rb_Array(VALUE); // :splat
VALUE rb_ary_to_ary(VALUE);
VALUE rb_hash_aref(VALUE, VALUE);
VALUE rb_funcall3(VALUE, unsigned, int, VALUE*);
VALUE rb_singleton_class(VALUE);
VALUE rb_block_proc(void);
void rb_define_method(VALUE, char *, VALUE (*)(), int);
void *rb_method_node(VALUE, unsigned);
EOS
class Fail < RuntimeError
end
def self.compile(klass, *methlist)
@rcp ||= new
methlist.each { |meth|
ast = RubyHack.read_method_ast(klass, meth)
n = @rcp.compile(ast, klass, meth)
next if not n
raw = RubyHack.compile_c(@rcp.cp.dump_definition(n)).encoded
RubyHack.set_method_binary(klass, meth, raw)
}
self
end
def dump(m=nil)
m ? @cp.dump_definition(m) : @cp.to_s
end
attr_accessor :optim_hint
def initialize(cp=nil)
@cp = cp || DynLdr.host_cpu.new_cparser
@cp.parse RUBY_H
@iter_break = nil
@optim_hint = {}
end
# convert a ruby AST to a new C function
# returns the new function name
def compile(ast, klass, meth, singleton=false)
return if not ast
# TODO handle arbitrary block/yield constructs
# TODO analyse to find/optimize numeric locals that never need a ruby VALUE (ie native int vs INT2FIX)
# TODO detect block/closure exported out of the func & abort compilation
@klass = klass
@meth = meth
@meth_singleton = singleton
mname = escape_varname("m_#{@klass}#{singleton ? '.' : '#'}#{@meth}".gsub('::', '_'))
@cp.parse "static void #{mname}(VALUE self) { }"
@cur_cfunc = @cp.toplevel.symbol[mname]
@cur_cfunc.type.type = value # return type = VALUE, w/o 'missing return statement' warning
@scope = @cur_cfunc.initializer
case ast[0]
when :ivar # attr_reader
ret = fcall('rb_ivar_get', rb_self, rb_intern(ast[1]))
when :attrset # attr_writer
compile_args(@cur_cfunc, [nil, 1])
ret = fcall('rb_ivar_set', rb_self, rb_intern(ast[1]), local(2))
when :scope # standard ruby function
@cref = ast[1][:cref]
if ast[2] and ast[2][0] == :block and ast[2][1] and ast[2][1][0] == :args
compile_args(@cur_cfunc, ast[2][1])
end
want_value = true
if meth.to_s == 'initialize' and not singleton
want_value = false
end
ret = ast_to_c(ast[2], @scope, want_value)
ret = rb_nil if not want_value
#when :cfunc # native ruby extension
else raise "unhandled function ast #{ast.inspect}"
end
@scope.statements << C::Return.new(ret)
mname
end
# return the arity of method 'name' on self
def method_arity(name=@meth)
@meth_singleton ? @klass.method(name).arity : @klass.instance_method(name).arity
end
# find the scope where constname is defined from @cref
def resolve_const_owner(constname)
@cref.find { |cr| cr.constants.map { |c| c.to_s }.include? constname.to_s }
end
# checks if ast maps to a constant, returns it if it does
def check_const(ast)
case ast[0]
when :const
resolve_const_owner(ast[1])
when :colon2
if cst = check_const(ast[1])
cst.const_get(ast[2])
end
when :colon3
::Object.const_get(ast[2])
end
end
def compile_args(func, args)
case method_arity
when -1 # args[1] == 0 and (args[2] or args[3])
compile_args_m1(func, args)
when -2 # args[1] > 0 and (args[2] or args[3])
compile_args_m2(func, args)
else
# fixed arity = args[1]: VALUE func(VALUE self, VALUE local_2, VALUE local_3)
args[1].times { |i|
v = C::Variable.new("local_#{i+2}", value)
@scope.symbol[v.name] = v
func.type.args << v
}
end
end
# update func prototype to reflect arity -1
# VALUE func(int argc, VALUE *argv, VALUE self)
def compile_args_m1(func, args)
c = C::Variable.new("arg_c", C::BaseType.new(:int, :unsigned))
v = C::Variable.new("arg_v", C::Pointer.new(value))
@scope.symbol[c.name] = c
@scope.symbol[v.name] = v
func.type.args.unshift v
func.type.args.unshift c
args[1].times { |i|
local(i+2, C::CExpression[v, :'[]', [i]])
}
if args[2]
# [:block, [:lasgn, 2, [:lit, 4]]]
raise Fail, "unhandled vararglist #{args.inspect}" if args[2][0] != :block
args[2][1..-1].each_with_index { |a, i|
raise Fail, "unhandled arg #{a.inspect}" if a[0] != :lasgn
cnd = C::CExpression[c, :>, i]
thn = C::CExpression[local(a[1], :none), :'=', [v, :'[]', [i]]]
els = C::Block.new(@scope)
ast_to_c(a, els, false)
@scope.statements << C::If.new(cnd, thn, els)
}
end
if args[3]
raise Fail, "unhandled vararglist3 #{args.inspect}" if args[3][0] != :lasgn
skiplen = args[1] + args[2].length - 1
alloc = fcall('rb_ary_new4', [c, :-, [skiplen]], [v, :+, [skiplen]])
local(args[3][1], C::CExpression[[c, :>, skiplen], :'?:', [alloc, fcall('rb_ary_new')]])
end
end
# update func prototype to reflect arity -2
# VALUE func(VALUE self, VALUE arg_array)
def compile_args_m2(func, args)
v = C::Variable.new("arglist", value)
@scope.symbol[v.name] = v
func.type.args << v
args[1].times { |i|
local(i+2, fcall('rb_ary_shift', v))
}
# populate arguments with default values
if args[2]
# [:block, [:lasgn, 2, [:lit, 4]]]
raise Fail, "unhandled vararglist #{args.inspect}" if args[2][0] != :block
args[2][1..-1].each { |a|
raise Fail, "unhandled arg #{a.inspect}" if a[0] != :lasgn
t = C::CExpression[local(a[1], :none), :'=', fcall('rb_ary_shift', v)]
e = C::Block.new(@scope)
ast_to_c([:lasgn, a[1], a[2]], e, false)
@scope.statements << C::If.new(rb_ary_len(v), t, e)
}
end
if args[3]
raise Fail, "unhandled vararglist3 #{args.inspect}" if args[3][0] != :lasgn
local(args[3][1], C::CExpression[v])
end
end
# compile a case/when
# create a real C switch() for Fixnums, and put the others === in the default case
# XXX will get the wrong order for "case x; when 1; when Fixnum; when 3;" ...
def compile_case(ast, scope, want_value)
# this generates
# var = stuff_to_test()
# if (var & 1)
# switch (var >> 1) {
# case 12:
# stuff();
# break;
# default:
# goto default_case;
# }
# else
# default_case:
# if (var == true.object_id || rb_test(rb_funcall(bla, '===', var)))
# foo();
# else {
# default();
# }
#
if want_value == true
ret = get_new_tmp_var('case', want_value)
want_value = ret
elsif want_value
ret = want_value
end
var = ast_to_c(ast[1], scope, want_value || true)
if not var.kind_of? C::Variable
ret ||= get_new_tmp_var('case', want_value)
scope.statements << C::CExpression[ret, :'=', var]
var = ret
end
# the scope to put all case int in
body_int = C::Block.new(scope)
# the scope to put the if (cs === var) cascade
body_other_head = body_other = nil
default = nil
ast[2..-1].each { |cs|
if cs[0] == :when
raise Fail if cs[1][0] != :array
# numeric case, add a case to body_int
if cs[1][1..-1].all? { |cd| cd[0] == :lit and (cd[1].kind_of? Fixnum or cd[1].kind_of? Range) }
cs[1][1..-1].each { |cd|
if cd[1].kind_of? Range
b = cd[1].begin
e = cd[1].end
e -= 1 if cd[1].exclude_end?
raise Fail unless b.kind_of? Integer and e.kind_of? Integer
body_int.statements << C::Case.new(b, e, nil)
else
body_int.statements << C::Case.new(cd[1], nil, nil)
end
}
cb = C::Block.new(scope)
v = ast_to_c(cs[2], cb, want_value)
cb.statements << C::CExpression[ret, :'=', v] if want_value and v != ret
cb.statements << C::Break.new
body_int.statements << cb
# non-numeric (or mixed) case, add if ( cs === var )
else
cnd = nil
cs[1][1..-1].each { |cd|
if (cd[0] == :lit and (cd[1].kind_of?(Fixnum) or cd[1].kind_of?(Symbol))) or
[:nil, :true, :false].include?(cd[0])
# true C equality
cd = C::CExpression[var, :==, ast_to_c(cd, scope)]
else
# own block for ast_to_c to honor lazy evaluation
tb = C::Block.new(scope)
test = rb_test(rb_funcall(ast_to_c(cd, tb), '===', var), tb)
# discard own block unless needed
if tb.statements.empty?
cd = test
else
tb.statements << test
cd = C::CExpression[tb, value]
end
end
cnd = (cnd ? C::CExpression[cnd, :'||', cd] : cd)
}
cb = C::Block.new(scope)
v = ast_to_c(cs[2], cb, want_value)
cb.statements << C::CExpression[ret, :'=', v] if want_value and v != ret
fu = C::If.new(cnd, cb, nil)
if body_other
body_other.belse = fu
else
body_other_head = fu
end
body_other = fu
end
# default case statement
else
cb = C::Block.new(scope)
v = ast_to_c(cs, cb, want_value)
cb.statements << C::CExpression[ret, :'=', v] if want_value and v != ret
default = cb
end
}
# if we use the value of the case, we must add an 'else: nil'
if want_value and not default
default = C::Block.new(scope)
default.statements << C::CExpression[ret, :'=', rb_nil]
end
# assemble everything
scope.statements <<
if body_int.statements.empty?
if body_other
body_other.belse = default
body_other_head
else
raise Fail, "empty case? #{ast.inspect}" if not default
default
end
else
if body_other_head
@default_label_cnt ||= 0
dfl = "default_label_#{@default_label_cnt += 1}"
body_other_head = C::Label.new(dfl, body_other_head)
body_int.statements << C::Case.new('default', nil, C::Goto.new(dfl))
body_other.belse = default if default
end
body_int = C::Switch.new(C::CExpression[var, :>>, 1], body_int)
C::If.new(C::CExpression[var, :&, 1], body_int, body_other_head)
end
ret
end
# create a C::CExpr[toplevel.symbol[name], :funcall, args]
# casts int/strings in arglist to CExpr
def fcall(fname, *arglist)
args = arglist.map { |a| (a.kind_of?(Integer) or a.kind_of?(String)) ? [a] : a }
fv = @cp.toplevel.symbol[fname]
raise "need prototype for #{fname}!" if not fv
C::CExpression[fv, :funcall, args]
end
# the VALUE typedef
def value
@cp.toplevel.symbol['VALUE']
end
# declare a new function variable
# no initializer if init == :none
def declare_newvar(name, initializer)
v = C::Variable.new(name, value)
v.initializer = initializer if initializer != :none
@scope.symbol[v.name] = v
@scope.statements << C::Declaration.new(v)
v
end
# return a string suitable for use as a variable name
# hexencode any char not in [A-z0-9_]
def escape_varname(n)
n.gsub(/[^\w]/) { |c| c.unpack('H*')[0] }
end
# retrieve or create a local var
# pass :none to avoid initializer
def get_var(name, initializer=:none)
name = escape_varname(name)
@scope.symbol[name] ||= declare_newvar(name, initializer || rb_nil)
end
# create a new temporary variable
# XXX put_var ?
def get_new_tmp_var(base=nil, var=nil)
return var if var.kind_of? C::Variable
@tmp_var_id ||= 0
get_var("tmp_#{"#{base}_" if base}#{@tmp_var_id += 1}")
end
# retrieve/create a new local variable with optionnal initializer
def local(n, init=nil)
get_var "local_#{n}", init
end
# retrieve/create a new dynamic variable (block argument/variable)
# pass :none to avoid initializer
def dvar(n, init=nil)
get_var "dvar_#{n}", init
end
# retrieve self (1st func arg)
def rb_self
@scope.symbol['self']
end
# returns a CExpr casting expr to a VALUE*
def rb_cast_pvalue(expr, idx)
C::CExpression[[[expr], C::Pointer.new(value)], :'[]', [idx]]
end
# retrieve the current class, from self->klass
# XXX will segfault with self.kind_of? Fixnum/true/false/nil/sym
def rb_selfclass
rb_cast_pvalue(rb_self, 1)
end
def rb_nil
C::CExpression[[nil.object_id], value]
end
def rb_true
C::CExpression[[true.object_id], value]
end
def rb_false
C::CExpression[[false.object_id], value]
end
# call rb_intern on a string
def rb_intern(n)
# use the current interpreter's value
C::CExpression[n.to_sym.to_i]
end
# create a rb_funcall construct
def rb_funcall(recv, meth, *args)
fcall('rb_funcall', recv, rb_intern(meth), args.length, *args)
end
# ruby bool test of a var
# assigns to a temporary var, and check against false/nil
def rb_test(expr, scope)
if nil.object_id == 0 or false.object_id == 0 # just to be sure
nf = nil.object_id | false.object_id
C::CExpression[[expr, :|, nf], :'!=', nf]
else
if expr.kind_of? C::Variable
tmp = expr
else
tmp = get_new_tmp_var('test')
scope.statements << C::CExpression[tmp, :'=', expr]
end
C::CExpression[[tmp, :'!=', rb_nil], :'&&', [tmp, :'!=', rb_false]]
end
end
# generate C code to raise a RuntimeError, reason
def rb_raise(reason, cls='rb_eRuntimeError')
fcall('rb_raise', rb_global(cls), reason)
end
# return a C expr equivallent to TYPE(expr) == type for non-immediate types
# XXX expr evaluated 3 times
def rb_test_class_type(expr, type)
C::CExpression[[[expr, :>, [7]], :'&&', [[expr, :&, [3]], :==, [0]]], :'&&', [[rb_cast_pvalue(expr, 0), :&, [0x3f]], :'==', [type]]]
end
# return a C expr equivallent to TYPE(expr) == T_ARRAY
def rb_test_class_ary(expr)
rb_test_class_type(expr, 9)
end
# ARY_PTR(expr)
def rb_ary_ptr(expr, idx=nil)
p = C::CExpression[[rb_cast_pvalue(expr, 4)], C::Pointer.new(value)]
idx ? C::CExpression[p, :'[]', [idx]] : p
end
# ARY_LEN(expr)
def rb_ary_len(expr)
rb_cast_pvalue(expr, 2)
end
# TYPE(expr) == T_STRING
def rb_test_class_string(expr)
rb_test_class_type(expr, 7)
end
# STR_PTR(expr)
def rb_str_ptr(expr, idx=nil)
p = C::CExpression[[rb_cast_pvalue(expr, 3)], C::Pointer.new(C::BaseType.new(:char))]
idx ? C::CExpression[p, :'[]', [idx]] : p
end
# STR_LEN(expr)
def rb_str_len(expr)
rb_cast_pvalue(expr, 2)
end
def rb_test_class_hash(expr)
rb_test_class_type(expr, 0xb)
end
# returns a static pointer to the constant
def rb_const(constname, owner = resolve_const_owner(constname))
raise Fail, "no dynamic constant resolution #{constname}" if not owner
cst = owner.const_get(constname)
C::CExpression[[RubyHack.rb_obj_to_value(cst)], value]
end
# compile a :masgn
def rb_masgn(ast, scope, want_value)
raise Fail, "masgn with no rhs #{ast.inspect}" if not ast[2]
raise Fail, "masgn with no lhs array #{ast.inspect}" if not ast[1] or ast[1][0] != :array
if not want_value and ast[2][0] == :array and not ast[3] and ast[2].length == ast[1].length
rb_masgn_optimized(ast, scope)
return nil.object_id
end
full = get_new_tmp_var('masgn', want_value)
ary = ast_to_c(ast[2], scope, full)
scope.statements << C::CExpression[full, :'=', ary] if full != ary
ast[1][1..-1].each_with_index { |e, i|
raise Fail, "weird masgn lhs #{e.inspect} in #{ast.inspect}" if e[-1] != nil
# local_42 = full[i]
e = e.dup
e[-1] = [:rb2cstmt, rb_ary_ptr(full, i)]
ast_to_c(e, scope, false)
}
if ast[3]
raise Fail, "weird masgn lhs #{e.inspect} in #{ast.inspect}" if ast[3][-1] != nil
# local_28 = full[12..-1].to_a
e = ast[3].dup
e[-1] = [:call, [:call, [:rb2cvar, full.name], '[]', [:array, [:dot2, [:lit, ast[1].length-1], [:lit, -1]]]], 'to_a']
ast_to_c(e, scope, false)
end
full
end
def rb_global(cname)
@cp.toplevel.symbol[cname]
end
# compile an optimized :masgn with rhs.length == lhs.length (no need of a ruby array)
def rb_masgn_optimized(ast, scope)
vars = []
ast[2][1..-1].each { |rhs|
var = get_new_tmp_var('masgn_opt')
vars << var
r = ast_to_c(rhs, scope, var)
scope.statements << C::CExpression[var, :'=', r] if var != r
}
ast[1][1..-1].each { |lhs|
var = vars.shift
lhs = lhs.dup
raise Fail, "weird masgn lhs #{lhs.inspect} in #{ast.inspect}" if lhs[-1] != nil
lhs[-1] = [:rb2cvar, var.name]
ast_to_c(lhs, scope, false)
}
end
# the recursive AST to C compiler
# may append C statements to scope
# returns the C::CExpr holding the VALUE of the current ruby statement
# want_value is an optionnal hint as to the returned VALUE is needed or not
# if want_value is a C::Variable, the statements should try to populate this var instead of some random tmp var
# eg to simplify :if encoding unless we have 'foo = if 42;..'
def ast_to_c(ast, scope, want_value = true)
ret =
case ast.to_a[0]
when :block
if ast[1]
ast[1..-2].each { |a| ast_to_c(a, scope, false) }
ast_to_c(ast.last, scope, want_value)
end
when :lvar
local(ast[1])
when :lasgn
if scope == @scope
l = local(ast[1], :none)
else
# w = 4 if false ; p w => should be nil
l = local(ast[1])
end
st = ast_to_c(ast[2], scope, l)
scope.statements << C::CExpression[l, :'=', st] if st != l
l
when :dvar
dvar(ast[1])
when :dasgn_curr
l = dvar(ast[1])
st = ast_to_c(ast[2], scope, l)
scope.statements << C::CExpression[l, :'=', st] if st != l
l
when :ivar
fcall('rb_ivar_get', rb_self, rb_intern(ast[1]))
when :iasgn
if want_value
tmp = get_new_tmp_var("ivar_#{ast[1]}", want_value)
scope.statements << C::CExpression[tmp, :'=', ast_to_c(ast[2], scope)]
scope.statements << fcall('rb_ivar_set', rb_self, rb_intern(ast[1]), tmp)
tmp
else
scope.statements << fcall('rb_ivar_set', rb_self, rb_intern(ast[1]), ast_to_c(ast[2], scope))
end
when :cvar
fcall('rb_cvar_get', rb_selfclass, rb_intern(ast[1]))
when :cvasgn
if want_value
tmp = get_new_tmp_var("cvar_#{ast[1]}", want_value)
scope.statements << C::CExpression[tmp, :'=', ast_to_c(ast[2], scope)]
scope.statements << fcall('rb_cvar_set', rb_selfclass, rb_intern(ast[1]), tmp, rb_false)
tmp
else
scope.statements << fcall('rb_cvar_set', rb_selfclass, rb_intern(ast[1]), ast_to_c(ast[2], scope), rb_false)
end
when :gvar
fcall('rb_gv_get', ast[1])
when :gasgn
if want_value
tmp = get_new_tmp_var("gvar_#{ast[1]}", want_value)
scope.statements << C::CExpression[tmp, :'=', ast_to_c(ast[2], scope)]
scope.statements << fcall('rb_gv_set', ast[1], tmp)
tmp
else
scope.statements << fcall('rb_gv_set', ast[1], ast_to_c(ast[2], scope))
end
when :attrasgn # foo.bar= 42 (same as :call, except for return value)
recv = ast_to_c(ast[1], scope)
raise Fail, "unsupported #{ast.inspect}" if not ast[3] or ast[3][0] != :array
if ast[3].length != 2
if ast[2] != '[]=' or ast[3].length != 3
raise Fail, "unsupported #{ast.inspect}"
end
# foo[4] = 2
idx = ast_to_c(ast[3][1], scope)
end
arg = ast_to_c(ast[3].last, scope)
if want_value
tmp = get_new_tmp_var('call', want_value)
scope.statements << C::CExpression[tmp, :'=', arg]
end
if idx
scope.statements << rb_funcall(recv, ast[2], idx, arg)
else
scope.statements << rb_funcall(recv, ast[2], arg)
end
tmp
when :rb2cvar # hax, used in vararg parsing
get_var(ast[1])
when :rb2cstmt
ast[1]
when :block_arg
local(ast[3], fcall('rb_block_proc'))
when :lit
case ast[1]
when Symbol
# XXX ID2SYM
C::CExpression[[rb_intern(ast[1].to_s), :<<, 8], :|, 0xe]
when Range
fcall('rb_range_new', ast[1].begin.object_id, ast[1].end.object_id, ast[1].exclude_end? ? 0 : 1)
else # true/false/nil/fixnum
ast[1].object_id
end
when :self
rb_self
when :str
fcall('rb_str_new2', ast[1])
when :array
tmp = get_new_tmp_var('ary', want_value)
scope.statements << C::CExpression[tmp, :'=', fcall('rb_ary_new')]
ast[1..-1].each { |e|
scope.statements << fcall('rb_ary_push', tmp, ast_to_c(e, scope))
}
tmp
when :hash
raise Fail, "bad #{ast.inspect}" if ast[1][0] != :array
tmp = get_new_tmp_var('hash', want_value)
scope.statements << C::CExpression[tmp, :'=', fcall('rb_hash_new')]
ki = nil
ast[1][1..-1].each { |k|
if not ki
ki = k
else
scope.statements << fcall('rb_hash_aset', tmp, ast_to_c(ki, scope), ast_to_c(k, scope))
ki = nil
end
}
tmp
when :iter
if v = optimize_iter(ast, scope, want_value)
return v
end
# for full support of :iter, we need access to the interpreter's ruby_block private global variable in eval.c
# we can find it by analysing rb_block_given_p, but this won't work with a static precompiled rubyhack...
# even with access to ruby_block, there we would need to redo PUSH_BLOCK, create a temporary dvar list,
# handle [:break, lol], and do all the stack magic reused in rb_yield (probably incl setjmp etc)
raise Fail, "unsupported iter #{ast[3].inspect} { | #{ast[1].inspect} | #{ast[2].inspect} }"
when :call, :vcall, :fcall
if v = optimize_call(ast, scope, want_value)
return v
end
recv = ((ast[0] == :call) ? ast_to_c(ast[1], scope) : rb_self)
if not ast[3]
f = rb_funcall(recv, ast[2])
elsif ast[3][0] == :array
args = ast[3][1..-1].map { |a| ast_to_c(a, scope) }
f = rb_funcall(recv, ast[2], *args)
elsif ast[3][0] == :splat
args = ast_to_c(ast[3], scope)
if not args.kind_of? C::Variable
tmp = get_new_tmp_var('args', want_value)
scope.statements << C::CExpression[tmp, :'=', args]
args = tmp
end
f = fcall('rb_funcall3', recv, rb_intern(ast[2]), rb_ary_len(args), rb_ary_ptr(args))
# elsif ast[3][0] == :argscat
else
raise Fail, "unsupported #{ast.inspect}"
end
if want_value
tmp ||= get_new_tmp_var('call', want_value)
scope.statements << C::CExpression[tmp, :'=', f]
tmp
else
scope.statements << f
f
end
when :if, :when
if ast[0] == :when and ast[1][0] == :array
cnd = nil
ast[1][1..-1].map { |cd| rb_test(ast_to_c(cd, scope), scope) }.each { |cd|
cnd = (cnd ? C::CExpression[cnd, :'||', cd] : cd)
}
else
cnd = rb_test(ast_to_c(ast[1], scope), scope)
end
tbdy = C::Block.new(scope)
ebdy = C::Block.new(scope) if ast[3] or want_value
if want_value
tmp = get_new_tmp_var('if', want_value)
thn = ast_to_c(ast[2], tbdy, tmp)
tbdy.statements << C::CExpression[tmp, :'=', thn] if tmp != thn
if ast[3]
els = ast_to_c(ast[3], ebdy, tmp)
else
# foo = if bar ; baz ; end => nil if !bar
els = rb_nil
end
ebdy.statements << C::CExpression[tmp, :'=', els] if tmp != els
else
ast_to_c(ast[2], tbdy, false)
ast_to_c(ast[3], ebdy, false)
end
scope.statements << C::If.new(cnd, tbdy, ebdy)
tmp
when :while, :until
pib = @iter_break
@iter_break = nil # XXX foo = while ()...
body = C::Block.new(scope)
if ast[3] == 0 # do .. while();
ast_to_c(ast[2], body, false)
end
t = nil
e = C::Break.new
t, e = e, t if ast[0] == :until
body.statements << C::If.new(rb_test(ast_to_c(ast[1], body), body), t, e)
if ast[3] != 0 # do .. while();
ast_to_c(ast[2], body, false)
end
scope.statements << C::For.new(nil, nil, nil, body)
@iter_break = pib
nil.object_id
when :and, :or, :not
# beware lazy evaluation !
tmp = get_new_tmp_var('and', want_value)
v1 = ast_to_c(ast[1], scope, tmp)
# and/or need that tmp has the actual v1 value (returned when shortcircuit)
scope.statements << C::CExpression[tmp, :'=', v1] if v1 != tmp
v1 = tmp
case ast[0]
when :and
t = C::Block.new(scope)
v2 = ast_to_c(ast[2], t, tmp)
t.statements << C::CExpression[tmp, :'=', v2] if v2 != tmp
when :or
e = C::Block.new(scope)
v2 = ast_to_c(ast[2], e, tmp)
e.statements << C::CExpression[tmp, :'=', v2] if v2 != tmp
when :not
t = C::CExpression[tmp, :'=', rb_false]
e = C::CExpression[tmp, :'=', rb_true]
end
scope.statements << C::If.new(rb_test(v1, scope), t, e)
tmp
when :return
scope.statements << C::Return.new(ast_to_c(ast[1], scope))
nil.object_id
when :break
if @iter_break
v = (ast[1] ? ast_to_c(ast[1], scope, @iter_break) : nil.object_id)
scope.statements << C::CExpression[@iter_break, :'=', [[v], value]] if @iter_break != v
end
scope.statements << C::Break.new
nil.object_id
when nil, :args
nil.object_id
when :nil
rb_nil
when :false
rb_false
when :true
rb_true
when :const
rb_const(ast[1])
when :colon2
if cst = check_const(ast[1])
rb_const(ast[2], cst)
else
fcall('rb_const_get', ast_to_c(ast[1], scope), rb_intern(ast[2]))
end
when :colon3
rb_const(ast[1], ::Object)
when :defined
case ast[1][0]
when :ivar
fcall('rb_ivar_defined', rb_self, rb_intern(ast[1][1]))
else
raise Fail, "unsupported #{ast.inspect}"
end
when :masgn
# parallel assignment: put everything in an Array, then pop everything back?
rb_masgn(ast, scope, want_value)
when :evstr
fcall('rb_obj_as_string', ast_to_c(ast[1], scope))
when :dot2, :dot3
fcall('rb_range_new', ast_to_c(ast[1], scope), ast_to_c(ast[2], scope), ast[0] == :dot2 ? 0 : 1)
when :splat
fcall('rb_Array', ast_to_c(ast[1], scope))
when :to_ary
fcall('rb_ary_to_ary', ast_to_c(ast[1], scope))
when :dstr
# dynamic string: "foo#{bar}baz"
tmp = get_new_tmp_var('dstr')
scope.statements << C::CExpression[tmp, :'=', fcall('rb_str_new2', ast[1][1])]
ast[2..-1].compact.each { |s|
if s[0] == :str # directly append the char*
scope.statements << fcall('rb_str_cat2', tmp, s[1])
else
scope.statements << fcall('rb_str_append', tmp, ast_to_c(s, scope))
end
}
tmp
when :case
compile_case(ast, scope, want_value)
when :ensure
# TODO
ret = ast_to_c(ast[1], scope, want_value)
ast_to_c(ast[3], scope, false)
ret
else
raise Fail, "unsupported #{ast.inspect}"
end
if want_value
ret = C::CExpression[[ret], value] if ret.kind_of? Integer or ret.kind_of? String
ret
end
end
# optional optimization of a call (eg a == 1, c+2, ...)
# return nil for normal rb_funcall, or a C::CExpr to use as retval.
def optimize_call(ast, scope, want_value)
ce = C::CExpression
op = ast[2]
int = C::BaseType.new(:ptr) # signed VALUE
args = ast[3][1..-1] if ast[3] and ast[3][0] == :array
arg0 = args[0] if args and args[0]
if arg0 and arg0[0] == :lit and arg0[1].kind_of? Fixnum
# optimize 'x==42', 'x+42', 'x-42'
o2 = arg0[1]
return if not %w[== > < >= <= + -].include? op
if o2 < 0 and ['+', '-'].include? op
# need o2 >= 0 for overflow detection
op = {'+' => '-', '-' => '+'}[op]
o2 = -o2
return if not o2.kind_of? Fixnum # -0x40000000
end
int_v = o2.object_id
recv = ast_to_c(ast[1], scope)
tmp = get_new_tmp_var('opt', want_value)
if not recv.kind_of? C::Variable
scope.statements << ce[tmp, :'=', recv]
recv = tmp
end
case op
when '=='
# XXX assume == only return true for full equality: if not Fixnum, then always false
# which breaks 1.0 == 1 and maybe others, but its ok
scope.statements << C::If.new(ce[recv, :'==', [int_v]], ce[tmp, :'=', rb_true], ce[tmp, :'=', rb_false])
when '>', '<', '>=', '<='
# do the actual comparison on signed >>1 if both Fixnum
t = C::If.new(
ce[[[[recv], int], :>>, [1]], op.to_sym, [[[int_v], int], :>>, [1]]],
ce[tmp, :'=', rb_true],
ce[tmp, :'=', rb_false])
# fallback to actual rb_funcall
e = ce[tmp, :'=', rb_funcall(recv, op, o2.object_id)]
add_optimized_statement scope, ast[1], recv, 'fixnum' => t, 'other' => e
when '+'
e = ce[recv, :+, [int_v-1]] # overflow to Bignum ?
cnd = ce[[recv, :&, [1]], :'&&', [[[recv], int], :<, [[e], int]]]
t = ce[tmp, :'=', e]
e = ce[tmp, :'=', rb_funcall(recv, op, o2.object_id)]
if @optim_hint[ast[1]] == 'fixnum'
# add_optimized_statement wont handle the overflow check correctly
scope.statements << t
else
scope.statements << C::If.new(cnd, t, e)
end
when '-'
e = ce[recv, :-, [int_v-1]]
cnd = ce[[recv, :&, [1]], :'&&', [[[recv], int], :>, [[e], int]]]
t = ce[tmp, :'=', e]
e = ce[tmp, :'=', rb_funcall(recv, op, o2.object_id)]
if @optim_hint[ast[1]] == 'fixnum'
scope.statements << t
else
scope.statements << C::If.new(cnd, t, e)
end
end
tmp
# Symbol#==
elsif arg0 and arg0[0] == :lit and arg0[1].kind_of? Symbol and op == '=='
s_v = ast_to_c(arg0, scope)
tmp = get_new_tmp_var('opt', want_value)
recv = ast_to_c(ast[1], scope, tmp)
if not recv.kind_of? C::Variable
scope.statements << ce[tmp, :'=', recv]
recv = tmp
end
scope.statements << C::If.new(ce[recv, :'==', [s_v]], ce[tmp, :'=', rb_true], ce[tmp, :'=', rb_false])
tmp
elsif arg0 and op == '<<'
tmp = get_new_tmp_var('opt', want_value)
recv = ast_to_c(ast[1], scope, tmp)
arg = ast_to_c(arg0, scope)
if recv != tmp
scope.statements << ce[tmp, :'=', recv]
recv = tmp
end
ar = fcall('rb_ary_push', recv, arg)
st = fcall('rb_str_concat', recv, arg)
oth = rb_funcall(recv, op, arg)
oth = ce[tmp, :'=', oth] if want_value
add_optimized_statement scope, ast[1], recv, 'ary' => ar, 'string' => st, 'other' => oth
tmp
elsif arg0 and args.length == 1 and op == '[]'
return if ast[1][0] == :const # Expression[42]
tmp = get_new_tmp_var('opt', want_value)
recv = ast_to_c(ast[1], scope, tmp)
if not recv.kind_of? C::Variable
scope.statements << ce[tmp, :'=', recv]
recv = tmp
end
idx = get_new_tmp_var('idx')
arg = ast_to_c(arg0, scope, idx)
if not arg.kind_of? C::Variable
scope.statements << ce[idx, :'=', arg]
arg = idx
end
idx = ce[[idx], int]
ar = C::Block.new(scope)
ar.statements << ce[idx, :'=', [[[arg], int], :>>, [1]]]
ar.statements << C::If.new(ce[idx, :<, [0]], ce[idx, :'=', [idx, :+, rb_ary_len(recv)]], nil)
ar.statements << C::If.new(ce[[idx, :<, [0]], :'||', [idx, :>=, [[rb_ary_len(recv)], int]]],
ce[tmp, :'=', rb_nil],
ce[tmp, :'=', rb_ary_ptr(recv, idx)])
st = C::Block.new(scope)
st.statements << ce[idx, :'=', [[[arg], int], :>>, [1]]]
st.statements << C::If.new(ce[idx, :<, [0]], ce[idx, :'=', [idx, :+, rb_str_len(recv)]], nil)
st.statements << C::If.new(ce[[idx, :<, [0]], :'||', [idx, :>=, [[rb_str_len(recv)], int]]],
ce[tmp, :'=', rb_nil],
ce[tmp, :'=', [[[[rb_str_ptr(recv, idx), :&, [0xff]], :<<, [1]], :|, [1]], value]])
hsh = ce[tmp, :'=', fcall('rb_hash_aref', recv, arg)]
oth = ce[tmp, :'=', rb_funcall(recv, op, arg)]
# ary/string only valid with fixnum argument !
add_optimized_statement scope, ast[1], recv, 'hash' => hsh, 'other' => oth,
'ary_bnd' => ce[tmp, :'=', rb_ary_ptr(recv, ce[[[arg], int], :>>, [1]])],
ce[[arg, :&, 1], :'&&', rb_test_class_ary(recv)] => ar,
ce[[arg, :&, 1], :'&&', rb_test_class_string(recv)] => st
tmp
elsif ast[1] and not arg0 and op == 'empty?'
tmp = get_new_tmp_var('opt', want_value)
recv = ast_to_c(ast[1], scope, tmp)
if not recv.kind_of? C::Variable
scope.statements << ce[tmp, :'=', recv]
recv = tmp
end
ar = C::If.new(rb_ary_len(recv), ce[tmp, :'=', rb_false], ce[tmp, :'=', rb_true])
add_optimized_statement scope, ast[1], recv, 'ary' => ar,
'other' => ce[tmp, :'=', rb_funcall(recv, op)]
tmp
elsif ast[1] and not arg0 and op == 'pop'
tmp = get_new_tmp_var('opt', want_value)
recv = ast_to_c(ast[1], scope, tmp)
if not recv.kind_of? C::Variable
scope.statements << ce[tmp, :'=', recv]
recv = tmp
end
t = fcall('rb_ary_pop', recv)
e = rb_funcall(recv, op)
if want_value
t = ce[tmp, :'=', t]
e = ce[tmp, :'=', e]
end
add_optimized_statement scope, ast[1], recv, 'ary' => t, 'other' => e
tmp
elsif ast[1] and op == 'kind_of?' and arg0 and (arg0[0] == :const or arg0[0] == :colon3)
# TODO check const maps to toplevel when :const
test =
case arg0[1]
when 'Symbol'
tmp = get_new_tmp_var('kindof', want_value)
ce[[ast_to_c(ast[1], scope, tmp), :'&', [0xf]], :'==', [0xe]]
#when 'Numeric', 'Integer'
when 'Fixnum'
tmp = get_new_tmp_var('kindof', want_value)
ce[ast_to_c(ast[1], scope, tmp), :'&', [0x1]]
when 'Array'
rb_test_class_ary(ast_to_c(ast[1], scope))
when 'String'
rb_test_class_string(ast_to_c(ast[1], scope))
else return
end
puts "shortcut may be incorrect for #{ast.inspect}" if arg0[0] == :const
tmp ||= get_new_tmp_var('kindof', want_value)
scope.statements << C::If.new(test, ce[tmp, :'=', rb_true], ce[tmp, :'=', rb_false])
tmp
elsif not ast[1] or ast[1] == [:self]
optimize_call_static(ast, scope, want_value)
end
end
# check if the var falls in an optim_hint, if so generate only selected code
# optim is a hash varclass (keyof @optim_hint) => c_stmt
# optim key can also be a C::Stmt that is used in the If clause
# if optim['ary'] == optim['ary_bnd'], you can omit the latter
# must have an 'other' key that is calls the generic ruby method
def add_optimized_statement(scope, varid, varc, optim={})
cat = @optim_hint[varid]
cat = 'ary' if cat == 'ary_bnd' and not optim['ary_bnd']
if not st = optim[cat]
st = optim['other']
if not cat and optim.keys.all? { |k| k.kind_of? String }
# no need to cascade if we have a hash and can optim ary only
optim.each { |i, s|
case i
when 'ary'; st = C::If.new(rb_test_class_ary(varc), s, st)
when 'hash'; st = C::If.new(rb_test_class_hash(varc), s, st)
when 'string'; st = C::If.new(rb_test_class_string(varc), s, st)
when 'other'; # already done as default case
when 'fixnum'; # add test last
when C::Statement; st = C::If.new(i, s, st)
end
}
if fs = optim['fixnum']
# first test to perform (fast path)
st = C::If.new(C::CExpression[varc, :&, 1], fs, st)
end
end
end
scope.statements << st
end
# return ptr, arity
# ptr is a CExpr pointing to the C func implementing klass#method
def get_cfuncptr(klass, method, singleton=false)
cls = singleton ? (class << klass ; self ; end) : klass
ptr = RubyHack.get_method_node_ptr(cls, method)
return if ptr == 0
ftype = RubyHack::NODETYPE[(RubyHack.memory_read_int(ptr) >> 11) & 0xff]
return if ftype != :cfunc
fast = RubyHack.read_node(ptr)
arity = fast[1][:arity]
fptr = fast[1][:fptr]
fproto = C::Function.new(value, [])
case arity
when -1; fproto.args << C::Variable.new(nil, C::BaseType.new(:int)) << C::Variable.new(nil, C::Pointer.new(value)) << C::Variable.new(nil, value)
when -2; fproto.args << C::Variable.new(nil, value) << C::Variable.new(nil, value)
else (arity+1).times { fproto.args << C::Variable.new(nil, value) }
end
C::CExpression[[fptr], C::Pointer.new(fproto)]
end
# call C funcs directly
# assume private function calls are not virtual and hardlink them here
def optimize_call_static(ast, scope, want_value)
arity = method_arity(ast[2]) rescue return
if ast[2].to_s == @meth.to_s
# self is recursive
fptr = @cur_cfunc
else
fptr = get_cfuncptr(@klass, ast[2], @meth_singleton)
return if not fptr
end
c_arglist = []
if not ast[3]
args = []
elsif ast[3][0] == :array
args = ast[3][1..-1]
elsif ast[3][0] == :splat
args = ast_to_c(ast[3], scope)
if arity != -2 and !args.kind_of?(C::Variable)
tmp = get_new_tmp_var('arg')
scope.statements << C::CExpression[tmp, :'=', args]
args = tmp
end
case arity
when -2
c_arglist << rb_self << args
when -1
c_arglist << [rb_ary_len(args)] << rb_ary_ptr(args) << rb_self
else
cnd = C::CExpression[rb_ary_len(args), :'!=', [arity]]
scope.statements << C::If.new(cnd, rb_raise("#{arity} args expected", 'rb_eArgumentError'), nil)
c_arglist << rb_self
arity.times { |i| c_arglist << rb_ary_ptr(args, i) }
end
arity = :canttouchthis
else return # TODO
end
case arity
when :canttouchthis
when -2
arg = get_new_tmp_var('arg')
scope.statements << C::CExpression[arg, :'=', fcall('rb_ary_new')]
args.each { |a|
scope.statements << fcall('rb_ary_push', arg, ast_to_c(a, scope))
}
c_arglist << rb_self << arg
when -1
case args.length
when 0
argv = C::CExpression[[0], C::Pointer.new(value)]
when 1
val = ast_to_c(args[0], scope)
if not val.kind_of? C::Variable
argv = get_new_tmp_var('argv')
scope.statements << C::CExpression[argv, :'=', val]
val = argv
end
argv = C::CExpression[:'&', val]
else
argv = get_new_tmp_var('argv')
argv.type = C::Array.new(value, args.length)
args.each_with_index { |a, i|
val = ast_to_c(a, scope)
scope.statements << C::CExpression[[argv, :'[]', [i]], :'=', val]
}
end
c_arglist << [args.length] << argv << rb_self
else
c_arglist << rb_self
args.each { |a|
va = get_new_tmp_var('arg')
val = ast_to_c(a, scope, va)
scope.statements << C::CExpression[va, :'=', val] if val != va
c_arglist << va
}
end
f = C::CExpression[fptr, :funcall, c_arglist]
if want_value
ret = get_new_tmp_var('ccall', want_value)
scope.statements << C::CExpression[ret, :'=', f]
ret
else
scope.statements << f
end
end
def optimize_iter(ast, scope, want_value)
b_args, b_body, b_recv = ast[1, 3]
old_ib = @iter_break
if want_value
# a new tmpvar, so we can overwrite it in 'break :foo'
@iter_break = get_new_tmp_var('iterbreak')
else
@iter_break = nil
end
if b_recv[0] == :call and b_recv[2] == 'reverse_each'
# convert ary.reverse_each to ary.reverse.each
b_recv = b_recv.dup
b_recv[1] = [:call, b_recv[1], 'reverse']
b_recv[2] = 'each'
elsif b_recv[0] == :call and b_recv[2] == 'each_key'
# convert hash.each_key to hash.keys.each
b_recv = b_recv.dup
b_recv[1] = [:call, b_recv[1], 'keys']
b_recv[2] = 'each'
end
# loop { }
if b_recv[0] == :fcall and b_recv[2] == 'loop'
body = C::Block.new(scope)
ast_to_c(b_body, body, false)
scope.statements << C::For.new(nil, nil, nil, body)
# int.times { |i| }
elsif b_recv[0] == :call and not b_recv[3] and b_recv[2] == 'times'
limit = get_new_tmp_var('limit')
recv = ast_to_c(b_recv[1], scope, limit)
scope.statements << C::If.new(C::CExpression[:'!', [recv, :&, 1]], rb_raise('only Fixnum#times handled'), nil)
if want_value
scope.statements << C::CExpression[@iter_break, :'=', recv]
end
scope.statements << C::CExpression[limit, :'=', [recv, :>>, 1]]
cntr = get_new_tmp_var('cntr')
cntr.type = C::BaseType.new(:int, :unsigned)
body = C::Block.new(scope)
if b_args and b_args[0] == :dasgn_curr
body.statements << C::CExpression[dvar(b_args[1]), :'=', [[cntr, :<<, 1], :|, 1]]
end
ast_to_c(b_body, body, false)
scope.statements << C::For.new(C::CExpression[cntr, :'=', [[0], cntr.type]], C::CExpression[cntr, :<, limit], C::CExpression[:'++', cntr], body)
# ary.each { |e| }
elsif b_recv[0] == :call and not b_recv[3] and b_recv[2] == 'each' and b_args and
b_args[0] == :dasgn_curr
ary = get_new_tmp_var('ary')
recv = ast_to_c(b_recv[1], scope, ary)
scope.statements << C::CExpression[ary, :'=', recv] if ary != recv
scope.statements << C::If.new(rb_test_class_ary(ary), nil, rb_raise('only Array#each { |e| } handled'))
if want_value
scope.statements << C::CExpression[@iter_break, :'=', ary]
end
cntr = get_new_tmp_var('cntr')
cntr.type = C::BaseType.new(:int, :unsigned)
body = C::Block.new(scope)
if b_args and b_args[0] == :dasgn_curr
body.statements << C::CExpression[dvar(b_args[1]), :'=', [rb_ary_ptr(ary), :'[]', [cntr]]]
end
ast_to_c(b_body, body, false)
scope.statements << C::For.new(C::CExpression[cntr, :'=', [[0], cntr.type]], C::CExpression[cntr, :<, rb_ary_len(ary)], C::CExpression[:'++', cntr], body)
# ary.find { |e| }
elsif b_recv[0] == :call and not b_recv[3] and b_recv[2] == 'find' and b_args and
b_args[0] == :dasgn_curr
ary = get_new_tmp_var('ary')
recv = ast_to_c(b_recv[1], scope, ary)
scope.statements << C::CExpression[ary, :'=', recv] if ary != recv
scope.statements << C::If.new(rb_test_class_ary(ary), nil, rb_raise('only Array#find { |e| } handled'))
if want_value
scope.statements << C::CExpression[@iter_break, :'=', rb_nil]
end
cntr = get_new_tmp_var('cntr')
cntr.type = C::BaseType.new(:int, :unsigned)
body = C::Block.new(scope)
if b_args and b_args[0] == :dasgn_curr
body.statements << C::CExpression[dvar(b_args[1]), :'=', [rb_ary_ptr(ary), :'[]', [cntr]]]
end
# same as #each up to this point (except default retval), now add a 'if (body_value) break ary[cntr];'
# XXX 'find { next true }'
found = ast_to_c(b_body, body)
t = C::Block.new(body)
t.statements << C::CExpression[@iter_break, :'=', rb_ary_ptr(ary, cntr)]
t.statements << C::Break.new
body.statements << C::If.new(rb_test(found, body), t, nil)
scope.statements << C::For.new(C::CExpression[cntr, :'=', [[0], cntr.type]], C::CExpression[cntr, :<, rb_ary_len(ary)], C::CExpression[:'++', cntr], body)
# ary.map { |e| }
elsif b_recv[0] == :call and not b_recv[3] and b_recv[2] == 'map' and b_args and
b_args[0] == :dasgn_curr
ary = get_new_tmp_var('ary')
recv = ast_to_c(b_recv[1], scope, ary)
scope.statements << C::CExpression[ary, :'=', recv] if ary != recv
scope.statements << C::If.new(rb_test_class_ary(ary), nil, rb_raise('only Array#map { |e| } handled'))
if want_value
scope.statements << C::CExpression[@iter_break, :'=', fcall('rb_ary_new')]
end
cntr = get_new_tmp_var('cntr')
cntr.type = C::BaseType.new(:int, :unsigned)
body = C::Block.new(scope)
if b_args and b_args[0] == :dasgn_curr
body.statements << C::CExpression[dvar(b_args[1]), :'=', [rb_ary_ptr(ary), :'[]', [cntr]]]
end
# same as #each up to this point (except default retval), now add a '@iter_break << body_value'
# XXX 'next' unhandled
val = ast_to_c(b_body, body)
body.statements << fcall('rb_ary_push', @iter_break, val)
scope.statements << C::For.new(C::CExpression[cntr, :'=', [[0], cntr.type]], C::CExpression[cntr, :<, rb_ary_len(ary)], C::CExpression[:'++', cntr], body)
else
@iter_break = old_ib
return
end
ret = @iter_break
@iter_break = old_ib
ret || nil.object_id
end
end
# a ruby2c C generator for use in the any ruby interpreter (generates C suitable for use as a standard Ruby extension)
class RubyStaticCompiler < RubyLiveCompiler
# add a new ruby function to the current @cp
def self.compile(klass, *methlist)
@rcp ||= new
methlist.each { |meth|
ast = RubyHack.read_method_ast(klass, meth)
@rcp.compile(ast, klass, meth)
}
self
end
def self.compile_singleton(klass, *methlist)
@rcp ||= new
methlist.each { |meth|
ast = RubyHack.read_singleton_method_ast(klass, meth)
@rcp.compile(ast, klass, meth, true)
}
self
end
def self.dump
<<EOS + @rcp.cp.dump_definition('Init_compiledruby')
#ifdef __ELF__
asm .pt_gnu_stack rw;
#endif
EOS
end
def dump(m="Init_compiledruby")
m ? @cp.dump_definition(m, 'do_init_once') : @cp.to_s
end
def initialize(cp=nil)
super(cp)
@cp.parse <<EOS
// static VALUE method(VALUE self, VALUE arg0, VALUE arg1) { return (VALUE)0; }
// static VALUE const_Lol;
static void do_init_once(void) {
// const_Lol = rb_const_get(*rb_cObject, rb_intern("Lol"));
// rb_define_method(const_Lol, "method", method, 2);
}
int Init_compiledruby(void) __attribute__((export)) {
// use a separate func to avoid having to append statements before the 'return'
do_init_once();
return 0;
}
EOS
end
# returns the 'do_init_once' function body
def init
@cp.toplevel.symbol['do_init_once'].initializer
end
def compile(ast, klass, method, singleton=false)
@compiled_func_cache ||= {}
mname = super(ast, klass, method, singleton)
return if not mname
@compiled_func_cache[[klass, method.to_s, singleton]] = @cur_cfunc
cls = rb_const(nil, klass)
init.statements << fcall("rb_define#{'_singleton' if singleton}_method", cls, method.to_s, @cur_cfunc, method_arity)
mname
end
def declare_newtopvar(name, initializer, type=value)
v = C::Variable.new(name, type)
v.storage = :static
@cp.toplevel.symbol[v.name] = v
pos = @cp.toplevel.statements.index @cp.toplevel.statements.find { |st|
st.kind_of? C::Declaration and st.var.type.kind_of? C::Function and st.var.initializer
} || -1
@cp.toplevel.statements.insert pos, C::Declaration.new(v)
if initializer
pos = -1
if name =~ /^intern_/
pos = 0
init.statements.each { |st|
break unless st.kind_of? C::CExpression and st.op == :'=' and st.lexpr.kind_of? C::Variable and st.lexpr.name < name
pos += 1
}
end
init.statements.insert(pos, C::CExpression[v, :'=', initializer])
end
v
end
def rb_intern(sym)
n = escape_varname("intern_#{sym}")
@cp.toplevel.symbol[n] || declare_newtopvar(n, fcall('rb_intern', sym.to_s), C::BaseType.new(:int, :unsigned))
end
# rb_const 'FOO', Bar::Baz ==>
# const_Bar = rb_const_get(rb_cObject, rb_intern("Bar"));
# const_Bar_Baz = rb_const_get(const_Bar, rb_intern("Baz"));
# const_Bar_Baz_FOO = rb_const_get(const_Bar_Baz, rb_intern("FOO"));
# use rb_const(nil, class) to get a pointer to a class/module
def rb_const(constname, owner = resolve_const_owner(constname))
raise Fail, "no dynamic constant resolution #{constname}" if not owner
@const_value ||= { [::Object, 'Object'] => rb_global('rb_cObject') }
k = ::Object
v = nil
cname = owner.name
cname += '::' + constname if constname
cname.split('::').each { |n|
kk = k.const_get(n)
if not v = @const_value[[k, n]]
# class A ; end ; B = A => B.name => 'A'
vn = "const_#{escape_varname((k.name + '::' + n).sub(/^Object::/, '').gsub('::', '_'))}"
vi = fcall('rb_const_get', rb_const(nil, k), fcall('rb_intern', n))
v = declare_newtopvar(vn, vi)
# n wont be reused, so do not alloc a global intern_#{n} for this
@const_value[[k, n]] = v
end
k = kk
}
v
end
# TODO remove this when the C compiler is fixed
def rb_global(cname)
C::CExpression[:*, @cp.toplevel.symbol[cname]]
end
def get_cfuncptr(klass, method, singleton=false)
# is it a func we have in the current cparser ?
if ptr = @compiled_func_cache[[klass, method.to_s, singleton]]
return ptr
end
# check if it's a C or ruby func in the current interpreter
cls = singleton ? (class << klass ; self ; end) : klass
ptr = RubyHack.get_method_node_ptr(cls, method)
return if ptr == 0
ftype = RubyHack::NODETYPE[(RubyHack.memory_read_int(ptr) >> 11) & 0xff]
return if ftype != :cfunc
# ok, so assume it will be the same next time
n = escape_varname "fptr_#{klass.name}#{singleton ? '.' : '#'}#{method}".gsub('::', '_')
if not v = @cp.toplevel.symbol[n]
v = get_cfuncptr_dyn(klass, method, singleton, n)
end
v
end
def get_cfuncptr_dyn(klass, method, singleton, n)
arity = singleton ? klass.method(method).arity : klass.instance_method(method).arity
fproto = C::Function.new(value, [])
case arity
when -1; fproto.args << C::Variable.new(nil, C::BaseType.new(:int)) << C::Variable.new(nil, C::Pointer.new(value)) << C::Variable.new(nil, value)
when -2; fproto.args << C::Variable.new(nil, value) << C::Variable.new(nil, value)
else (arity+1).times { fproto.args << C::Variable.new(nil, value) }
end
if not ptr = init.symbol['ptr']
ptr = C::Variable.new('ptr', C::Pointer.new(C::BaseType.new(:int)))
init.symbol[ptr.name] = ptr
init.statements << C::Declaration.new(ptr)
end
cls = rb_const(nil, klass)
cls = fcall('rb_singleton_class', cls) if singleton
init.statements << C::CExpression[ptr, :'=', fcall('rb_method_node', cls, rb_intern(method))]
# dynamically recheck that klass#method is a :cfunc
cnd = C::CExpression[[:'!', ptr], :'||', [[[[ptr, :'[]', [0]], :>>, [11]], :&, [0xff]], :'!=', [RubyHack::NODETYPE.index(:cfunc)]]]
init.statements << C::If.new(cnd, rb_raise("CFunc expected at #{klass}#{singleton ? '.' : '#'}#{method}"), nil)
vi = C::CExpression[[ptr, :'[]', [1]], C::Pointer.new(fproto)]
declare_newtopvar(n, vi, C::Pointer.new(fproto))
end
if defined? $trace_rbfuncall and $trace_rbfuncall
# dynamic trace of all rb_funcall made from our module
def rb_funcall(recv, meth, *args)
if not defined? @rb_fcid
@cp.parse <<EOS
int atexit(void(*)(void));
int printf(char*, ...);
static unsigned rb_fcid_max = 1;
static unsigned rb_fcntr[1];
static void rb_fcstat(void)
{
unsigned i;
for (i=0 ; i<rb_fcid_max ; ++i)
if (rb_fcntr[i])
printf("%u %u\\n", i, rb_fcntr[i]);
}
EOS
@rb_fcid = -1
@rb_fcntr = @cp.toplevel.symbol['rb_fcntr']
@rb_fcid_max = @cp.toplevel.symbol['rb_fcid_max']
init.statements << fcall('atexit', @cp.toplevel.symbol['rb_fcstat'])
end
@rb_fcid += 1
@rb_fcid_max.initializer = C::CExpression[[@rb_fcid+1], @rb_fcid_max.type]
@rb_fcntr.type.length = @rb_fcid+1
ctr = C::CExpression[:'++', [@rb_fcntr, :'[]', [@rb_fcid]]]
C::CExpression[ctr, :',', super(recv, meth, *args)]
end
end
end
end
if __FILE__ == $0 or ARGV.delete('ignore_argv0')
demo = case ARGV.first
when nil; :test_jit
when 'asm'; :inlineasm
when 'generate'; :generate_persistent
else :compile_ruby
end
case demo
when :inlineasm
# cnt.times { sys_write str }
src_asm = <<EOS
mov ecx, [ebp+8]
again:
push ecx
mov eax, 4
mov ebx, 1
mov ecx, [ebp+12]
mov edx, [ebp+16]
int 80h
pop ecx
loop again
EOS
src = <<EOS
#{Metasm::RubyLiveCompiler::RUBY_H}
void doit(int, char*, int);
VALUE foo(VALUE self, VALUE count, VALUE str) {
doit(VAL2INT(count), STR_PTR(str), STR_LEN(str));
return count;
}
void doit(int count, char *str, int strlen) { asm(#{src_asm.inspect}); }
EOS
class Foo
end
m = Metasm::RubyHack.compile_c(src).encoded
Metasm::RubyHack.set_method_binary(Foo, 'bar', m, 2)
Foo.new.bar(4, "blabla\n")
Foo.new.bar(2, "foo\n")
when :compile_ruby
abort 'need <class#method>' if ARGV.empty?
require 'pp'
puts '#if 0'
ARGV.each { |av|
next if not av =~ /^(.*)([.#])(.*)$/
cls, sg, meth = $1, $2, $3.to_sym
sg = { '.' => true, '#' => false }[sg]
cls = cls.split('::').inject(::Object) { |o, cst| o.const_get(cst) }
if sg
ast = Metasm::RubyHack.read_singleton_method_ast(cls, meth)
cls.method(meth) if not ast # raise NoMethodError
puts ' --- ast ---'
pp ast
Metasm::RubyStaticCompiler.compile_singleton(cls, meth)
else
ast = Metasm::RubyHack.read_method_ast(cls, meth)
cls.instance_method(meth) if not ast
puts ' --- ast ---'
pp ast
Metasm::RubyStaticCompiler.compile(cls, meth)
end
}
puts '', ' --- C ---', '#endif'
puts Metasm::RubyStaticCompiler.dump
when :test_jit
class Foo
def bla(x=500)
i = 0
x.times { i += 16 }
i
end
end
t0 = Time.now
Metasm::RubyLiveCompiler.compile(Foo, :bla)
t1 = Time.now
ret = Foo.new.bla(0x401_0000)
puts ret.to_s(16), ret.class
t2 = Time.now
puts "compile %.3fs run %.3fs" % [t1-t0, t2-t1]
when :generate_persistent
Metasm::RubyStaticCompiler.compile(Metasm::Preprocessor, :getchar, :ungetchar, :unreadtok, :readtok_nopp_str, :readtok_nopp, :readtok)
Metasm::RubyStaticCompiler.compile(Metasm::Expression, :reduce_rec, :initialize)
Metasm::RubyStaticCompiler.compile_singleton(Metasm::Expression, :[])
c_src = Metasm::RubyStaticCompiler.dump
File.open('compiledruby.c', 'w') { |fd| fd.puts c_src } if $VERBOSE
puts 'compiling..'
begin ; require 'compiledruby' ; rescue LoadError ; end
# To encode to a different file, you must also rename the Init_compliedruby() function to match the lib name
Metasm::ELF.compile_c(Metasm::Ia32.new, c_src).encode_file('compiledruby.so')
puts 'ruby -r metasm -r compiledruby ftw'
end
end