crystalruby 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 117525a6b5905a6c4aa86b0b2fed0c25e46d0e97b6de8ce187b90f1438394f19
4
- data.tar.gz: 55b35d8731c2d7e68f6cb0a1a4ea3eb068805bc4297f1b85fceac08cb288682a
3
+ metadata.gz: eeae38802618342fade685584872134cd905229adfe493619130bf008c649390
4
+ data.tar.gz: ff35b13a1ff26f2aa42063a91ebfea888efd03ffd4fabb32911baf70557a3cdb
5
5
  SHA512:
6
- metadata.gz: 585ad3697b06ba3660989ea538ddae90816d0e93a947e95ab5e6bb83ace64d35988c436cf8e07cad077529691baebbdbdeac6bcc0e7a991297d67f2efada5438
7
- data.tar.gz: bd06431cc0c059481db8215676e493f16d485cb14a8b15dedc3de6e79df79716c4e6499bb4f9678257659601224b249798dc8c4c492c1659c46157714e45499e
6
+ metadata.gz: 43b28e5a1739a80ebe77064612fbf11024d9b6f7a9e3fd4357a0d3c6d99f90322953b34b484d7b8c010e40336fa1ba2d64bb4c59ec59bd0cef2ebaaea1fefd65
7
+ data.tar.gz: 70a9068038a5bc107ab6beb398491dd04cec70c2bf402dda35c80e70282033c2a1e7da482a15a0f378676835daee3eb220af66d02ab98a4d2be7991fb18d818d
data/README.md CHANGED
@@ -258,7 +258,7 @@ E.g.
258
258
 
259
259
  IntArrOrBoolArr = crtype{ Array(Bool) | Array(Int32) }
260
260
 
261
- crystalize [a: IntArrOrBoolArr] => json{ IntArrOrBoolArr }
261
+ crystalize [a: json{ IntArrOrBoolArr }] => json{ IntArrOrBoolArr }
262
262
  def method_with_named_types(a)
263
263
  return a
264
264
  end
@@ -273,16 +273,19 @@ Exceptions thrown in Crystal code can be caught in Ruby.
273
273
  You can use any Crystal shards and write ordinary, stand-alone Crystal code.
274
274
 
275
275
  The default entry point for the crystal shared library generated by the gem is
276
- inside `./crystalruby/src/main.cr`. This file is not automatically overridden by the gem, and is safe for you to define and require new files relative to this location to write additional stand-alone Crystal code.
276
+ inside `./crystalruby/{library_name}/src/main.cr`.
277
+ `{library_name}` defaults to `crystalruby` if you haven't explicitly specific a different library target.
277
278
 
278
- You can define shards inside `./crystalruby/src/shard.yml`
279
+ This file is not automatically overridden by the gem, and is safe for you to define and require new files relative to this location to write additional stand-alone Crystal code.
280
+
281
+ You can define shard dependencies inside `./crystalruby/{library_name}/src/shard.yml`
279
282
  Run the below to install new shards
280
283
 
281
284
  ```bash
282
285
  bundle exec crystalruby install
283
286
  ```
284
287
 
285
- Remember to require these installed shards after installing them. E.g. inside `./crystalruby/src/main.cr`
288
+ Remember to also require these dependencies after installing them to make them available to `crystalruby` code. E.g. inside `./crystalruby/{libraryname}/src/main.cr`
286
289
 
287
290
  You can edit the default paths for crystal source and library files from within the `./crystalruby.yaml` config file.
288
291
 
@@ -309,7 +312,7 @@ MyModule.add("1", "2")
309
312
 
310
313
  ## Inline Chunks
311
314
 
312
- `crystalruby` also allows you to write inline Crystal code that does not require binding to Ruby. This can be useful for e.g. performing setup or teardown operations.
315
+ `crystalruby` also allows you to write inline Crystal code that does not require binding to Ruby. This can be useful for e.g. performing setup operations or initializations.
313
316
 
