bootsnap 1.1.0-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ }