crystalruby 0.1.1 → 0.1.2

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: db2722469a6d98fb5696b55dd0980337db19ce92fde42b4c1c023cfa6fd877d0
4
- data.tar.gz: a2914be29451a124cca3dcdd83173e8f4713c5e1dbd1121c1c4179e6e8a55ea6
3
+ metadata.gz: 46edf2cddbf8fc124cca7cab14a4c3d745fcf88b9a841468cdbc7f2fc517187a
4
+ data.tar.gz: c350e0049aff5d88f8326314e0a70fbe57ea3866dcdf247010a0c1c64f656899
5
5
  SHA512:
6
- metadata.gz: 9253f9b6b953021570fb31ee958e4ca2c798d3e586d5be8da59d5f26b895f1bce22672bbb1c0c14322732d61d9c4fe780d6833d9410d9e16732683862fd3330f
7
- data.tar.gz: d14edd179ed85907f90038b303f409a6c3868b2cf2bbfd60695350886d67ee5bf9d98b2ee3e1d05421e3d55b9b3ce155f7d9b0d04d2c6bee0165f36e1598abc7
6
+ metadata.gz: 54ca6d542370d360f1f48959e4edf5a4308985d8e952317e2780b980c068674cb56c257c9d8241d0fc929bb152ca9960a8c7397f90c08725acf9bd268f794842
7
+ data.tar.gz: 303d6c379501dcf8966dd3941f2e667f35efeec67b24eb68bf2965abee06e5afa4b631c7b23e7493e4a3e088f650bd5a85271ae2bedb18b7a0120ca4c4bc605c
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # CrystalRuby
1
+ # crystalruby
2
2
 
3
- Crystal Ruby is a gem that allows you to write Crystal code, inlined in Ruby. All you need is a modern crystal compiler installed on your system.
3
+ `crystalruby` is a gem that allows you to write Crystal code, inlined in Ruby. All you need is a modern crystal compiler installed on your system.
4
4
 
5
5
  You can then turn simple methods into Crystal methods as easily as demonstrated below:
6
6
 
@@ -169,6 +169,60 @@ Remember to require these installed shards after installing them. E.g. inside `.
169
169
 
170
170
  You can edit the default paths for crystal source and library files from within the `./crystalruby.yaml` config file.
171
171
 
172
+ ### Wrapping Crystal code in Ruby
173
+
174
+ Sometimes you may want to wrap a Crystal method in Ruby, so that you can use Ruby before the Crystal code to prepare arguments, or after the Crystal code, to apply transformations to the result. A real-life example of this might be an ActionController method, where you might want to use Ruby to parse the request, perform auth etc., and then use Crystal to perform some heavy computation, before returning the result from Ruby.
175
+ To do this, you simply pass a block to the `crystalize` method, which will serve as the Ruby entry point to the function. From within this block, you can invoke `super` to call the Crystal method, and then apply any Ruby transformations to the result.
176
+
177
+ ```ruby
178
+ module MyModule
179
+ crystalize [a: :int32, b: :int32] => :int32 do |a, b|
180
+ # In this example, we perform automated conversion to integers inside Ruby.
181
+ # Then add 1 to the result of the Crystal method.
182
+ result = super(a.to_i, b.to_i)
183
+ result + 1
184
+ end
185
+ def add(a, b)
186
+ a + b
187
+ end
188
+ end
189
+
190
+ MyModule.add("1", "2")
191
+ ```
192
+
193
+ ### Release Builds
194
+
195
+ You can control whether CrystalRuby builds in debug or release mode by setting following config option
196
+
197
+ ```ruby
198
+ CrystalRuby.configure do |config|
199
+ config.debug = false
200
+ end
201
+ ```
202
+
203
+ By default, Crystal code is only JIT compiled. In production, you likely want to compile the Crystal code ahead of time. To do this, you can create a dedicated file which
204
+
205
+ - Preloads all files Ruby code with embedded crystal
206
+ - Forces compilation.
207
+
208
+ E.g.
209
+
210
+ ```ruby
211
+ # E.g. crystalruby_build.rb
212
+ require "crystalruby"
213
+
214
+ CrystalRuby.configure do |config|
215
+ config.debug = false
216
+ end
217
+
218
+ require_relative "foo"
219
+ require_relative "bar"
220
+
221
+ CrystalRuby.compile!
222
+ ```
223
+
224
+ Then you can run this file as part of your build step, to ensure all Crystal code is compiled ahead of time.
225
+
172
226
  ### Troubleshooting
173
227
 
174
228
  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.
@@ -179,6 +233,26 @@ To do this execute:
179
233
  bundle exec crystalruby clean
180
234
  ```