314
317
  Follow these steps for a toy example of how we can use crystalized ruby and inline chunks to expose the [crystal-redis](https://github.com/stefanwille/crystal-redis) library to Ruby.
315
318
 
@@ -467,13 +470,13 @@ Then you can run this file as part of your build step, to ensure all Crystal cod
467
470
 
468
471
  ## Concurrency
469
472
 
470
- While Ruby programs allow multi-threading, Crystal by default uses only a single thread, while utilising Fiber based cooperative-multitasking to allow for concurrent execution. This means that by default, Crystal libraries can not safely be invoked in parallel across multiple Ruby threads.
473
+ While Ruby programs allow multi-threading, Crystal (if not using experimental multi-thread support) uses only a single thread and utilises Fiber based cooperative-multitasking to allow for concurrent execution. This means that by default, Crystal libraries can not safely be invoked in parallel across multiple Ruby threads.
471
474
 
472
- To safely expose this behaviour, `crystalruby` implements a Reactor, which multiplexes all Ruby calls to Crystal across a single thread. This way you can safely use `crystalruby` in a multi-threaded Ruby environment.
475
+ To safely utilise `crystalruby` in a multithreaded environment, `crystalruby` implements a Reactor, which multiplexes all Ruby calls to Crystal across a single thread.
473
476
 
474
- By default `crystalruby` methods are blocking/synchronous, this means that for blocking operations, a single crystalruby call can block the entire reactor.
477
+ By default `crystalruby` methods are blocking/synchronous, this means that for blocking operations, a single crystalruby call can block the entire reactor across _all_ threads.
475
478
 
476
- To allow you to benefit from Crystal's fiber based concurrency, you can use the `async` option on crystalized ruby methods. This allows several Ruby threads to invoke Crystal code simultaneously.
479
+ To allow you to benefit from Crystal's fiber based concurrency, you can use the `async: true` option on crystalized ruby methods. This allows several Ruby threads to invoke Crystal code simultaneously.
477
480
 
478
481
  E.g.
479
482
 
@@ -498,7 +501,7 @@ end
498
501
 
499
502
  ### Reactor performance
500
503
 
501
- There is a small amount of synchronization overhead to multiplexing calls across a single thread. Ad-hoc testing amounts this to be around 10 nanoseconds per call.
504
+ There is a small amount of synchronization overhead to multiplexing calls across a single thread. Ad-hoc testing on a fast machine amounts this to be within the order of 10 nanoseconds per call.
502
505
  For most use-cases this overhead is negligible, especially if the bulk of your CPU heavy task occurs exclusively in Crystal code. However, if you are invoking very fast Crystal code from Ruby in a tight loop (e.g. a simple 1 + 2)
503
506
  then the overhead of the reactor can become significant.
504
507
 
@@ -517,9 +520,9 @@ recompile Crystal code only when it detects changes to the embedded function or
517
520
 
518
521
  ## Multi-library support
519
522
 
520
- Large Crystal projects are known to have long compile times. To mitigate this, `crystalruby` supports splitting your Crystal code into multiple libraries. This allows you to only recompile the library that has changed, rather than the entire project.
521
- To indicate which library a piece of embedded Crystal code belongs to, you can use the `library` option in the `crystalize` and `crystal` methods.
522
- If the "lib" option is not provided, the code will be compiled into the default library (simply named `crystalruby`).
523
+ Large Crystal projects are known to have long compile times. To mitigate this, `crystalruby` supports splitting your Crystal code into multiple libraries. This allows you to only recompile any libraries that have changed, rather than all crystalcode within the entire project.
524
+ To indicate which library a piece of embedded Crystal code belongs to, you can use the `lib` option in the `crystalize` and `crystal` methods.
525
+ If the `lib` option is not provided, the code will be compiled into the default library (simply named `crystalruby`).
523
526
 
524
527
  ```ruby
525
528
  module Foo
@@ -534,12 +537,12 @@ module Foo
534
537
  end
535
538
  ```
536
539
 
537
- Naturally Crystal code must be in the same library to interact directly.
538
- Interaction across multiple libraries can be coordinated via Ruby code.
540
+ Naturally, Crystal methods must reside in the same library to natively interact.
541
+ Cross library interaction can be facilitated via Ruby code.
539
542
 
540
543
  ## Troubleshooting
541
544
 
542
- The logic to detect when to JIT recompile is not robust and can end up in an inconsistent state. To remedy this it is useful to clear out all generated assets and build from scratch.
545
+ In cases where compiled assets are in left an invalid state, it can be useful to clear out generated assets and rebuild from scratch.
543
546
 
544
547
  To do this execute:
545
548
 
data/exe/crystalruby CHANGED
@@ -36,6 +36,9 @@ def clean
36
36
  Dir["#{CrystalRuby.config.crystal_src_dir}/**/src/generated"].each do |codegen_dir|
37
37
  FileUtils.rm_rf(codegen_dir)
38
38
  end
39
+ Dir["#{CrystalRuby.config.crystal_src_dir}/**/lib"].each do |lib_dir|
40
+ FileUtils.rm_rf(lib_dir)
41
+ end
39
42
  end
40
43
 
41
44
  def build
@@ -1,4 +1,3 @@
1
- require "open3"
2
1
  require "tmpdir"
3
2
  require "shellwords"
4
3
 
@@ -6,7 +6,7 @@ module CrystalRuby
6
6
  include Typemaps
7
7
  include Config
8
8
 
9
- attr_accessor :owner, :method_name, :args, :returns, :function_body, :lib, :async, :block
9
+ attr_accessor :owner, :method_name, :args, :returns, :function_body, :lib, :async, :block, :attached
10
10
 
11
11
  def initialize(method:, args:, returns:, function_body:, lib:, async: false, &block)
12
12
  self.owner = method.owner
@@ -17,12 +17,13 @@ module CrystalRuby
17
17
  self.lib = lib
18
18
  self.async = async
19
19
  self.block = block
20
+ self.attached = false
20
21
  end
21
22
 
22
23
  # This is where we write/overwrite the class and instance methods
23
24
  # with their crystalized equivalents.
24
25
  # We also perform JIT compilation and JIT attachment of the FFI functions.
25
- # Crystalized methods work in a live-reloading manner environment.
26
+ # Crystalized methods can be redefined without restarting, if running in a live-reloading environment.
26
27
  # If they are redefined with a different function body, the new function body
27
28
  # will result in a new digest and the FFI function will be recompiled and reattached.
28
29
  def define_crystalized_methods!(lib)
@@ -34,12 +35,12 @@ module CrystalRuby
34
35
  lib.build!
35
36
  return send(func.method_name, *args)
36
37
  end
37
- unless lib.attached?(func.owner)
38
+ unless func.attached?
38
39
  should_reenter = func.attach_ffi_lib_functions!
39
40
  return send(func.method_name, *args) if should_reenter
40
41
  end
41
- # All crystalruby functions are executed on the reactor to ensure
42
- # all Crystal interop is executed from the same thread. (Needed to make GC and Fiber scheduler happy)
42
+ # All crystalruby functions are executed on the reactor to ensure Crystal/Ruby interop code is executed
43
+ # from a single same thread. (Needed to make GC and Fiber scheduler happy)
43
44
  # Type mapping (if required) is applied on arguments and on return values.
44
45
  func.map_retval(
45
46
  Reactor.schedule_work!(
@@ -58,14 +59,11 @@ module CrystalRuby
58
59
  # This is where we attach the top-level FFI functions of the shared object
59
60
  # to our library (yield and init) needed for successful operation of the reactor.
60
61
  # We also initialize the shared object (needed to start the GC) and
61
- # start the reactor unless we are in single-thread mode.
62
+ # start the reactor, unless we are in single-thread mode.
62
63
  def attach_ffi_lib_functions!
63
64
  should_reenter = unwrapped?
64
65
  lib_file = lib.lib_file
65
- lib.attachments[owner] = true
66
- lib.methods.each_value do |method|
67
- method.attach_ffi_func!
68
- end
66
+ lib.methods.each_value(&:attach_ffi_func!)
69
67
  lib.singleton_class.class_eval do
70
68
  extend FFI::Library
71
69
  ffi_lib lib_file
@@ -86,8 +84,8 @@ module CrystalRuby
86
84
  should_reenter
87
85
  end
88
86
 
89
- # This is where we attach the crystalized FFI functions to their related
90
- # Ruby modules and classes. If a wrapper block has been passed to the crystalize function,
87
+ # Attaches the crystalized FFI functions to their related Ruby modules and classes.
88
+ # If a wrapper block has been passed to the crystalize function,
91
89
  # then the we also wrap the crystalized function using a prepended Module.
92
90
  def attach_ffi_func!
93
91
  argtypes = ffi_types
@@ -114,7 +112,7 @@ module CrystalRuby
114
112
  owner.attach_function ffi_name, argtypes, rettype, blocking: true
115
113
  around_wrapper_block = block
116
114
  method_name = self.method_name
117
-
115
+ @attached = true
118
116
  return unless around_wrapper_block
119
117
 
120
118
  @around_wrapper ||= begin
@@ -136,6 +134,14 @@ module CrystalRuby
136
134
  block && !@around_wrapper
137
135
  end
138
136
 
137
+ def attached?
138
+ @attached
139
+ end
140
+
141
+ def unattach!
142
+ @attached = false
143
+ end
144
+
139
145
  def ffi_name
140
146
  lib_fn_name + (async && !config.single_thread_mode ? "_async" : "")
141
147
  end
@@ -8,7 +8,7 @@ module CrystalRuby
8
8
  CR_COMPILE_MUX = Mutex.new
9
9
  CR_ATTACH_MUX = Mutex.new
10
10
 
11
- attr_accessor :name, :methods, :chunks, :root_dir, :lib_dir, :src_dir, :codegen_dir, :attachments, :reactor
11
+ attr_accessor :name, :methods, :chunks, :root_dir, :lib_dir, :src_dir, :codegen_dir, :reactor
12
12
 
13
13
  @libs_by_name = {}
14
14
 
@@ -30,13 +30,10 @@ module CrystalRuby
30
30
  self.name = name
31
31
  self.methods = {}
32
32
  self.chunks = []
33
- self.attachments = Hash.new(false)
34
33
  initialize_library!
35
34
  end
36
35
 
37
- # This method is used to
38
- # bootstrap a library filesystem,
39
- # and generate a top level index.cr and shard file if
36
+ # Bootstraps the library filesystem and generates top level index.cr and shard files if
40
37
  # these do not already exist.
41
38
  def initialize_library!
42
39
  @root_dir, @lib_dir, @src_dir, @codegen_dir = [
@@ -57,11 +54,11 @@ module CrystalRuby
57
54
  YAML
58
55
  end
59
56
 
60
- # This is where we instantiate the crystalized method as a CrystalRuby::Function
61
- # and trigger the generation of the crystal code.
57
+ # Generates and stores a reference to a new CrystalRuby::Function
58
+ # and triggers the generation of the crystal code. (See write_chunk)
62
59
  def crystalize_method(method, args, returns, function_body, async, &block)
63
60
  CR_ATTACH_MUX.synchronize do
64
- attachments.delete(method.owner)
61
+ methods.each_value(&:unattach!)
65
62
  method_key = "#{method.owner.name}/#{method.name}"
66
63
  methods[method_key] = Function.new(
67
64
  method: method,
@@ -100,8 +97,7 @@ module CrystalRuby
100
97
  end
101
98
 
102
99
  def compiled?
103
- index_contents = self.index_contents
104
- File.exist?(lib_file) && chunks.all? do |chunk|
100
+ @compiled ||= File.exist?(lib_file) && chunks.all? do |chunk|
105
101
  chunk_data = chunk[:body]
106
102
  file_digest = Digest::MD5.hexdigest chunk_data
107
103
  fname = chunk[:chunk_name]
@@ -115,10 +111,6 @@ module CrystalRuby
115
111
  ""
116
112
  end
117
113
 
118
- def attached?(owner)
119
- attachments[owner]
120
- end
121
-
122
114
  def register_type!(type)
123
115
  @types_cache ||= {}
124
116
  @types_cache[type.name] = type.type_defn
@@ -183,7 +175,8 @@ module CrystalRuby
183
175
  filename = (codegen_dir / module_name / "#{chunk_name}_#{file_digest}.cr").to_s
184
176
 
185
177
  unless existing.delete(filename)
186
- @attached = false
178
+ methods.each_value(&:unattach!)
179
+ @compiled = false
187
180
  FileUtils.mkdir_p(codegen_dir / module_name)
188
181
  File.write(filename, body)
189
182
  end
@@ -1,10 +1,8 @@
1
1
  module CrystalRuby
2
- # The Reactor represents a singleton Thread
3
- # responsible for running all Ruby/crystal interop code.
4
- # Crystal's Fiber scheduler and GC assumes all code is run on a single thread.
5
- # This class is responsible for multiplexing Ruby and Crystal code on a single thread,
6
- # to allow safe invocation of Crystal code from across any number of Ruby threads.
7
- # Functions annotated with async: true, are executed using callbacks to allow these to be multi-plexed in a non-blocking manner.
2
+ # The Reactor represents a singleton Thread responsible for running all Ruby/crystal interop code.
3
+ # Crystal's Fiber scheduler and GC assume all code is run on a single thread.
4
+ # This class is responsible for multiplexing Ruby and Crystal code on a single thread.
5
+ # Functions annotated with async: true, are executed using callbacks to allow these to be interleaved without blocking.
8
6
 
9
7
  module Reactor
10
8
  module_function
@@ -45,7 +43,6 @@ module CrystalRuby
45
43
  error_type = error_type.to_sym
46
44
  is_exception_type = Object.const_defined?(error_type) && Object.const_get(error_type).ancestors.include?(Exception)
47
45
  error_type = is_exception_type ? Object.const_get(error_type) : RuntimeError
48
- tid = tid.zero? ? Reactor.current_thread_id : tid
49
46
  raise error_type.new(message) unless THREAD_MAP.key?(tid)
50
47
 
51
48
  THREAD_MAP[tid][:error] = error_type.new(message)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CrystalRuby::Types
2
4
  Array = Type.new(
3
5
  :Array,
@@ -6,9 +8,8 @@ module CrystalRuby::Types
6
8
 
7
9
  def self.Array(type)
8
10
  Type.validate!(type)
9
- Type.new("Array", inner_types: [type], accept_if: [::Array]
10
- ) do |a|
11
- a.map!{|v| type.interpret!(v) }
11
+ Type.new("Array", inner_types: [type], accept_if: [::Array]) do |a|
12
+ a.map! { |v| type.interpret!(v) }
12
13
  end
13
14
  end
14
15
  end
@@ -1,15 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CrystalRuby::Types
2
4
  Hash = Type.new(
3
5
  :Hash,
4
- error: "Hash type must have 2 type parameters. E.g. Hash(Float64, String)",
6
+ error: "Hash type must have 2 type parameters. E.g. Hash(Float64, String)"
5
7
  )
6
8
 
7
9
  def self.Hash(key_type, value_type)
8
10
  Type.validate!(key_type)
9
11
  Type.validate!(value_type)
10
12
  Type.new("Hash", inner_types: [key_type, value_type], accept_if: [::Hash]) do |h|
11
- h.transform_keys!{|k| key_type.interpret!(k) }
12
- h.transform_values!{|v| value_type.interpret!(v) }
13
+ h.transform_keys! { |k| key_type.interpret!(k) }
14
+ h.transform_values! { |v| value_type.interpret!(v) }
13
15
  end
14
16
  end
15
17
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CrystalRuby::Types
2
4
  NamedTuple = Type.new(
3
5
  :NamedTuple,
@@ -6,7 +8,7 @@ module CrystalRuby::Types
6
8
 
7
9
  def self.NamedTuple(types_hash)
8
10
  types_hash.keys.each do |key|
9
- raise "NamedTuple keys must be symbols" unless key.kind_of?(::Symbol) || key.respond_to?(:to_sym)
11
+ raise "NamedTuple keys must be symbols" unless key.is_a?(::Symbol) || key.respond_to?(:to_sym)
10
12
  end
11
13
  types_hash.values.each do |value_type|
12
14
  Type.validate!(value_type)
@@ -14,9 +16,10 @@ module CrystalRuby::Types
14
16
  keys = types_hash.keys.map(&:to_sym)
15
17
  values = types_hash.values
16
18
  Type.new("NamedTuple", inner_types: values, inner_keys: keys, accept_if: [::Hash]) do |h|
17
- h.transform_keys!{|k| k.to_sym }
19
+ h.transform_keys! { |k| k.to_sym }
18
20
  raise "Invalid keys for named tuple" unless h.keys.length == keys.length
19
- raise "Invalid keys for named tuple" unless h.keys.all?{|k| keys.include?(k)}
21
+ raise "Invalid keys for named tuple" unless h.keys.all? { |k| keys.include?(k) }
22
+
20
23
  h.each do |key, value|
21
24
  h[key] = values[keys.index(key)].interpret!(value)
22
25
  end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CrystalRuby::Types
2
- require 'date'
4
+ require "date"
3
5
  Time = Type.new(:Time, accept_if: [::Time, ::String]) do |v|
4
6
  DateTime.parse(v)
5
7
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CrystalRuby::Types
2
4
  Tuple = Type.new(
3
5
  :Tuple,
@@ -9,7 +11,7 @@ module CrystalRuby::Types
9
11
  Type.validate!(value_type)
10
12
  end
11
13
  Type.new("Tuple", inner_types: types, accept_if: [::Array]) do |a|
12
- a.map!.with_index{|v, i| self.inner_types[i].interpret!(v) }
14
+ a.map!.with_index { |v, i| inner_types[i].interpret!(v) }
13
15
  end
14
16
  end
15
17
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "type_serializer"
2
4
 
3
5
  module CrystalRuby
@@ -5,7 +7,7 @@ module CrystalRuby
5
7
  class Typedef; end
6
8
 
7
9
  def self.Typedef(type)
8
- return type if type.kind_of?(Class) && type < Typedef
10
+ return type if type.is_a?(Class) && type < Typedef
9
11
 
10
12
  Class.new(Typedef) do
11
13
  define_singleton_method(:union_types) do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CrystalRuby
2
4
  module Types
3
5
  class UnionType < Type
@@ -26,12 +28,12 @@ module CrystalRuby
26
28
 
27
29
  def interpret!(raw)
28
30
  union_types.each do |type|
29
- if type.interprets?(raw)
30
- begin
31
- return type.interpret!(raw)
32
- rescue
33
- # Pass
34
- end
31
+ next unless type.interprets?(raw)
32
+
33
+ begin
34
+ return type.interpret!(raw)
35
+ rescue StandardError
36
+ # Pass
35
37
  end
36
38
  end
37
39
  raise "Invalid deserialized value #{raw} for type #{inspect}"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Crystalruby
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: crystalruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-04-24 00:00:00.000000000 Z
11
+ date: 2024-04-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: digest