bootsnap 1.1.0-java

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,39 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'bootsnap/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "bootsnap"
8
+ spec.version = Bootsnap::VERSION
9
+ spec.authors = ["Burke Libbey"]
10
+ spec.email = ["burke.libbey@shopify.com"]
11
+
12
+ spec.license = "MIT"
13
+
14
+ spec.summary = "wip"
15
+ spec.description = "wip."
16
+ spec.homepage = "https://github.com/Shopify/bootsnap"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
+ f.match(%r{^(test|spec|features)/})
20
+ end
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.required_ruby_version = '>= 2.0.0'
24
+
25
+ if RUBY_PLATFORM =~ /java/
26
+ spec.platform = 'java'
27
+ else
28
+ spec.platform = Gem::Platform::RUBY
29
+ spec.extensions = ['ext/bootsnap/extconf.rb']
30
+ end
31
+
32
+ spec.add_development_dependency "bundler", '~> 1'
33
+ spec.add_development_dependency 'rake', '~> 10.0'
34
+ spec.add_development_dependency 'rake-compiler', '~> 0'
35
+ spec.add_development_dependency "minitest", "~> 5.0"
36
+ spec.add_development_dependency "mocha", "~> 1.2"
37
+
38
+ spec.add_runtime_dependency "msgpack", "~> 1.0"
39
+ end
data/dev.yml ADDED
@@ -0,0 +1,8 @@
1
+ env:
2
+ BOOTSNAP_PEDANTIC: '1'
3
+
4
+ up:
5
+ - ruby: 2.3.3
6
+ - bundler
7
+ commands:
8
+ test: 'rake compile && bin/testunit'
@@ -0,0 +1,742 @@
1
+ /*
2
+ * Suggested reading order:
3
+ * 1. Skim Init_bootsnap
4
+ * 2. Skim bs_fetch
5
+ * 3. The rest of everything
6
+ *
7
+ * Init_bootsnap sets up the ruby objects and binds bs_fetch to
8
+ * Bootsnap::CompileCache::Native.fetch.
9
+ *
10
+ * bs_fetch is the ultimate caller for for just about every other function in
11
+ * here.
12
+ */
13
+
14
+ #include "bootsnap.h"
15
+ #include "ruby.h"
16
+ #include <stdint.h>
17
+ #include <sys/types.h>
18
+ #include <errno.h>
19
+ #include <fcntl.h>
20
+ #include <sys/stat.h>
21
+ #ifndef _WIN32
22
+ #include <sys/utsname.h>
23
+ #endif
24
+
25
+ /* 1000 is an arbitrary limit; FNV64 plus some slashes brings the cap down to
26
+ * 981 for the cache dir */
27
+ #define MAX_CACHEPATH_SIZE 1000
28
+ #define MAX_CACHEDIR_SIZE 981
29
+
30
+ #define KEY_SIZE 64
31
+
32
+ /*
33
+ * An instance of this key is written as the first 64 bytes of each cache file.
34
+ * The mtime and size members track whether the file contents have changed, and
35
+ * the version, os_version, compile_option, and ruby_revision members track
36
+ * changes to the environment that could invalidate compile results without
37
+ * file contents having changed. The data_size member is not truly part of the
38
+ * "key". Really, this could be called a "header" with the first six members
39
+ * being an embedded "key" struct and an additional data_size member.
40
+ *
41
+ * The data_size indicates the remaining number of bytes in the cache file
42
+ * after the header (the size of the cached artifact).
43
+ *
44
+ * After data_size, the struct is padded to 64 bytes.
45
+ */
46
+ struct bs_cache_key {
47
+ uint32_t version;
48
+ uint32_t os_version;
49
+ uint32_t compile_option;
50
+ uint32_t ruby_revision;
51
+ uint64_t size;
52
+ uint64_t mtime;
53
+ uint64_t data_size; /* not used for equality */
54
+ uint8_t pad[24];
55
+ } __attribute__((packed));
56
+
57
+ /*
58
+ * If the struct padding isn't correct to pad the key to 64 bytes, refuse to
59
+ * compile.
60
+ */
61
+ #define STATIC_ASSERT(X) STATIC_ASSERT2(X,__LINE__)
62
+ #define STATIC_ASSERT2(X,L) STATIC_ASSERT3(X,L)
63
+ #define STATIC_ASSERT3(X,L) STATIC_ASSERT_MSG(X,at_line_##L)
64
+ #define STATIC_ASSERT_MSG(COND,MSG) typedef char static_assertion_##MSG[(!!(COND))*2-1]
65
+ STATIC_ASSERT(sizeof(struct bs_cache_key) == KEY_SIZE);
66
+
67
+ /* Effectively a schema version. Bumping invalidates all previous caches */
68
+ static const uint32_t current_version = 2;
69
+
70
+ /* Derived from kernel or libc version; intended to roughly correspond to when
71
+ * ABIs have changed, requiring recompilation of native gems. */
72
+ static uint32_t current_os_version;
73
+ /* Invalidates cache when switching ruby versions */
74
+ static uint32_t current_ruby_revision;
75
+ /* Invalidates cache when RubyVM::InstructionSequence.compile_option changes */
76
+ static uint32_t current_compile_option_crc32 = 0;
77
+
78
+ /* Bootsnap::CompileCache::{Native, Uncompilable} */
79
+ static VALUE rb_mBootsnap;
80
+ static VALUE rb_mBootsnap_CompileCache;
81
+ static VALUE rb_mBootsnap_CompileCache_Native;
82
+ static VALUE rb_eBootsnap_CompileCache_Uncompilable;
83
+ static ID uncompilable;
84
+
85
+ /* Functions exposed as module functions on Bootsnap::CompileCache::Native */
86
+ static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32_v);
87
+ static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler);
88
+
89
+ /* Helpers */
90
+ static uint64_t fnv1a_64(const char *str);
91
+ static void bs_cache_path(const char * cachedir, const char * path, char ** cache_path);
92
+ static int bs_read_key(int fd, struct bs_cache_key * key);
93
+ static int cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2);
94
+ static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler);
95
+ static int open_current_file(char * path, struct bs_cache_key * key);
96
+ static int fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag);
97
+ static VALUE prot_exception_for_errno(VALUE err);
98
+ static uint32_t get_os_version(void);
99
+
100
+ /*
101
+ * Helper functions to call ruby methods on handler object without crashing on
102
+ * exception.
103
+ */
104
+ static int bs_storage_to_output(VALUE handler, VALUE storage_data, VALUE * output_data);
105
+ static VALUE prot_storage_to_output(VALUE arg);
106
+ static VALUE prot_input_to_output(VALUE arg);
107
+ static void bs_input_to_output(VALUE handler, VALUE input_data, VALUE * output_data, int * exception_tag);
108
+ static VALUE prot_input_to_storage(VALUE arg);
109
+ static int bs_input_to_storage(VALUE handler, VALUE input_data, VALUE pathval, VALUE * storage_data);
110
+ struct s2o_data;
111
+ struct i2o_data;
112
+ struct i2s_data;
113
+
114
+ /* https://bugs.ruby-lang.org/issues/13667 */
115
+ extern VALUE rb_get_coverages(void);
116
+ static VALUE
117
+ bs_rb_coverage_running(VALUE self)
118
+ {
119
+ VALUE cov = rb_get_coverages();
120
+ return RTEST(cov) ? Qtrue : Qfalse;
121
+ }
122
+
123
+ /*
124
+ * Ruby C extensions are initialized by calling Init_<extname>.
125
+ *
126
+ * This sets up the module hierarchy and attaches functions as methods.
127
+ *
128
+ * We also populate some semi-static information about the current OS and so on.
129
+ */
130
+ void
131
+ Init_bootsnap(void)
132
+ {
133
+ rb_mBootsnap = rb_define_module("Bootsnap");
134
+ rb_mBootsnap_CompileCache = rb_define_module_under(rb_mBootsnap, "CompileCache");
135
+ rb_mBootsnap_CompileCache_Native = rb_define_module_under(rb_mBootsnap_CompileCache, "Native");
136
+ rb_eBootsnap_CompileCache_Uncompilable = rb_define_class_under(rb_mBootsnap_CompileCache, "Uncompilable", rb_eStandardError);
137
+
138
+ current_ruby_revision = FIX2INT(rb_const_get(rb_cObject, rb_intern("RUBY_REVISION")));
139
+ current_os_version = get_os_version();
140
+
141
+ uncompilable = rb_intern("__bootsnap_uncompilable__");
142
+
143
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "coverage_running?", bs_rb_coverage_running, 0);
144
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 3);
145
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "compile_option_crc32=", bs_compile_option_crc32_set, 1);
146
+ }
147
+
148
+ /*
149
+ * Bootsnap's ruby code registers a hook that notifies us via this function
150
+ * when compile_option changes. These changes invalidate all existing caches.
151
+ */
152
+ static VALUE
153
+ bs_compile_option_crc32_set(VALUE self, VALUE crc32_v)
154
+ {
155
+ Check_Type(crc32_v, T_FIXNUM);
156
+ current_compile_option_crc32 = FIX2UINT(crc32_v);
157
+ return Qnil;
158
+ }
159
+
160
+ /*
161
+ * We use FNV1a-64 to derive cache paths. The choice is somewhat arbitrary but
162
+ * it has several nice properties:
163
+ *
164
+ * - Tiny implementation
165
+ * - No external dependency
166
+ * - Solid performance
167
+ * - Solid randomness
168
+ * - 32 bits doesn't feel collision-resistant enough; 64 is nice.
169
+ */
170
+ static uint64_t
171
+ fnv1a_64(const char *str)
172
+ {
173
+ unsigned char *s = (unsigned char *)str;
174
+ uint64_t h = (uint64_t)0xcbf29ce484222325ULL;
175
+
176
+ while (*s) {
177
+ h ^= (uint64_t)*s++;
178
+ h += (h << 1) + (h << 4) + (h << 5) + (h << 7) + (h << 8) + (h << 40);
179
+ }
180
+
181
+ return h;
182
+ }
183
+
184
+ /*
185
+ * The idea here is that we want a cache key member that changes when the OS
186
+ * changes in such a way as to make existing compiled ISeqs unloadable.
187
+ */
188
+ static uint32_t
189
+ get_os_version(void)
190
+ {
191
+ #ifdef _WIN32
192
+ return (uint32_t)GetVersion();
193
+ #else
194
+ uint64_t hash;
195
+ struct utsname utsname;
196
+
197
+ /* Not worth crashing if this fails; lose cache invalidation potential */
198
+ if (uname(&utsname) < 0) return 0;
199
+
200
+ hash = fnv1a_64(utsname.version);
201
+
202
+ return (uint32_t)(hash >> 32);
203
+ #endif
204
+ }
205
+
206
+ /*
207
+ * Given a cache root directory and the full path to a file being cached,
208
+ * generate a path under the cache directory at which the cached artifact will
209
+ * be stored.
210
+ *
211
+ * The path will look something like: <cachedir>/12/34567890abcdef
212
+ */
213
+ static void
214
+ bs_cache_path(const char * cachedir, const char * path, char ** cache_path)
215
+ {
216
+ uint64_t hash = fnv1a_64(path);
217
+
218
+ uint8_t first_byte = (hash >> (64 - 8));
219
+ uint64_t remainder = hash & 0x00ffffffffffffff;
220
+
221
+ sprintf(*cache_path, "%s/%02x/%014llx", cachedir, first_byte, remainder);
222
+ }
223
+
224
+ /*
225
+ * Test whether a newly-generated cache key based on the file as it exists on
226
+ * disk matches the one that was generated when the file was cached (or really
227
+ * compare any two keys).
228
+ *
229
+ * The data_size member is not compared, as it serves more of a "header"
230
+ * function.
231
+ */
232
+ static int
233
+ cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2)
234
+ {
235
+ return (
236
+ k1->version == k2->version &&
237
+ k1->os_version == k2->os_version &&
238
+ k1->compile_option == k2->compile_option &&
239
+ k1->ruby_revision == k2->ruby_revision &&
240
+ k1->size == k2->size &&
241
+ k1->mtime == k2->mtime
242
+ );
243
+ }
244
+
245
+ /*
246
+ * Entrypoint for Bootsnap::CompileCache::Native.fetch. The real work is done
247
+ * in bs_fetch; this function just performs some basic typechecks and
248
+ * conversions on the ruby VALUE arguments before passing them along.
249
+ */
250
+ static VALUE
251
+ bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler)
252
+ {
253
+ Check_Type(cachedir_v, T_STRING);
254
+ Check_Type(path_v, T_STRING);
255
+
256
+ if (RSTRING_LEN(cachedir_v) > MAX_CACHEDIR_SIZE) {
257
+ rb_raise(rb_eArgError, "cachedir too long");
258
+ }
259
+
260
+ char * cachedir = RSTRING_PTR(cachedir_v);
261
+ char * path = RSTRING_PTR(path_v);
262
+ char cache_path[MAX_CACHEPATH_SIZE];
263
+
264
+ { /* generate cache path to cache_path */
265
+ char * tmp = (char *)&cache_path;
266
+ bs_cache_path(cachedir, path, &tmp);
267
+ }
268
+
269
+ return bs_fetch(path, path_v, cache_path, handler);
270
+ }
271
+
272
+ /*
273
+ * Open the file we want to load/cache and generate a cache key for it if it
274
+ * was loaded.
275
+ */
276
+ static int
277
+ open_current_file(char * path, struct bs_cache_key * key)
278
+ {
279
+ struct stat statbuf;
280
+ int fd;
281
+
282
+ fd = open(path, O_RDONLY);
283
+ if (fd < 0) return fd;
284
+ #ifdef _WIN32
285
+ setmode(fd, O_BINARY);
286
+ #endif
287
+
288
+ if (fstat(fd, &statbuf) < 0) {
289
+ close(fd);
290
+ return -1;
291
+ }
292
+
293
+ key->version = current_version;
294
+ key->os_version = current_os_version;
295
+ key->compile_option = current_compile_option_crc32;
296
+ key->ruby_revision = current_ruby_revision;
297
+ key->size = (uint64_t)statbuf.st_size;
298
+ key->mtime = (uint64_t)statbuf.st_mtime;
299
+
300
+ return fd;
301
+ }
302
+
303
+ #define ERROR_WITH_ERRNO -1
304
+ #define CACHE_MISSING_OR_INVALID -2
305
+
306
+ /*
307
+ * Read the cache key from the given fd, which must have position 0 (e.g.
308
+ * freshly opened file).
309
+ *
310
+ * Possible return values:
311
+ * - 0 (OK, key was loaded)
312
+ * - CACHE_MISSING_OR_INVALID (-2)
313
+ * - ERROR_WITH_ERRNO (-1, errno is set)
314
+ */
315
+ static int
316
+ bs_read_key(int fd, struct bs_cache_key * key)
317
+ {
318
+ ssize_t nread = read(fd, key, KEY_SIZE);
319
+ if (nread < 0) return ERROR_WITH_ERRNO;
320
+ if (nread < KEY_SIZE) return CACHE_MISSING_OR_INVALID;
321
+ return 0;
322
+ }
323
+
324
+ /*
325
+ * Open the cache file at a given path, if it exists, and read its key into the
326
+ * struct.
327
+ *
328
+ * Possible return values:
329
+ * - 0 (OK, key was loaded)
330
+ * - CACHE_MISSING_OR_INVALID (-2)
331
+ * - ERROR_WITH_ERRNO (-1, errno is set)
332
+ */
333
+ static int
334
+ open_cache_file(const char * path, struct bs_cache_key * key)
335
+ {
336
+ int fd, res;
337
+
338
+ fd = open(path, O_RDONLY);
339
+ if (fd < 0) {
340
+ if (errno == ENOENT) return CACHE_MISSING_OR_INVALID;
341
+ return ERROR_WITH_ERRNO;
342
+ }
343
+ #ifdef _WIN32
344
+ setmode(fd, O_BINARY);
345
+ #endif
346
+
347
+ res = bs_read_key(fd, key);
348
+ if (res < 0) {
349
+ close(fd);
350
+ return res;
351
+ }
352
+
353
+ return fd;
354
+ }
355
+
356
+ /*
357
+ * The cache file is laid out like:
358
+ * 0...64 : bs_cache_key
359
+ * 64..-1 : cached artifact
360
+ *
361
+ * This function takes a file descriptor whose position is pre-set to 64, and
362
+ * the data_size (corresponding to the remaining number of bytes) listed in the
363
+ * cache header.
364
+ *
365
+ * We load the text from this file into a buffer, and pass it to the ruby-land
366
+ * handler with exception handling via the exception_tag param.
367
+ *
368
+ * Data is returned via the output_data parameter, which, if there's no error
369
+ * or exception, will be the final data returnable to the user.
370
+ */
371
+ static int
372
+ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag)
373
+ {
374
+ char * data = NULL;
375
+ ssize_t nread;
376
+ int ret;
377
+
378
+ VALUE storage_data;
379
+
380
+ if (data_size > 100000000000) {
381
+ errno = EINVAL; /* because wtf? */
382
+ ret = -1;
383
+ goto done;
384
+ }
385
+ data = ALLOC_N(char, data_size);
386
+ nread = read(fd, data, data_size);
387
+ if (nread < 0) {
388
+ ret = -1;
389
+ goto done;
390
+ }
391
+ if (nread != data_size) {
392
+ ret = CACHE_MISSING_OR_INVALID;
393
+ goto done;
394
+ }
395
+
396
+ storage_data = rb_str_new_static(data, data_size);
397
+
398
+ *exception_tag = bs_storage_to_output(handler, storage_data, output_data);
399
+ ret = 0;
400
+ done:
401
+ if (data != NULL) xfree(data);
402
+ return ret;
403
+ }
404
+
405
+ /*
406
+ * Like mkdir -p, this recursively creates directory parents of a file. e.g.
407
+ * given /a/b/c, creates /a and /a/b.
408
+ */
409
+ static int
410
+ mkpath(char * file_path, mode_t mode)
411
+ {
412
+ /* It would likely be more efficient to count back until we
413
+ * find a component that *does* exist, but this will only run
414
+ * at most 256 times, so it seems not worthwhile to change. */
415
+ char * p;
416
+ for (p = strchr(file_path + 1, '/'); p; p = strchr(p + 1, '/')) {
417
+ *p = '\0';
418
+ #ifdef _WIN32
419
+ if (mkdir(file_path) == -1) {
420
+ #else
421
+ if (mkdir(file_path, mode) == -1) {
422
+ #endif
423
+ if (errno != EEXIST) {
424
+ *p = '/';
425
+ return -1;
426
+ }
427
+ }
428
+ *p = '/';
429
+ }
430
+ return 0;
431
+ }
432
+
433
+ /*
434
+ * Write a cache header/key and a compiled artifact to a given cache path by
435
+ * writing to a tmpfile and then renaming the tmpfile over top of the final
436
+ * path.
437
+ */
438
+ static int
439
+ atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data)
440
+ {
441
+ char template[MAX_CACHEPATH_SIZE + 20];
442
+ char * dest;
443
+ char * tmp_path;
444
+ int fd;
445
+ ssize_t nwrite;
446
+
447
+ dest = strncpy(template, path, MAX_CACHEPATH_SIZE);
448
+ strcat(dest, ".tmp.XXXXXX");
449
+
450
+ tmp_path = mktemp(template);
451
+ fd = open(tmp_path, O_WRONLY | O_CREAT, 0644);
452
+ if (fd < 0) {
453
+ if (mkpath(path, 0755) < 0) return -1;
454
+ fd = open(tmp_path, O_WRONLY | O_CREAT, 0644);
455
+ if (fd < 0) return -1;
456
+ }
457
+ #ifdef _WIN32
458
+ setmode(fd, O_BINARY);
459
+ #endif
460
+
461
+ key->data_size = RSTRING_LEN(data);
462
+ nwrite = write(fd, key, KEY_SIZE);
463
+ if (nwrite < 0) return -1;
464
+ if (nwrite != KEY_SIZE) {
465
+ errno = EIO; /* Lies but whatever */
466
+ return -1;
467
+ }
468
+
469
+ nwrite = write(fd, RSTRING_PTR(data), RSTRING_LEN(data));
470
+ if (nwrite < 0) return -1;
471
+ if (nwrite != RSTRING_LEN(data)) {
472
+ errno = EIO; /* Lies but whatever */
473
+ return -1;
474
+ }
475
+
476
+ close(fd);
477
+ return rename(tmp_path, path);
478
+ }
479
+
480
+ /*
481
+ * Given an errno value (converted to a ruby Fixnum), return the corresponding
482
+ * Errno::* constant. If none is found, return StandardError instead.
483
+ */
484
+ static VALUE
485
+ prot_exception_for_errno(VALUE err)
486
+ {
487
+ if (err != INT2FIX(0)) {
488
+ VALUE mErrno = rb_const_get(rb_cObject, rb_intern("Errno"));
489
+ VALUE constants = rb_funcall(mErrno, rb_intern("constants"), 0);
490
+ VALUE which = rb_funcall(constants, rb_intern("[]"), 1, err);
491
+ return rb_funcall(mErrno, rb_intern("const_get"), 1, which);
492
+ }
493
+ return rb_eStandardError;
494
+ }
495
+
496
+
497
+ /* Read contents from an fd, whose contents are asserted to be +size+ bytes
498
+ * long, into a buffer */
499
+ static ssize_t
500
+ bs_read_contents(int fd, size_t size, char ** contents)
501
+ {
502
+ *contents = ALLOC_N(char, size);
503
+ return read(fd, *contents, size);
504
+ }
505
+
506
+ /*
507
+ * This is the meat of the extension. bs_fetch is
508
+ * Bootsnap::CompileCache::Native.fetch.
509
+ *
510
+ * There are three "formats" in use here:
511
+ * 1. "input" fomat, which is what we load from the source file;
512
+ * 2. "storage" format, which we write to the cache;
513
+ * 3. "output" format, which is what we return.
514
+ *
515
+ * E.g., For ISeq compilation:
516
+ * input: ruby source, as text
517
+ * storage: binary string (RubyVM::InstructionSequence#to_binary)
518
+ * output: Instance of RubyVM::InstructionSequence
519
+ *
520
+ * And for YAML:
521
+ * input: yaml as text
522
+ * storage: MessagePack or Marshal text
523
+ * output: ruby object, loaded from yaml/messagepack/marshal
524
+ *
525
+ * The handler passed in must support three messages:
526
+ * * storage_to_output(s) -> o
527
+ * * input_to_output(i) -> o
528
+ * * input_to_storage(i) -> s
529
+ * (input_to_storage may raise Bootsnap::CompileCache::Uncompilable, which
530
+ * will prevent caching and cause output to be generated with
531
+ * input_to_output)
532
+ *
533
+ * The semantics of this function are basically:
534
+ *
535
+ * return storage_to_output(cache[path]) if cache[path]
536
+ * storage = input_to_storage(input)
537
+ * cache[path] = storage
538
+ * return storage_to_output(storage)
539
+ *
540
+ * Or expanded a bit:
541
+ *
542
+ * - Check if the cache file exists and is up to date.
543
+ * - If it is, load this data to storage_data.
544
+ * - return storage_to_output(storage_data)
545
+ * - Read the file to input_data
546
+ * - Generate storage_data using input_to_storage(input_data)
547
+ * - Write storage_data data, with a cache key, to the cache file.
548
+ * - Return storage_to_output(storage_data)
549
+ */
550
+ static VALUE
551
+ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
552
+ {
553
+ struct bs_cache_key cached_key, current_key;
554
+ char * contents = NULL;
555
+ int cache_fd = -1, current_fd = -1;
556
+ int res, valid_cache, exception_tag = 0;
557
+
558
+ VALUE input_data; /* data read from source file, e.g. YAML or ruby source */
559
+ VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */
560
+ VALUE output_data; /* return data, e.g. ruby hash or loaded iseq */
561
+
562
+ VALUE exception; /* ruby exception object to raise instead of returning */
563
+
564
+ /* Open the source file and generate a cache key for it */
565
+ current_fd = open_current_file(path, &current_key);
566
+ if (current_fd < 0) goto fail_errno;
567
+
568
+ /* Open the cache key if it exists, and read its cache key in */
569
+ cache_fd = open_cache_file(cache_path, &cached_key);
570
+ if (cache_fd < 0 && cache_fd != CACHE_MISSING_OR_INVALID) goto fail_errno;
571
+
572
+ /* True if the cache existed and no invalidating changes have occurred since
573
+ * it was generated. */
574
+ valid_cache = cache_key_equal(&current_key, &cached_key);
575
+
576
+ if (valid_cache) {
577
+ /* Fetch the cache data and return it if we're able to load it successfully */
578
+ res = fetch_cached_data(cache_fd, (ssize_t)cached_key.data_size, handler, &output_data, &exception_tag);
579
+ if (exception_tag != 0) goto raise;
580
+ else if (res == CACHE_MISSING_OR_INVALID) valid_cache = 0;
581
+ else if (res == ERROR_WITH_ERRNO) goto fail_errno;
582
+ else if (!NIL_P(output_data)) goto succeed; /* fast-path, goal */
583
+ }
584
+ close(cache_fd);
585
+ cache_fd = -1;
586
+ /* Cache is stale, invalid, or missing. Regenerate and write it out. */
587
+
588
+ /* Read the contents of the source file into a buffer */
589
+ if (bs_read_contents(current_fd, current_key.size, &contents) < 0) goto fail_errno;
590
+ input_data = rb_str_new_static(contents, current_key.size);
591
+
592
+ /* Try to compile the input_data using input_to_storage(input_data) */
593
+ exception_tag = bs_input_to_storage(handler, input_data, path_v, &storage_data);
594
+ if (exception_tag != 0) goto raise;
595
+ /* If input_to_storage raised Bootsnap::CompileCache::Uncompilable, don't try
596
+ * to cache anything; just return input_to_output(input_data) */
597
+ if (storage_data == uncompilable) {
598
+ bs_input_to_output(handler, input_data, &output_data, &exception_tag);
599
+ if (exception_tag != 0) goto raise;
600
+ goto succeed;
601
+ }
602
+ /* If storage_data isn't a string, we can't cache it */
603
+ if (!RB_TYPE_P(storage_data, T_STRING)) goto invalid_type_storage_data;
604
+
605
+ /* Write the cache key and storage_data to the cache directory */
606
+ res = atomic_write_cache_file(cache_path, &current_key, storage_data);
607
+ if (res < 0) goto fail_errno;
608
+
609
+ /* Having written the cache, now convert storage_data to output_data */
610
+ exception_tag = bs_storage_to_output(handler, storage_data, &output_data);
611
+ if (exception_tag != 0) goto raise;
612
+
613
+ /* If output_data is nil, delete the cache entry and generate the output
614
+ * using input_to_output */
615
+ if (NIL_P(output_data)) {
616
+ if (unlink(cache_path) < 0) goto fail_errno;
617
+ bs_input_to_output(handler, input_data, &output_data, &exception_tag);
618
+ if (exception_tag != 0) goto raise;
619
+ }
620
+
621
+ goto succeed; /* output_data is now the correct return. */
622
+
623
+ #define CLEANUP \
624
+ if (contents != NULL) xfree(contents); \
625
+ if (current_fd >= 0) close(current_fd); \
626
+ if (cache_fd >= 0) close(cache_fd);
627
+
628
+ succeed:
629
+ CLEANUP;
630
+ return output_data;
631
+ fail_errno:
632
+ CLEANUP;
633
+ exception = rb_protect(prot_exception_for_errno, INT2FIX(errno), &res);
634
+ if (res) exception = rb_eStandardError;
635
+ rb_exc_raise(exception);
636
+ __builtin_unreachable();
637
+ raise:
638
+ CLEANUP;
639
+ rb_jump_tag(exception_tag);
640
+ __builtin_unreachable();
641
+ invalid_type_storage_data:
642
+ CLEANUP;
643
+ Check_Type(storage_data, T_STRING);
644
+ __builtin_unreachable();
645
+
646
+ #undef CLEANUP
647
+ }
648
+
649
+ /*****************************************************************************/
650
+ /********************* Handler Wrappers **************************************/
651
+ /*****************************************************************************
652
+ * Everything after this point in the file is just wrappers to deal with ruby's
653
+ * clunky method of handling exceptions from ruby methods invoked from C.
654
+ */
655
+
656
+ struct s2o_data {
657
+ VALUE handler;
658
+ VALUE storage_data;
659
+ };
660
+
661
+ struct i2o_data {
662
+ VALUE handler;
663
+ VALUE input_data;
664
+ };
665
+
666
+ struct i2s_data {
667
+ VALUE handler;
668
+ VALUE input_data;
669
+ VALUE pathval;
670
+ };
671
+
672
+ static VALUE
673
+ prot_storage_to_output(VALUE arg)
674
+ {
675
+ struct s2o_data * data = (struct s2o_data *)arg;
676
+ return rb_funcall(data->handler, rb_intern("storage_to_output"), 1, data->storage_data);
677
+ }
678
+
679
+ static int
680
+ bs_storage_to_output(VALUE handler, VALUE storage_data, VALUE * output_data)
681
+ {
682
+ int state;
683
+ struct s2o_data s2o_data = {
684
+ .handler = handler,
685
+ .storage_data = storage_data,
686
+ };
687
+ *output_data = rb_protect(prot_storage_to_output, (VALUE)&s2o_data, &state);
688
+ return state;
689
+ }
690
+
691
+ static void
692
+ bs_input_to_output(VALUE handler, VALUE input_data, VALUE * output_data, int * exception_tag)
693
+ {
694
+ struct i2o_data i2o_data = {
695
+ .handler = handler,
696
+ .input_data = input_data,
697
+ };
698
+ *output_data = rb_protect(prot_input_to_output, (VALUE)&i2o_data, exception_tag);
699
+ }
700
+
701
+ static VALUE
702
+ prot_input_to_output(VALUE arg)
703
+ {
704
+ struct i2o_data * data = (struct i2o_data *)arg;
705
+ return rb_funcall(data->handler, rb_intern("input_to_output"), 1, data->input_data);
706
+ }
707
+
708
+ static VALUE
709
+ try_input_to_storage(VALUE arg)
710
+ {
711
+ struct i2s_data * data = (struct i2s_data *)arg;
712
+ return rb_funcall(data->handler, rb_intern("input_to_storage"), 2, data->input_data, data->pathval);
713
+ }
714
+
715
+ static VALUE
716
+ rescue_input_to_storage(VALUE arg)
717
+ {
718
+ return uncompilable;
719
+ }
720
+
721
+ static VALUE
722
+ prot_input_to_storage(VALUE arg)
723
+ {
724
+ struct i2s_data * data = (struct i2s_data *)arg;
725
+ return rb_rescue2(
726
+ try_input_to_storage, (VALUE)data,
727
+ rescue_input_to_storage, Qnil,
728
+ rb_eBootsnap_CompileCache_Uncompilable, 0);
729
+ }
730
+
731
+ static int
732
+ bs_input_to_storage(VALUE handler, VALUE input_data, VALUE pathval, VALUE * storage_data)
733
+ {
734
+ int state;
735
+ struct i2s_data i2s_data = {
736
+ .handler = handler,
737
+ .input_data = input_data,
738
+ .pathval = pathval,
739
+ };
740
+ *storage_data = rb_protect(prot_input_to_storage, (VALUE)&i2s_data, &state);
741
+ return state;
742
+ }