181
235
 
236
+ ## Design Goals
237
+
238
+ `crystalruby`'s primary purpose is provide ergonomic access to Crystal from Ruby, over FFI.
239
+ For simple usage, advanced knowledge of Crystal should not be required.
240
+
241
+ However, the abstraction it provides should remain simple, transparent, and easy to hack on and it should not preclude users from supplementing its capabilities with a more direct integration using ffi primtives.
242
+
243
+ It should support escape hatches to allow it to coexist with code that performs a more direct [FFI](https://github.com/ffi/ffi) integration to implement advanced functionality not supported by `crystalruby`.
244
+
245
+ The library is currently in its infancy. Planned additions are:
246
+
247
+ - Replace existing checksum process, with one that combines results of inline and external crystal to more accurately detect when recompilation is necessary.
248
+ - Support for automatic serialization of nested data structures (holding _ONLY_ primitives), using JSON as our serialization protocol (prioritizing portability over raw serialization performance. JSON generation and parsing is bundled into the stdlib in both languages).
249
+ - Simple mixin/concern that utilises `FFI::Struct` for bi-directional passing of Ruby objects and Crystal objects (by value).
250
+ - Install command to generate a sample build script, and supports build command (which simply verifies then invokes this script)
251
+ - Call Ruby from Crystal using FFI callbacks (implement `.expose_to_crystal`)
252
+ - Support long-lived synchronized objects (through use of synchronized memory arena to prevent GC).
253
+ - Support for passing `crystalruby` types by reference (need to contend with GC).
254
+ - Explore mechanisms to safely expose true parallelism using [FFI over Ractors](https://github.com/ffi/ffi/wiki/Ractors)
255
+
182
256
  ## Installation
183
257
 
184
258
  To get started, add this line to your application's Gemfile:
@@ -199,7 +273,7 @@ Or install it yourself as:
199
273
  $ gem install crystalruby
200
274
  ```
201
275
 
202
- Crystal Ruby requires some basic initialization options inside a crystalruby.yaml file in the root of your project.
276
+ `crystalruby` requires some basic initialization options inside a crystalruby.yaml file in the root of your project.
203
277
  You can run `crystalruby init` to generate a configuration file with sane defaults.
204
278
 
205
279
  ```bash
@@ -221,7 +295,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
221
295
 
222
296
  ## Contributing
223
297
 
224
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/crystalruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/crystalruby/blob/master/CODE_OF_CONDUCT.md).
298
+ Bug reports and pull requests are welcome on GitHub at https://github.com/wouterken/crystalruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/wouterken/crystalruby/blob/master/CODE_OF_CONDUCT.md).
225
299
 
226
300
  ## License
227
301
 
@@ -229,4 +303,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
229
303
 
230
304
  ## Code of Conduct
231
305
 
232
- Everyone interacting in the Crystalruby project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/crystalruby/blob/master/CODE_OF_CONDUCT.md).
306
+ Everyone interacting in the `crystalruby` project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/wouterken/crystalruby/blob/master/CODE_OF_CONDUCT.md).
data/crystalruby.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.description = "Embed Crystal code directly in Ruby."
13
13
  spec.homepage = "https://github.com/wouterken/crystalruby"
