crystalruby 0.1.0 → 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: 2e591c4976e8380217b87b6202767b2ffea5585ebabe16a1aba23ddf43d0c35c
4
- data.tar.gz: 16ef2b22329ba24a2aacaf7e2eb4f12d2a320be987eb0097168f738256991397
3
+ metadata.gz: 46edf2cddbf8fc124cca7cab14a4c3d745fcf88b9a841468cdbc7f2fc517187a
4
+ data.tar.gz: c350e0049aff5d88f8326314e0a70fbe57ea3866dcdf247010a0c1c64f656899
5
5
  SHA512:
6
- metadata.gz: 228cecadfa04acd614cf0b3070e8afe3cb2123b3c648b280388f575459997b7cde19cc18a439b9f108a0765f7f4b10979744ee384168394a588fc49668aaba04
7
- data.tar.gz: f9a2294670024fe994e12648479da83d6ce366412595d29312f796848ca7151da1b6b63fa9fe35f820a5f5096a31f8f7d4062f3c4e5764e41b41c2df5c1fc59c
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
 
@@ -109,7 +109,7 @@ Some Crystal syntax is not valid Ruby, for methods of this form, we need to
109
109
  define our functions using a :raw parameter.
110
110
 
111
111
  ```ruby
112
- crystalize [a: :int, b: :int] => :int
112
+ crystalize :raw, [a: :int, b: :int] => :int
113
113
  def add(a, b)
114
114
  <<~CRYSTAL
115
115
  c = 0_u64
@@ -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,9 +273,13 @@ 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
 
279
+ ```bash
280
+ crystalruby init
281
+ ```
282
+
205
283
  ```yaml
206
284
  crystal_src_dir: "./crystalruby/src"
207
285
  crystal_lib_dir: "./crystalruby/lib"
@@ -209,8 +287,6 @@ crystal_main_file: "main.cr"
209
287
  crystal_lib_name: "crlib"
