crystalruby 0.1.5 → 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +29 -5
- data/lib/crystalruby/templates/function.cr +9 -0
- data/lib/crystalruby/templates/index.cr +27 -19
- data/lib/crystalruby/version.rb +1 -1
- data/lib/crystalruby.rb +45 -29
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fe68e58781b8314afb53b268ba9314472ee93c611565f7398d0ff29b445f8ece
|
4
|
+
data.tar.gz: acd5e80d5709cc6f831daa39f58f74fe95642e16a4c44d25c8fab24106b47bcd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 85516d1372b2467147cb5c61e6d33dc27e6e2c0d11876b32dbee1007152096fd54551926177b56e066fa9bc8e4329b8ef13d133280305018428775267eb4a24e
|
7
|
+
data.tar.gz: 207284740bef726d1a8c5df35b21ebe33b8611725363acac451bbadc3b7c714585513027be2398dd4cdd6ff94761c00585c94546fb198a4b90047d91edbef2ce
|
data/README.md
CHANGED
@@ -25,7 +25,7 @@ require 'crystalruby'
|
|
25
25
|
module MyTestModule
|
26
26
|
# The below method will be replaced by a compiled Crystal version
|
27
27
|
# linked using FFI.
|
28
|
-
crystalize [:int, :int] => :int
|
28
|
+
crystalize [a: :int, b: :int] => :int
|
29
29
|
def add(a, b)
|
30
30
|
a + b
|
31
31
|
end
|
@@ -133,6 +133,31 @@ def add(a, b)
|
|
133
133
|
end
|
134
134
|
```
|
135
135
|
|
136
|
+
## Getting Started
|
137
|
+
|
138
|
+
The below is a stand-alone one-file script that allows you to quickly see crystalruby in action.
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
# crystalrubytest.rb
|
142
|
+
require 'bundler/inline'
|
143
|
+
|
144
|
+
gemfile do
|
145
|
+
source 'https://rubygems.org'
|
146
|
+
gem 'crystalruby', path: '../crystalruby'
|
147
|
+
end
|
148
|
+
|
149
|
+
require 'crystalruby'
|
150
|
+
|
151
|
+
module Adder
|
152
|
+
crystalize [a: :int, b: :int] => :int
|
153
|
+
def add(a, b)
|
154
|
+
a + b
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
puts Adder.add(1, 2)
|
159
|
+
```
|
160
|
+
|
136
161
|
## Types
|
137
162
|
|
138
163
|
Currently primitive types are supported.
|
@@ -249,7 +274,7 @@ Remember to require these installed shards after installing them. E.g. inside `.
|
|
249
274
|
|
250
275
|
You can edit the default paths for crystal source and library files from within the `./crystalruby.yaml` config file.
|
251
276
|
|
252
|
-
|
277
|
+
## Wrapping Crystal code in Ruby
|
253
278
|
|
254
279
|
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.
|
255
280
|
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.
|
@@ -270,7 +295,7 @@ end
|
|
270
295
|
MyModule.add("1", "2")
|
271
296
|
```
|
272
297
|
|
273
|
-
|
298
|
+
## Release Builds
|
274
299
|
|
275
300
|
You can control whether CrystalRuby builds in debug or release mode by setting following config option
|
276
301
|
|
@@ -303,7 +328,7 @@ CrystalRuby.compile!
|
|
303
328
|
|
304
329
|
Then you can run this file as part of your build step, to ensure all Crystal code is compiled ahead of time.
|
305
330
|
|
306
|
-
|
331
|
+
## Troubleshooting
|
307
332
|
|
308
333
|
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.
|
309
334
|
|
@@ -324,7 +349,6 @@ It should support escape hatches to allow it to coexist with code that performs
|
|
324
349
|
|
325
350
|
The library is currently in its infancy. Planned additions are:
|
326
351
|
|
327
|
-
- Replace existing checksum process, with one that combines results of inline and external crystal to more accurately detect when recompilation is necessary.
|
328
352
|
- Simple mixin/concern that utilises `FFI::Struct` for bi-directional passing of Ruby objects and Crystal objects (by value).
|
329
353
|
- Install command to generate a sample build script, and supports build command (which simply verifies then invokes this script)
|
330
354
|
- Call Ruby from Crystal using FFI callbacks (implement `.expose_to_crystal`)
|
@@ -1,9 +1,18 @@
|
|
1
|
+
# This is the template used for all CrystalRuby functions
|
2
|
+
# Calls to this method *from ruby* are first transformed through the lib function.
|
3
|
+
# Crystal code can simply call this method directly, enabling generated crystal code
|
4
|
+
# to call other generated crystal code without overhead.
|
5
|
+
|
1
6
|
module %{module_name}
|
2
7
|
def self.%{fn_name}(%{fn_args}) : %{fn_ret_type}
|
3
8
|
%{fn_body}
|
4
9
|
end
|
5
10
|
end
|
6
11
|
|
12
|
+
# This function is the entry point for the CrystalRuby code, exposed through FFI.
|
13
|
+
# We apply some basic error handling here, and convert the arguments and return values
|
14
|
+
# to ensure that we are using Crystal native types.
|
15
|
+
|
7
16
|
fun %{lib_fn_name}(%{lib_fn_args}): %{lib_fn_ret_type}
|
8
17
|
begin
|
9
18
|
%{convert_lib_args}
|
@@ -1,38 +1,46 @@
|
|
1
1
|
FAKE_ARG = "crystal"
|
2
|
-
|
2
|
+
|
3
|
+
alias ErrorCallback = (Pointer(UInt8), Pointer(UInt8) -> Void)
|
3
4
|
|
4
5
|
module CrystalRuby
|
6
|
+
# Initializing Crystal Ruby invokes init on the Crystal garbage collector.
|
7
|
+
# We need to be sure to only do this once.
|
5
8
|
@@initialized = false
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
|
10
|
+
# We won't natively handle Crystal Exceptions in Ruby
|
11
|
+
# Instead, we'll catch them in Crystal, and explicitly expose them to Ruby via
|
12
|
+
# the error_callback.
|
13
|
+
@@error_callback
|
14
|
+
|
15
|
+
# This is the entry point for instantiating CrystalRuby
|
16
|
+
# We:
|
17
|
+
# 1. Initialize the Crystal garbage collector
|
18
|
+
# 2. Set the error callback
|
19
|
+
# 3. Call the Crystal main function
|
20
|
+
def self.init(error_callback : ErrorCallback)
|
21
|
+
return if @@initialized
|
11
22
|
GC.init
|
23
|
+
@@initialized = true
|
24
|
+
@@error_callback = error_callback
|
12
25
|
ptr = FAKE_ARG.to_unsafe
|
13
26
|
LibCrystalMain.__crystal_main(1, pointerof(ptr))
|
14
27
|
end
|
15
28
|
|
16
|
-
|
17
|
-
@@rb_error_handler = cb
|
18
|
-
end
|
19
|
-
|
29
|
+
# Explicit error handling (triggers exception within Ruby on the same thread)
|
20
30
|
def self.report_error(error_type : String, str : String)
|
21
|
-
handler = @@
|
22
|
-
if handler
|
31
|
+
if handler = @@error_callback
|
23
32
|
handler.call(error_type.to_unsafe, str.to_unsafe)
|
24
33
|
end
|
25
34
|
end
|
26
35
|
end
|
27
36
|
|
28
|
-
|
29
|
-
|
30
|
-
CrystalRuby.init
|
31
|
-
end
|
32
|
-
|
33
|
-
fun attach_rb_error_handler(cb : Callback) : Void
|
34
|
-
CrystalRuby.attach_rb_error_handler(cb)
|
37
|
+
fun init(cb : ErrorCallback): Void
|
38
|
+
CrystalRuby.init(cb)
|
35
39
|
end
|
36
40
|
|
41
|
+
# This is where we define all our Crystal modules and types
|
42
|
+
# derived from their Ruby counterparts.
|
37
43
|
%{type_modules}
|
44
|
+
|
45
|
+
# Require all generated crystal files
|
38
46
|
%{requires}
|
data/lib/crystalruby/version.rb
CHANGED
data/lib/crystalruby.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "ffi"
|
2
4
|
require "digest"
|
3
5
|
require "fileutils"
|
@@ -10,6 +12,7 @@ require_relative "crystalruby/typebuilder"
|
|
10
12
|
require_relative "crystalruby/template"
|
11
13
|
|
12
14
|
module CrystalRuby
|
15
|
+
CR_SRC_FILES_PATTERN = "./**/*.cr"
|
13
16
|
# Define a method to set the @crystalize proc if it doesn't already exist
|
14
17
|
def crystalize(type = :src, **options, &block)
|
15
18
|
(args,), returns = options.first
|
@@ -29,14 +32,6 @@ module CrystalRuby
|
|
29
32
|
crtype(&block).serialize_as(:json)
|
30
33
|
end
|
31
34
|
|
32
|
-
def with_temporary_constant(constant_name, new_value)
|
33
|
-
old_value = const_get(constant_name)
|
34
|
-
const_set(constant_name, new_value)
|
35
|
-
yield
|
36
|
-
ensure
|
37
|
-
const_set(constant_name, old_value)
|
38
|
-
end
|
39
|
-
|
40
35
|
def method_added(method_name)
|
41
36
|
if @crystalize_next
|
42
37
|
attach_crystalized_method(method_name)
|
@@ -64,8 +59,7 @@ module CrystalRuby
|
|
64
59
|
CrystalRuby.write_function(self, name: function[:name], body: function[:body]) do
|
65
60
|
extend FFI::Library
|
66
61
|
ffi_lib "#{config.crystal_lib_dir}/#{config.crystal_lib_name}"
|
67
|
-
attach_function
|
68
|
-
attach_function "init!", "init", [], :void
|
62
|
+
attach_function method_name, fname, function[:ffi_types], function[:return_ffi_type]
|
69
63
|
if block
|
70
64
|
[singleton_class, self].each do |receiver|
|
71
65
|
receiver.prepend(Module.new do
|
@@ -73,8 +67,6 @@ module CrystalRuby
|
|
73
67
|
end)
|
74
68
|
end
|
75
69
|
end
|
76
|
-
|
77
|
-
init!
|
78
70
|
end
|
79
71
|
|
80
72
|
[singleton_class, self].each do |receiver|
|
@@ -219,6 +211,9 @@ module CrystalRuby
|
|
219
211
|
"require \"./#{config.crystal_codegen_dir}/index\"\n"
|
220
212
|
)
|
221
213
|
end
|
214
|
+
|
215
|
+
attach_crystal_ruby_lib! if compiled?
|
216
|
+
|
222
217
|
return if File.exist?("#{config.crystal_src_dir}/shard.yml")
|
223
218
|
|
224
219
|
IO.write("#{config.crystal_src_dir}/shard.yml", <<~CRYSTAL)
|
@@ -227,11 +222,25 @@ module CrystalRuby
|
|
227
222
|
CRYSTAL
|
228
223
|
end
|
229
224
|
|
225
|
+
def attach_crystal_ruby_lib!
|
226
|
+
extend FFI::Library
|
227
|
+
ffi_lib "#{config.crystal_lib_dir}/#{config.crystal_lib_name}"
|
228
|
+
attach_function "init!", :init, [:pointer], :void
|
229
|
+
const_set(:ErrorCallback, FFI::Function.new(:void, %i[string string]) do |error_type, message|
|
230
|
+
error_type = error_type.to_sym
|
231
|
+
is_exception_type = Object.const_defined?(error_type) && Object.const_get(error_type).ancestors.include?(Exception)
|
232
|
+
error_type = is_exception_type ? Object.const_get(error_type) : RuntimeError
|
233
|
+
raise error_type.new(message)
|
234
|
+
end)
|
235
|
+
init!(ErrorCallback)
|
236
|
+
end
|
237
|
+
|
230
238
|
def self.instantiated?
|
231
239
|
@instantiated
|
232
240
|
end
|
233
241
|
|
234
242
|
def self.compiled?
|
243
|
+
@compiled = get_current_crystal_lib_digest == get_cr_src_files_digest unless defined?(@compiled)
|
235
244
|
@compiled
|
236
245
|
end
|
237
246
|
|
@@ -292,21 +301,15 @@ module CrystalRuby
|
|
292
301
|
"crystal build --release --no-debug -o #{lib_target} #{config.crystal_main_file}"
|
293
302
|
end
|
294
303
|
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
304
|
+
unless result = system(cmd)
|
305
|
+
File.delete(digest_file_name) if File.exist?(digest_file_name)
|
306
|
+
raise "Error compiling crystal code"
|
307
|
+
end
|
299
308
|
end
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
error_type = error_type.to_sym
|
305
|
-
is_exception_type = Object.const_defined?(error_type) && Object.const_get(error_type).ancestors.include?(Exception)
|
306
|
-
error_type = is_exception_type ? Object.const_get(error_type) : RuntimeError
|
307
|
-
raise error_type.new(message)
|
308
|
-
end)
|
309
|
-
attach_rb_error_handler(ErrorCallback)
|
309
|
+
|
310
|
+
IO.write(digest_file_name, get_cr_src_files_digest)
|
311
|
+
@compiled = true
|
312
|
+
attach_crystal_ruby_lib!
|
310
313
|
end
|
311
314
|
|
312
315
|
def self.attach!
|
@@ -316,10 +319,23 @@ module CrystalRuby
|
|
316
319
|
@attached = true
|
317
320
|
end
|
318
321
|
|
322
|
+
def self.get_cr_src_files_digest
|
323
|
+
file_digests = Dir.glob(CR_SRC_FILES_PATTERN).sort.map do |file_path|
|
324
|
+
content = File.read(file_path)
|
325
|
+
Digest::MD5.hexdigest(content)
|
326
|
+
end.join
|
327
|
+
Digest::MD5.hexdigest(file_digests)
|
328
|
+
end
|
329
|
+
|
330
|
+
def self.digest_file_name
|
331
|
+
@digest_file_name ||= "#{config.crystal_lib_dir}/#{config.crystal_lib_name}.digest"
|
332
|
+
end
|
333
|
+
|
334
|
+
def self.get_current_crystal_lib_digest
|
335
|
+
File.read(digest_file_name) if File.exist?(digest_file_name)
|
336
|
+
end
|
337
|
+
|
319
338
|
def self.write_function(owner, name:, body:, &compile_callback)
|
320
|
-
unless defined?(@compiled)
|
321
|
-
@compiled = File.exist?("#{config.crystal_src_dir}/#{config.crystal_codegen_dir}/index.cr")
|
322
|
-
end
|
323
339
|
@block_store ||= []
|
324
340
|
@block_store << { owner: owner, name: name, body: body, compile_callback: compile_callback }
|
325
341
|
FileUtils.mkdir_p("#{config.crystal_src_dir}/#{config.crystal_codegen_dir}")
|