14
14
  spec.license = "MIT"
15
- spec.required_ruby_version = ">= 3.0.0"
15
+ spec.required_ruby_version = ">= 2.7.2"
16
16
 
17
17
  spec.metadata["homepage_uri"] = spec.homepage
18
18
  spec.metadata["source_code_uri"] = spec.homepage
@@ -32,10 +32,10 @@ Gem::Specification.new do |spec|
32
32
 
33
33
  # Uncomment to register a new dependency of your gem
34
34
  # spec.add_dependency "example-gem", "~> 1.0"
35
- spec.add_dependency 'ffi'
36
- spec.add_dependency 'digest'
37
- spec.add_dependency 'fileutils'
38
- spec.add_dependency 'method_source'
35
+ spec.add_dependency "digest"
36
+ spec.add_dependency "ffi"
37
+ spec.add_dependency "fileutils"
38
+ spec.add_dependency "method_source"
39
39
  # For more information and examples about making a new gem, check out our
40
40
  # guide at: https://bundler.io/guides/creating_gem.html
41
41
  end
@@ -1,5 +1,5 @@
1
- require 'singleton'
2
- require 'yaml'
1
+ require "singleton"
2
+ require "yaml"
3
3
 
4
4
  module CrystalRuby
5
5
  def self.config
@@ -14,10 +14,11 @@ module CrystalRuby
14
14
  def initialize
15
15
  # Set default configuration options
16
16
  @debug = true
17
- if File.exist?("crystalruby.yaml")
18
- @crystal_src_dir, @crystal_lib_dir, @crystal_main_file, @crystal_lib_name =
19
- YAML.safe_load_file("crystalruby.yaml").values_at("crystal_src_dir","crystal_lib_dir","crystal_main_file", "crystal_lib_name")
20
- end
17
+ return unless File.exist?("crystalruby.yaml")
18
+
19
+ @crystal_src_dir, @crystal_lib_dir, @crystal_main_file, @crystal_lib_name =
20
+ YAML.safe_load(IO.read("crystalruby.yaml")).values_at("crystal_src_dir", "crystal_lib_dir", "crystal_main_file",
21
+ "crystal_lib_name")
21
22
  end
22
23
  end
23
24
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Crystalruby
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/crystalruby.rb CHANGED
@@ -1,33 +1,20 @@
1
- require 'ffi'
2
- require 'digest'
3
- require 'fileutils'
4
- require 'method_source'
1
+ require "ffi"
2
+ require "digest"
3
+ require "fileutils"
4
+ require "method_source"
5
5
  require_relative "crystalruby/config"
6
6
  require_relative "crystalruby/version"
7
7
  require_relative "crystalruby/typemaps"
8
- # TODO
9
- # Shards
10
- # Object methods
11
- # Fix bigint issues
12
- # * Initialize Crystal project
13
- # * Clear build artifacts
14
- # * Add config file
15
- # * Set release flag (Changes build target locations)
16
- # Struct Conversions
17
- # Classes
18
- # Test Nesting
19
-
20
8
 
21
9
  module CrystalRuby
22
-
23
10
  # Define a method to set the @crystalize proc if it doesn't already exist
24
- def crystalize(type=:src, **options, &block)
11
+ def crystalize(type = :src, **options, &block)
25
12
  (args,), returns = options.first
26
13
  args ||= {}
27
- raise "Arguments should be of the form name: :type. Got #{args}" unless args.kind_of?(Hash)
28
- @crystalize_next = {raw: type.to_sym == :raw, args:, returns:, block: }
29
- end
14
+ raise "Arguments should be of the form name: :type. Got #{args}" unless args.is_a?(Hash)
30
15
 
16
+ @crystalize_next = { raw: type.to_sym == :raw, args: args, returns: returns, block: block }
17
+ end
31
18
 
32
19
  def method_added(method_name)