210
288
  ```
211
289
 
212
- ## Usage
213
-
214
290
  ## Development
215
291
 
216
292
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -219,7 +295,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
219
295
 
220
296
  ## Contributing
221
297
 
222
- 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).
223
299
 
224
300
  ## License
225
301
 
@@ -227,4 +303,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
227
303
 
228
304
  ## Code of Conduct
229
305
 
230
- 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.0"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/crystalruby.rb CHANGED
@@ -1,34 +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
- require 'pry-byebug'
9
- # TODO
10
- # Shards
11
- # Object methods
12
- # Fix bigint issues
13
- # * Initialize Crystal project
14
- # * Clear build artifacts
15
- # * Add config file
16
- # * Set release flag (Changes build target locations)
17
- # Struct Conversions
18
- # Classes
19
- # Test Nesting
20
-
21
8
 
22
9
  module CrystalRuby
23
-
24
10
  # Define a method to set the @crystalize proc if it doesn't already exist
25
- def crystalize(type=:src, **options, &block)
11
+ def crystalize(type = :src, **options, &block)
26
12
  (args,), returns = options.first
27
13
  args ||= {}
28
- raise "Arguments should be of the form name: :type. Got #{args}" unless args.kind_of?(Hash)
29
- @crystalize_next = {raw: type.to_sym == :raw, args:, returns:, block: }
30
- end
14
+ raise "Arguments should be of the form name: :type. Got #{args}" unless args.is_a?(Hash)
31
15
 
16
+ @crystalize_next = { raw: type.to_sym == :raw, args: args, returns: returns, block: block }
17
+ end
32
18
 
33
19
  def method_added(method_name)
34
20
  if @crystalize_next
@@ -43,7 +29,6 @@ module CrystalRuby
43
29
  end
44
30
 
45
31
  def attach_crystalized_method(method_name)
46
-
47
32
  CrystalRuby.instantiate_crystal_ruby! unless CrystalRuby.instantiated?
48
33
 
49
34
  function_body = instance_method(method_name).source.lines[
@@ -60,12 +45,14 @@ module CrystalRuby
60
45
  extend FFI::Library
61
46
  ffi_lib "#{config.crystal_lib_dir}/#{config.crystal_lib_name}"
62
47
  attach_function "#{method_name}", fname, args.map(&:last), returns
63
- attach_function 'init!', 'init', [], :void
64
- [singleton_class, self].each do |receiver|
65
- receiver.prepend(Module.new do
66
- define_method(method_name, &block)
67
- end)
68
- 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
69
56
 
70
57
  init!
71
58
  end
@@ -83,21 +70,20 @@ module CrystalRuby
83
70
 
84
71
  module_function
85
72
 
86
-
87
73
  def build_function(owner, name, args, returns, body)
88
74
  fnname = "#{owner.name.downcase}_#{name}"
89
75
  args ||= {}
90
76
  string_conversions = args.select { |_k, v| v.eql?(:string) }.keys
91
77
  function_body = <<~CRYSTAL
92
78
  module #{owner.name}
93
- 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)}
94
80
  #{body}
95
81
  end
96
82
  end
97
83
 
98
- 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)}
99
85
  #{args.map { |k, v| "#{k} = #{convert_to_native_type("_#{k}", v)}" }.join("\n\t")}
100
- #{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)}
101
87
  end
102
88
  CRYSTAL
103
89
 
@@ -124,22 +110,27 @@ module CrystalRuby
124
110
  end
125
111
 
126
112
  def self.instantiate_crystal_ruby!
127
- 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
+
128
117
  @instantiated = true
129
118
  %w[crystal_lib_dir crystal_main_file crystal_src_dir crystal_lib_name].each do |config_key|
130
- 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
131
122
  end
132
123
  FileUtils.mkdir_p "#{config.crystal_src_dir}/generated"
133
124
  FileUtils.mkdir_p "#{config.crystal_lib_dir}"
134
125
  unless File.exist?("#{config.crystal_src_dir}/#{config.crystal_main_file}")
135
126
  IO.write("#{config.crystal_src_dir}/#{config.crystal_main_file}", "require \"./generated/index\"\n")
136
127
  end
137
- unless File.exist?("#{config.crystal_src_dir}/shard.yml")
138
- 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)
139
131
  name: src
140
132
  version: 0.1.0
141
- CRYSTAL
142
- end
133
+ CRYSTAL
143
134
  end
144
135
 
145
136
  def self.instantiated?
@@ -156,13 +147,14 @@ module CrystalRuby
156
147
 
157
148
  def self.compile!
158
149
  return unless @block_store
150
+
159
151
  index_content = <<~CRYSTAL
160
- FAKE_ARG = "crystal"
161
- fun init(): Void
162
- GC.init
163
- ptr = FAKE_ARG.to_unsafe
164
- LibCrystalMain.__crystal_main(1, pointerof(ptr))
165
- 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
166
158
  CRYSTAL
167
159
 
168
160
  index_content += @block_store.map do |function|
@@ -176,14 +168,16 @@ module CrystalRuby
176
168
  begin
177
169
  lib_target = "#{Dir.pwd}/#{config.crystal_lib_dir}/#{config.crystal_lib_name}"
178
170
  Dir.chdir(config.crystal_src_dir) do
179
- config.debug ?
180
- `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
181
174
  `crystal build --release --no-debug -o #{lib_target} #{config.crystal_main_file}`
175
+ end
182
176
  end
183
177
 
184
178
  @compiled = true
185
179
  rescue StandardError => e
186
- puts 'Error compiling crystal code'
180
+ puts "Error compiling crystal code"
187
181
  puts e
188
182
  File.delete("#{config.crystal_src_dir}/generated/index.cr")
189
183
  end
@@ -199,7 +193,7 @@ module CrystalRuby
199
193
  def self.write_function(owner, name:, body:, &compile_callback)
200
194
  @compiled = File.exist?("#{config.crystal_src_dir}/generated/index.cr") unless defined?(@compiled)
201
195
  @block_store ||= []
202
- @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 }
203
197
  FileUtils.mkdir_p("#{config.crystal_src_dir}/generated")
204
198
  existing = Dir.glob("#{config.crystal_src_dir}/generated/**/*.cr")
205
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.0
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
  - - ">="