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 +4 -4
- data/README.md +84 -8
- data/crystalruby.gemspec +5 -5
- data/lib/crystalruby/config.rb +7 -6
- data/lib/crystalruby/version.rb +1 -1
- data/lib/crystalruby.rb +43 -49
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 46edf2cddbf8fc124cca7cab14a4c3d745fcf88b9a841468cdbc7f2fc517187a
|
4
|
+
data.tar.gz: c350e0049aff5d88f8326314e0a70fbe57ea3866dcdf247010a0c1c64f656899
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 54ca6d542370d360f1f48959e4edf5a4308985d8e952317e2780b980c068674cb56c257c9d8241d0fc929bb152ca9960a8c7397f90c08725acf9bd268f794842
|
7
|
+
data.tar.gz: 303d6c379501dcf8966dd3941f2e667f35efeec67b24eb68bf2965abee06e5afa4b631c7b23e7493e4a3e088f650bd5a85271ae2bedb18b7a0120ca4c4bc605c
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
#
|
1
|
+
# crystalruby
|
2
2
|
|
3
|
-
|
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
|
-
|
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/
|
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
|
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 = ">=
|
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
|
36
|
-
spec.add_dependency
|
37
|
-
spec.add_dependency
|
38
|
-
spec.add_dependency
|
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
|
data/lib/crystalruby/config.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require
|
2
|
-
require
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
|
data/lib/crystalruby/version.rb
CHANGED
data/lib/crystalruby.rb
CHANGED
@@ -1,34 +1,20 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
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
|
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.
|
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
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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(
|
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(
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
138
|
-
|
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
|
-
|
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
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
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.
|
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-
|
11
|
+
date: 2024-04-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
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:
|
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:
|
106
|
+
version: 2.7.2
|
107
107
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
108
|
requirements:
|
109
109
|
- - ">="
|