33
20
  if @crystalize_next
@@ -42,7 +29,6 @@ module CrystalRuby
42
29
  end
43
30
 
44
31
  def attach_crystalized_method(method_name)
45
-
46
32
  CrystalRuby.instantiate_crystal_ruby! unless CrystalRuby.instantiated?
47
33
 
48
34
  function_body = instance_method(method_name).source.lines[
@@ -59,12 +45,14 @@ module CrystalRuby
59
45
  extend FFI::Library
60
46
  ffi_lib "#{config.crystal_lib_dir}/#{config.crystal_lib_name}"
61
47
  attach_function "#{method_name}", fname, args.map(&:last), returns
62
- attach_function 'init!', 'init', [], :void
63
- [singleton_class, self].each do |receiver|
64
- receiver.prepend(Module.new do
65
- define_method(method_name, &block)
66
- end)
67
- end if block
48
+ attach_function "init!", "init", [], :void
49
+ if block
50
+ [singleton_class, self].each do |receiver|
51
+ receiver.prepend(Module.new do
52
+ define_method(method_name, &block)
53
+ end)
54
+ end
55
+ end
68
56
 
69
57
  init!
70
58
  end
@@ -82,21 +70,20 @@ module CrystalRuby
82
70
 
83
71
  module_function
84
72
 
85
-
86
73
  def build_function(owner, name, args, returns, body)
87
74
  fnname = "#{owner.name.downcase}_#{name}"
88
75
  args ||= {}
89
76
  string_conversions = args.select { |_k, v| v.eql?(:string) }.keys
90
77
  function_body = <<~CRYSTAL
91
78
  module #{owner.name}
92
- def self.#{name}(#{args.map { |k, v| "#{k} : #{native_type(v)}" }.join(',')}) : #{native_type(returns)}
79
+ def self.#{name}(#{args.map { |k, v| "#{k} : #{native_type(v)}" }.join(",")}) : #{native_type(returns)}
93
80
  #{body}
94
81
  end
95
82
  end
96
83
 
97
- fun #{fnname}(#{args.map { |k, v| "_#{k}: #{lib_type(v)}" }.join(',')}): #{lib_type(returns)}
84
+ fun #{fnname}(#{args.map { |k, v| "_#{k}: #{lib_type(v)}" }.join(",")}): #{lib_type(returns)}
98
85
  #{args.map { |k, v| "#{k} = #{convert_to_native_type("_#{k}", v)}" }.join("\n\t")}
99
- #{convert_to_return_type("#{owner.name}.#{name}(#{args.keys.map { |k| "#{k}" }.join(',')})", returns)}
86
+ #{convert_to_return_type("#{owner.name}.#{name}(#{args.keys.map { |k| "#{k}" }.join(",")})", returns)}
100
87
  end
101
88
  CRYSTAL
102
89
 
@@ -123,22 +110,27 @@ module CrystalRuby
123
110
  end
124
111
 
125
112
  def self.instantiate_crystal_ruby!
126
- raise "Crystal executable not found. Please ensure Crystal is installed and in your PATH." unless system("which crystal > /dev/null 2>&1")
113
+ unless system("which crystal > /dev/null 2>&1")
114
+ raise "Crystal executable not found. Please ensure Crystal is installed and in your PATH."
115
+ end
116
+
127
117
  @instantiated = true
128
118
  %w[crystal_lib_dir crystal_main_file crystal_src_dir crystal_lib_name].each do |config_key|
129
- raise "Missing config option `#{config_key}`. \nProvide this inside crystalruby.yaml (run `bundle exec crystalruby init` to generate this file with detaults)" unless config.send(config_key)
119
+ unless config.send(config_key)
120
+ raise "Missing config option `#{config_key}`. \nProvide this inside crystalruby.yaml (run `bundle exec crystalruby init` to generate this file with detaults)"
121
+ end
130
122
  end
131
123
  FileUtils.mkdir_p "#{config.crystal_src_dir}/generated"
132
124
  FileUtils.mkdir_p "#{config.crystal_lib_dir}"
133
125
  unless File.exist?("#{config.crystal_src_dir}/#{config.crystal_main_file}")
134
126
  IO.write("#{config.crystal_src_dir}/#{config.crystal_main_file}", "require \"./generated/index\"\n")
135
127
  end
136
- unless File.exist?("#{config.crystal_src_dir}/shard.yml")
137
- IO.write("#{config.crystal_src_dir}/shard.yml", <<~CRYSTAL)
128
+ return if File.exist?("#{config.crystal_src_dir}/shard.yml")
129
+
130
+ IO.write("#{config.crystal_src_dir}/shard.yml", <<~CRYSTAL)
138
131
  name: src
139
132
  version: 0.1.0
140
- CRYSTAL
141
- end
133
+ CRYSTAL
142
134
  end
143
135
 
144
136
  def self.instantiated?
@@ -155,13 +147,14 @@ module CrystalRuby
155
147
 
156
148
  def self.compile!
157
149
  return unless @block_store
150
+
158
151
  index_content = <<~CRYSTAL
159
- FAKE_ARG = "crystal"
160
- fun init(): Void
161
- GC.init
162
- ptr = FAKE_ARG.to_unsafe
163
- LibCrystalMain.__crystal_main(1, pointerof(ptr))
164
- end
152
+ FAKE_ARG = "crystal"
153
+ fun init(): Void
154
+ GC.init
155
+ ptr = FAKE_ARG.to_unsafe
156
+ LibCrystalMain.__crystal_main(1, pointerof(ptr))
157
+ end
165
158
  CRYSTAL
166
159
 
167
160
  index_content += @block_store.map do |function|
@@ -175,14 +168,16 @@ module CrystalRuby
175
168
  begin
176
169
  lib_target = "#{Dir.pwd}/#{config.crystal_lib_dir}/#{config.crystal_lib_name}"
177
170
  Dir.chdir(config.crystal_src_dir) do
178
- config.debug ?
179
- `crystal build -o #{lib_target} #{config.crystal_main_file}` :
171
+ if config.debug
172
+ `crystal build -o #{lib_target} #{config.crystal_main_file}`
173
+ else
180
174
  `crystal build --release --no-debug -o #{lib_target} #{config.crystal_main_file}`
175
+ end
181
176
  end
182
177
 
183
178
  @compiled = true
184
179
  rescue StandardError => e
185
- puts 'Error compiling crystal code'
180
+ puts "Error compiling crystal code"
186
181
  puts e
187
182
  File.delete("#{config.crystal_src_dir}/generated/index.cr")
188
183
  end
@@ -198,7 +193,7 @@ module CrystalRuby
198
193
  def self.write_function(owner, name:, body:, &compile_callback)
199
194
  @compiled = File.exist?("#{config.crystal_src_dir}/generated/index.cr") unless defined?(@compiled)
200
195
  @block_store ||= []
201
- @block_store << {owner: owner, name: name, body: body, compile_callback: compile_callback}
196
+ @block_store << { owner: owner, name: name, body: body, compile_callback: compile_callback }
202
197
  FileUtils.mkdir_p("#{config.crystal_src_dir}/generated")
203
198
  existing = Dir.glob("#{config.crystal_src_dir}/generated/**/*.cr")
204
199
  @block_store.each do |function|
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: crystalruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
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-07 00:00:00.000000000 Z
11
+ date: 2024-04-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: ffi
14
+ name: digest
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: digest
28
+ name: ffi
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -103,7 +103,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
103
103
  requirements:
104
104
  - - ">="
105
105
  - !ruby/object:Gem::Version
106
- version: 3.0.0
106
+ version: 2.7.2
107
107
  required_rubygems_version: !ruby/object:Gem::Requirement
108
108
  requirements:
109
109
  - - ">="