/
rubyc
241 lines (210 loc) · 6.56 KB
/
rubyc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
#!/usr/bin/ruby
# MacRuby AOT Compiler.
#
# This file is covered by the Ruby license.
#
# Copyright (C) 2009, Apple Inc
require 'optparse'
require 'rbconfig'
class Compiler
NAME = File.basename(__FILE__)
def initialize(argv)
@mode = :normal
@frameworks = []
@internal = argv.delete('--internal')
# Parse arguments.
OptionParser.new do |opts|
opts.banner = "Usage: #{NAME} [options] file..."
opts.on('-c', 'Compile and assemble, but do not link') { @dont_link = true }
opts.on('-o <file>', 'Place the output into <file>') { |output| @output = output }
opts.on('--mode [MODE]', "Select compilation mode (normal or full)") { |mode| @mode = mode.intern }
#opts.on('--framework <framework>', 'Link against <framework>') { |path| @frameworks << path }
opts.on('-C', 'Compile, assemble and link a loadable object file') { @bundle = true }
opts.on('-v', '--version', 'Display the version') { puts RUBY_DESCRIPTION; exit 1 }
opts.on('-V', '--verbose', 'Print every command line executed') { @verbose = true }
opts.on('-h', '--help', 'Display this information') { die opts }
begin
opts.parse!(argv)
rescue OptionParser::InvalidOption => e
die e, opts
end
die opts if argv.empty?
@files = argv
if @mode != :normal and @mode != :full
die "invalid mode `#{@mode}' (possible choices are: normal, full)"
end
end
# Locate necessary programs.
@macruby = locate(@internal ? './miniruby' : 'macruby')
@llc = locate('llc')
@gcc = locate('gcc')
@gcxx = locate('g++')
@nm = locate('nm')
# Misc.
@tmpdir = (ENV['TMPDIR'] or '/tmp')
@tmpfiles = []
end
def run
if @mode == :full
die "full compilation mode is not implemented yet!"
end
if @dont_link or @bundle
die "cannot specify -c and -C at the same time" if @bundle and @dont_link
if @files.size > 1 and @output
die "cannot specify -o with -c or -C and multiple input files"
end
file = @files[0]
if File.extname(file) != '.rb'
die "given input file `#{file}' must be a Ruby source file (.rb)"
end
if @bundle
compile_bundle(file, @output)
else
compile_object(file, @output)
end
else
objs = @files.map do |file|
case File.extname(file)
when '.rb'
compile_object(file, nil)
when '.o'
die "given input file `#{file} must exist" unless File.exist?(file)
file
else
die "given input file `#{file}' must be either a Ruby source file (.rb) or a Mach-O object file (.o)"
end
end
compile_executable(objs, @output)
end
end
def cleanup
@tmpfiles.each { |x| File.delete(x) }
end
private
def compile_object(path, output)
base = File.basename(path, '.rb')
output ||= File.join(File.dirname(path), base + '.o')
# Compile the file into LLVM bitcode.
bc = gen_tmpfile(base, 'bc')
execute("#{@macruby} --emit-llvm \"#{bc}\" \"#{path}\"")
# Compile the bitcode as assembly.
asm = gen_tmpfile(base, 's')
execute("#{@llc} -f #{bc} -o=#{asm} -march=x86-64 -enable-eh")
# Finally compile the assembly.
execute("#{@gcc} -c -arch x86_64 #{asm} -o #{output}")
output
end
def compile_bundle(file, output)
base = File.basename(file, '.rb')
obj = gen_tmpfile(base, 'o')
compile_object(file, obj)
output ||= File.join(File.dirname(file), base + '.rbo')
real_init_func = guess_init_function_name(obj)
unless real_init_func
die "can't guess the init function of Ruby object file `#{obj}'"
end
init_func = "Init_" + base.gsub(/-/, '_')
main_txt = <<EOS
extern "C" {
void *#{real_init_func}(void *, void *);
void *rb_vm_top_self(void);
void
#{init_func}(void) {
#{real_init_func}(rb_vm_top_self(), 0);
}
}
EOS
main = gen_tmpfile('main', 'c')
File.open(main, 'w') { |io| io.write(main_txt) }
link = @internal ? "-L. -lmacruby" : "-framework MacRuby"
execute("#{@gcxx} #{main} -dynamic -bundle -undefined suppress -flat_namespace -arch x86_64 #{link} #{obj} -o #{output}")
end
def compile_executable(objs, output)
output ||= 'a.out'
# Guess which objects were originally MacRuby source files and memorize their init function.
init_funcs = []
objs.each do |file|
n = guess_init_function_name(file)
init_funcs << n if n
end
# Generate main file.
main_txt = <<EOS
extern "C" {
void ruby_sysinit(int *, char ***);
void ruby_init(void);
void ruby_set_argv(int, char **);
void rb_vm_init_compiler(void);
void *rb_vm_top_self(void);
void rb_vm_print_current_exception(void);
void rb_exit(int);
EOS
init_funcs.each { |x| main_txt << "void *#{x}(void *, void *);\n" }
main_txt << <<EOS
}
int main(int argc, char **argv)
{
ruby_sysinit(&argc, &argv);
if (argc > 0) {
argc--;
argv++;
}
ruby_init();
ruby_set_argv(argc, argv);
rb_vm_init_compiler();
try {
void *self = rb_vm_top_self();
EOS
init_funcs.each { |x| main_txt << "#{x}(self, 0);\n" }
main_txt << <<EOS
}
catch (...) {
rb_vm_print_current_exception();
rb_exit(1);
}
rb_exit(0);
}
EOS
# Compile main file.
main = gen_tmpfile('main', 'cpp')
File.open(main, 'w') { |io| io.write(main_txt) }
main_o = gen_tmpfile('main', 'o')
execute("#{@gcxx} #{main} -c -arch x86_64 -o #{main_o}")
objs.unshift(main_o)
# Link all objects into executable.
line = "#{@gcxx} -o #{output} -L#{RbConfig::CONFIG['libdir']} -lmacruby-static -arch x86_64 -framework Foundation -lobjc -lauto -I/usr/include/libxml2 -lxml2 "
@frameworks.each { |f| line << "-framework #{f} " }
line << execute("llvm-config --ldflags --libs core jit nativecodegen interpreter bitwriter").gsub(/\n/, '')
objs.each { |o| line << " #{o}" }
execute(line)
end
def guess_init_function_name(file)
ary = execute("#{@nm} -g #{file}").scan(/^\d+\sT\s(_MREP_.*)$/)
ary.empty? ? nil : ary[0][0][1..-1] # drop _ prefix
end
def execute(line)
$stderr.puts line if @verbose
ret = `#{line}`
die "Error when executing `#{line}'" unless $?.success?
ret
end
def locate(progname)
path = `which #{progname}`.strip
die "Can't locate program `#{progname}'" if path.empty?
path
end
def gen_tmpfile(base, ext)
file = File.join(@tmpdir, "#{base}.#{ext}")
@tmpfiles << file
file
end
def die(*args)
$stderr.puts args
exit 1
end
end
app = Compiler.new(ARGV)
begin
app.run
ensure
app.cleanup
end