crystalruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2e591c4976e8380217b87b6202767b2ffea5585ebabe16a1aba23ddf43d0c35c
4
+ data.tar.gz: 16ef2b22329ba24a2aacaf7e2eb4f12d2a320be987eb0097168f738256991397
5
+ SHA512:
6
+ metadata.gz: 228cecadfa04acd614cf0b3070e8afe3cb2123b3c648b280388f575459997b7cde19cc18a439b9f108a0765f7f4b10979744ee384168394a588fc49668aaba04
7
+ data.tar.gz: f9a2294670024fe994e12648479da83d6ce366412595d29312f796848ca7151da1b6b63fa9fe35f820a5f5096a31f8f7d4062f3c4e5764e41b41c2df5c1fc59c
data/.rubocop.yml ADDED
@@ -0,0 +1,13 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+
4
+ Style/StringLiterals:
5
+ Enabled: true
6
+ EnforcedStyle: double_quotes
7
+
8
+ Style/StringLiteralsInInterpolation:
9
+ Enabled: true
10
+ EnforcedStyle: double_quotes
11
+
12
+ Layout/LineLength:
13
+ Max: 120
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-04-07
4
+
5
+ - Initial release
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at wc@pico.net.nz. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Wouter Coppieters
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,230 @@
1
+ # CrystalRuby
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.
4
+
5
+ You can then turn simple methods into Crystal methods as easily as demonstrated below:
6
+
7
+ ```ruby
8
+ require 'crystalruby'
9
+
10
+ module MyTestModule
11
+ # The below method will be replaced by a compiled Crystal version
12
+ # linked using FFI.
13
+ crystalize [:int, :int] => :int
14
+ def add(a, b)
15
+ a + b
16
+ end
17
+ end
18
+
19
+ # This method is run in Crystal, not Ruby!
20
+ MyTestModule.add(1, 2) # => 3
21
+ ```
22
+
23
+ With as small a change as this, you should be able to see a significant increase in performance for some Ruby code.
24
+ E.g.
25
+
26
+ ```ruby
27
+
28
+ require 'crystalruby'
29
+ require 'benchmark'
30
+
31
+ module Fibonnaci
32
+ crystalize [n: :int32] => :int32
33
+ def fib_cr(n)
34
+ a = 0
35
+ b = 1
36
+ n.times { a, b = b, a + b }
37
+ a
38
+ end
39
+
40
+ module_function
41
+
42
+ def fib_rb(n)
43
+ a = 0
44
+ b = 1
45
+ n.times { a, b = b, a + b }
46
+ a
47
+ end
48
+ end
49
+
50
+ puts(Benchmark.realtime { 1_000_000.times { Fibonnaci.fib_rb(30) } })
51
+ puts(Benchmark.realtime { 1_000_000.times { Fibonnaci.fib_cr(30) } })
52
+
53
+ ```
54
+
55
+ ```bash
56
+ 3.193121999996947 # Ruby
57
+ 0.29086600001028273 # Crystal
58
+ ```
59
+
60
+ _Note_: The first run of the Crystal code will be slower, as it needs to compile the code first. The subsequent runs will be much faster.
61
+
62
+ You can call embedded crystal code, from within other embedded crystal code.
63
+ E.g.
64
+
65
+ ```ruby
66
+ module Cache
67
+
68
+ crystalize [key: :string] => :string
69
+ def redis_get(key)
70
+ rds = Redis::Client.new
71
+ value = rds.get(key).to_s
72
+ end
73
+
74
+ crystalize [key: :string, value: :string] => :string
75
+ def redis_set_and_return(key)
76
+ redis = Redis::Client.new
77
+ redis.set(key, value)
78
+ Cache.redis_get(key)
79
+ end
80
+ end
81
+ Cache.redis_set_and_return('test', 'abc')
82
+ puts Cache.redis_get('test')
83
+ ```
84
+
85
+ ```bash
86
+ $ abc
87
+ ```
88
+
89
+ ## Syntax
90
+
91
+ ### Ruby Compatible
92
+
93
+ Where the Crystal syntax is also valid Ruby syntax, you can just write Ruby.
94
+ It'll be compiled as Crystal automatically.
95
+
96
+ E.g.
97
+
98
+ ```ruby
99
+ crystalize [a: :int, b: :int] => :int
100
+ def add(a, b)
101
+ puts "Adding #{a} and #{b}"
102
+ a + b
103
+ end
104
+ ```
105
+
106
+ ### Crystal Compatible
107
+
108
+ Some Crystal syntax is not valid Ruby, for methods of this form, we need to
109
+ define our functions using a :raw parameter.
110
+
111
+ ```ruby
112
+ crystalize [a: :int, b: :int] => :int
113
+ def add(a, b)
114
+ <<~CRYSTAL
115
+ c = 0_u64
116
+ a + b + c
117
+ CRYSTAL
118
+ end
119
+ ```
120
+
121
+ ## Types
122
+
123
+ Currently only primitive types are supported. Structures are a WIP.
124
+ To see the list of currently supported type mappings of FFI types to crystal types, you can check: `CrystalRuby::Typemaps::CRYSTAL_TYPE_MAP`
125
+ E.g.
126
+
127
+ ```ruby
128
+ CrystalRuby::Typemaps::CRYSTAL_TYPE_MAP
129
+ => {:char=>"Int8",
130
+ :uchar=>"UInt8",
131
+ :int8=>"Int8",
132
+ :uint8=>"UInt8",
133
+ :short=>"Int16",
134
+ :ushort=>"UInt16",
135
+ :int16=>"Int16",
136
+ :uint16=>"UInt16",
137
+ :int=>"Int32",
138
+ :uint=>"UInt32",
139
+ :int32=>"Int32",
140
+ :uint32=>"UInt32",
141
+ :long=>"Int32 | Int64",
142
+ :ulong=>"UInt32 | UInt64",
143
+ :int64=>"Int64",
144
+ :uint64=>"UInt64",
145
+ :long_long=>"Int64",
146
+ :ulong_long=>"UInt64",
147
+ :float=>"Float32",
148
+ :double=>"Float64",
149
+ :bool=>"Bool",
150
+ :void=>"Void",
151
+ :string=>"String"}
152
+ ```
153
+
154
+ ### Installing shards and writing non-embedded Crystal code
155
+
156
+ You can use any Crystal shards and write ordinary, stand-alone Crystal code.
157
+
158
+ The default entry point for the crystal shared library generated by the gem is
159
+ 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.
160
+
161
+ You can define shards inside `./crystalruby/src/shard.yml`
162
+ Run the below to install new shards
163
+
164
+ ```bash
165
+ bundle exec crystalruby install
166
+ ```
167
+
168
+ Remember to require these installed shards after installing them. E.g. inside `./crystalruby/src/main.cr`
169
+
170
+ You can edit the default paths for crystal source and library files from within the `./crystalruby.yaml` config file.
171
+
172
+ ### Troubleshooting
173
+
174
+ 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.
175
+
176
+ To do this execute:
177
+
178
+ ```bash
179
+ bundle exec crystalruby clean
180
+ ```
181
+
182
+ ## Installation
183
+
184
+ To get started, add this line to your application's Gemfile:
185
+
186
+ ```ruby
187
+ gem 'crystalruby'
188
+ ```
189
+
190
+ And then execute:
191
+
192
+ ```bash
193
+ $ bundle
194
+ ```
195
+
196
+ Or install it yourself as:
197
+
198
+ ```bash
199
+ $ gem install crystalruby
200
+ ```
201
+
202
+ Crystal Ruby requires some basic initialization options inside a crystalruby.yaml file in the root of your project.
203
+ You can run `crystalruby init` to generate a configuration file with sane defaults.
204
+
205
+ ```yaml
206
+ crystal_src_dir: "./crystalruby/src"
207
+ crystal_lib_dir: "./crystalruby/lib"
208
+ crystal_main_file: "main.cr"
209
+ crystal_lib_name: "crlib"
210
+ ```
211
+
212
+ ## Usage
213
+
214
+ ## Development
215
+
216
+ 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.
217
+
218
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
219
+
220
+ ## Contributing
221
+
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).
223
+
224
+ ## License
225
+
226
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
227
+
228
+ ## Code of Conduct
229
+
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).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/crystalruby/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "crystalruby"
7
+ spec.version = Crystalruby::VERSION
8
+ spec.authors = ["Wouter Coppieters"]
9
+ spec.email = ["wc@pico.net.nz"]
10
+
11
+ spec.summary = "Embed Crystal code directly in Ruby."
12
+ spec.description = "Embed Crystal code directly in Ruby."
13
+ spec.homepage = "https://github.com/wouterken/crystalruby"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.0.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(__dir__) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (File.expand_path(f) == __FILE__) ||
26
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
27
+ end
28
+ end
29
+ spec.bindir = "exe"
30
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ["lib"]
32
+
33
+ # Uncomment to register a new dependency of your gem
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'
39
+ # For more information and examples about making a new gem, check out our
40
+ # guide at: https://bundler.io/guides/creating_gem.html
41
+ end
data/exe/crystalruby ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "crystalruby"
5
+ require 'fileutils'
6
+
7
+ # Define the actions for the commands
8
+ def init
9
+ # Define some dummy content for the YAML file
10
+ yaml_content = <<~YAML
11
+ # crystalruby configuration file
12
+ crystal_src_dir: "./crystalruby/src"
13
+ crystal_lib_dir: "./crystalruby/lib"
14
+ crystal_main_file: "main.cr"
15
+ crystal_lib_name: "crlib"
16
+ YAML
17
+
18
+ # Create the file at the root of the current directory
19
+ File.write('crystalruby.yaml', yaml_content)
20
+ puts 'Initialized crystalruby.yaml file with dummy content.'
21
+ end
22
+
23
+
24
+ def install
25
+ Dir.chdir("#{CrystalRuby.config.crystal_src_dir}") do
26
+ if system('shards update')
27
+ puts 'Shards installed successfully.'
28
+ else
29
+ puts 'Error installing shards.'
30
+ end
31
+ end
32
+ clean
33
+ end
34
+
35
+ def clean
36
+ # This is a stub for the clear command
37
+ FileUtils.rm_rf("#{CrystalRuby.config.crystal_src_dir}/generated")
38
+ end
39
+
40
+ def build
41
+ # This is a stub for the build command
42
+ puts 'Build command is not implemented yet.'
43
+ end
44
+
45
+ # Main program
46
+ if ARGV.empty?
47
+ puts "Usage: crystalruby [command]"
48
+ puts "Commands: init, clear, build"
49
+ exit 1
50
+ end
51
+
52
+ case ARGV[0]
53
+ when "init"
54
+ init
55
+ when "clean"
56
+ clean
57
+ when "install"
58
+ install
59
+ when "build"
60
+ build
61
+ else
62
+ puts "Invalid command: #{ARGV[0]}"
63
+ end
@@ -0,0 +1,28 @@
1
+ require 'singleton'
2
+ require 'yaml'
3
+
4
+ module CrystalRuby
5
+ def self.config
6
+ Config.instance
7
+ end
8
+
9
+ # Define a nested Config class
10
+ class Config
11
+ include Singleton
12
+ attr_accessor :debug, :crystal_src_dir, :crystal_lib_dir, :crystal_main_file, :crystal_lib_name
13
+
14
+ def initialize
15
+ # Set default configuration options
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
21
+ end
22
+ end
23
+
24
+ def self.configure
25
+ setup
26
+ yield(config)
27
+ end
28
+ end
@@ -0,0 +1,40 @@
1
+ module CrystalRuby
2
+ module Typemaps
3
+ CRYSTAL_TYPE_MAP = {
4
+ :char => "Int8", # In Crystal, :char is typically represented as Int8
5
+ :uchar => "UInt8", # Unsigned char
6
+ :int8 => "Int8", # Same as :char
7
+ :uint8 => "UInt8", # Same as :uchar
8
+ :short => "Int16", # Short integer
9
+ :ushort => "UInt16", # Unsigned short integer
10
+ :int16 => "Int16", # Same as :short
11
+ :uint16 => "UInt16", # Same as :ushort
12
+ :int => "Int32", # Integer, Crystal defaults to 32 bits
13
+ :uint => "UInt32", # Unsigned integer
14
+ :int32 => "Int32", # 32-bit integer
15
+ :uint32 => "UInt32", # 32-bit unsigned integer
16
+ :long => "Int32 | Int64", # Long integer, size depends on the platform (32 or 64 bits)
17
+ :ulong => "UInt32 | UInt64", # Unsigned long integer, size depends on the platform
18
+ :int64 => "Int64", # 64-bit integer
19
+ :uint64 => "UInt64", # 64-bit unsigned integer
20
+ :long_long => "Int64", # Same as :int64
21
+ :ulong_long => "UInt64", # Same as :uint64
22
+ :float => "Float32", # Floating point number (single precision)
23
+ :double => "Float64", # Double precision floating point number
24
+ :bool => "Bool", # Boolean type
25
+ :void => "Void", # Void type
26
+ :string => "String" # String type
27
+ }
28
+
29
+ C_TYPE_MAP = CRYSTAL_TYPE_MAP.merge({
30
+ :string => "UInt8*"
31
+ })
32
+
33
+ C_TYPE_CONVERSIONS = {
34
+ :string => {
35
+ from: "String.new(%s)",
36
+ to: "%s.to_unsafe"
37
+ }
38
+ }
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crystalruby
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,226 @@
1
+ require 'ffi'
2
+ require 'digest'
3
+ require 'fileutils'
4
+ require 'method_source'
5
+ require_relative "crystalruby/config"
6
+ require_relative "crystalruby/version"
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
+
22
+ module CrystalRuby
23
+
24
+ # Define a method to set the @crystalize proc if it doesn't already exist
25
+ def crystalize(type=:src, **options, &block)
26
+ (args,), returns = options.first
27
+ 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
31
+
32
+
33
+ def method_added(method_name)
34
+ if @crystalize_next
35
+ attach_crystalized_method(method_name)
36
+ @crystalize_next = nil
37
+ end
38
+ super
39
+ end
40
+
41
+ def config
42
+ CrystalRuby.config
43
+ end
44
+
45
+ def attach_crystalized_method(method_name)
46
+
47
+ CrystalRuby.instantiate_crystal_ruby! unless CrystalRuby.instantiated?
48
+
49
+ function_body = instance_method(method_name).source.lines[
50
+ @crystalize_next[:raw] ? 2...-2 : 1...-1
51
+ ].join("\n")
52
+
53
+ fname = "#{name.downcase}_#{method_name}"
54
+ args, returns, block = @crystalize_next.values_at(:args, :returns, :block)
55
+ args ||= {}
56
+ @crystalize_next = nil
57
+ function = build_function(self, method_name, args, returns, function_body)
58
+
59
+ CrystalRuby.write_function(self, **function) do
60
+ extend FFI::Library
61
+ ffi_lib "#{config.crystal_lib_dir}/#{config.crystal_lib_name}"
62
+ 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
69
+
70
+ init!
71
+ end
72
+
73
+ [singleton_class, self].each do |receiver|
74
+ receiver.prepend(Module.new do
75
+ define_method(method_name) do |*args|
76
+ CrystalRuby.compile! unless CrystalRuby.compiled?
77
+ CrystalRuby.attach! unless CrystalRuby.attached?
78
+ super(*args)
79
+ end
80
+ end)
81
+ end
82
+ end
83
+
84
+ module_function
85
+
86
+
87
+ def build_function(owner, name, args, returns, body)
88
+ fnname = "#{owner.name.downcase}_#{name}"
89
+ args ||= {}
90
+ string_conversions = args.select { |_k, v| v.eql?(:string) }.keys
91
+ function_body = <<~CRYSTAL
92
+ module #{owner.name}
93
+ def self.#{name}(#{args.map { |k, v| "#{k} : #{native_type(v)}" }.join(',')}) : #{native_type(returns)}
94
+ #{body}
95
+ end
96
+ end
97
+
98
+ fun #{fnname}(#{args.map { |k, v| "_#{k}: #{lib_type(v)}" }.join(',')}): #{lib_type(returns)}
99
+ #{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)}
101
+ end
102
+ CRYSTAL
103
+
104
+ {
105
+ name: fnname,
106
+ body: function_body
107
+ }
108
+ end
109
+
110
+ def lib_type(type)
111
+ Typemaps::C_TYPE_MAP[type]
112
+ end
113
+
114
+ def native_type(type)
115
+ Typemaps::CRYSTAL_TYPE_MAP[type]
116
+ end
117
+
118
+ def convert_to_native_type(expr, outtype)
119
+ Typemaps::C_TYPE_CONVERSIONS[outtype] ? Typemaps::C_TYPE_CONVERSIONS[outtype][:from] % expr : expr
120
+ end
121
+
122
+ def convert_to_return_type(expr, outtype)
123
+ Typemaps::C_TYPE_CONVERSIONS[outtype] ? Typemaps::C_TYPE_CONVERSIONS[outtype][:to] % expr : expr
124
+ end
125
+
126
+ 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")
128
+ @instantiated = true
129
+ %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)
131
+ end
132
+ FileUtils.mkdir_p "#{config.crystal_src_dir}/generated"
133
+ FileUtils.mkdir_p "#{config.crystal_lib_dir}"
134
+ unless File.exist?("#{config.crystal_src_dir}/#{config.crystal_main_file}")
135
+ IO.write("#{config.crystal_src_dir}/#{config.crystal_main_file}", "require \"./generated/index\"\n")
136
+ end
137
+ unless File.exist?("#{config.crystal_src_dir}/shard.yml")
138
+ IO.write("#{config.crystal_src_dir}/shard.yml", <<~CRYSTAL)
139
+ name: src
140
+ version: 0.1.0
141
+ CRYSTAL
142
+ end
143
+ end
144
+
145
+ def self.instantiated?
146
+ @instantiated
147
+ end
148
+
149
+ def self.compiled?
150
+ @compiled
151
+ end
152
+
153
+ def self.attached?
154
+ !!@attached
155
+ end
156
+
157
+ def self.compile!
158
+ return unless @block_store
159
+ 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
166
+ CRYSTAL
167
+
168
+ index_content += @block_store.map do |function|
169
+ function_data = function[:body]
170
+ file_digest = Digest::MD5.hexdigest function_data
171
+ fname = function[:name]
172
+ "require \"./#{function[:owner].name}/#{fname}_#{file_digest}.cr\"\n"
173
+ end.join("\n")
174
+
175
+ File.write("#{config.crystal_src_dir}/generated/index.cr", index_content)
176
+ begin
177
+ lib_target = "#{Dir.pwd}/#{config.crystal_lib_dir}/#{config.crystal_lib_name}"
178
+ Dir.chdir(config.crystal_src_dir) do
179
+ config.debug ?
180
+ `crystal build -o #{lib_target} #{config.crystal_main_file}` :
181
+ `crystal build --release --no-debug -o #{lib_target} #{config.crystal_main_file}`
182
+ end
183
+
184
+ @compiled = true
185
+ rescue StandardError => e
186
+ puts 'Error compiling crystal code'
187
+ puts e
188
+ File.delete("#{config.crystal_src_dir}/generated/index.cr")
189
+ end
190
+ end
191
+
192
+ def self.attach!
193
+ @block_store.each do |function|
194
+ function[:compile_callback].call
195
+ end
196
+ @attached = true
197
+ end
198
+
199
+ def self.write_function(owner, name:, body:, &compile_callback)
200
+ @compiled = File.exist?("#{config.crystal_src_dir}/generated/index.cr") unless defined?(@compiled)
201
+ @block_store ||= []
202
+ @block_store << {owner: owner, name: name, body: body, compile_callback: compile_callback}
203
+ FileUtils.mkdir_p("#{config.crystal_src_dir}/generated")
204
+ existing = Dir.glob("#{config.crystal_src_dir}/generated/**/*.cr")
205
+ @block_store.each do |function|
206
+ owner_name = function[:owner].name
207
+ FileUtils.mkdir_p("#{config.crystal_src_dir}/generated/#{owner_name}")
208
+ function_data = function[:body]
209
+ fname = function[:name]
210
+ file_digest = Digest::MD5.hexdigest function_data
211
+ filename = "#{config.crystal_src_dir}/generated/#{owner_name}/#{fname}_#{file_digest}.cr"
212
+ unless existing.delete(filename)
213
+ @compiled = false
214
+ @attached = false
215
+ File.write(filename, function_data)
216
+ end
217
+ existing.select do |f|
218
+ f =~ %r{#{config.crystal_src_dir}/generated/#{owner_name}/#{fname}_[a-f0-9]{32}\.cr}
219
+ end.each do |fl|
220
+ File.delete(fl) unless fl.eql?(filename)
221
+ end
222
+ end
223
+ end
224
+ end
225
+
226
+ require_relative "module"
data/lib/module.rb ADDED
@@ -0,0 +1,3 @@
1
+ class Module
2
+ prepend CrystalRuby
3
+ end
@@ -0,0 +1,4 @@
1
+ module Crystalruby
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: crystalruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Wouter Coppieters
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-04-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: digest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: fileutils
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: method_source
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Embed Crystal code directly in Ruby.
70
+ email:
71
+ - wc@pico.net.nz
72
+ executables:
73
+ - crystalruby
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".rubocop.yml"
78
+ - CHANGELOG.md
79
+ - CODE_OF_CONDUCT.md
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - crystalruby.gemspec
84
+ - exe/crystalruby
85
+ - lib/crystalruby.rb
86
+ - lib/crystalruby/config.rb
87
+ - lib/crystalruby/typemaps.rb
88
+ - lib/crystalruby/version.rb
89
+ - lib/module.rb
90
+ - sig/crystalruby.rbs
91
+ homepage: https://github.com/wouterken/crystalruby
92
+ licenses:
93
+ - MIT
94
+ metadata:
95
+ homepage_uri: https://github.com/wouterken/crystalruby
96
+ source_code_uri: https://github.com/wouterken/crystalruby
97
+ changelog_uri: https://github.com/wouterken/crystalruby/CHANGELOG.md
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 3.0.0
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubygems_version: 3.5.6
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: Embed Crystal code directly in Ruby.
117
+ test_files: []