/
rubyc
360 lines (321 loc) · 10.4 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
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
#!/usr/bin/ruby
# MacRuby AOT Compiler.
#
# This file is covered by the Ruby license.
#
# Copyright (C) 2009-2010, Apple Inc
require 'optparse'
require 'rbconfig'
class Compiler
NAME = File.basename(__FILE__)
VALID_ARCHS = ['i386', 'x86_64']
def initialize(argv)
@mode = :normal
@archs = []
@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('--static', "Create a standalone static executable") { @static = true }
opts.on('--dylib', "Create a dynamic library") { @dylib = true }
opts.on('-C', 'Compile, assemble and link a loadable object file') { @bundle = true }
opts.on('-a', '--arch <ARCH>', 'Compile for specified CPU architecture') { |arch| @archs << arch }
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
@archs.uniq!
@archs << RUBY_ARCH if @archs.empty?
@archs.each do |arch|
if not VALID_ARCHS.include?(arch)
die "Invalid CPU architecture `#{arch}'. Possible values are: " + VALID_ARCHS.join(", ")
end
end
end
# Locate necessary programs.
@macruby = locate(@internal ? './miniruby' : 'macruby')
if @internal
@llc = File.join(RbConfig::CONFIG['LLVM_PATH'], 'bin/llc')
die "llc not found as #{@llc}" unless File.exist?(@llc)
else
@llc = locate('llc', true)
end
@gcc = locate('gcc')
@gcxx = locate('g++')
@nm = locate('nm')
@lipo = locate('lipo')
# 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 --static when not building an executable" if @static
die "Cannot specify -c or -C when building a dynamic library" if @dylib
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
@files.each do |file|
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
end
else
die "Cannot specify --static and --dylib at the same time" if @dylib and @static
objs = @files.map do |file|
die "Given input file `#{file} must exist" unless File.exist?(file)
case File.extname(file)
when '.rb'
compile_object(file, nil)
when '.o'
[file, find_init_func(file)]
when '.dylib'
[file, nil]
else
die "Given input file `#{file}' must be either a Ruby source file (.rb) or a Mach-O object file (.o) or dynamic library (.dylib)"
end
end
if @dylib
die "-o must be specified when building a dynamic library" unless @output
compile_dylib(objs, @output)
else
compile_executable(objs, @output)
end
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')
# Generate init function (must be unique).
uuid = `uuidgen`.strip.gsub('-', '')
init_func = "MREP_#{uuid}"
tmp_objs = []
@archs.each do |arch|
# Compile the file into LLVM bitcode.
bc = gen_tmpfile(base + arch, 'bc')
execute("arch -#{arch} #{@macruby} --emit-llvm \"#{bc}\" #{init_func} \"#{path}\"")
# Compile the bitcode as assembly.
asm = gen_tmpfile(base + arch, 's')
execute("#{@llc} -f \"#{bc}\" -o=\"#{asm}\" -march=#{llc_arch(arch)} -relocation-model=pic -enable-eh")
# Compile the assembly.
tmp_obj = gen_tmpfile(base + arch, 'o')
execute("#{@gcc} -c -arch #{arch} \"#{asm}\" -o \"#{tmp_obj}\"")
tmp_objs << tmp_obj
end
# Link the architecture objects.
cli_tmp_objs = tmp_objs.map do |obj|
'"' + obj + '"'
end
execute("#{@lipo} -create #{cli_tmp_objs.join(' ')} -output \"#{output}\"")
[output, init_func]
end
def compile_bundle(file, output)
base = File.basename(file, '.rb')
obj = gen_tmpfile(base, 'o')
obj, init_func = compile_object(file, obj)
output ||= File.join(File.dirname(file), base + '.rbo')
# Generate main file.
main_txt = <<EOS
extern "C" {
void *#{init_func}(void *, void *);
void *rb_vm_top_self(void);
__attribute__((constructor)) static void __init__(void) {
#{init_func}(rb_vm_top_self(), 0);
}
}
EOS
# Build.
main = gen_tmpfile('main', 'c')
File.open(main, 'w') { |io| io.write(main_txt) }
linkf = @internal ? "-L. -lmacruby" : "-framework MacRuby"
execute("#{@gcxx} \"#{main}\" -dynamic -bundle -undefined suppress -flat_namespace #{arch_flags} #{linkf} \"#{obj}\" -o \"#{output}\"")
end
def compile_dylib(objs_data, output)
# Generate main file.
main_txt = <<EOS
extern "C" {
void rb_vm_aot_feature_provide(const char *, void *);
EOS
objs_data.each do |obj, init_func|
next if init_func == nil
main_txt << "void *#{init_func}(void *, void *);\n"
end
main_txt << <<EOS
__attribute__((constructor)) static void __init__(void) {
EOS
objs_data.each do |obj, init_func|
main_txt << "rb_vm_aot_feature_provide(\"#{feature_name(obj)}\", (void *)#{init_func});\n"
end
main_txt << "}}"
# Build.
main = gen_tmpfile('main', 'c')
File.open(main, 'w') { |io| io.write(main_txt) }
linkf = @internal ? "-L. -lmacruby" : "-framework MacRuby"
objs = objs_data.map { |obj, f| "\"#{obj}\"" }.join(' ')
execute("#{@gcxx} \"#{main}\" -dynamiclib -dynamic -undefined suppress -flat_namespace #{arch_flags} #{linkf} #{objs} -o \"#{output}\"")
end
def compile_executable(objs_data, output)
output ||= 'a.out'
raise if objs_data.empty?
die "first object file must be a Ruby source file or object" if objs_data[0][1] == nil
# Generate main file.
main_txt = <<EOS
extern "C" {
void ruby_sysinit(int *, char ***);
void ruby_init(void);
void ruby_init_loadpath(void);
void ruby_script(const char *);
void ruby_set_argv(int, char **);
void rb_vm_init_compiler(void);
void rb_vm_aot_feature_provide(const char *, void *);
void *rb_vm_top_self(void);
void rb_vm_print_current_exception(void);
void rb_exit(int);
EOS
objs_data.each do |obj, init_func|
next if init_func == nil
main_txt << "void *#{init_func}(void *, void *);\n"
end
main_txt << <<EOS
}
int main(int argc, char **argv)
{
const char *progname = argv[0];
ruby_sysinit(&argc, &argv);
if (argc > 0) {
argc--;
argv++;
}
ruby_init();
ruby_init_loadpath();
ruby_set_argv(argc, argv);
rb_vm_init_compiler();
ruby_script(progname);
try {
EOS
objs_data[1..-1].each do |obj, init_func|
next if init_func == nil
main_txt << "rb_vm_aot_feature_provide(\"#{feature_name(obj)}\", (void *)#{init_func});\n"
end
main_txt << <<EOS
void *self = rb_vm_top_self();
#{objs_data[0][1]}(self, 0);
}
catch (...) {
rb_vm_print_current_exception();
rb_exit(1);
}
rb_exit(0);
}
EOS
# Prepare objects.
objs = []
objs_data.each { |o, _| objs << o }
# Compile main file.
main = gen_tmpfile('main', 'mm')
File.open(main, 'w') { |io| io.write(main_txt) }
main_o = gen_tmpfile('main', 'o')
execute("#{@gcxx} \"#{main}\" -c #{arch_flags} -o \"#{main_o}\" -fobjc-gc")
objs.unshift(main_o)
# Link all objects into executable.
linkf = @static ?
"-L#{RbConfig::CONFIG['libdir']} #{RbConfig::CONFIG['LIBRUBYARG_STATIC_REALLY']}" :
"-framework MacRuby -lobjc"
line = "#{@gcxx} -o \"#{output}\" #{arch_flags} #{linkf} "
objs.each { |o| line << " \"#{o}\"" }
execute(line)
end
def execute(line)
$stderr.puts line if @verbose
ret = `#{line}`
unless $?.success?
die_str = "Error when executing `#{line}'"
die_str += "\n#{ret}" unless ret.empty?
die die_str
end
ret
end
def locate(progname, must_be_in_bindir=false)
path = File.join(Config::CONFIG['bindir'], progname)
unless File.exist?(path)
if must_be_in_bindir
die "Can't locate program `#{progname}' in #{Config::CONFIG['bindir']}"
end
path = `which #{progname}`.strip
die "Can't locate program `#{progname}'" if path.empty?
end
path
end
def llc_arch(arch)
# LLVM uses a different convention for architecture names.
case arch
when 'i386'; 'x86'
when 'x86_64'; 'x86-64'
else; arch
end
end
def arch_flags
@archs.map { |x| "-arch #{x}" }.join(' ')
end
def find_init_func(obj)
output = `#{@nm} -j "#{obj}"`
output.scan(/^_MREP_.*$/).reject { |func|
# Ignore non-main functions.
func.include?('ruby_scope')
}.map { |func|
# Ignore the very first character (_).
func[1..-1]
}[0]
end
def feature_name(obj)
# Remove trailing ./ if exists.
if obj[0..1] == './'
obj[0..1] = ''
end
if obj[0] == '/'
$stderr.puts "warning: object file path `#{obj}' is absolute and not relative, this might cause a problem later at runtime"
end
# Strip the extension.
obj = obj.sub(/#{File.extname(obj)}$/, '')
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