├── logo.png ├── lib ├── crystalruby │ ├── version.rb │ ├── templates │ │ ├── inline_chunk.cr │ │ ├── top_level_ruby_interface.cr │ │ ├── ruby_interface.cr │ │ ├── top_level_function.cr │ │ ├── function.cr │ │ └── index.cr │ ├── types │ │ ├── primitive_types │ │ │ ├── bool.rb │ │ │ ├── nil.rb │ │ │ ├── time.rb │ │ │ ├── nil.cr │ │ │ ├── bool.cr │ │ │ ├── time.cr │ │ │ ├── numbers.rb │ │ │ ├── numbers.cr │ │ │ ├── symbol.rb │ │ │ └── symbol.cr │ │ ├── variable_width.cr │ │ ├── primitive.cr │ │ ├── variable_width │ │ │ ├── string.rb │ │ │ ├── string.cr │ │ │ ├── array.cr │ │ │ ├── array.rb │ │ │ ├── hash.rb │ │ │ └── hash.cr │ │ ├── variable_width.rb │ │ ├── fixed_width │ │ │ ├── tagged_union.cr │ │ │ ├── proc.cr │ │ │ ├── tuple.cr │ │ │ ├── tuple.rb │ │ │ ├── named_tuple.cr │ │ │ ├── named_tuple.rb │ │ │ ├── tagged_union.rb │ │ │ └── proc.rb │ │ ├── type.cr │ │ ├── concerns │ │ │ └── allocator.rb │ │ ├── primitive.rb │ │ ├── fixed_width.cr │ │ ├── fixed_width.rb │ │ └── type.rb │ ├── typebuilder.rb │ ├── template.rb │ ├── types.rb │ ├── arc_mutex.rb │ ├── compilation.rb │ ├── config.rb │ ├── source_reader.rb │ ├── typemaps.rb │ ├── reactor.rb │ └── adapter.rb └── crystalruby.rb ├── sig └── crystalruby.rbs ├── .dockerignore ├── bin ├── setup └── console ├── .gitignore ├── examples └── adder │ └── adder.rb ├── Rakefile ├── test ├── test_crystalruby_version.rb ├── test_all.rb ├── test_ruby_wrapped_crystalized_methods.rb ├── test_raw_methods.rb ├── test_exception_handling.rb ├── test_helper.rb ├── types │ ├── test_bool.rb │ ├── test_time.rb │ ├── test_numbers.rb │ ├── test_symbol.rb │ ├── test_proc.rb │ ├── test_string.rb │ ├── test_named_tuple.rb │ ├── test_enumerable.rb │ ├── test_tuple.rb │ ├── test_hash.rb │ └── test_array.rb ├── test_performance.rb ├── test_multi_compile.rb ├── test_top_level_crystal.rb ├── test_multi_lib.rb ├── test_async_methods.rb ├── test_inline_crystal_blocks.rb ├── test_expose_to_crystal.rb ├── test_gc_active.rb ├── test_gc_stress.rb ├── test_crystalize_dsl.rb ├── test_instance.rb ├── test_type_dsl.rb └── test_type_transforms.rb ├── .rubocop.yml ├── Gemfile ├── CHANGELOG.md ├── LICENSE.txt ├── Dockerfile ├── crystalruby.gemspec ├── exe └── crystalruby ├── Gemfile.lock └── CODE_OF_CONDUCT.md /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterken/crystalruby/HEAD/logo.png -------------------------------------------------------------------------------- /lib/crystalruby/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CrystalRuby 4 | VERSION = "0.3.4" 5 | end 6 | -------------------------------------------------------------------------------- /sig/crystalruby.rbs: -------------------------------------------------------------------------------- 1 | module Crystalruby 2 | VERSION: String 3 | # See the writing guide of rbs: https://github.com/ruby/rbs#guides 4 | end 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | crystalruby-*.gem 10 | /crystalruby/ 11 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | crystalruby-*.gem 10 | /crystalruby/ 11 | .DS_Store 12 | .mise.toml 13 | -------------------------------------------------------------------------------- /examples/adder/adder.rb: -------------------------------------------------------------------------------- 1 | require "crystalruby" 2 | 3 | module Adder 4 | crystallize :int 5 | def add(a: :int, b: :int) 6 | a + b 7 | end 8 | end 9 | 10 | puts Adder.add(1, 2) 11 | -------------------------------------------------------------------------------- /lib/crystalruby/templates/inline_chunk.cr: -------------------------------------------------------------------------------- 1 | # This is the template used for writing inline chunks of Crystal code without direct 2 | # Ruby integration 3 | 4 | %{mod_or_class} %{module_name} %{superclass} 5 | %{body} 6 | end 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | 5 | require "rubocop/rake_task" 6 | 7 | RuboCop::RakeTask.new 8 | 9 | task default: %i[test rubocop] 10 | 11 | task :test do 12 | require_relative "test/test_all" 13 | end 14 | -------------------------------------------------------------------------------- /test/test_crystalruby_version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | class TestCrystalRubyVersion < Minitest::Test 6 | def test_that_it_has_a_version_number 7 | refute_nil ::CrystalRuby::VERSION 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 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 14 | -------------------------------------------------------------------------------- /test/test_all.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dir["#{__dir__}/**/test_*.rb"].each do |file| 4 | require_relative file unless File.basename(file) == "test_all.rb" 5 | end 6 | 7 | require_relative "test_helper" 8 | require "minitest/reporters" 9 | Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new()] 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in crystalruby.gemspec 6 | gemspec 7 | 8 | gem 'ffi' 9 | gem "rake", "~> 13.0" 10 | 11 | gem "minitest", "~> 5.16" 12 | gem "minitest-reporters", "~> 1.4" 13 | 14 | gem "rubocop", "~> 1.21" 15 | gem "debug", ">= 1.1.0" 16 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "debug" 6 | 7 | require "crystalruby" 8 | # You can add fixtures and/or initialization code here to make experimenting 9 | # with your gem easier. You can also use a different console, if you like. 10 | 11 | require "irb" 12 | IRB.start(__FILE__) 13 | -------------------------------------------------------------------------------- /lib/crystalruby/types/primitive_types/bool.rb: -------------------------------------------------------------------------------- 1 | module CrystalRuby::Types 2 | Bool = Primitive.build(:Bool, convert_if: [::TrueClass, ::FalseClass], ffi_type: :uint8, memsize: 1) do 3 | def value(native: false) 4 | super == 1 5 | end 6 | 7 | def value=(val) 8 | !!val && val != 0 ? super(1) : super(0) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/crystalruby/types/primitive_types/nil.rb: -------------------------------------------------------------------------------- 1 | module CrystalRuby::Types 2 | Nil = Primitive.build(:Nil, convert_if: [::NilClass], memsize: 0) do 3 | def initialize(val = nil) 4 | super 5 | @value = 0 6 | end 7 | 8 | def nil? 9 | true 10 | end 11 | 12 | def value(native: false) 13 | nil 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/crystalruby/types/variable_width.cr: -------------------------------------------------------------------------------- 1 | module CrystalRuby 2 | module Types 3 | class VariableWidth < FixedWidth 4 | 5 | def self.size_offset 6 | 4 7 | end 8 | 9 | def self.data_offset 10 | 8 11 | end 12 | 13 | def self.memsize 14 | 8 15 | end 16 | 17 | def variable_width? 18 | true 19 | end 20 | 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/crystalruby/types/primitive.cr: -------------------------------------------------------------------------------- 1 | module CrystalRuby 2 | module Types 3 | class Primitive < Type 4 | def return_value 5 | @value 6 | end 7 | 8 | def native 9 | value 10 | end 11 | 12 | def self.fetch_single(pointer : Pointer(::UInt8)) 13 | new(pointer) 14 | end 15 | 16 | def self.refsize 17 | self.memsize 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/test_ruby_wrapped_crystalized_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | class TestRubyWrappedCrystalizedMethods < Minitest::Test 6 | module MyModule 7 | crystallize ->{ :int32 } do |a, b| 8 | result = super(a.to_i, b.to_i) 9 | result + 1 10 | end 11 | def add(a: :int32, b: :int32) 12 | a + b 13 | end 14 | end 15 | 16 | def test_ruby_wrapped 17 | assert MyModule.add("1", "2") == 4 18 | assert MyModule.add(300, "5") == 306 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.3.4] - 2025-05-04 2 | 3 | - Added support for Crystal 1.16.0 4 | 5 | ## [Unreleased] 6 | 7 | - Added support for running `crustalruby` without the `crystal` binary in dry mode. [#15] 8 | 9 | ## [0.1.4] - 2024-04-10 10 | 11 | - Fix bug in type checking on deserialization of union types 12 | 13 | ## [0.1.3] - 2024-04-10 14 | 15 | - Support exceptions thrown in Crystal being caught in Ruby 16 | - Support complex Ruby type passing (while preserving type checking), using `JSON` as serialization format. 17 | 18 | ## [0.1.0] - 2024-04-07 19 | 20 | - Initial release 21 | -------------------------------------------------------------------------------- /lib/crystalruby/types/primitive_types/time.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "date" 4 | 5 | module CrystalRuby::Types 6 | Time = Primitive.build(:Time, convert_if: [Root::Time, Root::String, DateTime], ffi_type: :double) do 7 | def initialize(val = Root::Time.now) 8 | super 9 | end 10 | 11 | def value=(val) 12 | super( 13 | if val.respond_to?(:to_time) 14 | val.to_time.to_f 15 | else 16 | val.respond_to?(:to_f) ? val.to_f : 0 17 | end 18 | ) 19 | end 20 | 21 | def value(native: false) 22 | ::Time.at(super) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/test_raw_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | class TestRawMethods < Minitest::Test 6 | def test_raw_methods 7 | 8 | Adder.class_eval do 9 | crystallize :int32, raw: true 10 | def add_raw(a: :int, b: :int) 11 | <<~CRYSTAL 12 | c = 0_u64 13 | a + b + c 14 | CRYSTAL 15 | end 16 | 17 | crystallize :int32, raw: true 18 | def add_raw_endless(a: :int, b: :int) = "c = 0_u64 19 | a + b + c" 20 | end 21 | 22 | assert Adder.add_raw(1, 2) == 3 23 | assert Adder.add_raw_endless(1, 2) == 3 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/test_exception_handling.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | class TestExceptionHandling < Minitest::Test 6 | module Exceptional 7 | crystallize 8 | def throws(a: :int32, b: :int32, returns: :int32) 9 | raise "Exception" 10 | a + b 11 | end 12 | 13 | crystallize 14 | def for_type_error(a: Int32) 15 | puts "Expecting a Hash(String, String)" 16 | end 17 | end 18 | 19 | def test_exception_handling 20 | assert_raises(RuntimeError) { Exceptional.throws(1, 2) } 21 | assert_raises(TypeError) { Exceptional.for_type_error("Test") } 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/crystalruby/types/primitive_types/nil.cr: -------------------------------------------------------------------------------- 1 | class <%= base_crystal_class_name %> < CrystalRuby::Types::Primitive 2 | 3 | property value : ::Nil = nil 4 | 5 | def initialize(nilval : ::Nil) 6 | end 7 | 8 | def initialize(ptr : Pointer(::UInt8)) 9 | end 10 | 11 | def initialize(raw : UInt8) 12 | end 13 | 14 | def value : ::Nil 15 | nil 16 | end 17 | 18 | def ==(other : ::Nil) 19 | value.nil? 20 | end 21 | 22 | def value=(val : ::Nil) 23 | end 24 | 25 | def self.memsize 26 | 0 27 | end 28 | 29 | def return_value 30 | 0_u8 31 | end 32 | 33 | def self.write_single(pointer : Pointer(::UInt8), value) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__) 4 | require "debug" 5 | require "crystalruby" 6 | require "minitest/autorun" 7 | 8 | Minitest.parallel_executor = Struct.new(:shutdown).new(nil) 9 | 10 | CrystalRuby.configure do |config| 11 | config.verbose = false 12 | config.log_level = :warn 13 | config.colorize_log_output = true 14 | config.debug = true 15 | config.single_thread_mode = !!ENV["CRYSTAL_RUBY_SINGLE_THREAD_MODE"] 16 | end 17 | 18 | FileUtils.rm_rf File.expand_path("./crystalruby") if ENV["RESET_CRYSTALRUBY_COMPILE_CACHE"] 19 | CrystalRuby.initialize_crystal_ruby! 20 | 21 | module Adder; end 22 | -------------------------------------------------------------------------------- /lib/crystalruby/types/variable_width/string.rb: -------------------------------------------------------------------------------- 1 | module CrystalRuby::Types 2 | String = VariableWidth.build(:String, ffi_primitive: :string, convert_if: [String, Root::String]) do 3 | def self.cast!(rbval) 4 | rbval.to_s 5 | end 6 | 7 | def self.copy_to!(rbval, memory:) 8 | data_pointer = malloc(rbval.bytesize) 9 | data_pointer.write_string(rbval) 10 | memory[size_offset].write_uint32(rbval.size) 11 | memory[data_offset].write_pointer(data_pointer) 12 | end 13 | 14 | def value(native: false) 15 | # Strings in Crystal are UTF-8 encoded by default 16 | data_pointer.read_string(size).force_encoding("UTF-8") 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/types/test_bool.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | class TestBool < Minitest::Test 6 | class BoolClass < CRType{ Bool } 7 | end 8 | 9 | def test_it_acts_like_a_bool 10 | bl = BoolClass[true] 11 | assert_equal bl, true 12 | bl = BoolClass[false] 13 | assert_equal bl, false 14 | end 15 | 16 | def test_it_works_with_nil 17 | bl = BoolClass[nil] 18 | assert_equal bl, false 19 | end 20 | 21 | crystallize 22 | def negate_bool(value: Bool, returns: Bool) 23 | !value 24 | end 25 | 26 | def test_it_negates 27 | assert_equal negate_bool(true), false 28 | assert_equal negate_bool(false), true 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /lib/crystalruby/typebuilder.rb: -------------------------------------------------------------------------------- 1 | require_relative "types" 2 | 3 | module CrystalRuby 4 | module TypeBuilder 5 | module_function 6 | 7 | def build_from_source(src, context: ) 8 | source_type = Types.with_binding_fallback(context) do |binding| 9 | eval(src.is_a?(String) ? src : SourceReader.extract_source_from_proc(src), binding) 10 | end 11 | 12 | return source_type if source_type.is_a?(Types::Root::Symbol) 13 | 14 | unless source_type.kind_of?(Class) && source_type < Types::Type 15 | raise "Invalid type #{source_type.inspect}" 16 | end 17 | 18 | return source_type unless source_type.anonymous? 19 | 20 | source_type.tap do |new_type| 21 | Types::Type.validate!(new_type) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/crystalruby/template.rb: -------------------------------------------------------------------------------- 1 | module CrystalRuby 2 | module Template 3 | class Renderer < Struct.new(:raw_value) 4 | require 'erb' 5 | def render(context) 6 | if context.kind_of?(::Hash) 7 | raw_value % context 8 | else 9 | ERB.new(raw_value, trim_mode: "%").result(context) 10 | end 11 | end 12 | end 13 | 14 | ( 15 | Dir[File.join(File.dirname(__FILE__), "templates", "**", "*.cr")] + 16 | Dir[File.join(File.dirname(__FILE__), "types", "**", "*.cr")] 17 | ).each do |file| 18 | template_name = File.basename(file, File.extname(file)).split("_").map(&:capitalize).join 19 | template_value = File.read(file) 20 | const_set(template_name, Renderer.new(template_value)) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/crystalruby/types/primitive_types/bool.cr: -------------------------------------------------------------------------------- 1 | class <%= base_crystal_class_name %> < CrystalRuby::Types::Primitive 2 | 3 | def initialize(value : ::Bool) 4 | @value = value ? 1_u8 : 0_u8 5 | end 6 | 7 | def initialize(ptr : Pointer(::UInt8)) 8 | @value = ptr[0] 9 | end 10 | 11 | def initialize(value : UInt8) 12 | @value = value 13 | end 14 | 15 | def value=(value : ::Bool) 16 | @value = value ? 1_u8 : 0_u8 17 | end 18 | 19 | def value : <%= native_type_expr %> 20 | @value == 1_u8 21 | end 22 | 23 | def ==(other : ::Bool) 24 | value == other 25 | end 26 | 27 | def self.memsize 28 | <%= memsize %> 29 | end 30 | 31 | def self.write_single(pointer : Pointer(::UInt8), value) 32 | pointer.as(Pointer(::UInt8)).value = value ? 1_u8 : 0_u8 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/test_performance.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | require "benchmark" 5 | require "crystalruby" 6 | 7 | class TestPerformance < Minitest::Test 8 | module PrimeCounter 9 | 10 | crystallize 11 | def count_primes_upto_cr n: :int32, returns: :int32 12 | (2..n).each.count do |i| 13 | is_prime = true 14 | (2..Math.sqrt(i).to_i).each do |j| 15 | if i % j == 0 16 | is_prime = false 17 | break 18 | end 19 | end 20 | is_prime 21 | end 22 | end 23 | end 24 | 25 | include PrimeCounter 26 | def test_performance 27 | count_primes_upto_cr(0) # Compile 28 | assert Benchmark.realtime { 29 | count_primes_upto_cr(1_000_000) 30 | } < 2 # Not a robust test. May fail on older or emulated devices 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/types/test_time.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | class TestTime < Minitest::Test 6 | class TimeClass < CRType { Time } 7 | end 8 | 9 | def test_it_acts_like_a_time 10 | tm = TimeClass.new(0) 11 | assert_equal tm, Time.at(0) 12 | end 13 | 14 | def test_it_can_turn_into_a_timestamp 15 | tm = TimeClass.new 16 | assert (tm.to_f - Time.now.to_f) >= -1 17 | end 18 | 19 | crystallize 20 | def time_diff(time1: Time, time2: Time, returns: Float64) 21 | (time1 - time2).to_f 22 | end 23 | 24 | crystallize 25 | def one_day_from(time: Time, returns: Time) 26 | time + (24 * 60 * 60).seconds 27 | end 28 | 29 | def test_it_can_do_time_math 30 | assert_equal time_diff(Time.at(100), Time.at(50)), 50.0 31 | assert_equal one_day_from(Time.at(0)), Time.at(24 * 60 * 60) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/crystalruby/types/primitive_types/time.cr: -------------------------------------------------------------------------------- 1 | class <%= base_crystal_class_name %> < CrystalRuby::Types::Primitive 2 | 3 | def initialize(time : ::Time) 4 | @value = time.to_unix_ns / 1000_000_000.0 5 | end 6 | 7 | def initialize(ptr : Pointer(::UInt8)) 8 | @value = ptr.as(Pointer(::Float64)).value 9 | end 10 | 11 | def initialize(@value : ::Float64) 12 | end 13 | 14 | def ==(other : ::Time) 15 | value == other 16 | end 17 | 18 | def value=(time : ::Time) 19 | @value = time.to_unix_ns / 1000_000_000.0 20 | end 21 | 22 | def value : ::Time 23 | ::Time.unix_ns((@value * 1000_000_000).to_i128) 24 | end 25 | 26 | def self.memsize 27 | <%= memsize %> 28 | end 29 | 30 | 31 | def self.write_single(pointer : Pointer(::UInt8), time) 32 | pointer.as(Pointer(::Float64)).value = time.to_unix_ns / 1000_000_000.0 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /lib/crystalruby/types/primitive_types/numbers.rb: -------------------------------------------------------------------------------- 1 | module CrystalRuby::Types 2 | %i[UInt8 UInt16 UInt32 UInt64 Int8 Int16 Int32 Int64 Float32 Float64].each do |type_name| 3 | ffi_type = CrystalRuby::Typemaps::FFI_TYPE_MAP.fetch("::#{type_name}") 4 | const_set(type_name, Primitive.build(type_name, convert_if: [::Numeric], ffi_type: ffi_type, ffi_primitive: ffi_type) do 5 | def value=(val) 6 | raise "Expected a numeric value, got #{val}" unless val.is_a?(::Numeric) 7 | 8 | super(typename.to_s.start_with?("Float") ? val.to_f : val.to_i) 9 | end 10 | 11 | def value(native: false) 12 | @value 13 | end 14 | 15 | def self.from_ffi_array_repr(value) 16 | value 17 | end 18 | 19 | def self.numeric? 20 | true 21 | end 22 | 23 | def self.template_name 24 | "Numbers" 25 | end 26 | end) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/crystalruby/types/primitive_types/numbers.cr: -------------------------------------------------------------------------------- 1 | class <%= base_crystal_class_name %> < CrystalRuby::Types::Primitive 2 | 3 | def initialize(value : <%= native_type_expr %>) 4 | @value = value 5 | end 6 | 7 | def initialize(ptr : Pointer(::UInt8)) 8 | @value = ptr.as(Pointer( <%= native_type_expr %>))[0] 9 | end 10 | 11 | def value 12 | @value 13 | end 14 | 15 | def ==(other : <%= native_type_expr %>) 16 | value == other 17 | end 18 | 19 | def self.memsize 20 | <%= memsize %> 21 | end 22 | 23 | def value=(value : <%= native_type_expr %>) 24 | @value = value 25 | end 26 | 27 | def self.copy_to!(value : <%= native_type_expr %>, ptr : Pointer(::UInt8)) 28 | ptr.as(Pointer( <%= native_type_expr %>))[0] = value 29 | end 30 | 31 | # Write a data type into a pointer at a given index 32 | # (Type can be a byte-array, pointer or numeric type) 33 | def self.write_single(pointer : Pointer(::UInt8), value) 34 | pointer.as(Pointer( <%= native_type_expr %>)).value = value 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /test/test_multi_compile.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | class TestMultiCompile < Minitest::Test 5 | module MultiCompile 6 | end 7 | 8 | include MultiCompile 9 | 10 | def test_inline_crystal 11 | MultiCompile.class_eval do 12 | crystallize :int32, lib: "multi-compile" 13 | def add(a: :int32, b: :int32) 14 | a + b 15 | end 16 | end 17 | 18 | CrystalRuby::Library["multi-compile"].build! 19 | MultiCompile.add(1, 3) 20 | 21 | MultiCompile.class_eval do 22 | crystallize :int32, lib: "multi-compile" 23 | def mult(a: :int32, b: :int32) 24 | a * b 25 | end 26 | end 27 | 28 | CrystalRuby::Library["multi-compile"].build! 29 | 30 | MultiCompile.class_eval do 31 | crystallize -> { Int32 } 32 | def sub(a: Int32, b: Int32) 33 | a - b 34 | end 35 | end 36 | 37 | CrystalRuby::Library["multi-compile-2"].build! 38 | assert_equal MultiCompile.sub(4, 2), 2 39 | assert_equal MultiCompile.add(4, 2), 6 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/test_top_level_crystal.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | require "benchmark" 5 | 6 | crystal do 7 | TOP_LEVEL_CONSTANT = "At the very top!" 8 | 9 | def top_level_method 10 | TOP_LEVEL_CONSTANT 11 | end 12 | end 13 | 14 | crystallize :int, raw: true 15 | def top_level_crystallized_method 16 | %Q{ 88 + 12 } 17 | end 18 | 19 | expose_to_crystal ->{ Int32 } 20 | def top_level_ruby_method 21 | 33 22 | end 23 | 24 | crystallize ->{ Int32 } 25 | def call_top_level_ruby_method 26 | top_level_ruby_method 27 | end 28 | 29 | class TestTopLevelCrystal < Minitest::Test 30 | module AccessTopLevelCrystal 31 | crystallize :string 32 | def access_top_level_constant 33 | top_level_method 34 | end 35 | end 36 | 37 | def test_top_level_crystal 38 | assert AccessTopLevelCrystal.access_top_level_constant == "At the very top!" 39 | assert top_level_crystallized_method == 100 40 | end 41 | 42 | def test_top_level_exposed_ruby 43 | assert_equal call_top_level_ruby_method, 33 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 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. 22 | -------------------------------------------------------------------------------- /lib/crystalruby/types/variable_width/string.cr: -------------------------------------------------------------------------------- 1 | class <%= base_crystal_class_name %> < CrystalRuby::Types::VariableWidth 2 | 3 | def initialize(string : ::String) 4 | @memory = malloc(data_offset + 8) 5 | self.value = string 6 | increment_ref_count! 7 | end 8 | 9 | def self.copy_to!(value : ::String, memory : Pointer(::UInt8)) 10 | data_pointer = malloc(value.bytesize.to_u32).as(Pointer(::UInt8)) 11 | data_pointer.copy_from(value.to_unsafe, value.bytesize) 12 | (memory+size_offset).as(Pointer(::UInt32)).value = value.bytesize.to_u32 13 | (memory+data_offset).as(Pointer(::UInt64)).value = data_pointer.address 14 | end 15 | 16 | def value=(string : ::String) 17 | if self.ref_count > 0 18 | self.class.decr_inner_ref_counts!(memory) 19 | end 20 | self.class.copy_to!(string, self.memory) 21 | end 22 | 23 | def ==(other : <%= native_type_expr %>) 24 | native == other 25 | end 26 | 27 | def value : ::String 28 | char_ptr = (memory + data_offset).as(Pointer(Pointer(::UInt8))) 29 | size = (memory + size_offset).as(Pointer(::UInt32)) 30 | ::String.new(char_ptr[0], size[0]) 31 | end 32 | 33 | def native : ::String 34 | value 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/crystalruby/types.rb: -------------------------------------------------------------------------------- 1 | 2 | module CrystalRuby 3 | module Types 4 | # Store references to root types so that we can reference these in places where 5 | # we load the CrystalRuby equivalents into the global namespace 6 | module Root 7 | Symbol = ::Symbol 8 | String = ::String 9 | Array = ::Array 10 | Hash = ::Hash 11 | Time = ::Time 12 | end 13 | 14 | def self.const_missing(const_name) 15 | return @fallback.const_get(const_name) if @fallback&.const_defined?(const_name) 16 | super 17 | end 18 | 19 | def self.method_missing(method_name, *args) 20 | return @fallback.send(method_name, *args) if @fallback&.method_defined?(method_name) 21 | super 22 | end 23 | 24 | def self.with_binding_fallback(fallback) 25 | @fallback, previous_fallback = fallback, @fallback 26 | @fallback = @fallback.class unless @fallback.kind_of?(Module) 27 | yield binding 28 | ensure 29 | @fallback = previous_fallback 30 | end 31 | end 32 | end 33 | require_relative "types/concerns/allocator" 34 | require_relative "types/type" 35 | require_relative "types/primitive" 36 | require_relative "types/fixed_width" 37 | require_relative "types/variable_width" 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Use the Crystal official image to build Crystal 2 | FROM crystallang/crystal:1.14 AS crystal-builder 3 | 4 | # Stage 2: Use the Ruby image and copy Crystal from the previous stage 5 | FROM ruby:3.3 6 | 7 | MAINTAINER Wouter Coppieters 8 | 9 | # Install necessary dependencies for Ruby and Crystal 10 | RUN apt-get update && apt-get install -y \ 11 | curl \ 12 | gnupg2 \ 13 | software-properties-common \ 14 | build-essential \ 15 | lsb-release 16 | 17 | # Copy Crystal binaries and libraries from the Crystal image 18 | COPY --from=crystal-builder /usr/share/crystal /usr/share/crystal 19 | COPY --from=crystal-builder /usr/lib/crystal /usr/lib/crystal 20 | COPY --from=crystal-builder /usr/bin/crystal /usr/bin/crystal 21 | COPY --from=crystal-builder /usr/bin/shards /usr/bin/shards 22 | 23 | # Set the working directory 24 | WORKDIR /usr/src/app 25 | 26 | # Copy the Ruby dependencies 27 | COPY Gemfile Gemfile.lock ./ 28 | COPY crystalruby.gemspec ./ 29 | COPY lib/crystalruby/version.rb ./lib/crystalruby/version.rb 30 | 31 | # Install Ruby dependencies 32 | RUN bundle install 33 | 34 | # Copy the rest of your application 35 | COPY . . 36 | 37 | # Define the command to run your application 38 | CMD ["bundle", "exec", "irb"] 39 | -------------------------------------------------------------------------------- /lib/crystalruby/arc_mutex.rb: -------------------------------------------------------------------------------- 1 | module CrystalRuby 2 | module LibC 3 | extend FFI::Library 4 | ffi_lib "c" 5 | class PThreadMutexT < FFI::Struct 6 | layout :__align, :int64, :__size, :char, 40 7 | end 8 | 9 | attach_function :pthread_mutex_init, [PThreadMutexT.by_ref, :pointer], :int 10 | attach_function :pthread_mutex_lock, [PThreadMutexT.by_ref], :int 11 | attach_function :pthread_mutex_unlock, [PThreadMutexT.by_ref], :int 12 | end 13 | 14 | class ArcMutex 15 | def phtread_mutex 16 | @phtread_mutex ||= init_mutex! 17 | end 18 | 19 | def synchronize 20 | lock 21 | yield 22 | unlock 23 | end 24 | 25 | def to_ptr 26 | phtread_mutex.pointer 27 | end 28 | 29 | def init_mutex! 30 | mutex = LibC::PThreadMutexT.new 31 | res = LibC.pthread_mutex_init(mutex, nil) 32 | raise "Failed to initialize mutex" unless res.zero? 33 | 34 | mutex 35 | end 36 | 37 | def lock 38 | res = LibC.pthread_mutex_lock(phtread_mutex) 39 | raise "Failed to lock mutex" unless res.zero? 40 | end 41 | 42 | def unlock 43 | res = LibC.pthread_mutex_unlock(phtread_mutex) 44 | raise "Failed to unlock mutex" unless res.zero? 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/crystalruby/templates/top_level_ruby_interface.cr: -------------------------------------------------------------------------------- 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 | 6 | module TopLevelCallbacks 7 | class_property %{fn_name}_callback : Proc(%{lib_fn_types} %{lib_fn_ret_type})? 8 | end 9 | 10 | def %{fn_scope}%{fn_name}(%{fn_args}) : %{fn_ret_type} 11 | %{convert_lib_args} 12 | cb = TopLevelCallbacks.%{fn_name}_callback 13 | unless cb.nil? 14 | callback_done_channel = Channel(Nil).new 15 | return_value = nil 16 | if Fiber.current == Thread.current.main_fiber 17 | return_value = cb.call(%{lib_fn_arg_names}) 18 | return %{convert_return_type} 19 | else 20 | CrystalRuby.queue_callback(->{ 21 | return_value = cb.call(%{lib_fn_arg_names}) 22 | callback_done_channel.send(nil) 23 | }) 24 | end 25 | callback_done_channel.receive 26 | return %{convert_return_type} 27 | end 28 | raise "No callback registered for %{fn_name}" 29 | end 30 | 31 | fun register_%{fn_name}_callback(callback : Proc(%{lib_fn_types} %{lib_fn_ret_type})) : Void 32 | TopLevelCallbacks.%{fn_name}_callback = callback 33 | end 34 | -------------------------------------------------------------------------------- /lib/crystalruby/templates/ruby_interface.cr: -------------------------------------------------------------------------------- 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 | 6 | %{module_or_class} %{module_name} %{superclass} 7 | def %{fn_scope}%{fn_name}(%{fn_args}) : %{fn_ret_type} 8 | %{convert_lib_args} 9 | cb = %{module_name}.%{callback_name} 10 | unless cb.nil? 11 | callback_done_channel = Channel(Nil).new 12 | return_value = nil 13 | if Fiber.current == Thread.current.main_fiber 14 | return_value = cb.call(%{lib_fn_arg_names}) 15 | return %{convert_return_type} 16 | else 17 | CrystalRuby.queue_callback(->{ 18 | return_value = cb.call(%{lib_fn_arg_names}) 19 | callback_done_channel.send(nil) 20 | }) 21 | end 22 | callback_done_channel.receive 23 | return %{convert_return_type} 24 | end 25 | raise "No callback registered for %{fn_name}" 26 | 27 | end 28 | 29 | class_property %{callback_name} : Proc(%{lib_fn_types} %{lib_fn_ret_type})? 30 | end 31 | 32 | fun register_%{callback_name}(callback : Proc(%{lib_fn_types} %{lib_fn_ret_type})) : Void 33 | %{module_name}.%{callback_name} = callback 34 | end 35 | -------------------------------------------------------------------------------- /test/types/test_numbers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | class TestNumbers < Minitest::Test 6 | class Int32Class < CRType{ Int32 } 7 | end 8 | 9 | def test_int32 10 | assert_equal 4, Int32Class.memsize 11 | assert_equal 4, Int32Class.memsize 12 | assert_equal 4, Int32Class.new(0).memsize 13 | assert_equal 4, Int32Class.new(4).value 14 | assert Int32Class.new(0).primitive? 15 | end 16 | 17 | crystallize 18 | def can_take_anon_int32(value: Int32) 19 | value * 2 20 | end 21 | 22 | crystallize 23 | def can_take_named_int32(value: Int32Class) 24 | value * 2 25 | end 26 | 27 | crystallize 28 | def can_return_named_int32(value: UInt64, returns: Int32Class) 29 | return (value * 2).to_i32 30 | end 31 | 32 | crystallize 33 | def can_return_anonymous_int32(value: UInt64, returns: Int32) 34 | return (value * 2).to_i32 35 | end 36 | 37 | def test_can_take_anon_int32 38 | assert can_take_anon_int32(2) || true 39 | end 40 | 41 | def test_can_take_named_int32 42 | assert can_take_named_int32(2) || true 43 | i32 = Int32Class.new(2) 44 | assert can_take_named_int32(i32) || true 45 | end 46 | 47 | def test_can_return_named_int32 48 | assert_equal can_return_named_int32(Int32Class[15]), 30 49 | end 50 | 51 | def test_can_return_anonymous_int32 52 | assert_equal can_return_anonymous_int32(15), 30 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/test_multi_lib.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | class TestMultiLib < Minitest::Test 6 | def test_two_libs_one_module 7 | Object.const_set(:AdderLib, Module.new {}) 8 | 9 | AdderLib.class_eval do 10 | crystallize :int, async: false, lib: "adder" 11 | def add(a: :int, b: :int) 12 | a + b 13 | end 14 | 15 | crystallize :int, async: false, lib: "adder-2" 16 | def add_v2(a: :int, b: :int) 17 | a + b 18 | end 19 | end 20 | 21 | assert AdderLib.add(1, 2) == AdderLib.add_v2(1, 2) 22 | end 23 | 24 | def test_ropen_two_libs_one_module 25 | Object.const_set(:MathLib, Module.new {}) 26 | 27 | MathLib.class_eval do 28 | crystallize :int, async: true, lib: "math" 29 | def add(a: :int, b: :int) 30 | a + b 31 | end 32 | 33 | crystallize :int, async: true, lib: "math-2" 34 | def add_v2(a: :int, b: :int) 35 | a + b 36 | end 37 | end 38 | 39 | assert MathLib.add(1, 2) == MathLib.add_v2(1, 2) 40 | 41 | MathLib.class_eval do 42 | crystallize :int, async: true, lib: "math-lib" 43 | def mult(a: :int, b: :int) 44 | a + b 45 | end 46 | 47 | crystallize :int, async: true, lib: "math-lib-2" 48 | def mult_v2(a: :int, b: :int) 49 | a + b 50 | end 51 | end 52 | 53 | assert MathLib.mult(14, 2) == MathLib.mult_v2(14, 2) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/crystalruby/types/primitive_types/symbol.rb: -------------------------------------------------------------------------------- 1 | module CrystalRuby::Types 2 | Symbol = Primitive.build( 3 | error: "Symbol CrystalRuby types should indicate a list of possible values shared between Crystal and Ruby. "\ 4 | "E.g. Symbol(:green, :blue, :orange). If this list is not known at compile time, you should use a String instead." 5 | ) 6 | 7 | def self.Symbol(*allowed_values) 8 | raise "Symbol must have at least one value" if allowed_values.empty? 9 | 10 | allowed_values.flatten! 11 | raise "Symbol allowed values must all be symbols" unless allowed_values.all? { |v| v.is_a?(::Symbol) } 12 | 13 | Primitive.build(:Symbol, ffi_type: :uint32, convert_if: [Root::String, Root::Symbol], memsize: 4) do 14 | bind_local_vars!(%i[allowed_values], binding) 15 | define_method(:value=) do |val| 16 | val = allowed_values[val] if val.is_a?(::Integer) && val >= 0 && val < allowed_values.size 17 | raise "Symbol must be one of #{allowed_values}" unless allowed_values.include?(val) 18 | 19 | super(allowed_values.index(val)) 20 | end 21 | 22 | define_singleton_method(:valid_cast?) do |raw| 23 | super(raw) && allowed_values.include?(raw) 24 | end 25 | 26 | define_method(:value) do |native: false| 27 | allowed_values[super()] 28 | end 29 | 30 | define_singleton_method(:type_digest) do 31 | Digest::MD5.hexdigest(native_type_expr.to_s + allowed_values.map(&:to_s).join(",")) 32 | end 33 | 34 | def self.ffi_primitive_type 35 | nil 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/crystalruby/types/primitive_types/symbol.cr: -------------------------------------------------------------------------------- 1 | class <%= base_crystal_class_name %> < CrystalRuby::Types::Primitive 2 | 3 | def initialize(value : ::Symbol) 4 | @value = 0.to_u32 5 | self.value = value 6 | end 7 | 8 | def initialize(value : UInt32) 9 | @value = value 10 | end 11 | 12 | def initialize(ptr : Pointer(::UInt8)) 13 | initialize(ptr.as(Pointer(::UInt32))[0]) 14 | end 15 | 16 | def ==(other : ::Symbol) 17 | value == other 18 | end 19 | 20 | def value=(value : ::Symbol) 21 | case value 22 | <% allowed_values.each_with_index do |v, i| %> 23 | when :<%= v %> then @value = <%= i %>.to_u32 24 | <% end %> 25 | else raise "Symbol must be one of <%= allowed_values %>. Got #{value}" 26 | end 27 | end 28 | 29 | def value : ::Symbol 30 | case @value 31 | <% allowed_values.each_with_index do |v, i| %> 32 | when <%= i %> then :<%= v %> 33 | <% end %> 34 | else raise "Symbol must be one of <%= allowed_values %>. Got #{value}" 35 | end 36 | end 37 | 38 | def self.copy_to!(value : ::Symbol, ptr : Pointer(::UInt8)) 39 | ptr.as(Pointer(::UInt32))[0] = new(value).return_value 40 | end 41 | 42 | def self.memsize 43 | <%= memsize %> 44 | end 45 | 46 | def self.write_single(pointer : Pointer(::UInt8), value) 47 | as_uint32 = case value 48 | <% allowed_values.each_with_index do |v, i| %> 49 | when :<%= v %> then <%= i %>.to_u32 50 | <% end %> 51 | else raise "Symbol must be one of <%= allowed_values %>. Got #{value}" 52 | end 53 | pointer.as(Pointer(::UInt32)).value = as_uint32 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/crystalruby/types/variable_width.rb: -------------------------------------------------------------------------------- 1 | module CrystalRuby 2 | module Types 3 | # A variable with type operates much like a fixed width type, but 4 | # it writes a size and a pointer to the type memory instead of the data itself. 5 | # When we decrement our internal ref count we need to resolve the pointer. 6 | # The layout is a tiny bit different (data begins at byte 8 to allow room for size uint32 at byte 4) 7 | class VariableWidth < FixedWidth 8 | def self.variable_width? 9 | true 10 | end 11 | 12 | def self.crystal_supertype 13 | "CrystalRuby::Types::VariableWidth" 14 | end 15 | 16 | def self.build( 17 | typename = nil, 18 | error: nil, 19 | inner_types: nil, 20 | inner_keys: nil, 21 | ffi_type: :pointer, 22 | ffi_primitive: false, 23 | size_offset: 4, 24 | data_offset: 8, 25 | memsize: FFI.type_size(ffi_type), 26 | refsize: 8, 27 | convert_if: [], 28 | superclass: VariableWidth, 29 | &block 30 | ) 31 | inner_types&.each(&Type.method(:validate!)) 32 | 33 | Class.new(superclass) do 34 | bind_local_vars!( 35 | %i[typename error inner_types inner_keys ffi_type memsize convert_if data_offset size_offset 36 | refsize ffi_primitive], binding 37 | ) 38 | class_eval(&block) if block_given? 39 | end 40 | end 41 | end 42 | end 43 | end 44 | 45 | require_relative "variable_width/string" 46 | require_relative "variable_width/array" 47 | require_relative "variable_width/hash" 48 | -------------------------------------------------------------------------------- /lib/crystalruby/types/fixed_width/tagged_union.cr: -------------------------------------------------------------------------------- 1 | class <%= base_crystal_class_name %> < CrystalRuby::Types::FixedWidth 2 | 3 | def initialize(value : <%= native_type_expr %>) 4 | @memory = malloc(data_offset + <%= memsize %>_u64) 5 | self.value = value 6 | increment_ref_count! 7 | end 8 | 9 | def value=(value : <%= native_type_expr %>) 10 | self.class.copy_to!(value, @memory) 11 | end 12 | 13 | def ==(other : <%= native_type_expr %>) 14 | native == other 15 | end 16 | 17 | def value 18 | data_pointer = @memory + data_offset 19 | case data_pointer.value 20 | <% union_types.each_with_index do |type, index| %> 21 | when <%= index %> 22 | <%= type.crystal_class_name %>.fetch_single(data_pointer+1) 23 | <% end %> 24 | else raise "Invalid union type #{data_pointer.value}" 25 | end 26 | end 27 | 28 | def native 29 | data_pointer = @memory + data_offset 30 | case data_pointer.value 31 | <% union_types.each_with_index do |type, index| %> 32 | when <%= index %> 33 | <%= type.crystal_class_name %>.fetch_single(data_pointer+1).native 34 | <% end %> 35 | else raise "Invalid union type #{data_pointer.value}" 36 | end 37 | end 38 | 39 | def self.copy_to!(value : <%= native_type_expr %>, ptr : Pointer(::UInt8)) 40 | data_pointer = ptr + data_offset 41 | case value 42 | <% union_types.each_with_index do |type, index| %> 43 | when <%= type.native_type_expr %> 44 | data_pointer[0] = <%= index %> 45 | <%= type.crystal_class_name %>.write_single(data_pointer + 1, value) 46 | <% end %> 47 | end 48 | end 49 | 50 | def self.memsize 51 | <%= memsize %> 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /crystalruby.gemspec: -------------------------------------------------------------------------------- 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 = ">= 2.7.2" 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 "digest" 36 | spec.add_dependency "ffi" 37 | spec.add_dependency "fileutils", "~> 1.7" 38 | spec.add_dependency "prism", ">= 1.3.0", "< 1.5.0" 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 42 | -------------------------------------------------------------------------------- /test/test_async_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | require "benchmark" 5 | class TestAsyncMethods < Minitest::Test 6 | module Sleeper 7 | crystallize async: true 8 | def sleep_async(a: :float) 9 | sleep(a.seconds) 10 | end 11 | 12 | crystallize async: false 13 | def sleep_sync(a: :float) 14 | sleep(a.seconds) 15 | end 16 | end 17 | 18 | def test_sync_sleep_is_serial_async_concurrent 19 | # Multi threaded invocation of Crystal code is specific to multi-threaded mode 20 | return if CrystalRuby.config.single_thread_mode 21 | 22 | total_sleep_time = Benchmark.realtime do 23 | 5.times.map do 24 | Thread.new do 25 | Sleeper.sleep_sync(0.2) 26 | end 27 | end.each(&:join) 28 | end 29 | assert total_sleep_time > 1 30 | 31 | total_sleep_time = Benchmark.realtime do 32 | 5.times.map do 33 | Thread.new do 34 | Sleeper.sleep_async(0.2) 35 | end 36 | end.each(&:join) 37 | end 38 | assert total_sleep_time < 0.5 39 | end 40 | 41 | crystallize async: true 42 | def callback_ruby(returns: Int32) 43 | ruby_callback() + ruby_callback() 44 | end 45 | 46 | expose_to_crystal 47 | def ruby_callback(returns: Int32) 48 | 10 49 | end 50 | 51 | def test_can_callback_ruby_from_async 52 | assert_equal callback_ruby, 20 53 | end 54 | 55 | crystallize async: true 56 | def yield_to_ruby(yield: Proc(Int32, Nil)) 57 | yield 10 58 | yield 20 59 | yield 30 60 | end 61 | 62 | def test_can_yield_to_ruby_from_async 63 | yielded = [] 64 | yield_to_ruby do |val| 65 | yielded << val 66 | end 67 | 68 | assert_equal yielded, [10, 20, 30] 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /exe/crystalruby: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "crystalruby" 4 | require "fileutils" 5 | 6 | # Define the actions for the commands 7 | def init 8 | # Define some dummy content for the YAML file 9 | yaml_content = <<~YAML 10 | # crystalruby configuration file 11 | crystal_src_dir: "./crystalruby" 12 | crystal_codegen_dir: "generated" 13 | crystal_missing_ignore: false 14 | log_level: "info" 15 | single_thread_mode: false 16 | debug: true 17 | YAML 18 | 19 | # Create the file at the root of the current directory 20 | File.write("crystalruby.yaml", yaml_content) 21 | puts "Initialized crystalruby.yaml file with dummy content." 22 | end 23 | 24 | def install 25 | Dir["#{CrystalRuby.config.crystal_src_dir}/**/src"].each do |src_dir| 26 | Dir.chdir(src_dir) do 27 | if system("shards check") || system("shards update") 28 | puts "Shards installed successfully." 29 | else 30 | puts "Error installing shards." 31 | end 32 | end 33 | end 34 | end 35 | 36 | def clean 37 | Dir["#{CrystalRuby.config.crystal_src_dir}/**/src/generated"].each do |codegen_dir| 38 | FileUtils.rm_rf(codegen_dir) 39 | end 40 | Dir["#{CrystalRuby.config.crystal_src_dir}/**/lib"].each do |lib_dir| 41 | FileUtils.rm_rf(lib_dir) 42 | end 43 | end 44 | 45 | def build 46 | # TODO: Iterate through all generated libs and build 47 | puts "Build command is not implemented yet." 48 | end 49 | 50 | # Main program 51 | if ARGV.empty? 52 | puts "Usage: crystalruby [command]" 53 | puts "Commands: init, clear, build" 54 | exit 1 55 | end 56 | 57 | case ARGV[0] 58 | when "init" 59 | init 60 | when "clean" 61 | clean 62 | when "install" 63 | install 64 | when "build" 65 | build 66 | else 67 | puts "Invalid command: #{ARGV[0]}" 68 | end 69 | -------------------------------------------------------------------------------- /lib/crystalruby/types/type.cr: -------------------------------------------------------------------------------- 1 | module CrystalRuby 2 | module Types 3 | class Type 4 | property memory : Pointer(::UInt8) = Pointer(::UInt8).null 5 | 6 | macro method_missing(call) 7 | current_value = self.native 8 | current_hash = current_value.hash 9 | return_value = current_value.{{ call }} 10 | 11 | if current_hash != current_value.hash 12 | self.value = current_value 13 | end 14 | return_value 15 | end 16 | 17 | def to_s 18 | native.to_s 19 | end 20 | 21 | def self.new_decr(arg) 22 | self.new(arg) 23 | end 24 | 25 | def native_decr 26 | native 27 | end 28 | 29 | def synchronize 30 | CrystalRuby.synchronize do 31 | yield 32 | end 33 | end 34 | 35 | def self.synchronize 36 | yield 37 | end 38 | 39 | def variable_width? 40 | false 41 | end 42 | 43 | def return_value 44 | memory 45 | end 46 | 47 | def self.free(memory : Pointer(::UInt8)) 48 | LibC.free(memory) 49 | end 50 | 51 | def self.malloc(size : Int) : Pointer(::UInt8) 52 | LibC.calloc(size, 1).as(Pointer(::UInt8)) 53 | end 54 | 55 | def malloc(memsize) 56 | self.class.malloc(memsize) 57 | end 58 | 59 | def self.each_child_address(pointer : Pointer(::UInt8), &block : Pointer(::UInt8) -> Nil) 60 | # Do nothing 61 | end 62 | 63 | def self.fetch_multi!(pointer : Pointer(::UInt8), size) 64 | size.times.map { |i| fetch_single(pointer + i * refsize) }.to_a 65 | end 66 | 67 | def self.fetch_multi_native!(pointer : Pointer(::UInt8), size) 68 | size.times.map { |i| fetch_single(pointer + i * refsize).native }.to_a 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test/test_inline_crystal_blocks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | class TestInlineCrystalBlocks < Minitest::Test 6 | module InlineCrystalModule 7 | crystal raw: true do 8 | <<~CRYSTAL 9 | def self.inline_crystal(a : Int32, b : Int32) : Int32 10 | return a + b 11 | end 12 | CRYSTAL 13 | end 14 | 15 | crystal lib: "inline-crystal-test" do 16 | def self.mult(a, b) 17 | a * b 18 | end 19 | end 20 | 21 | crystallize :int32 22 | def call_inline_crystal(a: :int32, b: :int32) 23 | TestInlineCrystalBlocks::InlineCrystalModule.inline_crystal(a, b) 24 | end 25 | 26 | crystallize :int32, lib: "inline-crystal-test" 27 | def call_inline_crystal_multi_lib(a: :int32, b: :int32) 28 | TestInlineCrystalBlocks::InlineCrystalModule.mult(a, b) 29 | end 30 | 31 | crystallize :int32, lib: "dangling-lib" 32 | def call_inline_crystal_bad_lib(a: :int32, b: :int32) 33 | TestInlineCrystalBlocks::InlineCrystalModule.mult(a, b) 34 | end 35 | end 36 | 37 | # Suppress compilation errors in cases where we expect them as part of the test. 38 | def suppress_compile_stdout 39 | original_log_level = CrystalRuby::Config.log_level 40 | original_verbose = CrystalRuby::Config.verbose 41 | CrystalRuby::Config.log_level = :info 42 | CrystalRuby::Config.verbose = false 43 | yield 44 | ensure 45 | CrystalRuby::Config.log_level = original_log_level 46 | CrystalRuby::Config.verbose = original_verbose 47 | end 48 | 49 | include InlineCrystalModule 50 | def test_inline_crystal 51 | assert call_inline_crystal(1, 2) == 3 52 | assert call_inline_crystal_multi_lib(3, 10) == 30 53 | assert_raises(StandardError){ suppress_compile_stdout{ call_inline_crystal_bad_lib(3, 10) } } 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/types/test_symbol.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | class TestSymbol < Minitest::Test 6 | crystallize 7 | def match(a: Symbol(:green, :red, :blue), returns: Symbol(:orange, :yellow, :other)) 8 | case a 9 | when :green then :orange 10 | when :red then :yellow 11 | else :other 12 | end 13 | end 14 | 15 | crystallize raw: true 16 | def count_acknowledgement_statuses( 17 | statuses: Array(Symbol(%i[acknowledged unacknowledged unknown])), 18 | returns: Hash(Symbol(%i[acknowledged unacknowledged unknown]), Int32) 19 | ) 20 | ' 21 | result = statuses.each_with_object({} of Symbol => Int32) do |status, hash| 22 | hash[status] ||= 0 23 | hash[status] += 1 24 | end 25 | ' 26 | end 27 | 28 | StatusEnum = CRType { Symbol(:active, :inactive) } 29 | ItemWithStatus = CRType { NamedTuple(status: StatusEnum) } 30 | 31 | crystallize 32 | def is_active?(status: ItemWithStatus, returns: Bool) 33 | status.status == :active 34 | end 35 | 36 | def test_top_level_symbols 37 | assert match(:green) == :orange 38 | assert match(:red) == :yellow 39 | assert match(:blue) == :other 40 | assert_raises(RuntimeError) { match(:not_found) } 41 | end 42 | 43 | def test_symbols_in_containers 44 | assert count_acknowledgement_statuses( 45 | %i[acknowledged unacknowledged unknown] 46 | ) == { acknowledged: 1, unacknowledged: 1, unknown: 1 } 47 | 48 | assert count_acknowledgement_statuses( 49 | %i[acknowledged unacknowledged unknown acknowledged unacknowledged unknown] 50 | ) == { acknowledged: 2, unacknowledged: 2, unknown: 2 } 51 | end 52 | 53 | def test_named_symbol_types 54 | assert_equal true, is_active?(ItemWithStatus.new(status: :active)) 55 | assert_equal false, is_active?(ItemWithStatus.new(status: :inactive)) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | crystalruby (0.3.3) 5 | digest 6 | ffi 7 | fileutils (~> 1.7) 8 | prism (>= 1.3.0, < 1.5.0) 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | ansi (1.5.0) 14 | ast (2.4.2) 15 | builder (3.3.0) 16 | debug (1.9.2) 17 | irb (~> 1.10) 18 | reline (>= 0.3.8) 19 | digest (3.1.1) 20 | ffi (1.17.0) 21 | ffi (1.17.0-arm64-darwin) 22 | fileutils (1.7.2) 23 | io-console (0.7.2) 24 | irb (1.12.0) 25 | rdoc 26 | reline (>= 0.4.2) 27 | json (2.7.1) 28 | language_server-protocol (3.17.0.3) 29 | minitest (5.22.3) 30 | minitest-reporters (1.7.1) 31 | ansi 32 | builder 33 | minitest (>= 5.0) 34 | ruby-progressbar 35 | parallel (1.24.0) 36 | parser (3.3.0.5) 37 | ast (~> 2.4.1) 38 | racc 39 | prism (1.4.0) 40 | psych (5.1.2) 41 | stringio 42 | racc (1.7.3) 43 | rainbow (3.1.1) 44 | rake (13.1.0) 45 | rdoc (6.6.3.1) 46 | psych (>= 4.0.0) 47 | regexp_parser (2.9.0) 48 | reline (0.5.2) 49 | io-console (~> 0.5) 50 | rexml (3.2.6) 51 | rubocop (1.62.1) 52 | json (~> 2.3) 53 | language_server-protocol (>= 3.17.0) 54 | parallel (~> 1.10) 55 | parser (>= 3.3.0.2) 56 | rainbow (>= 2.2.2, < 4.0) 57 | regexp_parser (>= 1.8, < 3.0) 58 | rexml (>= 3.2.5, < 4.0) 59 | rubocop-ast (>= 1.31.1, < 2.0) 60 | ruby-progressbar (~> 1.7) 61 | unicode-display_width (>= 2.4.0, < 3.0) 62 | rubocop-ast (1.31.2) 63 | parser (>= 3.3.0.4) 64 | ruby-progressbar (1.13.0) 65 | stringio (3.1.0) 66 | unicode-display_width (2.5.0) 67 | 68 | PLATFORMS 69 | arm64-darwin-22 70 | ruby 71 | 72 | DEPENDENCIES 73 | crystalruby! 74 | debug (>= 1.1.0) 75 | ffi 76 | minitest (~> 5.16) 77 | minitest-reporters (~> 1.4) 78 | rake (~> 13.0) 79 | rubocop (~> 1.21) 80 | 81 | BUNDLED WITH 82 | 2.6.8 83 | -------------------------------------------------------------------------------- /test/types/test_proc.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | class TestProcAsync < Minitest::Test 6 | crystallize async: true 7 | def crystal_method_takes_bool_int32_closure(yield: Proc(Bool, Int32), returns: Int32) 8 | yield true 9 | end 10 | 11 | def test_passes_ruby_proc_to_crystal 12 | closure_state = [] 13 | return_value = crystal_method_takes_bool_int32_closure do |input| 14 | assert_equal input, true 15 | closure_state << 1 16 | 15 17 | end 18 | assert_equal closure_state, [1] 19 | assert_equal return_value, 15 20 | end 21 | 22 | expose_to_crystal 23 | def ruby_method_takes_bool_int32_closure(yield: Proc(Bool, Int32), returns: Int32) 24 | yield true 25 | end 26 | 27 | crystallize 28 | def crystal_ruby_method_invoker(returns: Int32) 29 | crystal_method_takes_bool_int32_closure do |_input| 30 | 15 31 | end 32 | end 33 | 34 | def test_passes_crystal_proc_to_ruby 35 | assert_equal crystal_ruby_method_invoker, 15 36 | end 37 | end 38 | 39 | class TestProcSync < Minitest::Test 40 | crystallize async: false 41 | def crystal_method_takes_bool_int32_closure(yield: Proc(Bool, Int32), returns: Int32) 42 | yield true 43 | end 44 | 45 | def test_passes_ruby_proc_to_crystal 46 | closure_state = [] 47 | return_value = crystal_method_takes_bool_int32_closure do |input| 48 | assert_equal input, true 49 | closure_state << 1 50 | 15 51 | end 52 | assert_equal closure_state, [1] 53 | assert_equal return_value, 15 54 | end 55 | 56 | expose_to_crystal 57 | def ruby_method_takes_bool_int32_closure(yield: Proc(Bool, Int32), returns: Int32) 58 | yield true 59 | end 60 | 61 | crystallize 62 | def crystal_ruby_method_invoker(returns: Int32) 63 | crystal_method_takes_bool_int32_closure do |_input| 64 | 15 65 | end 66 | end 67 | 68 | # def test_passes_crystal_proc_to_ruby 69 | # assert_equal crystal_ruby_method_invoker, 15 70 | # end 71 | end 72 | -------------------------------------------------------------------------------- /lib/crystalruby/types/fixed_width/proc.cr: -------------------------------------------------------------------------------- 1 | class <%= base_crystal_class_name %> < CrystalRuby::Types::FixedWidth 2 | 3 | def initialize(inner_proc : Proc(<%= inner_types.map(&:native_type_expr).join(",") %>)) 4 | @memory = malloc(20) # 4 bytes RC + 2x 8 Byte Pointer 5 | self.value = inner_proc 6 | increment_ref_count! 7 | end 8 | 9 | def ==(other : <%= native_type_expr %>) 10 | native == other 11 | end 12 | 13 | def value : Proc(<%= inner_types.map(&:crystal_type).join(",") %>) 14 | func_ptr = Pointer(Void).new((@memory+4).as(Pointer(::UInt64)).value) 15 | Proc(<%= inner_types.map(&:crystal_type).join(",") %>).new(func_ptr, Pointer(Void).null) 16 | end 17 | 18 | def native : Proc(<%= inner_types.map(&:crystal_type).join(",") %>) 19 | value 20 | end 21 | 22 | def value=(inner_proc : Proc(<%= inner_types.map(&:native_type_expr).join(",") %>)) 23 | # We can't maintain a direct reference to our inner_proc within our callback 24 | # as this turns it into a closure, which we cannot pass over FFI. 25 | # Instead, we box the inner_proc and pass a pointer to the box to the callback. 26 | # and then unbox it within the callback. 27 | 28 | boxed_data = Box.box(inner_proc) 29 | 30 | callback_ptr = Proc(Pointer(::UInt8), <%= inner_types.map(&:crystal_type).join(",") %>).new do |<%= inner_types.size.times.map{|i| "_v#{i}" }.join(",") %>| 31 | 32 | inner_prc = Box(typeof(inner_proc)).unbox(_v0.as(Pointer(Void))) 33 | <% inner_types.each.with_index do |inner_type, i| %> 34 | <% next if i == inner_types.size - 1 %> 35 | v<%= i.succ %> = <%= inner_type.crystal_class_name %>.new(_v<%= i.succ %>)<%= inner_type.anonymous? ? ".value" : "" %> 36 | <% end %> 37 | 38 | return_value = inner_prc.call(<%= inner_types.size.-(1).times.map{|i| "v#{i.succ}" }.join(",") %>) 39 | <%= inner_types[-1].crystal_class_name %>.new(return_value).return_value 40 | end.pointer 41 | 42 | (@memory+4).as(Pointer(::UInt64)).value = callback_ptr.address 43 | (@memory+12).as(Pointer(::UInt64)).value = boxed_data.address 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/crystalruby.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "ffi" 4 | require "digest" 5 | require "fileutils" 6 | require "prism" 7 | require "pathname" 8 | 9 | require_relative "crystalruby/config" 10 | require_relative "crystalruby/version" 11 | require_relative "crystalruby/arc_mutex" 12 | require_relative "crystalruby/typemaps" 13 | require_relative "crystalruby/types" 14 | require_relative "crystalruby/typebuilder" 15 | require_relative "crystalruby/template" 16 | require_relative "crystalruby/compilation" 17 | require_relative "crystalruby/adapter" 18 | require_relative "crystalruby/reactor" 19 | require_relative "crystalruby/library" 20 | require_relative "crystalruby/function" 21 | require_relative "crystalruby/source_reader" 22 | 23 | module CrystalRuby 24 | module_function 25 | 26 | def initialized? 27 | !!@initialized 28 | end 29 | 30 | def initialize_crystal_ruby! 31 | return if initialized? 32 | 33 | check_crystal_ruby! 34 | check_config! 35 | @initialized = true 36 | end 37 | 38 | def check_crystal_ruby! 39 | return if system("which crystal > /dev/null 2>&1") 40 | 41 | msg = "Crystal executable not found. Please ensure Crystal is installed and in your PATH. " \ 42 | "See https://crystal-lang.org/install/." 43 | 44 | if config.crystal_missing_ignore 45 | config.logger.error msg 46 | else 47 | raise msg 48 | end 49 | end 50 | 51 | def check_config! 52 | return if config.crystal_src_dir 53 | 54 | raise "Missing config option `crystal_src_dir`. \nProvide this inside crystalruby.yaml " \ 55 | "(run `bundle exec crystalruby init` to generate this file with detaults)" 56 | end 57 | 58 | %w[debug info warn error].each do |level| 59 | define_method(:"log_#{level}") do |*msg| 60 | prefix = config.colorize_log_output ? "\e[33mcrystalruby\e[0m\e[90m [#{Thread.current.object_id}]\e[0m" : "[crystalruby] #{Thread.current.object_id}" 61 | 62 | config.logger.send(level, "#{prefix} #{msg.join(", ")}") 63 | end 64 | end 65 | 66 | def compile! 67 | CrystalRuby::Library.all.each(&:build!) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/test_expose_to_crystal.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | class TestExposeToCrystal < Minitest::Test 6 | crystallize :int 7 | def call_ruby_from_crystal(a: Int32, b: Int32) 8 | ruby_call(a, b) 9 | end 10 | 11 | expose_to_crystal -> { Int32 } 12 | def ruby_call(a: Int32, b: Int32) 13 | a + b % [a, b].min 14 | end 15 | 16 | def test_simple_ruby_call 17 | assert_equal call_ruby_from_crystal(14, 7), 14 18 | end 19 | 20 | crystallize :int 21 | def will_call_ruby(seed: Int32) 22 | initial = ruby_call(seed + 1, seed + 2) * 3 23 | ruby_yield(initial) do |value| 24 | res = value - 4 25 | res 26 | end 27 | end 28 | 29 | expose_to_crystal -> { Int32 } 30 | def ruby_yield(initial: Int32, yield: Proc(Int32, Int32)) 31 | initial += yield 14 32 | initial += yield 28 33 | initial += yield 32 34 | initial 35 | end 36 | 37 | expose_to_crystal -> { Int32 } 38 | def top_level_rescue(a: Int32, b: Int32) 39 | raise "Error!" 40 | a + b 41 | rescue StandardError => e 42 | 5 43 | end 44 | 45 | expose_to_crystal -> { :string } 46 | def returns_string(a: Int32, b: Int32) 47 | a + b 48 | return "Hello" 49 | end 50 | 51 | crystalize -> { String } 52 | def call_returns_string 53 | returns_string(1, 2) 54 | end 55 | 56 | crystallize :int 57 | def will_call_top_level_rescue(seed: Int32) 58 | top_level_rescue(seed + 1, seed + 2) 59 | end 60 | 61 | def test_bidirectional_calls 62 | [1, 9, 15, 54, 88].each do |seed| 63 | assert_equal( 64 | # Crystal method, performs simple Ruby method call and call with callbacks. 65 | will_call_ruby(seed), 66 | # Pure Ruby equivalent. 67 | # Invoke ruby_call directly, and replicate the yield logic. 68 | ruby_call(seed + 1, seed + 2) * 3 + (14 - 4) + (28 - 4) + (32 - 4) 69 | ) 70 | end 71 | end 72 | 73 | def test_top_level_rescue 74 | assert_equal will_call_top_level_rescue(1), 5 75 | end 76 | 77 | def test_returns_string 78 | assert_equal call_returns_string, "Hello" 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/crystalruby/types/variable_width/array.cr: -------------------------------------------------------------------------------- 1 | class <%= base_crystal_class_name %> < CrystalRuby::Types::VariableWidth 2 | 3 | include Enumerable(<%= inner_type.native_type_expr %>) 4 | 5 | def initialize(array : Array(<%= inner_type.native_type_expr %>)) 6 | @memory = malloc(data_offset + 8) 7 | self.value = array 8 | increment_ref_count! 9 | end 10 | 11 | def each 12 | size.times do |i| 13 | yield self[i].native 14 | end 15 | end 16 | 17 | def ==(other : Array(<%= inner_type.native_type_expr %>)) 18 | native == other 19 | end 20 | 21 | def data_pointer 22 | Pointer(::UInt8).new((@memory + data_offset).as(Pointer(::UInt64)).value) 23 | end 24 | 25 | def value=(array : Array(<%= inner_type.native_type_expr %>)) 26 | if self.ref_count > 0 27 | self.class.decr_inner_ref_counts!(memory) 28 | end 29 | self.class.copy_to!(array, self.memory) 30 | end 31 | 32 | def <<(value : <%= inner_type.native_type_expr %>) 33 | self.value = self.native + [value] 34 | end 35 | 36 | def []=(index : Int, value : <%= inner_type.native_type_expr %>) 37 | index += size if index < 0 38 | <%= inner_type.crystal_class_name %>.write_single(data_pointer + index * <%= inner_type.refsize %>, value) 39 | end 40 | 41 | def [](index : Int) 42 | index += size if index < 0 43 | <%= inner_type.crystal_class_name %>.fetch_single(data_pointer + index * <%= inner_type.refsize %>) 44 | end 45 | 46 | def self.copy_to!(array : Array(<%= inner_type.native_type_expr %>), memory : Pointer(::UInt8)) 47 | data_pointer = malloc(array.size * <%= inner_type.refsize %>) 48 | array.size.times do |i| 49 | <%= inner_type.crystal_class_name %>.write_single(data_pointer + i * <%= inner_type.refsize %>, array[i]) 50 | end 51 | (memory+size_offset).as(Pointer(::UInt32)).value = array.size.to_u32 52 | (memory+data_offset).as(Pointer(::UInt64)).value = data_pointer.address 53 | end 54 | 55 | def ==(other : <%= native_type_expr %>) 56 | native == other 57 | end 58 | 59 | def value 60 | <%= inner_type.crystal_class_name %>.fetch_multi!(data_pointer, size) 61 | end 62 | 63 | def native : ::Array(<%= inner_type.native_type_expr %>) 64 | <%= inner_type.crystal_class_name %>.fetch_multi_native!(data_pointer, size) 65 | end 66 | 67 | def size 68 | (@memory + self.class.size_offset).as(Pointer(::UInt32))[0] 69 | end 70 | 71 | def self.memsize 72 | <%= memsize %> 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/crystalruby/templates/top_level_function.cr: -------------------------------------------------------------------------------- 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 | 6 | def %{fn_name}(%{fn_args}) : %{fn_ret_type} 7 | %{fn_body} 8 | end 9 | 10 | # This function is the entry point for the CrystalRuby code, exposed through FFI. 11 | # We apply some basic error handling here, and convert the arguments and return values 12 | # to ensure that we are using Crystal native types. 13 | fun %{lib_fn_name}(%{lib_fn_args}): %{lib_fn_ret_type} 14 | begin 15 | %{convert_lib_args} 16 | begin 17 | return_value = %{fn_name}(%{arg_names})%{block_converter} 18 | return %{convert_return_type} 19 | rescue ex 20 | CrystalRuby.report_error("RuntimeError", ex.message.to_s, ex.backtrace.to_json, 0) 21 | end 22 | rescue ex 23 | CrystalRuby.report_error("ArgumentError", ex.message.to_s, ex.backtrace.to_json, 0) 24 | end 25 | return %{error_value} 26 | end 27 | 28 | 29 | # This function is the async entry point for the CrystalRuby code, exposed through FFI. 30 | # We apply some basic error handling here, and convert the arguments and return values 31 | # to ensure that we are using Crystal native types. 32 | fun %{lib_fn_name}_async(%{lib_fn_args} thread_id: UInt32, callback : %{callback_type}): Void 33 | begin 34 | %{convert_lib_args} 35 | CrystalRuby.increment_task_counter 36 | spawn do 37 | begin 38 | return_value = %{fn_name}(%{arg_names})%{block_converter} 39 | CrystalRuby.queue_callback(->{ 40 | converted = %{convert_return_type} 41 | %{callback_call} 42 | CrystalRuby.decrement_task_counter 43 | }) 44 | rescue ex 45 | exception = ex.message.to_s 46 | backtrace = ex.backtrace.to_json 47 | CrystalRuby.queue_callback(->{ 48 | CrystalRuby.report_error("RuntimeError", exception, backtrace, thread_id) 49 | CrystalRuby.decrement_task_counter 50 | }) 51 | end 52 | end 53 | rescue ex 54 | 55 | exception = ex.message.to_s 56 | backtrace = ex.backtrace.to_json 57 | CrystalRuby.queue_callback(->{ 58 | CrystalRuby.report_error("RuntimeError", ex.message.to_s, backtrace, thread_id) 59 | CrystalRuby.decrement_task_counter 60 | }) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/test_gc_active.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | class TestGCActive < Minitest::Test 6 | module ::MemoryGobbler 7 | crystal lib: "memory_gobbler" do 8 | @@leaked_memory = "" 9 | end 10 | 11 | crystallize lib: "memory_gobbler" 12 | def gobble_gcable_memory(mb: :float) 13 | "a" * (mb * 1024 * 1024).to_i 14 | end 15 | 16 | crystallize lib: "memory_gobbler" 17 | def leak_memory(mb: :float) 18 | @@leaked_memory += "a" * (mb * 1024 * 1024).to_i 19 | end 20 | 21 | crystallize :uint64, async: true, lib: "memory_gobbler" 22 | def trigger_gc 23 | 5.times do 24 | sleep 0.001.seconds 25 | GC.collect 26 | end 27 | GC.stats.heap_size 28 | end 29 | end 30 | 31 | class ObjectAllocTest < CRType do 32 | NamedTuple(hash: Hash(Int32, Int32), string: String, array: Array(Int32)) 33 | end 34 | end 35 | 36 | crystallize 37 | def crystal_gc 38 | GC.collect 39 | end 40 | 41 | crystallize 42 | def crystal_alloc(returns: ObjectAllocTest) 43 | ObjectAllocTest.new({ hash: { 1 => 2 }, string: "hello", array: [1, 2, 3] }) 44 | end 45 | 46 | crystallize 47 | def store_for_later(value: ObjectAllocTest) 48 | @@value = value 49 | end 50 | 51 | crystallize 52 | def clear_stored_value 53 | @@value = nil 54 | GC.collect 55 | end 56 | 57 | def test_crystal_alloc_crystal_free 58 | object = crystal_alloc 59 | ptr = FFI::Pointer.new(object.address) 60 | assert_equal ptr.read_int32, 2 61 | object = nil 62 | GC.start 63 | assert_equal ptr.read_int32, 1 64 | crystal_gc 65 | refute_equal ptr.read_int32, 1 66 | refute_equal ptr.read_int32, 0 67 | end 68 | 69 | def test_ruby_alloc_crystal_free 70 | object = ObjectAllocTest.new({ hash: { 1 => 2 }, string: "hello", array: [1, 2, 3] }) 71 | store_for_later(object) 72 | ptr = FFI::Pointer.new(object.address) 73 | assert_equal ptr.read_int32, 2 74 | object = nil 75 | GC.start 76 | assert_equal ptr.read_int32, 1 77 | clear_stored_value 78 | refute_equal ptr.read_int32, 1 79 | refute_equal ptr.read_int32, 0 80 | end 81 | 82 | def test_crystal_alloc_ruby_free 83 | object = crystal_alloc 84 | ptr = FFI::Pointer.new(object.address) 85 | assert_equal ptr.read_int32, 2 86 | crystal_gc 87 | assert_equal ptr.read_int32, 1 88 | object = nil 89 | GC.start 90 | refute_equal ptr.read_int32, 1 91 | refute_equal ptr.read_int32, 0 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/crystalruby/templates/function.cr: -------------------------------------------------------------------------------- 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 | 6 | %{module_or_class} %{module_name} %{superclass} 7 | def %{fn_scope}%{fn_name}(%{fn_args}) : %{fn_ret_type} 8 | %{fn_body} 9 | end 10 | end 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 | fun %{lib_fn_name}(%{lib_fn_args}): %{lib_fn_ret_type} 16 | begin 17 | %{convert_lib_args} 18 | begin 19 | return_value = %{receiver}.%{fn_name}(%{arg_names})%{block_converter} 20 | return %{convert_return_type} 21 | rescue ex 22 | CrystalRuby.report_error("RuntimeError", ex.message.to_s, ex.backtrace.to_json, 0) 23 | end 24 | rescue ex 25 | CrystalRuby.report_error("ArgumentError", ex.message.to_s, ex.backtrace.to_json, 0) 26 | end 27 | return %{error_value} 28 | end 29 | 30 | 31 | # This function is the async entry point for the CrystalRuby code, exposed through FFI. 32 | # We apply some basic error handling here, and convert the arguments and return values 33 | # to ensure that we are using Crystal native types. 34 | fun %{lib_fn_name}_async(%{lib_fn_args} thread_id: UInt32, callback : %{callback_type}): Void 35 | begin 36 | %{convert_lib_args} 37 | CrystalRuby.increment_task_counter 38 | spawn do 39 | begin 40 | return_value = %{receiver}.%{fn_name}(%{arg_names})%{block_converter} 41 | CrystalRuby.queue_callback(->{ 42 | converted = %{convert_return_type} 43 | %{callback_call} 44 | CrystalRuby.decrement_task_counter 45 | }) 46 | rescue ex 47 | exception = ex.message.to_s 48 | backtrace = ex.backtrace.to_json 49 | CrystalRuby.queue_callback(->{ 50 | CrystalRuby.report_error("RuntimeError", exception, backtrace, thread_id) 51 | CrystalRuby.decrement_task_counter 52 | }) 53 | end 54 | end 55 | rescue ex 56 | exception = ex.message.to_s 57 | backtrace = ex.backtrace.to_json 58 | CrystalRuby.queue_callback(->{ 59 | CrystalRuby.report_error("RuntimeError", ex.message.to_s, backtrace, thread_id) 60 | CrystalRuby.decrement_task_counter 61 | }) 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/crystalruby/compilation.rb: -------------------------------------------------------------------------------- 1 | require "tmpdir" 2 | require "shellwords" 3 | require "timeout" 4 | 5 | module CrystalRuby 6 | module Compilation 7 | class CompilationFailedError < StandardError; end 8 | 9 | # Simple wrapper around invocation of the Crystal compiler 10 | # @param src [String] path to the source file 11 | # @param lib [String] path to the library file 12 | # @param verbose [Boolean] whether to print the compiler output 13 | # @param debug [Boolean] whether to compile in debug mode 14 | # @raise [CompilationFailedError] if the compilation fails 15 | # @return [void] 16 | def self.compile!( 17 | src:, 18 | lib:, 19 | verbose: CrystalRuby.config.verbose, 20 | debug: CrystalRuby.config.debug 21 | ) 22 | compile_command = build_compile_command(verbose: verbose, debug: debug, lib: lib, src: src) 23 | CrystalRuby.log_debug "Compiling Crystal code #{verbose ? ": #{compile_command}" : ""}" 24 | IO.popen(compile_command, chdir: File.dirname(src), &:read) 25 | raise CompilationFailedError, "Compilation failed in #{src}" unless $?&.success? 26 | end 27 | 28 | # Builds the command to compile the Crystal source file 29 | # @param verbose [Boolean] whether to print the compiler output 30 | # @param debug [Boolean] whether to compile in debug mode 31 | # @param lib [String] path to the library file 32 | # @param src [String] path to the source file 33 | # @return [String] the command to compile the Crystal source file 34 | def self.build_compile_command(verbose:, debug:, lib:, src:) 35 | verbose_flag = verbose ? "--verbose --progress" : "" 36 | debug_flag = debug ? "" : "--release --no-debug" 37 | redirect_output = " > /dev/null " unless verbose 38 | lib, src = [lib, src].map(&Shellwords.method(:escape)) 39 | %(crystal build #{verbose_flag} #{debug_flag} --single-module --link-flags "-shared" -o #{lib} #{src}#{redirect_output}) 40 | end 41 | 42 | # Trigger the shards install command in the given source directory 43 | def self.install_shards!(src_dir) 44 | CrystalRuby.log_debug "Running shards install inside #{src_dir}" 45 | output = IO.popen("shards update", chdir: src_dir, &:read) 46 | CrystalRuby.log_debug output if CrystalRuby.config.verbose 47 | raise CompilationFailedError, "Shards install failed" unless $?&.success? 48 | end 49 | 50 | # Return whether the shards check command succeeds in the given source directory 51 | def self.shard_check?(src_dir) 52 | IO.popen("shards check", chdir: src_dir, &:read) 53 | $?&.success? 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/types/test_string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | class TestString < Minitest::Test 6 | class StringClass < CRType { String } 7 | end 8 | 9 | def test_it_has_a_length 10 | ss = StringClass["Test This"] 11 | assert_equal 9, ss.length 12 | end 13 | 14 | def test_it_can_be_indexed 15 | ss = StringClass["Test This"] 16 | assert_equal "T", ss[0] 17 | end 18 | 19 | def test_it_can_be_overwritten 20 | ss = StringClass["Test This"] 21 | ss[0] = "W" 22 | assert_equal ss, "West This" 23 | end 24 | 25 | def test_shallow_copies_share_memory 26 | ss = StringClass["Test This"] 27 | ss2 = ss.dup 28 | ss[0] = "W" 29 | assert_equal ss2, "West This" 30 | end 31 | 32 | def test_deep_copies_dont_share_memory 33 | ss = StringClass["Test This"] 34 | ss2 = ss.deep_dup 35 | ss[0] = "W" 36 | assert_equal ss2, "Test This" 37 | end 38 | 39 | def test_it_has_a_ref_count 40 | ss = StringClass["Test This"] 41 | assert_equal ss.ref_count, 1 42 | ss2 = ss.dup 43 | assert_equal ss.ref_count, 2 44 | ss3 = ss2.dup 45 | assert_equal ss.ref_count, 3 46 | ss3 = nil 47 | ss2 = nil 48 | 2.times do 49 | GC.start 50 | sleep 0.1 51 | end 52 | assert_equal ss.ref_count, 1 53 | end 54 | 55 | crystallize lib: "string_tests" 56 | def takes_string(a: String) 57 | end 58 | 59 | crystallize lib: "string_tests" 60 | def returns_string(a: String, returns: String) 61 | a 62 | end 63 | 64 | crystallize lib: "string_tests" 65 | def returns_named_string(a: StringClass, returns: StringClass) 66 | a 67 | end 68 | 69 | crystallize lib: "string_tests" 70 | def returns_crystal_created_named_string(returns: StringClass) 71 | StringClass.new("Test") 72 | end 73 | 74 | crystallize lib: "string_tests" 75 | def returns_utf8_string(returns: String) 76 | "foo різних мов світу" 77 | end 78 | 79 | def test_crystal_takes_string 80 | assert(takes_string("Test") || true) 81 | end 82 | 83 | def test_crystal_returns_string 84 | assert_equal returns_string("Test"), "Test" 85 | end 86 | 87 | def test_crystal_returns_named_string 88 | my_test_str = StringClass["Test"] 89 | assert_equal returns_named_string(my_test_str), StringClass["Test"] 90 | end 91 | 92 | def test_returns_crystal_created_named_string 93 | assert_equal returns_crystal_created_named_string, StringClass["Test"] 94 | end 95 | 96 | def test_returns_utf8_string 97 | assert_equal returns_utf8_string, "foo різних мов світу" 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /test/test_gc_stress.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | class TestGCStress < Minitest::Test 6 | module ::GCStress 7 | crystal lib: "gc_stress_v1" do 8 | @@leaked_memory = "" 9 | end 10 | 11 | crystallize async: true, lib: "gc_stress_v1" 12 | def gc_stress_v1(mb: :float) 13 | "a" * (mb * 1024 * 1024).to_i 14 | end 15 | 16 | crystallize async: true, lib: "gc_stress_v1" 17 | def gc_leak_v1(mb: :float) 18 | @@leaked_memory += "a" * (mb * 1024 * 1024).to_i 19 | end 20 | 21 | crystallize lib: "gc_stress_v1" 22 | def gc_free_v1 23 | @@leaked_memory = "" 24 | end 25 | 26 | crystallize async: true, lib: "gc_stress_v1" 27 | def trigger_gc_v1 28 | GC.collect 29 | end 30 | 31 | crystal lib: "gc_stress_v2" do 32 | @@leaked_memory = "" 33 | end 34 | 35 | crystallize lib: "gc_stress_v2" 36 | def gc_stress_v2(mb: :float) 37 | "a" * (mb * 1024 * 1024).to_i 38 | end 39 | 40 | crystallize async: true, lib: "gc_stress_v2" 41 | def trigger_gc_v2 42 | GC.collect 43 | end 44 | 45 | crystallize lib: "gc_stress_v2" 46 | def gc_leak_v2(mb: :float) 47 | @@leaked_memory += "a" * (mb * 1024 * 1024).to_i 48 | end 49 | 50 | crystallize lib: "gc_stress_v2" 51 | def gc_free_v2 52 | @@leaked_memory = "" 53 | end 54 | end 55 | 56 | LOG_STEPS = false 57 | 58 | def test_gc_stress 59 | # Multi threaded invocation of Crystal code is specific to multi-threaded mode 60 | return if CrystalRuby.config.single_thread_mode 61 | 62 | 10.times.map do |i| 63 | puts "Iteration #{i} GC stress v1" if LOG_STEPS 64 | GCStress.gc_stress_v1(100) 65 | puts "Iteration #{i} GC stress v1 DONE." if LOG_STEPS 66 | 67 | puts "Iteration #{i} GC stress v2" if LOG_STEPS 68 | GCStress.gc_stress_v2(100) 69 | puts "Iteration #{i} GC stress v2 DONE." if LOG_STEPS 70 | 71 | Thread.new do 72 | puts "Thread #{i} gc v1" if LOG_STEPS 73 | GCStress.gc_stress_v1(100) 74 | puts "Thread #{i} gc v2" if LOG_STEPS 75 | GCStress.gc_stress_v2(100) 76 | puts "Thread #{i} gc leak v1" if LOG_STEPS 77 | GCStress.gc_leak_v1(25) 78 | puts "Thread #{i} gc leak v2" if LOG_STEPS 79 | GCStress.gc_leak_v2(25) 80 | puts "Thread #{i} gc trigger v1" if LOG_STEPS 81 | GCStress.trigger_gc_v1 82 | puts "Thread #{i} gc trigger v2" if LOG_STEPS 83 | GCStress.trigger_gc_v2 84 | end 85 | end.each(&:join) 86 | puts "Main thread gc free v1" if LOG_STEPS 87 | GCStress.gc_free_v1 88 | puts "Main thread gc free v2" if LOG_STEPS 89 | GCStress.gc_free_v2 90 | puts "Main gc trigger v1" if LOG_STEPS 91 | GCStress.trigger_gc_v1 92 | puts "Main gc trigger v2" if LOG_STEPS 93 | GCStress.trigger_gc_v2 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /test/types/test_named_tuple.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | CrystalRuby::Types::Type.trace_live_objects! 6 | 7 | class TestNamedTuple < Minitest::Test 8 | class NamedTupPrimitive < CRType { NamedTuple(age: Int32, count: Int32) } 9 | end 10 | 11 | class NamedTup < CRType { NamedTuple(complex: Hash(Int32, Int32), age: Int32, name: String) } 12 | end 13 | 14 | class NamedTupNested < CRType do 15 | NamedTuple(nested: NamedTuple(complex: Hash(Int32, Int32), age: Int32, name: String)) 16 | end 17 | end 18 | 19 | def test_named_tuple_construction_primitive 20 | nt = NamedTupPrimitive.new(age: 25, count: 3) 21 | nt = nil 22 | end 23 | 24 | def test_named_tuple_construction 25 | nt = NamedTup.new(complex: { 1 => 3 }, age: 25, name: "John") 26 | assert_equal nt.complex, { 1 => 3 } 27 | assert_equal nt.age, 25 28 | assert_equal nt.name, "John" 29 | nt = nil 30 | 31 | assert_equal CrystalRuby::Types::Type.live_objects, 0 32 | end 33 | 34 | def test_named_tuple_updates 35 | nt = NamedTup.new(complex: { 1 => 3 }, age: 25, name: "John") 36 | nt2_shallow = nt.dup 37 | nt2_deep = nt.deep_dup 38 | 39 | nt.name = "Steve" 40 | nt.complex[1] = 88 41 | assert_equal nt2_deep.complex, { 1 => 3 } 42 | assert_equal nt2_shallow.complex, { 1 => 88 } 43 | assert_equal nt2_shallow.name, "Steve" 44 | refute_equal nt2_deep.name, "Steve" 45 | 46 | nt = nt2_shallow = nt2_deep = nil 47 | assert_equal CrystalRuby::Types::Type.live_objects, 0 48 | end 49 | 50 | def test_named_tuple_nested_construction 51 | nt = NamedTupNested.new(nested: { complex: { 1 => 3 }, age: 25, name: "John" }) 52 | 53 | assert_equal nt.nested[:complex], { 1 => 3 } 54 | assert_equal nt.nested[:age], 25 55 | assert_equal nt.nested[:name], "John" 56 | nt = nil 57 | 58 | assert_equal CrystalRuby::Types::Type.live_objects, 0 59 | end 60 | 61 | crystallize 62 | def accepts_nested_tuple(input: NamedTupNested, returns: Bool) 63 | true 64 | end 65 | 66 | crystallize 67 | def returns_nested_tuple(returns: NamedTupNested) 68 | NamedTupNested.new( 69 | { nested: { complex: { 1 => 3 }, age: 25, name: "John" } } 70 | ) 71 | end 72 | 73 | def test_accepts_nested_tuple 74 | assert_equal true, accepts_nested_tuple( 75 | NamedTupNested[nested: { complex: { 1 => 3 }, age: 25, name: "John" }] 76 | ) 77 | end 78 | 79 | def test_returns_nested_tuple 80 | val = NamedTupNested[nested: { complex: { 1 => 3 }, age: 25, name: "John" }] 81 | assert_equal returns_nested_tuple, val 82 | end 83 | 84 | crystallize 85 | def returns_simple_tuple(returns: NamedTupPrimitive) 86 | NamedTupPrimitive.new( 87 | { age: 18, count: 43 } 88 | ) 89 | end 90 | 91 | def test_returns_simple_tuple 92 | assert_equal returns_simple_tuple, NamedTupPrimitive[age: 18, count: 43] 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/crystalruby/types/fixed_width/tuple.cr: -------------------------------------------------------------------------------- 1 | class <%= base_crystal_class_name %> < CrystalRuby::Types::FixedWidth 2 | 3 | def initialize(tuple : ::Tuple(<%= inner_types.map(&:native_type_expr).join(',') %>)) 4 | @memory = malloc(data_offset + 8) 5 | self.value = tuple 6 | increment_ref_count! 7 | end 8 | 9 | def data_pointer 10 | Pointer(::UInt8).new((@memory + data_offset).as(Pointer(::UInt64)).value) 11 | end 12 | 13 | def [](index : Int) 14 | index += size if index < 0 15 | <% offset = 0 %> 16 | <% inner_types.each_with_index do |type, i| %> 17 | <% offset += type.refsize %> 18 | return <%= type.crystal_class_name %>.fetch_single(data_pointer + <%= offset - type.refsize %>) if <%= i %> == index 19 | <% end %> 20 | end 21 | 22 | <% inner_types.each_with_index.group_by{|t,i| t.native_type_expr }.each do |(native_type_expr, types_width_index)| %> 23 | def []=(index : Int, value : <%= native_type_expr %>) 24 | index += size if index < 0 25 | <% types_width_index.each do |type, i| %> 26 | return <%= type.crystal_class_name %>.write_single(data_pointer + <%= offset_for(i) %>, value) if <%= i %> == index 27 | <% end %> 28 | raise ArgumentError.new("Index out of bounds") 29 | end 30 | <% end %> 31 | 32 | def ==(other : ::Tuple(<%= inner_types.map(&:native_type_expr).join(',') %>)) 33 | value == other 34 | end 35 | 36 | def value=(tuple : ::Tuple(<%= inner_types.map(&:native_type_expr).join(',') %>)) 37 | self.class.copy_to!(tuple, @memory) 38 | end 39 | 40 | def self.copy_to!(tuple : ::Tuple(<%= inner_types.map(&:native_type_expr).join(',') %>), memory : Pointer(::UInt8)) 41 | 42 | data_pointer = malloc(self.memsize) 43 | address = data_pointer.address 44 | 45 | <% inner_types.each_with_index do |type, i| %> 46 | <%= type.crystal_class_name %>.write_single(data_pointer, tuple[<%= i %>]) 47 | data_pointer += <%= type.refsize %> 48 | <% end %> 49 | 50 | (memory+data_offset).as(Pointer(::UInt64)).value = address 51 | end 52 | 53 | def ==(other : <%= native_type_expr %>) 54 | native == other 55 | end 56 | 57 | def value : ::Tuple(<%= inner_types.map(&:native_type_expr).join(',') %>) 58 | address = data_pointer 59 | <% inner_types.each_with_index do |type, i| %> 60 | v<%= i %> = <%= type.crystal_class_name %>.fetch_single(address) 61 | address += <%= type.refsize %> 62 | <% end %> 63 | ::Tuple.new(<%= inner_types.map.with_index { |_, i| "v#{i}" }.join(", ") %>) 64 | end 65 | 66 | def native : ::Tuple(<%= inner_types.map(&:native_type_expr).join(',') %>) 67 | address = data_pointer 68 | <% inner_types.each_with_index do |type, i| %> 69 | v<%= i %> = <%= type.crystal_class_name %>.fetch_single(address).native 70 | address += <%= type.refsize %> 71 | <% end %> 72 | ::Tuple.new(<%= inner_types.map.with_index { |_, i| "v#{i}" }.join(", ") %>) 73 | end 74 | 75 | def self.memsize 76 | <%= memsize %> 77 | end 78 | 79 | def self.refsize 80 | <%= refsize %> 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/crystalruby/types/fixed_width/tuple.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CrystalRuby::Types 4 | Tuple = FixedWidth.build( 5 | :Tuple, 6 | error: "Tuple type must contain one or more types E.g. Tuple(Int32, String)" 7 | ) 8 | 9 | def self.Tuple(*types) 10 | FixedWidth.build(:Tuple, inner_types: types, convert_if: [Root::Array], superclass: Tuple) do 11 | @data_offset = 4 12 | 13 | # We only accept List-like values, which have all of the required keys 14 | # and values of the correct type 15 | # can successfully be cast to our inner types 16 | def self.cast!(value) 17 | unless (value.is_a?(Array) || value.is_a?(Tuple) || value.is_a?(Root::Array)) && value.zip(inner_types).each do |v, t| 18 | t && t.valid_cast?(v) 19 | end && value.length == inner_types.length 20 | raise CrystalRuby::InvalidCastError, "Cannot cast #{value} to #{inspect}" 21 | end 22 | 23 | value 24 | end 25 | 26 | def self.copy_to!(values, memory:) 27 | data_pointer = malloc(memsize) 28 | 29 | memory[data_offset].write_pointer(data_pointer) 30 | 31 | inner_types.each.reduce(0) do |offset, type| 32 | type.write_single(data_pointer[offset], values.shift) 33 | offset + type.refsize 34 | end 35 | end 36 | 37 | def self.each_child_address(pointer) 38 | data_pointer = pointer[data_offset].read_pointer 39 | inner_types.each do |type| 40 | yield type, data_pointer 41 | data_pointer += type.refsize 42 | end 43 | end 44 | 45 | def self.memsize 46 | inner_types.map(&:refsize).sum 47 | end 48 | 49 | def size 50 | inner_types.size 51 | end 52 | 53 | def checked_offset!(index, size) 54 | raise "Index out of bounds: #{index} >= #{size}" if index >= size 55 | 56 | if index < 0 57 | raise "Index out of bounds: #{index} < -#{size}" if index < -size 58 | 59 | index += size 60 | end 61 | self.class.offset_for(index) 62 | end 63 | 64 | def self.offset_for(index) 65 | inner_types[0...index].map(&:refsize).sum 66 | end 67 | 68 | # Return the element at the given index. 69 | # This will automatically increment 70 | # the reference count if not a primitive type. 71 | def [](index) 72 | inner_types[index].fetch_single(data_pointer[checked_offset!(index, size)]) 73 | end 74 | 75 | # Overwrite the element at the given index 76 | # The replaced element will have 77 | # its reference count decremented. 78 | def []=(index, value) 79 | inner_types[index].write_single(data_pointer[checked_offset!(index, size)], value) 80 | end 81 | 82 | def value(native: false) 83 | ptr = data_pointer 84 | inner_types.map do |type| 85 | result = type.fetch_single(ptr, native: native) 86 | ptr += type.refsize 87 | result 88 | end 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/crystalruby/types/fixed_width/named_tuple.cr: -------------------------------------------------------------------------------- 1 | class <%= base_crystal_class_name %> < CrystalRuby::Types::FixedWidth 2 | 3 | def initialize(tuple : ::NamedTuple(<%= inner_keys.zip(inner_types).map{|k,v| "#{k}: #{v.native_type_expr}"}.join(',') %>)) 4 | @memory = malloc(data_offset + 8) 5 | self.value = tuple 6 | increment_ref_count! 7 | end 8 | 9 | def ==(other : <%= native_type_expr %>) 10 | native == other 11 | end 12 | 13 | def data_pointer 14 | Pointer(::UInt8).new((@memory + data_offset).as(::Pointer(UInt64)).value) 15 | end 16 | 17 | def [](key : Symbol | String) 18 | <% offset = 0 %> 19 | <% inner_types.each_with_index do |type, i| %> 20 | <% offset += type.refsize %> 21 | return <%= type.crystal_class_name %>.fetch_single(data_pointer + <%= offset - type.refsize %>).value if :<%= inner_keys[i] %> == key || key == "<%= inner_keys[i] %>" 22 | <% end %> 23 | end 24 | 25 | <% offset = 0 %> 26 | <% inner_types.each_with_index do |type, i| %> 27 | <% offset += type.refsize %> 28 | def <%= inner_keys[i] %> 29 | return <%= type.crystal_class_name %>.fetch_single(data_pointer + <%= offset - type.refsize %>) 30 | end 31 | 32 | def <%= inner_keys[i] %>=(value : <%= type.native_type_expr %>) 33 | return <%= type.crystal_class_name %>.write_single(data_pointer + <%= offset - type.refsize %>, value) 34 | end 35 | <% end %> 36 | 37 | 38 | 39 | def value=(tuple : ::NamedTuple(<%= inner_keys.zip(inner_types).map{|k,v| "#{k}: #{v.native_type_expr}"}.join(',') %>)) 40 | self.class.copy_to!(tuple, @memory) 41 | end 42 | 43 | def self.copy_to!(tuple : ::NamedTuple(<%= inner_keys.zip(inner_types).map{|k,v| "#{k}: #{v.native_type_expr}"}.join(',') %>), memory : Pointer(::UInt8)) 44 | data_pointer = malloc(self.memsize) 45 | address = data_pointer.address 46 | 47 | <% inner_types.each_with_index do |type, i| %> 48 | <%= type.crystal_class_name %>.write_single(data_pointer, tuple[:<%= inner_keys[i] %>]) 49 | data_pointer += <%= type.refsize %> 50 | <% end %> 51 | 52 | (memory+data_offset).as(Pointer(::UInt64)).value = address 53 | end 54 | 55 | def value 56 | address = data_pointer 57 | <% inner_types.each_with_index do |type, i| %> 58 | v<%= i %> = <%= type.crystal_class_name %>.fetch_single(address) 59 | address += <%= type.refsize %> 60 | <% end %> 61 | ::NamedTuple.new(<%= inner_types.map.with_index { |_, i| "#{inner_keys[i]}: v#{i}" }.join(", ") %>) 62 | end 63 | 64 | def native : ::NamedTuple(<%= inner_types.map.with_index { |type, i| "#{inner_keys[i]}: #{type.native_type_expr}" }.join(", ") %>) 65 | address = data_pointer 66 | <% inner_types.each_with_index do |type, i| %> 67 | v<%= i %> = <%= type.crystal_class_name %>.fetch_single(address).native 68 | address += <%= type.refsize %> 69 | <% end %> 70 | ::NamedTuple.new(<%= inner_types.map.with_index { |_, i| "#{inner_keys[i]}: v#{i}" }.join(", ") %>) 71 | end 72 | 73 | def self.memsize 74 | <%= memsize %> 75 | end 76 | 77 | def self.refsize 78 | <%= refsize %> 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/crystalruby/types/fixed_width/named_tuple.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CrystalRuby::Types 4 | NamedTuple = FixedWidth.build(error: "NamedTuple type must contain one or more symbol -> type pairs. E.g. NamedTuple(hello: Int32, world: String)") 5 | 6 | def self.NamedTuple(types_hash) 7 | raise "NamedTuple must be instantiated with a hash" unless types_hash.is_a?(Root::Hash) 8 | 9 | types_hash.keys.each do |key| 10 | raise "NamedTuple keys must be symbols" unless key.is_a?(Root::Symbol) || key.respond_to?(:to_sym) 11 | end 12 | keys = types_hash.keys.map(&:to_sym) 13 | value_types = types_hash.values 14 | 15 | FixedWidth.build(:NamedTuple, ffi_type: :pointer, inner_types: value_types, inner_keys: keys, 16 | convert_if: [Root::Hash]) do 17 | @data_offset = 4 18 | 19 | # We only accept Hash-like values, which have all of the required keys 20 | # and values of the correct type 21 | # can successfully be cast to our inner types 22 | def self.cast!(value) 23 | value = value.transform_keys(&:to_sym) 24 | unless value.is_a?(Hash) || value.is_a?(Root::Hash) && inner_keys.each_with_index.all? do |k, i| 25 | value.key?(k) && inner_types[i].valid_cast?(value[k]) 26 | end 27 | raise CrystalRuby::InvalidCastError, "Cannot cast #{value} to #{inspect}" 28 | end 29 | 30 | inner_keys.map { |k| value[k] } 31 | end 32 | 33 | def self.copy_to!(values, memory:) 34 | data_pointer = malloc(memsize) 35 | 36 | memory[data_offset].write_pointer(data_pointer) 37 | 38 | inner_types.each.reduce(0) do |offset, type| 39 | type.write_single(data_pointer[offset], values.shift) 40 | offset + type.refsize 41 | end 42 | end 43 | 44 | def self.memsize 45 | inner_types.map(&:refsize).sum 46 | end 47 | 48 | def self.each_child_address(pointer) 49 | data_pointer = pointer[data_offset].read_pointer 50 | inner_types.each do |type| 51 | yield type, data_pointer 52 | data_pointer += type.refsize 53 | end 54 | end 55 | 56 | def self.offset_for(key) 57 | inner_types[0...inner_keys.index(key)].map(&:refsize).sum 58 | end 59 | 60 | def value(native: false) 61 | ptr = data_pointer 62 | inner_keys.zip(inner_types.map do |type| 63 | result = type.fetch_single(ptr, native: native) 64 | ptr += type.refsize 65 | result 66 | end).to_h 67 | end 68 | 69 | inner_keys.each.with_index do |key, index| 70 | type = inner_types[index] 71 | offset = offset_for(key) 72 | unless method_defined?(key) 73 | define_method(key) do 74 | type.fetch_single(data_pointer[offset]) 75 | end 76 | end 77 | 78 | unless method_defined?("#{key}=") 79 | define_method("#{key}=") do |value| 80 | type.write_single(data_pointer[offset], value) 81 | end 82 | end 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/crystalruby/config.rb: -------------------------------------------------------------------------------- 1 | require "singleton" 2 | require "yaml" 3 | require "logger" 4 | 5 | module CrystalRuby 6 | # Config mixin to easily access the configuration 7 | # from anywhere in the code 8 | module Config 9 | def config 10 | Configuration.instance 11 | end 12 | end 13 | 14 | # Defines our configuration singleton 15 | # Config can be specified through either: 16 | # - crystalruby.yaml OR 17 | # - CrystalRuby.configure block 18 | class Configuration 19 | include Singleton 20 | attr_accessor :debug, :verbose, :logger, :colorize_log_output, 21 | :single_thread_mode, :crystal_missing_ignore 22 | 23 | def initialize 24 | @paths_cache = {} 25 | config = read_config || {} 26 | @crystal_src_dir = config.fetch("crystal_src_dir", "./crystalruby") 27 | @crystal_codegen_dir = config.fetch("crystal_codegen_dir", "generated") 28 | @crystal_project_root = config.fetch("crystal_project_root", Pathname.pwd) 29 | @crystal_missing_ignore = config.fetch("crystal_missing_ignore", false) 30 | @debug = config.fetch("debug", false) 31 | @verbose = config.fetch("verbose", false) 32 | @single_thread_mode = config.fetch("single_thread_mode", false) 33 | @colorize_log_output = config.fetch("colorize_log_output", false) 34 | @log_level = config.fetch("log_level", ENV.fetch("CRYSTALRUBY_LOG_LEVEL", "info")) 35 | @logger = Logger.new($stdout) 36 | @logger.level = Logger.const_get(@log_level.to_s.upcase) 37 | end 38 | 39 | def file_config 40 | @file_config ||= File.exist?("crystalruby.yaml") && begin 41 | YAML.safe_load(IO.read("crystalruby.yaml")) 42 | rescue StandardError 43 | nil 44 | end || {} 45 | end 46 | 47 | def self.define_directory_accessors!(parent, directory_node) 48 | case directory_node 49 | when Array then directory_node.each(&method(:define_directory_accessors!).curry[parent]) 50 | when Hash 51 | directory_node.each do |par, children| 52 | define_directory_accessors!(parent, par) 53 | define_directory_accessors!(par, children) 54 | end 55 | else 56 | define_method(directory_node) do 57 | @paths_cache[directory_node] ||= Pathname.new(instance_variable_get(:"@#{directory_node}")) 58 | end 59 | define_method("#{directory_node}_abs") do 60 | @paths_cache["#{directory_node}_abs"] ||= parent ? send("#{parent}_abs") / Pathname.new(instance_variable_get(:"@#{directory_node}")) : send(directory_node) 61 | end 62 | end 63 | end 64 | 65 | define_directory_accessors!(nil, { crystal_project_root: { crystal_src_dir: [:crystal_codegen_dir] } }) 66 | 67 | def log_level=(level) 68 | @logger.level = Logger.const_get(@log_level = level.to_s.upcase) 69 | end 70 | 71 | private 72 | 73 | def read_config 74 | YAML.safe_load(IO.read("crystalruby.yaml")) 75 | rescue 76 | {} 77 | end 78 | end 79 | 80 | extend Config 81 | def self.configure 82 | yield(config) 83 | @paths_cache = {} 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/crystalruby/types/concerns/allocator.rb: -------------------------------------------------------------------------------- 1 | module CrystalRuby 2 | module Types 3 | # Module for memory allocation and tracking functionality 4 | module Allocator 5 | # Called when module is included in a class 6 | # @param base [Class] The class including this module 7 | 8 | def self.gc_hint!(size) 9 | @bytes_seen_since_gc = (@bytes_seen_since_gc || 0) + size 10 | end 11 | 12 | def self.gc_bytes_seen 13 | @bytes_seen_since_gc ||= 0 14 | end 15 | 16 | def self.gc_hint_reset! 17 | @bytes_seen_since_gc = 0 18 | end 19 | 20 | def self.included(base) 21 | base.class_eval do 22 | # Synchronizes a block using mutex 23 | # @yield Block to be synchronized 24 | def self.synchronize(&block) 25 | Type::ARC_MUTEX.synchronize(&block) 26 | end 27 | 28 | # Schedules a block for execution 29 | # @yield Block to be scheduled 30 | def self.schedule!(&block) 31 | Type::ARC_MUTEX.schedule!(&block) 32 | end 33 | 34 | extend FFI::Library 35 | ffi_lib "c" 36 | attach_function :_calloc, :calloc, %i[size_t size_t], :pointer 37 | attach_function :_free, :free, [:pointer], :void 38 | define_singleton_method(:ptr, &FFI::Pointer.method(:new)) 39 | define_method(:ptr, &FFI::Pointer.method(:new)) 40 | 41 | extend Forwardable 42 | 43 | # Instance method to allocate memory 44 | # @param size [Integer] Size in bytes to allocate 45 | # @return [FFI::Pointer] Pointer to allocated memory 46 | def malloc(size) 47 | self.class.malloc(size) 48 | end 49 | 50 | # Class method to allocate memory 51 | # @param size [Integer] Size in bytes to allocate 52 | # @return [FFI::Pointer] Pointer to allocated memory 53 | def self.malloc(size) 54 | result = _calloc(size, 1) 55 | traced_live_objects[result.address] = result if trace_live_objects? 56 | result 57 | end 58 | 59 | # Frees allocated memory 60 | # @param ptr [FFI::Pointer] Pointer to memory to free 61 | def self.free(ptr) 62 | traced_live_objects.delete(ptr.address) if trace_live_objects? 63 | _free(ptr) 64 | end 65 | 66 | # Returns hash of traced live objects 67 | # @return [Hash] Map of addresses to pointers 68 | def self.traced_live_objects 69 | @traced_live_objects ||= {} 70 | end 71 | 72 | # Enables tracing of live objects 73 | def self.trace_live_objects! 74 | @trace_live_objects = true 75 | end 76 | 77 | # Checks if live object tracing is enabled 78 | # @return [Boolean] True if tracing is enabled 79 | def self.trace_live_objects? 80 | !!@trace_live_objects 81 | end 82 | 83 | # Returns count of live objects being tracked 84 | # @return [Integer] Number of live objects 85 | def self.live_objects 86 | traced_live_objects.count 87 | end 88 | end 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/crystalruby/types/variable_width/array.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CrystalRuby::Types 4 | Array = VariableWidth.build(error: "Array type must have a type parameter. E.g. Array(Float64)") 5 | 6 | # An array-like, reference counted manually managed memory type. 7 | # Shareable between Crystal and Crystal. 8 | def self.Array(type) 9 | VariableWidth.build(:Array, inner_types: [type], convert_if: [Array, Root::Array], superclass: Array) do 10 | include Enumerable 11 | 12 | # Implement the Enumerable interface 13 | # Helps this object to act like an Array 14 | def each 15 | size.times { |i| yield self[i] } 16 | end 17 | 18 | # We only accept Array-like values, from which all elements 19 | # can successfully be cast to our inner type 20 | def self.cast!(value) 21 | unless value.is_a?(Array) || value.is_a?(Root::Array) && value.all?(&inner_type.method(:valid_cast?)) 22 | raise CrystalRuby::InvalidCastError, "Cannot cast #{value} to #{inspect}" 23 | end 24 | 25 | if inner_type.primitive? 26 | value.map(&inner_type.method(:to_ffi_repr)) 27 | else 28 | value 29 | end 30 | end 31 | 32 | def self.copy_to!(rbval, memory:) 33 | data_pointer = malloc(rbval.size * inner_type.refsize) 34 | 35 | memory[size_offset].write_uint32(rbval.size) 36 | memory[data_offset].write_pointer(data_pointer) 37 | 38 | if inner_type.primitive? 39 | data_pointer.send("put_array_of_#{inner_type.ffi_type}", 0, rbval) 40 | else 41 | rbval.each_with_index do |val, i| 42 | inner_type.write_single(data_pointer[i * refsize], val) 43 | end 44 | end 45 | end 46 | 47 | def self.each_child_address(pointer) 48 | size = pointer[size_offset].get_int32(0) 49 | pointer = pointer[data_offset].read_pointer 50 | size.times do |i| 51 | yield inner_type, pointer[i * inner_type.refsize] 52 | end 53 | end 54 | 55 | def checked_offset!(index, size) 56 | raise "Index out of bounds: #{index} >= #{size}" if index >= size 57 | 58 | if index < 0 59 | raise "Index out of bounds: #{index} < -#{size}" if index < -size 60 | 61 | index += size 62 | end 63 | index 64 | end 65 | 66 | # Return the element at the given index. 67 | # This will automatically increment 68 | # the reference count if not a primitive type. 69 | def [](index) 70 | inner_type.fetch_single(data_pointer[checked_offset!(index, size) * inner_type.refsize]) 71 | end 72 | 73 | # Overwrite the element at the given index 74 | # The replaced element will have 75 | # its reference count decremented. 76 | def []=(index, value) 77 | inner_type.write_single(data_pointer[checked_offset!(index, size) * inner_type.refsize], value) 78 | end 79 | 80 | # Load values stored inside array type. 81 | # If it's a primitive type, we can quickly bulk load the values. 82 | # Otherwise we need toinstantiate new ref-checked instances. 83 | def value(native: false) 84 | inner_type.fetch_multi(data_pointer, size, native: native) 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /test/test_crystalize_dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | class TestCrystalizeDSL < Minitest::Test 6 | 7 | def test_cryalize_backcompat 8 | Adder.class_eval do 9 | crystalize ->{ :int }, async: false 10 | def cryatlize_mult(a: :int, b: :int) 11 | a * b 12 | end 13 | end 14 | 15 | assert Adder.cryatlize_mult(4, 2) == 8 16 | end 17 | 18 | def test_simple_adder 19 | Adder.class_eval do 20 | crystallize :int, async: false 21 | def add(a: :int, b: :int) 22 | a + b 23 | end 24 | end 25 | 26 | assert Adder.add(1, 2) == 3 27 | end 28 | 29 | def test_reopen 30 | Adder.class_eval do 31 | crystallize ->{ :int }, async: false 32 | def mult(a: :int, b: :int) 33 | a * b 34 | end 35 | end 36 | 37 | assert Adder.mult(4, 2) == 8 38 | end 39 | 40 | def test_string_ops 41 | Adder.class_eval do 42 | crystallize async: false 43 | def atsrev(a: :string, b: :string, returns: :string) 44 | (a + b).reverse 45 | end 46 | end 47 | 48 | assert Adder.atsrev("one", "two") == "owteno" 49 | end 50 | 51 | crystallize 52 | def takes_two_arguments(a: Int32, b: Int32, yield: Proc(Int32), returns: Int32) 53 | yield 4 54 | 3 55 | end 56 | 57 | def test_argument_count_errors 58 | assert_raises(ArgumentError) { takes_two_arguments(1) } 59 | assert_raises(ArgumentError) { takes_two_arguments(1, 2, 3, 4) } 60 | assert_equal( 61 | takes_two_arguments(1, 2) do 62 | end, 3 63 | ) 64 | end 65 | 66 | module CrystalizeSyntax 67 | CustomType = CRType{ Int32 } 68 | end 69 | 70 | include CrystalizeSyntax 71 | def test_crystallize_syntax 72 | 73 | CrystalizeSyntax.class_eval do 74 | 75 | 76 | crystallize lib: 'multi-compile', raw: true 77 | def sub_cust(a: Int32, b: Int64 | String, returns: Int32) 78 | " 79 | 3 80 | " 81 | end 82 | 83 | crystallize lib: 'multi-compile', raw: true 84 | def sub_cust_heredoc(a: Int32, b: Int64 | String, returns: Int32) 85 | 86 | <<~CRYSTAL 87 | 9 88 | CRYSTAL 89 | end 90 | 91 | crystallize ->{ Int32 }, lib: 'multi-compile' 92 | def sub a: Int32, b: Int32 93 | a - b 94 | end 95 | 96 | crystallize ->{ Int32 }, lib: 'multi-compile', raw: true 97 | def sub2(a: Int32, b: Int32) = "a - b" 98 | 99 | crystallize ->{ Int32 }, lib: 'multi-compile', raw: false 100 | def sub3(a: Int32, b: Int32) = a - b 101 | 102 | # Unusual spacing is intentional to 103 | # test method parsing. 104 | crystallize ->{ Int32 }, lib: 'multi-compile' 105 | def add \ 106 | a: :int, 107 | 108 | b: :int 109 | 110 | a + b 111 | 112 | end 113 | 114 | 115 | crystallize :int, lib: 'multi-compile' 116 | def add_simple(a: :int, b: :int) 117 | a + b 118 | end 119 | 120 | crystal do 121 | temp = 1 + 2 122 | end 123 | 124 | 125 | crystal raw: true do 126 | <<~HD 127 | CONST3 = 1 + 2 128 | HD 129 | end 130 | end 131 | 132 | assert CrystalizeSyntax.add(4, 2) == 6 133 | assert CrystalizeSyntax.sub(4, 2) == 2 134 | assert CrystalizeSyntax.add_simple(4, 2) == 6 135 | assert CrystalizeSyntax.sub_cust(4, 88) == 3 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /test/test_instance.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class TestInstance < Minitest::Test 4 | class Person < CRType do 5 | NamedTuple( 6 | first_name: String, 7 | last_name: String, 8 | age: Int32 9 | ) 10 | end 11 | 12 | crystallize 13 | def first_name_cr=(first_name: String) 14 | self.first_name = first_name 15 | end 16 | 17 | crystallize 18 | def first_name_cr(returns: String) 19 | first_name.value 20 | end 21 | 22 | expose_to_crystal 23 | def last_name_rb=(last_name: String) 24 | self.last_name = last_name 25 | end 26 | 27 | expose_to_crystal 28 | def last_name_rb(returns: :string) 29 | last_name.value 30 | end 31 | 32 | crystallize 33 | def capitalize_full_name_cr 34 | self.first_name_cr = first_name_cr.capitalize 35 | self.last_name_rb = last_name_rb.capitalize 36 | end 37 | 38 | def lower_case_full_name_rb 39 | self.first_name_cr = first_name_cr.downcase 40 | self.last_name_rb = last_name_rb.downcase 41 | end 42 | 43 | crystallize 44 | def yield_cr_to_rb(big: Bool, yield: Proc(Bool, Int32), returns: Int32) 45 | 10 + yield(big) 46 | end 47 | 48 | expose_to_crystal 49 | def yield_rb_to_cr(big: Bool, yield: Proc(Bool, Int32), returns: Int32) 50 | 10 + yield(big) 51 | end 52 | 53 | crystallize 54 | def invoke_yield_rb_to_cr(big: Bool, returns: Int32) 55 | yield_rb_to_cr(big) do |big| 56 | if big 57 | 10_000 58 | else 59 | 1 60 | end 61 | end 62 | end 63 | end 64 | 65 | crystallize 66 | def construct_person(first_name: String, returns: Person) 67 | Person.new({ first_name: first_name, last_name: "Doe", age: 30 }) 68 | end 69 | 70 | def test_can_construct_instance 71 | assert Person.new(first_name: "John", last_name: "Doe", age: 30) 72 | end 73 | 74 | def test_can_construct_new_instance_in 75 | person = construct_person("Hi Crystal") 76 | assert_equal person, Person.new(first_name: "Hi Crystal", last_name: "Doe", age: 30) 77 | end 78 | 79 | def test_can_update_attribute_in_crystal 80 | person = Person.new(first_name: "John", last_name: "Doe", age: 30) 81 | person.first_name_cr = "Steve" 82 | assert_equal person.first_name_cr, "Steve" 83 | end 84 | 85 | def test_cross_language_setters 86 | Person.new(first_name: "john", last_name: "doe", age: 30).tap do |person| 87 | person.capitalize_full_name_cr 88 | assert_equal person.first_name, "John" 89 | assert_equal person.last_name, "Doe" 90 | end 91 | 92 | Person.new(first_name: "JOHN", last_name: "DOE", age: 30).tap do |person| 93 | person.lower_case_full_name_rb 94 | assert_equal person.first_name, "john" 95 | assert_equal person.last_name, "doe" 96 | end 97 | end 98 | 99 | def test_invoke_yield_rb_to_cr 100 | Person.new(first_name: "john", last_name: "doe", age: 30).tap do |person| 101 | assert_equal person.invoke_yield_rb_to_cr(true), 10_010 102 | end 103 | 104 | Person.new(first_name: "john", last_name: "doe", age: 30).tap do |person| 105 | assert_equal person.invoke_yield_rb_to_cr(false), 11 106 | end 107 | end 108 | 109 | def test_yield_cr_to_rb 110 | Person.new(first_name: "john", last_name: "doe", age: 30).tap do |person| 111 | assert_equal(person.yield_cr_to_rb(false) { 1234 }, 1244) 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/crystalruby/types/fixed_width/tagged_union.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CrystalRuby::Types 4 | TaggedUnion = Class.new(Type) { @error = "Union type must be instantiated from one or more concrete types" } 5 | 6 | def self.TaggedUnion(*union_types) 7 | Class.new(FixedWidth) do 8 | # We only accept List-like values, which have all of the required keys 9 | # and values of the correct type 10 | # can successfully be cast to our inner types 11 | def self.cast!(value) 12 | casteable_type_index = union_types.find_index do |type, _index| 13 | next false unless type.valid_cast?(value) 14 | 15 | type.cast!(value) 16 | next true 17 | rescue StandardError 18 | nil 19 | end 20 | unless casteable_type_index 21 | raise CrystalRuby::InvalidCastError, 22 | "Cannot cast #{value}:#{value.class} to #{inspect}" 23 | end 24 | 25 | [casteable_type_index, value] 26 | end 27 | 28 | def value(native: false) 29 | type = self.class.union_types[data_pointer.read_uint8] 30 | type.fetch_single(data_pointer[1], native: native) 31 | end 32 | 33 | def nil? 34 | value.nil? 35 | end 36 | 37 | def ==(other) 38 | value == other 39 | end 40 | 41 | def self.copy_to!((type_index, value), memory:) 42 | memory[data_offset].write_int8(type_index) 43 | union_types[type_index].write_single(memory[data_offset + 1], value) 44 | end 45 | 46 | def data_pointer 47 | memory[data_offset] 48 | end 49 | 50 | def self.each_child_address(pointer) 51 | pointer += data_offset 52 | type = self.union_types[pointer.read_uint8] 53 | yield type, pointer[1] 54 | end 55 | 56 | def self.inner_types 57 | union_types 58 | end 59 | 60 | def total_memsize 61 | type = self.class.union_types[data_pointer.read_uint8] 62 | memsize + refsize + (type.primitive? ? type.memsize : value.total_memsize) 63 | end 64 | 65 | define_singleton_method(:memsize) do 66 | union_types.map(&:refsize).max + 1 67 | end 68 | 69 | def self.refsize 70 | 8 71 | end 72 | 73 | def self.typename 74 | "TaggedUnion" 75 | end 76 | 77 | define_singleton_method(:union_types) do 78 | union_types 79 | end 80 | 81 | define_singleton_method(:valid?) do 82 | union_types.all?(&:valid?) 83 | end 84 | 85 | define_singleton_method(:error) do 86 | union_types.map(&:error).join(", ") if union_types.any?(&:error) 87 | end 88 | 89 | define_singleton_method(:inspect) do 90 | if anonymous? 91 | union_types.map(&:inspect).join(" | ") 92 | else 93 | crystal_class_name 94 | end 95 | end 96 | 97 | define_singleton_method(:native_type_expr) do 98 | union_types.map(&:native_type_expr).join(" | ") 99 | end 100 | 101 | define_singleton_method(:named_type_expr) do 102 | union_types.map(&:named_type_expr).join(" | ") 103 | end 104 | 105 | define_singleton_method(:type_expr) do 106 | anonymous? ? native_type_expr : name 107 | end 108 | 109 | define_singleton_method(:data_offset) do 110 | 4 111 | end 112 | 113 | define_singleton_method(:valid_cast?) do |raw| 114 | union_types.any? { |type| type.valid_cast?(raw) } 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/crystalruby/types/fixed_width/proc.rb: -------------------------------------------------------------------------------- 1 | module CrystalRuby::Types 2 | PROC_REGISTERY = {} 3 | Proc = FixedWidth.build(error: "Proc declarations should contain a list of 0 or more comma separated argument types,"\ 4 | "and a single return type (or Nil if it does not return a value)") 5 | 6 | def self.Proc(*types) 7 | proc_type = FixedWidth.build(:Proc, convert_if: [::Proc], inner_types: types, 8 | ffi_type: :pointer) do 9 | @data_offset = 4 10 | 11 | def self.cast!(rbval) 12 | raise "Value must be a proc" unless rbval.is_a?(::Proc) 13 | 14 | func = FFI::Function.new(FFI::Type.const_get(inner_types[-1].ffi_type.to_s.upcase), inner_types[0...-1].map do |v| 15 | FFI::Type.const_get(v.ffi_type.to_s.upcase) 16 | end) do |*args| 17 | args = args.map.with_index do |arg, i| 18 | arg = inner_types[i].new(arg) unless arg.is_a?(inner_types[i]) 19 | inner_types[i].anonymous? ? arg.native : arg 20 | end 21 | return_val = rbval.call(*args) 22 | return_val = inner_types[-1].new(return_val) unless return_val.is_a?(inner_types[-1]) 23 | return_val.memory 24 | end 25 | PROC_REGISTERY[func.address] = func 26 | func 27 | end 28 | 29 | def self.copy_to!(rbval, memory:) 30 | memory[4].write_pointer(rbval) 31 | end 32 | 33 | def invoke(*args) 34 | invoker = value 35 | invoker.call(memory[12].read_pointer, *args) 36 | end 37 | 38 | def value(native: false) 39 | FFI::VariadicInvoker.new( 40 | memory[4].read_pointer, 41 | [FFI::Type::POINTER, *(inner_types[0...-1].map { |v| FFI::Type.const_get(v.ffi_type.to_s.upcase) })], 42 | FFI::Type.const_get(inner_types[-1].ffi_type.to_s.upcase), 43 | { ffi_convention: :stdcall } 44 | ) 45 | end 46 | 47 | def self.block_converter 48 | <<~CRYSTAL 49 | { #{ 50 | inner_types.size > 1 ? "|#{inner_types.size.-(1).times.map { |i| "v#{i}" }.join(",")}|" : "" 51 | } 52 | #{ 53 | inner_types[0...-1].map.with_index do |type, i| 54 | <<~CRYS 55 | v#{i} = #{type.crystal_class_name}.new(v#{i}).return_value 56 | 57 | callback_done_channel = Channel(Nil).new 58 | result = nil 59 | if Fiber.current == Thread.current.main_fiber 60 | block_value = #{inner_types[-1].crystal_class_name}.new(__yield_to.call(#{inner_types.size.-(1).times.map { |i| "v#{i}" }.join(",")})) 61 | result = #{inner_types[-1].anonymous? ? "block_value.native_decr" : "block_value"} 62 | next #{inner_types.last == CrystalRuby::Types::Nil ? "result" : "result.not_nil!"} 63 | else 64 | CrystalRuby.queue_callback(->{ 65 | block_value = #{inner_types[-1].crystal_class_name}.new(__yield_to.call(#{inner_types.size.-(1).times.map { |i| "v#{i}" }.join(",")})) 66 | result = #{inner_types[-1].anonymous? ? "block_value.native_decr" : "block_value"} 67 | callback_done_channel.send(nil) 68 | }) 69 | end 70 | callback_done_channel.receive 71 | #{inner_types.last == CrystalRuby::Types::Nil ? "result" : "result.not_nil!"} 72 | CRYS 73 | end.join("\n") 74 | } 75 | } 76 | CRYSTAL 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/crystalruby/types/primitive.rb: -------------------------------------------------------------------------------- 1 | module CrystalRuby 2 | module Types 3 | class Primitive < Type 4 | # Primitives just store the Ruby value directly 5 | # (Or read it from memory if passed a pointer) 6 | def initialize(rbval) 7 | super(rbval) 8 | self.value = rbval.is_a?(FFI::Pointer) ? rbval.send("read_#{ffi_type}") : rbval 9 | end 10 | 11 | # Read a value from a pointer at a given index 12 | # (Type can be a byte-array, pointer or numeric type) 13 | def self.fetch_single(pointer, native: false) 14 | # Nothing to fetch for Nils 15 | return if memsize.zero? 16 | 17 | if numeric? 18 | pointer.send("read_#{ffi_type}") 19 | elsif primitive? 20 | single = new(pointer.send("read_#{ffi_type}")) 21 | if native 22 | single.value 23 | else 24 | single 25 | end 26 | end 27 | end 28 | 29 | # Write a data type into a pointer at a given index 30 | # (Type can be a byte-array, pointer or numeric type) 31 | def self.write_single(pointer, value) 32 | # Dont need to write nils 33 | return if memsize.zero? 34 | 35 | pointer.send("write_#{ffi_type}", to_ffi_repr(value)) 36 | end 37 | 38 | # Fetch an array of a given data type from a list pointer 39 | # (Type can be a byte-array, pointer or numeric type) 40 | def self.fetch_multi(pointer, size, native: false) 41 | if numeric? 42 | pointer.send("get_array_of_#{ffi_type}", 0, size) 43 | elsif primitive? 44 | pointer.send("get_array_of_#{ffi_type}", 0, size).map(&method(:from_ffi_array_repr)) 45 | end 46 | end 47 | 48 | def self.decrement_ref_count!(pointer) 49 | # Do nothing 50 | end 51 | 52 | # Define a new primitive type 53 | # Primitive types are stored by value 54 | # and efficiently copied using native FFI types 55 | # They are written directly into the memory of a container type 56 | # (No indirection) 57 | def self.build( 58 | typename = nil, 59 | ffi_type: :uint8, 60 | memsize: FFI.type_size(ffi_type), 61 | convert_if: [], 62 | error: nil, 63 | ffi_primitive: false, 64 | superclass: Primitive, 65 | &block 66 | ) 67 | Class.new(superclass) do 68 | %w[typename ffi_type memsize convert_if error ffi_primitive].each do |name| 69 | define_singleton_method(name) { binding.local_variable_get("#{name}") } 70 | define_method(name) { binding.local_variable_get("#{name}") } 71 | end 72 | 73 | class_eval(&block) if block_given? 74 | 75 | # Primitives are stored directly in memory as a raw numeric value 76 | def self.to_ffi_repr(value) 77 | new(value).inner_value 78 | end 79 | 80 | def self.refsize 81 | memsize 82 | end 83 | 84 | # Primiives are anonymous (Shouldn't be subclassed) 85 | def self.anonymous? 86 | true 87 | end 88 | 89 | def self.copy_to!(rbval, memory:) 90 | memory.send("write_#{self.ffi_type}", to_ffi_repr(rbval)) 91 | end 92 | 93 | def self.primitive? 94 | true 95 | end 96 | 97 | def self.inspect 98 | inspect_name 99 | end 100 | 101 | def memory 102 | @value 103 | end 104 | 105 | def self.crystal_supertype 106 | "CrystalRuby::Types::Primitive" 107 | end 108 | end 109 | end 110 | end 111 | end 112 | end 113 | 114 | require_relative "primitive_types/time" 115 | require_relative "primitive_types/symbol" 116 | require_relative "primitive_types/numbers" 117 | require_relative "primitive_types/nil" 118 | require_relative "primitive_types/bool" 119 | -------------------------------------------------------------------------------- /test/test_type_dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | class TestTypeDSL < Minitest::Test 6 | def test_simple_numeric_types 7 | %i[UInt8 UInt16 UInt32 UInt64 Int8 Int16 Int32 Int64 Float32 Float64].first(2).each do |typename| 8 | tp = CrystalRuby::Types.const_get(typename) 9 | 10 | numeric_object = tp.new(100) 11 | 12 | # We've allocated a single pointer 13 | assert numeric_object == 100 14 | numeric_object.value = 5 15 | 16 | # We've still only allocated a single pointer, but swapped out the value 17 | assert numeric_object == 5 18 | 19 | # Acts like pointed object if methods aren't defined on type 20 | assert numeric_object + 40 == 45 21 | 22 | assert_raises(RuntimeError) { numeric_object.value = :not_a_number } 23 | end 24 | end 25 | 26 | def test_simple_string 27 | string_tp = CRType { String } 28 | 29 | string_value = string_tp.new("Hello World") 30 | 31 | # We've allocated a pointer to our string (which in itself is a pointer to a char) 32 | 33 | assert string_value == "Hello World" 34 | 35 | # We create a new char * and update the our string value address to point to it 36 | string_value.value = "Goodbye World" 37 | 38 | assert string_value == "Goodbye World" 39 | end 40 | 41 | def test_simple_symbol 42 | symbol_tp = CRType { Symbol(:"Hello World", :"Goodbye World") } 43 | 44 | symbol = symbol_tp.new(:"Hello World") 45 | 46 | # We've allocated a pointer to our string (which in itself is a pointer to a char) 47 | 48 | assert symbol == :"Hello World" 49 | 50 | # We create a new char * and update the our string value address to point to it 51 | symbol.value = :"Goodbye World" 52 | 53 | assert symbol == :"Goodbye World" 54 | end 55 | 56 | def test_simple_time 57 | time_tp = CRType { Time } 58 | 59 | time = time_tp.new(Time.at(0)) 60 | 61 | # Times are just stored as doubles 62 | 63 | time.value += 86_400 # 1 day 64 | assert_equal time, Time.at(86_400) 65 | end 66 | 67 | def test_primitive_union 68 | int_or_bool = CRType { Int32 | Bool } 69 | 70 | iob = int_or_bool.new(38) 71 | assert iob == 38 72 | 73 | iob.value = true 74 | assert iob == true 75 | 76 | assert_raises(CrystalRuby::InvalidCastError) { iob.value = "not a number or bool" } 77 | end 78 | 79 | def test_primitive_array 80 | optional_int_or_bool_array = CRType { Array(Int32 | Bool | Nil) } 81 | 82 | ia = optional_int_or_bool_array[1, 2, 3, 4, 5, false, true, false, nil] 83 | assert ia[0] == 1 84 | 85 | ia[1] = 2 86 | assert ia[1] == 2 87 | 88 | ia[3] = nil 89 | 90 | assert ia[3].nil? 91 | 92 | ia.value = [5, nil, false, 4, 3, 2, 1] 93 | 94 | assert ia == [5, nil, false, 4, 3, 2, 1] 95 | 96 | assert_raises(CrystalRuby::InvalidCastError) { ia.value = [1, 2, 3, 4, "not a number or bool or nil"] } 97 | end 98 | 99 | def test_primitive_hash 100 | numeric_to_opt_bool_hash = CRType { Hash(Float64 | Int32, Bool | Nil) } 101 | 102 | hash = numeric_to_opt_bool_hash[5 => true, 7 => false, 8.8 => nil] 103 | assert hash[5] == true 104 | assert hash[7] == false 105 | assert hash[8.8].nil? 106 | hash[5] = nil 107 | hash[7] = true 108 | hash[8.8] = false 109 | 110 | assert hash[5].nil? 111 | assert hash[7] == true 112 | assert hash[8.8] == false 113 | assert hash == { 5.0 => nil, 7.0 => true, 8.8 => false } 114 | 115 | hash.value = { 9 => true, 8 => false, 0.4 => nil } 116 | assert hash == { 9.0 => true, 8.0 => false, 0.4 => nil } 117 | 118 | shallow_copy = hash.dup 119 | deep_copy = hash.deep_dup 120 | 121 | shallow_copy[9] = nil 122 | deep_copy[9] = false 123 | 124 | assert hash[9].nil? 125 | assert deep_copy[9] == false 126 | 127 | assert_raises(CrystalRuby::InvalidCastError) { hash[:not_found] = 33 } 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /test/types/test_enumerable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | class TestEnumerable < Minitest::Test 6 | TestComplexArrayType = CRType { Array(Hash(Int32, Int32)) } 7 | 8 | crystallize 9 | def cr_non_destructive_map(array: TestComplexArrayType, returns: Array(Int32)) 10 | array.map { |x| x.values.max } 11 | end 12 | 13 | crystallize 14 | def cr_desctructive_map(array: TestComplexArrayType) 15 | array.map! { |x| x.transform_values { |v| v * 2 } } 16 | end 17 | 18 | def test_arr_map_cr 19 | test_array = TestComplexArrayType[{ 1 => 3 }, { 2 => 4 }, { 3 => 5 }] 20 | # Non destructive map works as expected 21 | assert_equal cr_non_destructive_map(test_array), [3, 4, 5] 22 | 23 | # Destructive map must return a compatible type (Type system enforces this) 24 | cr_desctructive_map(test_array) 25 | assert_equal test_array, [{ 1 => 6 }, { 2 => 8 }, { 3 => 10 }] 26 | end 27 | 28 | crystallize 29 | def find_hash_with_key(array: TestComplexArrayType, key: Int32, returns: Hash(Int32, Int32)) 30 | array.find { |x| x.keys.includes?(key) }.not_nil! 31 | end 32 | 33 | def test_arr_find_cr 34 | test_array = TestComplexArrayType[{ 1 => 3 }, { 2 => 4 }, { 3 => 5 }] 35 | found = find_hash_with_key(test_array, 3) 36 | assert_equal found, { 3 => 5 } 37 | end 38 | 39 | crystallize 40 | def group_by_max_value(array: TestComplexArrayType, returns: Hash(Int32, Array(Hash(Int32, Int32)))) 41 | array.group_by { |x| x.values.max }.not_nil! 42 | end 43 | 44 | def test_arr_group_by_cr 45 | test_array = TestComplexArrayType[{ 1 => 3, 8 => 2 }, { 2 => 4, 1 => 5 }, { 3 => 5 }, { 5 => 2 }] 46 | found = group_by_max_value(test_array) 47 | assert_equal found, { 3 => [{ 1 => 3, 8 => 2 }], 5 => [{ 2 => 4, 1 => 5 }, { 3 => 5 }], 2 => [{ 5 => 2 }] } 48 | end 49 | 50 | crystallize 51 | def reduce_sum_values(array: TestComplexArrayType, returns: Int32) 52 | array.reduce(0) { |acc, x| acc + x.values.sum } 53 | end 54 | 55 | def test_arr_reduce_cr 56 | test_array = TestComplexArrayType[{ 1 => 3, 8 => 2 }, { 2 => 4, 1 => 5 }, { 3 => 5 }, { 5 => 2 }] 57 | assert_equal reduce_sum_values(test_array), 21 58 | end 59 | 60 | def test_arr_map_rb 61 | test_array = TestComplexArrayType[{ 1 => 3 }, { 2 => 4 }, { 3 => 5 }] 62 | # Non destructive map works as expected 63 | assert_equal test_array.map { |x| x.values.max }, [3, 4, 5] 64 | 65 | # Destructive map must return a compatible type 66 | test_array.map! { |x| x.transform_values { |v| v * 2 } } 67 | 68 | # Otherwise, it raises an error 69 | assert_raises(CrystalRuby::InvalidCastError) do 70 | test_array.map! do |x| 71 | x.transform_values do |_v| 72 | "Incompatible type" 73 | end 74 | end 75 | end 76 | end 77 | 78 | def test_arr_find_rb 79 | test_array = TestComplexArrayType[{ 1 => 3, 8 => 2 }, { 2 => 4, 1 => 5 }, { 3 => 5 }, { 5 => 2 }] 80 | assert_equal test_array.find { |x| x.keys.include?(3) }, { 3 => 5 } 81 | end 82 | 83 | def test_arr_group_by_rb 84 | test_array = TestComplexArrayType[{ 1 => 3, 8 => 2 }, { 2 => 4, 1 => 5 }, { 3 => 5 }, { 5 => 2 }] 85 | assert_equal test_array.group_by { |x| 86 | x.values.max 87 | }, { 3 => [{ 1 => 3, 8 => 2 }], 5 => [{ 2 => 4, 1 => 5 }, { 3 => 5 }], 2 => [{ 5 => 2 }] } 88 | end 89 | 90 | def test_arr_reduce_rb 91 | test_array = TestComplexArrayType[{ 1 => 3, 8 => 2 }, { 2 => 4, 1 => 5 }, { 3 => 5 }, { 5 => 2 }] 92 | assert_equal test_array.reduce(0) { |acc, x| acc + x.values.sum }, 21 93 | end 94 | 95 | TestComplexHashType = CRType { Hash(Int32, Array(Int32)) } 96 | 97 | def test_hash_map_cr; end 98 | 99 | def test_hash_map_rb; end 100 | 101 | def test_hash_find_cr; end 102 | 103 | def test_hash_find_rb; end 104 | 105 | def test_hash_group_by_cr; end 106 | 107 | def test_hash_group_by_rb; end 108 | 109 | def test_hash_reduce_cr; end 110 | 111 | def test_hash_reduce_rb; end 112 | end 113 | -------------------------------------------------------------------------------- /lib/crystalruby/types/variable_width/hash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CrystalRuby::Types 4 | Hash = VariableWidth.build(error: "Hash type must have 2 type parameters. E.g. Hash(Float64, String)") 5 | 6 | def self.Hash(key_type, value_type) 7 | VariableWidth.build(:Hash, inner_types: [key_type, value_type], convert_if: [Root::Hash], superclass: Hash) do 8 | include Enumerable 9 | 10 | def_delegators :@class, :value_type, :key_type 11 | 12 | # Implement the Enumerable interface 13 | # Helps this object to act like a true Hash 14 | def each 15 | if block_given? 16 | size.times { |i| yield key_for_index(i), value_for_index(i) } 17 | else 18 | to_enum(:each) 19 | end 20 | end 21 | 22 | def keys 23 | each.map { |k, _| k } 24 | end 25 | 26 | def values 27 | each.map { |_, v| v } 28 | end 29 | 30 | def self.key_type 31 | inner_types.first 32 | end 33 | 34 | def self.value_type 35 | inner_types.last 36 | end 37 | 38 | # We only accept Hash-like values, from which all elements 39 | # can successfully be cast to our inner types 40 | def self.cast!(value) 41 | unless (value.is_a?(Hash) || value.is_a?(Root::Hash)) && value.keys.all?(&key_type.method(:valid_cast?)) && value.values.all?(&value_type.method(:valid_cast?)) 42 | raise CrystalRuby::InvalidCastError, "Cannot cast #{value} to #{inspect}" 43 | end 44 | 45 | [[key_type, value.keys], [value_type, value.values]].map do |type, values| 46 | if type.primitive? 47 | values.map(&type.method(:to_ffi_repr)) 48 | else 49 | values 50 | end 51 | end 52 | end 53 | 54 | def self.copy_to!((keys, values), memory:) 55 | data_pointer = malloc(values.size * (key_type.refsize + value_type.refsize)) 56 | 57 | memory[size_offset].write_uint32(values.size) 58 | memory[data_offset].write_pointer(data_pointer) 59 | 60 | [ 61 | [key_type, data_pointer, keys], 62 | [value_type, data_pointer[values.length * key_type.refsize], values] 63 | ].each do |type, pointer, list| 64 | if type.primitive? 65 | pointer.send("put_array_of_#{type.ffi_type}", 0, list) 66 | else 67 | list.each_with_index do |val, i| 68 | type.write_single(pointer[i * type.refsize], val) 69 | end 70 | end 71 | end 72 | end 73 | 74 | def index_for_key(key) 75 | size.times { |i| return i if key_for_index(i) == key } 76 | nil 77 | end 78 | 79 | def key_for_index(index) 80 | key_type.fetch_single(data_pointer[index * key_type.refsize]) 81 | end 82 | 83 | def value_for_index(index) 84 | value_type.fetch_single(data_pointer[key_type.refsize * size + index * value_type.refsize]) 85 | end 86 | 87 | def self.each_child_address(pointer) 88 | size = pointer[size_offset].read_int32 89 | pointer = pointer[data_offset].read_pointer 90 | size.times do |i| 91 | yield key_type, pointer[i * key_type.refsize] 92 | yield value_type, pointer[size * key_type.refsize + i * value_type.refsize] 93 | end 94 | end 95 | 96 | def [](key) 97 | return nil unless index = index_for_key(key) 98 | 99 | value_for_index(index) 100 | end 101 | 102 | def []=(key, value) 103 | if index = index_for_key(key) 104 | value_type.write_single(data_pointer[key_type.refsize * size + index * value_type.refsize], value) 105 | else 106 | method_missing(:[]=, key, value) 107 | end 108 | end 109 | 110 | def value(native: false) 111 | keys = key_type.fetch_multi(data_pointer, size, native: native) 112 | values = value_type.fetch_multi(data_pointer[key_type.refsize * size], size, native: native) 113 | keys.zip(values).to_h 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/crystalruby/source_reader.rb: -------------------------------------------------------------------------------- 1 | module CrystalRuby 2 | module SourceReader 3 | module_function 4 | 5 | # Reads code line by line from a given source location and returns the first valid Ruby expression found 6 | def extract_expr_from_source_location(source_location) 7 | lines = source_location.then { |f, l| IO.readlines(f)[l - 1..] } 8 | lines[0] = lines[0][/CRType.*/] if lines[0] =~ /<\s+CRType/ || lines[0] =~ /= CRType/ 9 | lines.each.with_object([]) do |line, expr_source| 10 | break expr_source.join("") if Prism.parse((expr_source << line).join("")).success? 11 | end 12 | rescue StandardError 13 | raise "Failed to extract expression from source location: #{source_location}. Ensure the file exists and the line number is correct. Extraction from a REPL is not supported" 14 | end 15 | 16 | def search_node(result, node_type) 17 | result.breadth_first_search do |node| 18 | node_type === node 19 | end 20 | end 21 | 22 | # Given a proc, extracts the source code of the block passed to it 23 | # If raw is true, the source is expected to be Raw Crystal code captured 24 | # in a string or Heredoc literal. Otherwise the Ruby code (assumed to be valid Crystal) 25 | # is extracted. 26 | def extract_source_from_proc(block, raw: false) 27 | block_source = extract_expr_from_source_location(block.source_location) 28 | parsed_source = Prism.parse(block_source).value 29 | 30 | node = parsed_source.statements.body[0].arguments&.arguments&.find { |x| search_node(x, Prism::StatementsNode) } 31 | node ||= parsed_source.statements.body[0] 32 | body_node = search_node(node, Prism::StatementsNode) 33 | 34 | raw ? extract_raw_string_node(body_node) : node_to_s(body_node) 35 | end 36 | 37 | def extract_raw_string_node(node) 38 | search_node(node, Prism::InterpolatedStringNode)&.parts&.map do |p| 39 | p.respond_to?(:unescaped) ? p.unescaped : p.slice 40 | end&.join("") || 41 | search_node(node, Prism::StringNode).unescaped 42 | end 43 | 44 | # Simple helper function to turn a SyntaxTree node back into a Ruby string 45 | # The default formatter will turn a break/return of [1,2,3] into a brackless 1,2,3 46 | # Can't have that in Crystal as it turns it into a Tuple 47 | def node_to_s(node) 48 | node&.slice || "" 49 | end 50 | 51 | # Given a method, extracts the source code of the block passed to it 52 | # and also converts any keyword arguments given in the method definition as a 53 | # named map of keyword names to Crystal types. 54 | # Also supports basic ffi symbol types. 55 | # 56 | # E.g. 57 | # 58 | # def add a: Int32 | Int64, b: :int 59 | # 60 | # The above will be converted to: 61 | # { 62 | # a: Int32 | Int64, # Int32 | Int64 is a Crystal type 63 | # b: :int # :int is an FFI type shorthand 64 | # } 65 | # If raw is true, the source is expected to be Raw Crystal code captured 66 | # in a string or Heredoc literal. Otherwise the Ruby code (assumed to be valid Crystal) 67 | # is extracted. 68 | def extract_args_and_source_from_method(method, raw: false) 69 | method_source = extract_expr_from_source_location(method.source_location) 70 | parsed_source = Prism.parse(method_source).value 71 | params = search_node(parsed_source, Prism::ParametersNode) 72 | args = params ? params.keywords.map { |kw| [kw.name, node_to_s(kw.value)] }.to_h : {} 73 | body_node = parsed_source.statements.body[0].body 74 | if body_node.respond_to?(:rescue_clause) && body_node.rescue_clause 75 | wrapped = %(begin\n#{body_node.statements.slice}\n#{body_node.rescue_clause.slice}\nend) 76 | body_node = Prism.parse(wrapped).value 77 | end 78 | body = raw ? extract_raw_string_node(body_node) : node_to_s(body_node) 79 | 80 | args.transform_values! do |type_exp| 81 | if CrystalRuby::Typemaps::CRYSTAL_TYPE_MAP.key?(type_exp[1..-1].to_sym) 82 | type_exp[1..-1].to_sym 83 | else 84 | TypeBuilder.build_from_source(type_exp, context: method.owner) 85 | end 86 | end.to_h 87 | [args, body] 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/crystalruby/templates/index.cr: -------------------------------------------------------------------------------- 1 | module CrystalRuby 2 | ARGV1 = "crystalruby" 3 | 4 | alias ErrorCallback = (Pointer(::UInt8), Pointer(::UInt8), Pointer(::UInt8), ::UInt32 -> Void) 5 | 6 | class_property libname : String = "crystalruby" 7 | class_property callbacks : Channel(Proc(Nil)) = Channel(Proc(Nil)).new 8 | class_property rc_mux : Pointer(Void) = Pointer(Void).null 9 | class_property task_counter : Atomic(Int32) = Atomic(Int32).new(0) 10 | 11 | # Initializing Crystal Ruby invokes init on the Crystal garbage collector. 12 | # We need to be sure to only do this once. 13 | class_property initialized : Bool = false 14 | 15 | # We can override the error callback to catch errors in Crystal, 16 | # and explicitly expose them to Ruby. 17 | @@error_callback : ErrorCallback? 18 | 19 | # This is the entry point for instantiating CrystalRuby 20 | # We: 21 | # 1. Initialize the Crystal garbage collector 22 | # 2. Set the error callback 23 | # 3. Call the Crystal main function 24 | def self.init(libname : Pointer(::UInt8), @@error_callback : ErrorCallback, @@rc_mux : Pointer(Void)) 25 | return if self.initialized 26 | self.initialized = true 27 | argv_ptr = ARGV1.to_unsafe 28 | {%% if compare_versions(Crystal::VERSION, "1.16.0") >= 0 %%} 29 | Crystal.init_runtime 30 | {%% end %%} 31 | Crystal.main_user_code(0, pointerof(argv_ptr)) 32 | self.libname = String.new(libname) 33 | GC.init 34 | end 35 | 36 | # Explicit error handling (triggers exception within Ruby on the same thread) 37 | def self.report_error(error_type : String, message : String, backtrace : String, thread_id : UInt32) 38 | if error_reporter = @@error_callback 39 | error_reporter.call(error_type.to_unsafe, message.to_unsafe, backtrace.to_unsafe, thread_id) 40 | end 41 | end 42 | 43 | # New async task started 44 | def self.increment_task_counter 45 | @@task_counter.add(1) 46 | end 47 | 48 | # Async task finished 49 | def self.decrement_task_counter 50 | @@task_counter.sub(1) 51 | end 52 | 53 | # Get number of outstanding tasks 54 | def self.get_task_counter : Int32 55 | @@task_counter.get 56 | end 57 | 58 | # Queue a callback for an async task 59 | def self.queue_callback(callback : Proc(Nil)) 60 | self.callbacks.send(callback) 61 | end 62 | 63 | def self.synchronize(&) 64 | LibC.pthread_mutex_lock(self.rc_mux) 65 | yield 66 | LibC.pthread_mutex_unlock(self.rc_mux) 67 | end 68 | end 69 | 70 | # Initialize CrystalRuby 71 | fun init(libname : Pointer(::UInt8), cb : CrystalRuby::ErrorCallback, rc_mux : Pointer(Void)) : Void 72 | CrystalRuby.init(libname, cb, rc_mux) 73 | end 74 | 75 | fun stop : Void 76 | LibGC.deinit 77 | end 78 | 79 | @[Link("gc")] 80 | lib LibGC 81 | $stackbottom = GC_stackbottom : Void* 82 | fun deinit = GC_deinit 83 | fun set_finalize_on_demand = GC_set_finalize_on_demand(Int32) 84 | fun invoke_finalizers = GC_invoke_finalizers : Int 85 | end 86 | 87 | lib LibC 88 | fun calloc = calloc(Int32, Int32) : Void* 89 | end 90 | 91 | module GC 92 | def self.current_thread_stack_bottom 93 | {Pointer(Void).null, LibGC.stackbottom} 94 | end 95 | 96 | def self.set_stackbottom(stack_bottom : Void*) 97 | LibGC.stackbottom = stack_bottom 98 | end 99 | 100 | def self.collect 101 | LibGC.collect 102 | LibGC.invoke_finalizers 103 | end 104 | end 105 | 106 | # Trigger GC 107 | fun gc : Void 108 | GC.collect 109 | end 110 | 111 | # Yield to the Crystal scheduler from Ruby 112 | # If there's callbacks to process, we flush them 113 | # Otherwise, we yield to the Crystal scheduler and let Ruby know 114 | # how many outstanding tasks still remain (it will stop yielding to Crystal 115 | # once this figure reaches 0). 116 | fun yield : Int32 117 | Fiber.yield 118 | loop do 119 | select 120 | when callback = CrystalRuby.callbacks.receive 121 | callback.call 122 | else 123 | break 124 | end 125 | end 126 | CrystalRuby.get_task_counter 127 | end 128 | 129 | class Array(T) 130 | def initialize(size : Int32, @buffer : Pointer(T)) 131 | @size = size.to_i32 132 | @capacity = @size 133 | end 134 | end 135 | 136 | require "json" 137 | %{requires} 138 | -------------------------------------------------------------------------------- /test/types/test_tuple.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | CrystalRuby::Types::Type.trace_live_objects! 6 | 7 | class TestTuple < Minitest::Test 8 | class TupPrimitive < CRType { Tuple(Int32, Int32) } 9 | end 10 | 11 | class Tup < CRType { Tuple(Hash(Int32, Int32), Int32, String) } 12 | end 13 | 14 | class TupNested < CRType do 15 | Tuple(NamedTuple(complex: Hash(Int32, Int32), age: Int32, name: String)) 16 | end 17 | end 18 | 19 | def test_tuple_construction_primitive 20 | nt = TupPrimitive[25, 3] 21 | nt = nil 22 | end 23 | 24 | def test_tuple_construction 25 | nt = Tup[{ 1 => 3 }, 25, "John"] 26 | assert_equal nt[0], { 1 => 3 } 27 | assert_equal nt[1], 25 28 | assert_equal nt[2], "John" 29 | nt = nil 30 | 31 | assert_equal CrystalRuby::Types::Type.live_objects, 0 32 | end 33 | 34 | def test_tuple_updates 35 | nt = Tup[{ 1 => 3 }, 25, "John"] 36 | nt2_shallow = nt.dup 37 | nt2_deep = nt.deep_dup 38 | 39 | nt[2] = "Steve" 40 | nt[0][1] = 88 41 | assert_equal nt2_shallow[0], { 1 => 88 } 42 | 43 | assert_equal nt2_shallow[2], "Steve" 44 | refute_equal nt2_deep[2], "Steve" 45 | nt2_shallow = nil 46 | nt2_deep = nil 47 | nt = nil 48 | assert_equal CrystalRuby::Types::Type.live_objects, 0 49 | end 50 | 51 | def test_tuple_nested_construction 52 | nt = TupNested[{ complex: { 1 => 3 }, age: 25, name: "John" }] 53 | 54 | assert_equal nt[0][:complex], { 1 => 3 } 55 | assert_equal nt[0][:age], 25 56 | assert_equal nt[0][:name], "John" 57 | nt = nil 58 | 59 | assert_equal CrystalRuby::Types::Type.live_objects, 0 60 | end 61 | 62 | crystallize 63 | def accepts_nested_tuple(input: TupNested, returns: Bool) 64 | true 65 | end 66 | 67 | crystallize raw: true 68 | def returns_nested_tuple(returns: TupNested) 69 | %{ 70 | inner = { complex: { 1 => 3 }, age: 25, name: "John" } 71 | TupNested.new({ inner }) 72 | } 73 | end 74 | 75 | def test_accepts_nested_tuple 76 | assert_equal true, accepts_nested_tuple( 77 | TupNested[{ complex: { 1 => 3 }, age: 25, name: "John" }] 78 | ) 79 | end 80 | 81 | def test_returns_nested_tuple 82 | val = TupNested[{ complex: { 1 => 3 }, age: 25, name: "John" }] 83 | assert_equal returns_nested_tuple, val 84 | end 85 | 86 | crystallize raw: true 87 | def returns_simple_tuple(returns: TupPrimitive) 88 | %{ 89 | TupPrimitive.new({18, 43}) 90 | } 91 | end 92 | 93 | def test_returns_simple_tuple 94 | assert_equal returns_simple_tuple, TupPrimitive[18, 43] 95 | end 96 | 97 | crystallize 98 | def mutates_tuple_primitive!(input: TupPrimitive, returns: TupPrimitive) 99 | input[0] = 42 100 | input[1] = 79 101 | input 102 | end 103 | 104 | crystallize 105 | def doubles_tuple_values!(input: TupPrimitive) 106 | input[0] = input[0].not_nil! * 2 107 | input[1] = input[1].not_nil! * 2 108 | end 109 | 110 | def test_mutates_tuple_primitive 111 | tp = TupPrimitive[18, 43] 112 | mutates_tuple_primitive!(tp) 113 | assert_equal tp, TupPrimitive[42, 79] 114 | tp[0] = 99 115 | tp[1] = 158 116 | doubles_tuple_values!(tp) 117 | assert_equal tp, TupPrimitive[198, 316] 118 | end 119 | 120 | crystallize 121 | def mutates_tuple_complex!(input: TupNested, returns: TupNested) 122 | input[0].not_nil!.complex = { 1 => 42 } 123 | input[0].not_nil!.age = 79 124 | input 125 | end 126 | 127 | def test_mutates_tuple_complex 128 | tp = TupNested[{ complex: { 1 => 3 }, age: 25, name: "John" }] 129 | mutates_tuple_complex!(tp) 130 | assert_equal tp, TupNested[{ complex: { 1 => 42 }, age: 79, name: "John" }] 131 | end 132 | 133 | crystallize 134 | def mutates_tuple_nested!(input: TupNested, returns: TupNested) 135 | puts input[0].not_nil!.complex.class 136 | input[0].not_nil!.complex[1] = 42 137 | input[0].not_nil!.age = 79 138 | input 139 | end 140 | 141 | def test_mutates_tuple_nested 142 | tp = TupNested[{ complex: { 1 => 3 }, age: 25, name: "John" }] 143 | mutates_tuple_nested!(tp) 144 | assert_equal tp, TupNested[{ complex: { 1 => 42 }, age: 79, name: "John" }] 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /test/test_type_transforms.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | class TestTypeTransforms < Minitest::Test 6 | include Adder 7 | module ::Adder 8 | crystallize :bool 9 | def complex_argument_types(a: Int64 | Float64 | Nil, b: String | Array(Bool)) 10 | true 11 | end 12 | 13 | crystallize -> { Int32 | String | Hash(String, Array(NamedTuple(hello: Int32)) | Time) } 14 | def complex_return_type 15 | { 16 | "hello" => [ 17 | { 18 | hello: 1 19 | } 20 | ], 21 | "world" => Time.utc 22 | } 23 | end 24 | 25 | crystallize -> { Int32 | String | Hash(String, Array(NamedTuple(hello: Array(Int32))) | Time) } 26 | def complex_return_type 27 | { 28 | "hello" => [ 29 | { 30 | hello: [1] 31 | } 32 | ], 33 | "world" => Time.utc 34 | } 35 | end 36 | 37 | crystallize -> { Array(NamedTuple(hello: Array(Int32))) } 38 | def array_named_tuple_int_array 39 | [{ hello: [1, 2, 3] }] 40 | end 41 | 42 | crystallize -> { Array(Int32) } 43 | def prim_array 44 | [9, 8, 7] 45 | end 46 | 47 | crystallize -> { Array(Array(Int32)) } 48 | def nested_prim_array 49 | [[1, 8, 7], [5]] 50 | end 51 | 52 | crystallize -> { Array(Array(Array(Array(Int32 | Nil)))) } 53 | def triple_nested_union_array 54 | [[[[9, 8, 7, nil]], [[1, 2, 3, nil]]]] 55 | end 56 | 57 | crystallize -> { Hash(Hash(Hash(Hash(Hash(Int32, Int32), Int32), Int32), Int32), String) } 58 | def five_nested_key_nested_hash 59 | { 60 | { { { { 1 => 2 } => 3 } => 4 } => 5 } => "hello" 61 | } 62 | end 63 | 64 | crystallize -> { Hash(Int32, Hash(Int32, Hash(Int32, Hash(Int32, Hash(Int32, String))))) } 65 | def five_nested_value_nested_hash 66 | { 1 => { 2 => { 3 => { 4 => { 5 => "hello" } } } } } 67 | end 68 | 69 | crystallize -> { Tuple(Tuple(Tuple(Tuple(Int32, String, Array(Int32))))) }, raw: true 70 | def four_nested_tuple 71 | %({ { { { 1, "hello", [1,2,3] } } } }) 72 | end 73 | 74 | crystallize lambda { 75 | NamedTuple(value: NamedTuple(value: NamedTuple(value: NamedTuple(age: Int32, name: String, flags: Array(Int32))))) 76 | } 77 | def four_named_tuple 78 | { value: { value: { value: { age: 1, name: "hello", flags: [1, 2, 3] } } } } 79 | end 80 | 81 | IntArrOrBoolArr = CRType { Array(Bool) | Array(Int32) } 82 | 83 | crystallize 84 | def method_with_named_types(a: IntArrOrBoolArr, returns: IntArrOrBoolArr) 85 | a 86 | end 87 | end 88 | 89 | def test_triple_nested_union_array 90 | assert_equal triple_nested_union_array, [[[[9, 8, 7, nil]], [[1, 2, 3, nil]]]] 91 | end 92 | 93 | def test_five_nested_key_nested_hash 94 | assert_equal five_nested_key_nested_hash, { { { { { 1 => 2 } => 3 } => 4 } => 5 } => "hello" } 95 | end 96 | 97 | def test_five_nested_value_nested_hash 98 | assert_equal five_nested_value_nested_hash, { 1 => { 2 => { 3 => { 4 => { 5 => "hello" } } } } } 99 | end 100 | 101 | def test_four_nested_tuple 102 | assert_equal four_nested_tuple, [[[[1, "hello", [1, 2, 3]]]]] 103 | end 104 | 105 | def test_four_named_tuple 106 | assert_equal four_named_tuple, { value: { value: { value: { age: 1, name: "hello", flags: [1, 2, 3] } } } } 107 | end 108 | 109 | def test_complex_argument_types 110 | assert complex_argument_types(1, "hello") 111 | assert complex_argument_types(1.0, [true]) 112 | assert_raises(CrystalRuby::InvalidCastError) { complex_argument_types(1.0, [true, "not a bool"]) } 113 | assert_raises(CrystalRuby::InvalidCastError) { complex_argument_types(true, "string") } 114 | end 115 | 116 | def test_complex_return_type 117 | assert complex_return_type["hello"] == [ 118 | { 119 | hello: [1] 120 | } 121 | ] 122 | assert complex_return_type["world"].is_a?(Time) 123 | end 124 | 125 | def test_named_types 126 | assert method_with_named_types([1, 1, 1]) == [1, 1, 1] 127 | input = IntArrOrBoolArr[[true, false, true]] 128 | assert method_with_named_types(input) == [true, false, true] 129 | assert_raises(CrystalRuby::InvalidCastError) { method_with_named_types([true, 5, "bad"]) } 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/crystalruby/types/fixed_width.cr: -------------------------------------------------------------------------------- 1 | module CrystalRuby 2 | module Types 3 | # For a fixed width type, we allocate a single block block of memory of the form 4 | # [ref_count (uint32), data(uint8*)] 5 | class FixedWidth < Type 6 | 7 | # We can instantiate it with a Value (for a new object) 8 | # or a Pointer (for a copy of an existing object) 9 | macro inherited 10 | def initialize(@memory : Pointer(::UInt8)) 11 | increment_ref_count! 12 | end 13 | end 14 | 15 | def finalize 16 | self.class.decrement_ref_count!(@memory) 17 | end 18 | 19 | def increment_ref_count! 20 | self.class.increment_ref_count!(@memory) 21 | end 22 | 23 | def self.increment_ref_count!(memory, by=1) 24 | as_int32_ptr = memory.as(Pointer(::UInt32)) 25 | synchronize{ as_int32_ptr[0] += by } 26 | end 27 | 28 | def self.decrement_ref_count!(memory, by=1) 29 | as_int32_ptr = memory.as(Pointer(::UInt32)) 30 | synchronize{ as_int32_ptr[0] -= by } 31 | free!(memory) if as_int32_ptr[0] == 0 32 | end 33 | 34 | def self.refsize 35 | 8 36 | end 37 | 38 | def self.new_decr(arg) 39 | new_value = self.new(arg) 40 | self.decrement_ref_count!(new_value.memory) 41 | new_value 42 | end 43 | 44 | def native_decr 45 | self.class.decrement_ref_count!(@memory) 46 | native 47 | end 48 | 49 | 50 | def self.free!(memory) 51 | # Decrease ref counts for any data we are pointing to 52 | # Also responsible for freeing internal memory if ref count reaches zero 53 | decr_inner_ref_counts!(memory) 54 | # # Free slot memory 55 | free(memory) 56 | end 57 | 58 | def self.decr_inner_ref_counts!(pointer) 59 | self.each_child_address(pointer) do |child_type, child_address| 60 | child_type.decrement_ref_count!(child_address.read_pointer) 61 | end 62 | # Free data block, if we're a variable with type. 63 | as_pointer_ref = (pointer+data_offset).as(Pointer(Pointer(::UInt8))) 64 | free(as_pointer_ref[0]) if variable_width? 65 | end 66 | 67 | def self.variable_width? 68 | false 69 | end 70 | 71 | # Ref count is always the first UInt32 in the memory block 72 | def ref_count 73 | memory.as(Pointer(::UInt32))[0] 74 | end 75 | 76 | def ref_count=(val) 77 | memory.as(Pointer(::UInt32))[0] = value 78 | end 79 | 80 | # When we pass to Ruby, we increment the ref count 81 | # for Ruby to decrement again once it receives. 82 | def return_value 83 | FixedWidth.increment_ref_count!(memory) 84 | memory 85 | end 86 | 87 | # Data pointer follows the ref count (and size for variable width types) 88 | # In the case of variable width types the data pointer points to the start of a separate data block 89 | # So this method is overridden inside variable_width.rb to resolve this pointer. 90 | def data_pointer : Pointer(UInt8) 91 | (memory + data_offset) 92 | end 93 | 94 | # Create a brand new copy of this object 95 | def deep_dup 96 | self.class.new(value) 97 | end 98 | 99 | # Create a new reference to this object. 100 | def dup 101 | self.class.new(@memory) 102 | end 103 | 104 | def address 105 | memory.address 106 | end 107 | 108 | def data_offset 109 | self.class.data_offset 110 | end 111 | 112 | def memsize 113 | self.class.memsize 114 | end 115 | 116 | def size_offset 117 | self.class.size_offset 118 | end 119 | 120 | def self.size_offset 121 | 4 122 | end 123 | 124 | def self.data_offset 125 | 4 126 | end 127 | 128 | # Read a value of this type from the 129 | # contained pointer at a given index 130 | def self.fetch_single(pointer : Pointer(::UInt8)) 131 | value_pointer = pointer.as(Pointer(Pointer(::UInt8))) 132 | new(value_pointer.value) 133 | end 134 | 135 | # Write a data type into a pointer at a given index 136 | # (Type can be a byte-array, pointer or numeric type 137 | # 138 | # ) 139 | def self.write_single(pointer, value) 140 | value_pointer = pointer.as(Pointer(Pointer(::UInt8))) 141 | if !value_pointer[0].null? 142 | decrement_ref_count!(value_pointer[0]) 143 | end 144 | memory = malloc(self.data_offset + self.memsize) 145 | 146 | self.copy_to!(value, memory) 147 | value_pointer.value = memory 148 | increment_ref_count!(memory) 149 | end 150 | 151 | # Fetch an array of a given data type from a list pointer 152 | # (Type can be a byte-array, pointer or numeric type) 153 | 154 | end 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /lib/crystalruby/types/variable_width/hash.cr: -------------------------------------------------------------------------------- 1 | class <%= base_crystal_class_name %> < CrystalRuby::Types::VariableWidth 2 | 3 | def initialize(hash : Hash(<%= key_type.native_type_expr %>, <%= value_type.native_type_expr %>)) 4 | @memory = malloc(data_offset + 8) 5 | self.value = hash 6 | increment_ref_count! 7 | end 8 | 9 | def value=(hash : Hash(<%= key_type.native_type_expr %>, <%= value_type.native_type_expr %>)) 10 | if self.ref_count > 0 11 | self.class.decr_inner_ref_counts!(memory) 12 | end 13 | self.class.copy_to!(hash, self.memory) 14 | end 15 | 16 | def ==(other : <%= native_type_expr %>) 17 | native == other 18 | end 19 | 20 | def self.copy_to!(hash : Hash(<%= key_type.native_type_expr %>, <%= value_type.native_type_expr %>), memory : Pointer(::UInt8)) 21 | data_pointer = malloc(hash.size * (keysize + valsize)).as(Pointer(::UInt8)) 22 | 23 | hash.keys.each_with_index do |key, i| 24 | <%= key_type.crystal_class_name %>.write_single(data_pointer + i * keysize, key) 25 | end 26 | 27 | hash.values.each_with_index do |value, i| 28 | <%= value_type.crystal_class_name %>.write_single(data_pointer + hash.size * keysize + i * valsize, value) 29 | end 30 | 31 | (memory+size_offset).as(Pointer(::UInt32)).value = hash.size.to_u32 32 | (memory+data_offset).as(Pointer(::UInt64)).value = data_pointer.address 33 | end 34 | 35 | def value 36 | ::Hash.zip(keys, values) 37 | end 38 | 39 | def native : ::Hash(<%= key_type.native_type_expr %>, <%= value_type.native_type_expr %>) 40 | ::Hash.zip(keys_native, values_native) 41 | end 42 | 43 | include Enumerable(::Tuple(<%= key_type.native_type_expr %>, <%= value_type.native_type_expr %>)) 44 | 45 | def each 46 | size.times do |i| 47 | yield({keys_native[i], values_native[i]}) 48 | end 49 | end 50 | 51 | def size 52 | (memory+size_offset).as(Pointer(::UInt32)).value.to_i 53 | end 54 | 55 | def self.keysize 56 | <%= key_type.refsize %> 57 | end 58 | 59 | def keysize 60 | <%= key_type.refsize %> 61 | end 62 | 63 | def self.valsize 64 | <%= value_type.refsize %> 65 | end 66 | 67 | def valsize 68 | <%= value_type.refsize %> 69 | end 70 | 71 | def []=(key : <%= key_type.native_type_expr %>, value : <%= value_type.native_type_expr %>) 72 | index = index_of(key) 73 | if index 74 | <%= value_type.crystal_class_name %>.write_single(data_pointer + size * keysize + index * valsize, value) 75 | else 76 | self.value = self.native.merge({key => value}) 77 | end 78 | end 79 | 80 | def key_ptr : Pointer(<%= !key_type.numeric? ? "UInt64" : key_type.native_type_expr %>) 81 | data_pointer.as(Pointer(<%= !key_type.numeric? ? "UInt8" : key_type.native_type_expr %>)) 82 | end 83 | 84 | def value_ptr : Pointer(<%= !value_type.numeric? ? "UInt64" : value_type.native_type_expr %>) 85 | (data_pointer + size * keysize).as(Pointer(<%= !value_type.numeric? ? "UInt8" : value_type.native_type_expr %>)) 86 | end 87 | 88 | def []?(key : <%= key_type.native_type_expr %>) 89 | index = index_of(key) 90 | if index 91 | values[index] 92 | else 93 | nil 94 | end 95 | end 96 | 97 | def [](key : <%= key_type.native_type_expr %>) 98 | index = index_of(key) 99 | if index 100 | values[index] 101 | else 102 | raise "Key not found" 103 | end 104 | end 105 | 106 | 107 | def data_pointer 108 | Pointer(::UInt8).new((@memory + data_offset).as(::Pointer(UInt64)).value) 109 | end 110 | 111 | def keys 112 | keys = [] of <%= key_type.numeric? ? key_type.native_type_expr : key_type.crystal_class_name %> 113 | size.times do |i| 114 | keys << <%= !key_type.numeric? ? "#{key_type.crystal_class_name}.fetch_single(data_pointer + i * keysize)" : "key_ptr[i]" %> 115 | end 116 | keys 117 | end 118 | 119 | def values 120 | values = [] of <%= value_type.numeric? ? value_type.native_type_expr : value_type.crystal_class_name %> 121 | size.times do |i| 122 | values << <%= !value_type.numeric? ? "#{value_type.crystal_class_name}.fetch_single(data_pointer + size * keysize + i * valsize)" : "value_ptr[i]" %> 123 | end 124 | values 125 | end 126 | 127 | def keys_native 128 | keys = [] of <%= key_type.native_type_expr %> 129 | size.times do |i| 130 | keys << <%= !key_type.numeric? ? "#{key_type.crystal_class_name}.fetch_single(data_pointer + i * keysize).native" : "key_ptr[i]" %>.as(<%= key_type.native_type_expr %>) 131 | end 132 | keys 133 | end 134 | 135 | def values_native 136 | values = [] of <%= value_type.native_type_expr %> 137 | size.times do |i| 138 | values << <%= !value_type.numeric? ? "#{value_type.crystal_class_name}.fetch_single(data_pointer + size * keysize + i * valsize).native" : "value_ptr[i]" %>.as(<%= value_type.native_type_expr %>) 139 | end 140 | values 141 | end 142 | 143 | def index_of(key : <%= key_type.native_type_expr %>) 144 | keys.index(key) 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /test/types/test_hash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | class TestHash < Minitest::Test 6 | class PrimitiveHash < CRType { Hash(Int32, Int32) } 7 | end 8 | 9 | class ArrayHash < CRType { Hash(Int32, Array(Int32)) } 10 | end 11 | 12 | class ArrayHash2 < CRType { Hash(Array(Int32), Int32) } 13 | end 14 | 15 | class HashArray < CRType { Array(Hash(Int32, Int32)) } 16 | end 17 | 18 | class HashHash < CRType { Hash(Int32, Hash(Int32, Int32)) } 19 | end 20 | 21 | class HashHash2 < CRType { Hash(Hash(Int32, Int32), Int32) } 22 | end 23 | 24 | ComplexValue = CRType do 25 | Hash( 26 | Int32, 27 | Array( 28 | NamedTuple( 29 | name: String, 30 | age: Int32 31 | ) 32 | ) 33 | ) 34 | end 35 | 36 | ComplexKey = CRType do 37 | Hash( 38 | Array( 39 | NamedTuple( 40 | name: String, 41 | age: Int32 42 | ) 43 | ), 44 | Int32 45 | ) 46 | end 47 | 48 | ComplexHash = CRType do 49 | Hash( 50 | NamedTuple( 51 | name: String, 52 | age: Int32 53 | ), 54 | Array( 55 | NamedTuple( 56 | name: String, 57 | age: Int32 58 | ) 59 | ) 60 | ) 61 | end 62 | 63 | def test_primitive_hash_construction 64 | hsh = PrimitiveHash[1 => 2, 3 => 4] 65 | hsh = nil 66 | end 67 | 68 | def test_array_hash_construction 69 | hsh = ArrayHash[1 => [2, 3], 4 => [5, 6]] 70 | hsh = nil 71 | end 72 | 73 | def test_array_hash_construction2 74 | hsh = ArrayHash2[[1, 2] => 3, [4, 5] => 6] 75 | hsh = nil 76 | end 77 | 78 | def test_hash_array_construction 79 | hsh = HashArray[{ 1 => 2, 3 => 4 }, { 5 => 6, 7 => 8 }] 80 | hsh = nil 81 | end 82 | 83 | def test_hash_hash_construction 84 | hsh = HashHash[1 => { 2 => 3, 4 => 5 }, 6 => { 7 => 8, 9 => 10 }] 85 | hsh = nil 86 | end 87 | 88 | def test_hash_hash_construction2 89 | hsh = HashHash2[{ 1 => 2, 3 => 4 } => 5, { 6 => 7, 8 => 9 } => 10] 90 | hash = nil 91 | end 92 | 93 | def test_complex_key_construction 94 | hsh = ComplexKey[ 95 | [{ name: "John", age: 25 }] => 43 96 | ] 97 | hsh = nil 98 | end 99 | 100 | def test_complex_value_construction 101 | hsh = ComplexValue[ 102 | 43 => [{ name: "John", age: 25 }] 103 | ] 104 | hsh = nil 105 | end 106 | 107 | def test_complex_hash_construction 108 | hsh = ComplexHash[ 109 | { name: "John", age: 25 } => [ 110 | { name: "Steve", age: 30 }, 111 | { name: "Jane", age: 28 } 112 | ] 113 | ] 114 | hsh = nil 115 | end 116 | 117 | def test_complex_hash_manipulation 118 | hsh = ComplexHash[ 119 | { name: "John", age: 25 } => [ 120 | { name: "Steve", age: 30 }, 121 | { name: "Jane", age: 28 } 122 | ] 123 | ] 124 | 3.times.map do 125 | Thread.new do 126 | GC.start 127 | sleep 0.02 128 | end 129 | end.map(&:join) 130 | hsh2_shallow = hsh.dup 131 | hsh.keys.first[:name] = "Steve" 132 | assert_equal hsh2_shallow.keys.first[:name], "Steve" 133 | hsh = nil 134 | end 135 | 136 | 137 | crystallize 138 | def crystal_mutate_complex_hash!(hash: ComplexHash) 139 | hash[{ name: "John", age: 25 }][0] = { name: "Alice", age: 18 } 140 | hash[{ name: "John", age: 25 }][1] = { name: "Bob", age: 30 } 141 | end 142 | 143 | def test_complex_hash_manipulation_crystal 144 | hash = ComplexHash[ 145 | { name: "John", age: 25 } => [ 146 | { name: "Steve", age: 30 }, 147 | { name: "Jane", age: 28 } 148 | ] 149 | ] 150 | crystal_mutate_complex_hash!(hash) 151 | assert_equal hash[{ name: "John", age: 25 }][0][:name], "Alice" 152 | assert_equal hash[{ name: "John", age: 25 }][1][:name], "Bob" 153 | end 154 | 155 | class SimpleHash < CRType { Hash(String, Int32) } 156 | end 157 | 158 | crystallize 159 | def crystal_mutate_simple_hash!(hash: SimpleHash) 160 | hash["first"] = 10 161 | hash["second"] = 20 162 | end 163 | 164 | def test_simple_hash_manipulation_crystal 165 | hash = SimpleHash[ 166 | "a" => 1, 167 | "b" => 2, 168 | "c" => 3 169 | ] 170 | hash["a"] = 9 171 | hash["b"] = 11 172 | crystal_mutate_simple_hash!(hash) 173 | assert_equal hash.native, { "first" => 10, "second" => 20, "a" => 9, "b" => 11, "c" => 3 } 174 | end 175 | 176 | class UnionHash < CRType { Hash(String, Nil | Int32) } 177 | end 178 | 179 | crystallize 180 | def crystal_mutate_union_hash!(hash: UnionHash) 181 | hash["first"] = 10 182 | hash["second"] = nil 183 | end 184 | 185 | def test_union_hash_manipulation_crystal 186 | hash = UnionHash[ 187 | "a" => 1, 188 | "b" => nil, 189 | "c" => 3 190 | ] 191 | hash["a"] = nil 192 | hash["b"] = 11 193 | 194 | crystal_mutate_union_hash!(hash) 195 | assert_equal hash.native, { "first" => 10, "second" => nil, "a" => nil, "b" => 11, "c" => 3 } 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 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. 85 | -------------------------------------------------------------------------------- /test/types/test_array.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | class TestArray < Minitest::Test 6 | IntArrayType = CRType { Array(Int32) } 7 | NestedArrayType = CRType { Array(Array(Int32)) } 8 | 9 | def test_simply_array_constructor 10 | ia = IntArrayType[1, 2, 3, 4, 5] 11 | ia = nil 12 | assert_equal CrystalRuby::Types::Type.live_objects, 0 13 | end 14 | 15 | def test_nested_array_constructor 16 | ia = NestedArrayType[[1, 2, 3, 4, 5]] 17 | ia = nil 18 | assert_equal CrystalRuby::Types::Type.live_objects, 0 19 | end 20 | 21 | crystallize 22 | def double_list_of_ints!(a: IntArrayType) 23 | a.map! { |x| x * 2 } 24 | end 25 | 26 | def test_forward_mutable 27 | ia = IntArrayType.new([1, 2, 3, 4, 5]) 28 | double_list_of_ints!(ia) 29 | assert ia == [2, 4, 6, 8, 10] 30 | end 31 | 32 | crystallize 33 | def double_nested_list_of_ints!(a: NestedArrayType) 34 | a.map! { |b| b.map! { |c| c * 2 } } 35 | end 36 | 37 | def test_forward_mutable 38 | ia = NestedArrayType.new([[1, 2, 3, 4, 5]]) 39 | double_nested_list_of_ints!(ia) 40 | assert ia == [[2, 4, 6, 8, 10]] 41 | end 42 | 43 | crystallize 44 | def mutate_and_access_named_types!(a: NestedArrayType, value: Int32, returns: Int32) 45 | a[0][0] = value 46 | a[0][1] = 43 47 | raise "Expected #{value} but got #{a[0][0]}" if a[0][0].value != value 48 | 49 | a[0][1].value 50 | end 51 | 52 | def test_forward_mutable 53 | ia = NestedArrayType.new([[1, 2, 3, 4, 5]]) 54 | assert mutate_and_access_named_types!(ia, 42) == 43 55 | assert ia == [[42, 43, 3, 4, 5]] 56 | end 57 | 58 | crystallize 59 | def mutate_and_access_anonymous!(a: Array(Array(Bool)), value: Bool, returns: Bool) 60 | a[0][0] = value 61 | a[0][1] = true 62 | a[0][2] = false 63 | raise "Expected #{value} but got #{a[0][0]}" if a[0][0] != value 64 | 65 | a[0][1] 66 | end 67 | 68 | def test_mutate_and_access_anonymous 69 | assert mutate_and_access_anonymous!([[false, false, false]], true) == true 70 | end 71 | 72 | crystallize 73 | def mutate_and_access_nil_array!(a: Array(Array(Nil)), value: Nil, returns: Nil) 74 | a[0][0] = value 75 | a[0][1] = nil 76 | a[0][2] = nil 77 | raise "Expected #{value} but got #{a[0][0]}" if a[0][0] != value 78 | 79 | a[0][1] 80 | end 81 | 82 | def test_mutate_and_access_nil_array 83 | assert mutate_and_access_nil_array!([[nil, nil, nil]], nil).nil? 84 | end 85 | 86 | ColorSymbol = CRType { Symbol(:green, :blue, :orange) } 87 | crystallize 88 | def mutate_and_access_symbol_array!(a: Array(Array(ColorSymbol)), value: ColorSymbol, returns: ColorSymbol) 89 | a[0][0] = :orange 90 | a[0][1] = :green 91 | a[0][2] = value 92 | raise "Expected #{value} but got #{a[0][2]}" if a[0][2] != value 93 | 94 | a[0][1] 95 | end 96 | 97 | def test_mutate_and_access_symbol_array 98 | assert mutate_and_access_symbol_array!([%i[orange blue green]], :orange) == :green 99 | end 100 | 101 | crystallize 102 | def mutate_and_access_time_array!(a: Array(Array(Time)), value: Time, returns: Time) 103 | a[0][0] = Time.local 104 | a[0][1] = Time.local - 100.seconds 105 | a[0][2] = value 106 | raise "Expected #{value} but got #{a[0][2]}" if a[0][2] != value 107 | 108 | a[0][1] 109 | end 110 | 111 | def mutate_and_access_time_array 112 | assert mutate_and_access_time_array!([[Time.at(0), Time.at(1_000_000), Time.now]], 113 | Time.at(43)) == Time.at(1_000_000) 114 | end 115 | 116 | ComplexArray = CRType do 117 | Array( 118 | NamedTuple( 119 | name: String, 120 | age: Int32 121 | ) 122 | ) 123 | end 124 | 125 | crystallize 126 | def crystal_mutate_complex_array!(array: ComplexArray) 127 | array[0] = { name: "Alice", age: 30 } 128 | array << { name: "Bob", age: 25 } 129 | end 130 | 131 | def test_complex_array_manipulation_crystal 132 | array = ComplexArray[ 133 | { name: "Steve", age: 30 }, 134 | { name: "Jane", age: 28 } 135 | ] 136 | array[0] = { name: "Abbie", age: 39 } 137 | array << { name: "Albert", age: 30 } 138 | 139 | crystal_mutate_complex_array!(array) 140 | assert_equal array.native, [ 141 | { name: "Alice", age: 30 }, 142 | { name: "Jane", age: 28 }, 143 | { name: "Albert", age: 30 }, 144 | { name: "Bob", age: 25 } 145 | ] 146 | end 147 | 148 | class SimpleArray < CRType do 149 | Array(Int32) 150 | end 151 | end 152 | 153 | crystallize 154 | def crystal_mutate_simple_array!(array: SimpleArray) 155 | array[0] = 4 156 | array << 8 157 | end 158 | 159 | def test_simple_array_manipulation_crystal 160 | array = SimpleArray[1, 2, 3] 161 | array[0] = 9 162 | array << 11 163 | 164 | crystal_mutate_simple_array!(array) 165 | assert_equal array.native, [4, 2, 3, 11, 8] 166 | end 167 | 168 | class UnionArray < CRType do 169 | Array(Nil | Int32) 170 | end 171 | end 172 | 173 | crystallize 174 | def crystal_mutate_union_array!(array: UnionArray) 175 | array[0] = 4 176 | array[2] = nil 177 | array << 8 178 | array << nil 179 | end 180 | 181 | def test_union_array_manipulation_crystal 182 | array = UnionArray[1, nil, 2, nil, 3] 183 | array[0] = nil 184 | array[1] = 8 185 | array << 3 186 | array << nil 187 | 188 | crystal_mutate_union_array!(array) 189 | assert_equal array.native, [4, 8, nil, nil, 3, 3, nil, 8, nil] 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /lib/crystalruby/types/fixed_width.rb: -------------------------------------------------------------------------------- 1 | module CrystalRuby 2 | module Types 3 | # For a fixed width type, we allocate a single block block of memory of the form 4 | # [ref_count (uint32), data(uint8*)] 5 | class FixedWidth < Type 6 | # We can instantiate it with a Value (for a new object) 7 | # or a Pointer (for a copy of an existing object) 8 | def initialize(rbval) 9 | super 10 | case rbval 11 | when FFI::Pointer then allocate_new_from_reference!(rbval) 12 | else allocate_new_from_value!(rbval) 13 | end 14 | self.class.increment_ref_count!(memory) 15 | ObjectSpace.define_finalizer(self, self.class.finalize(memory, self.class)) 16 | Allocator.gc_hint!(total_memsize) 17 | end 18 | 19 | def self.finalize(memory, type) 20 | lambda do |_| 21 | decrement_ref_count!(memory) 22 | end 23 | end 24 | 25 | def allocate_new_from_value!(rbval) 26 | # New block of memory, to hold our object. 27 | # For variable with, this is 2x UInt32 for ref count and size, plus a data pointer (8 bytes) 28 | # Layout: 29 | # - ref_count (4 bytes) 30 | # - size (4 bytes) 31 | # - data (8 bytes) 32 | # 33 | # For fixed the data is inline 34 | # Layout: 35 | # - ref_count (4 bytes) 36 | # - size (0 bytes) (No size for fixed width types) 37 | # - data (memsize bytes) 38 | self.memory = malloc(refsize + data_offset) 39 | self.value = rbval 40 | end 41 | 42 | def allocate_new_from_reference!(memory) 43 | # When we point to an existing block of memory, we don't need to allocate anything. 44 | # This memory should be to a single, separately allocated block of the above size. 45 | # When this type is contained within another type, it should be as a pointer to this block (not the contents of the block itself). 46 | self.memory = memory 47 | end 48 | 49 | # Each type should be convertible to an FFI representation. (I.e. how is a value or reference to this value stored 50 | # within e.g. an Array, Hash, Tuple or any other containing type). 51 | # For both fixed and variable types these are simply stored within 52 | # the containing type as a pointer to the memory block. 53 | # We return the pointer to this memory here. 54 | def self.to_ffi_repr(value) 55 | to_store = new(value) 56 | increment_ref_count!(to_store.memory) 57 | to_store.memory 58 | end 59 | 60 | # Read a value of this type from the 61 | # contained pointer at a given index 62 | def self.fetch_single(pointer, native: false) 63 | # Nothing to fetch for Nils 64 | return if memsize.zero? 65 | 66 | value_pointer = pointer.read_pointer 67 | native ? new(value_pointer).native : new(value_pointer) 68 | end 69 | 70 | # Write a data type into a pointer at a given index 71 | # (Type can be a byte-array, pointer or numeric type 72 | # 73 | # ) 74 | def self.write_single(pointer, value) 75 | # Dont need to write nils 76 | return if memsize.zero? 77 | 78 | decrement_ref_count!(pointer.read_pointer) unless pointer.read_pointer.null? 79 | memory = malloc(refsize + data_offset) 80 | copy_to!(cast!(value), memory: memory) 81 | increment_ref_count!(memory) 82 | pointer.write_pointer(memory) 83 | end 84 | 85 | # Fetch an array of a given data type from a list pointer 86 | # (Type can be a byte-array, pointer or numeric type) 87 | def self.fetch_multi(pointer, size, native: false) 88 | size.times.map { |i| fetch_single(pointer[i * refsize], native: native) } 89 | end 90 | 91 | def self.increment_ref_count!(memory, by = 1) 92 | synchronize { memory.write_int32(memory.read_int32 + by) } 93 | end 94 | 95 | def self.decrement_ref_count!(memory, by = 1) 96 | synchronize { memory.write_int32(memory.read_int32 - by) } 97 | return unless memory.read_int32.zero? 98 | 99 | free!(memory) 100 | end 101 | 102 | def self.free!(memory) 103 | # Decrease ref counts for any data we are pointing to 104 | # Also responsible for freeing internal memory if ref count reaches zero 105 | decr_inner_ref_counts!(memory) 106 | 107 | # # Free slot memory 108 | free(memory) 109 | end 110 | 111 | def self.decr_inner_ref_counts!(pointer) 112 | each_child_address(pointer) do |child_type, child_address| 113 | child_type.decrement_ref_count!(child_address.read_pointer) if child_type.fixed_width? 114 | end 115 | # Free data block, if we're a variable width type. 116 | return unless variable_width? 117 | 118 | free(pointer[data_offset].read_pointer) 119 | end 120 | 121 | # Ref count is always the first Int32 in the memory block 122 | def ref_count 123 | memory.read_uint32 124 | end 125 | 126 | def ref_count=(val) 127 | memory.write_int32(val) 128 | end 129 | 130 | # Data pointer follows the ref count (and size for variable width types) 131 | # In the case of variable width types the data pointer points to the start of a separate data block 132 | # So this method is overridden inside variable_width.rb to resolve this pointer. 133 | def data_pointer 134 | memory[data_offset].read_pointer 135 | end 136 | 137 | def size 138 | memory[size_offset].read_int32 139 | end 140 | 141 | def total_memsize 142 | memsize + refsize + size 143 | end 144 | 145 | def address 146 | @memory.address 147 | end 148 | 149 | def self.crystal_supertype 150 | "CrystalRuby::Types::FixedWidth" 151 | end 152 | 153 | def self.crystal_type 154 | "Pointer(::UInt8)" 155 | end 156 | 157 | # If we are fixed with, 158 | # The memory we allocate a single block of memory, if not already given. 159 | # Within this block of memory, we copy our contents directly. 160 | # 161 | # If we are variable width, we allocate a small block of memory for the pointer only 162 | # and allocate a separate block of memory for the data. 163 | # We store the pointer to the data in the memory block. 164 | 165 | def value=(value) 166 | # If we're already pointing at something 167 | # Decrement the ref counts of anything we're pointing at 168 | value = cast!(value) 169 | 170 | self.class.decr_inner_ref_counts!(memory) if ref_count > 0 171 | self.class.copy_to!(value, memory: memory) 172 | end 173 | 174 | # Build a new FixedWith subtype 175 | # Layout varies according to the sizes of internal types 176 | def self.build( 177 | typename = nil, 178 | error: nil, 179 | inner_types: nil, 180 | inner_keys: nil, 181 | ffi_type: :pointer, 182 | memsize: FFI.type_size(ffi_type), 183 | refsize: 8, 184 | convert_if: [], 185 | superclass: FixedWidth, 186 | size_offset: 4, 187 | data_offset: 4, 188 | ffi_primitive: false, 189 | &block 190 | ) 191 | inner_types&.each(&Type.method(:validate!)) 192 | 193 | Class.new(superclass) do 194 | bind_local_vars!( 195 | %i[typename error inner_types inner_keys ffi_type memsize convert_if size_offset data_offset 196 | refsize ffi_primitive], binding 197 | ) 198 | class_eval(&block) if block_given? 199 | 200 | def self.fixed_width? 201 | true 202 | end 203 | end 204 | end 205 | end 206 | end 207 | end 208 | require_relative "fixed_width/proc" 209 | require_relative "fixed_width/named_tuple" 210 | require_relative "fixed_width/tuple" 211 | require_relative "fixed_width/tagged_union" 212 | -------------------------------------------------------------------------------- /lib/crystalruby/typemaps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CrystalRuby 4 | module Typemaps 5 | CRYSTAL_TYPE_MAP = { 6 | char: "::Int8", # In Crystal, :char is typically represented as Int8 7 | uchar: "::UInt8", # Unsigned char 8 | int8: "::Int8", # Same as :char 9 | uint8: "::UInt8", # Same as :uchar 10 | short: "::Int16", # Short integer 11 | ushort: "::UInt16", # Unsigned short integer 12 | int16: "::Int16", # Same as :short 13 | uint16: "::UInt16", # Same as :ushort 14 | int: "::Int32", # Integer, Crystal defaults to 32 bits 15 | uint: "::UInt32", # Unsigned integer 16 | int32: "::Int32", # 32-bit integer 17 | uint32: "::UInt32", # 32-bit unsigned integer 18 | long: "::Int32 | Int64", # Long integer, size depends on the platform (32 or 64 bits) 19 | ulong: "::UInt32 | UInt64", # Unsigned long integer, size depends on the platform 20 | int64: "::Int64", # 64-bit integer 21 | uint64: "::UInt64", # 64-bit unsigned integer 22 | long_long: "::Int64", # Same as :int64 23 | ulong_long: "::UInt64", # Same as :uint64 24 | float: "::Float32", # Floating point number (single precision) 25 | double: "::Float64", # Double precision floating point number 26 | bool: "::Bool", # Boolean type 27 | void: "::Void", # Void type 28 | string: "::String", # String type 29 | pointer: "::Pointer(Void)" # Pointer type 30 | } 31 | 32 | FFI_TYPE_MAP = CRYSTAL_TYPE_MAP.invert 33 | 34 | ERROR_VALUE = { 35 | char: "0i8", # In Crystal, :char is typically represented as Int8 36 | uchar: "0u8", # Unsigned char 37 | int8: "0i8", # Same as :char 38 | uint8: "0u8", # Same as :uchar 39 | short: "0i16", # Short integer 40 | ushort: "0u16", # Unsigned short integer 41 | int16: "0i16", # Same as :short 42 | uint16: "0u16", # Same as :ushort 43 | int: "0i32", # Integer, Crystal defaults to 32 bits 44 | uint: "0u32", # Unsigned integer 45 | int32: "0i32", # 32-bit integer 46 | uint32: "0u32", # 32-bit unsigned integer 47 | long: "0i64", # Long integer, size depends on the platform (32 or 64 bits) 48 | ulong: "0u64", # Unsigned long integer, size depends on the platform 49 | int64: "0_i64", # 64-bit integer 50 | uint64: "0_u64", # 64-bit unsigned integer 51 | long_long: "0_i64", # Same as :int64 52 | ulong_long: "0_u64", # Same as :uint64 53 | float: "0.0f32", # Floating point number (single precision) 54 | double: "0.0f64", # Double precision floating point number 55 | bool: "false", # Boolean type 56 | void: "Void", # Void type 57 | string: '"".to_unsafe', # String type 58 | pointer: "Pointer(Void).null" # Pointer type 59 | } 60 | 61 | C_TYPE_MAP = CRYSTAL_TYPE_MAP.merge( 62 | { 63 | string: "Pointer(::UInt8)" 64 | } 65 | ) 66 | 67 | C_TYPE_CONVERSIONS = { 68 | string: { 69 | from: "::String.new(%s.not_nil!)", 70 | to: "%s.to_unsafe" 71 | }, 72 | void: { 73 | to: "nil" 74 | } 75 | }.tap do |hash| 76 | hash.define_singleton_method(:convert) do |type, dir, expr| 77 | if hash.key?(type) 78 | conversion_string = hash[type][dir] 79 | conversion_string =~ /%/ ? conversion_string % expr : conversion_string 80 | else 81 | expr 82 | end 83 | end 84 | end 85 | 86 | def build_type_map(crystalruby_type) 87 | crystalruby_type = CRType(&crystalruby_type) if crystalruby_type.is_a?(Proc) 88 | 89 | if Types::Type.subclass?(crystalruby_type) && crystalruby_type.ffi_primitive_type 90 | crystalruby_type = crystalruby_type.ffi_primitive_type 91 | end 92 | 93 | { 94 | ffi_type: ffi_type(crystalruby_type), 95 | ffi_ret_type: ffi_type(crystalruby_type), 96 | crystal_type: crystal_type(crystalruby_type), 97 | crystalruby_type: crystalruby_type, 98 | lib_type: lib_type(crystalruby_type), 99 | error_value: error_value(crystalruby_type), 100 | arg_mapper: if Types::Type.subclass?(crystalruby_type) 101 | lambda { |arg| 102 | arg = crystalruby_type.new(arg.memory) if arg.is_a?(Types::Type) && !arg.is_a?(crystalruby_type) 103 | arg = crystalruby_type.new(arg) unless arg.is_a?(Types::Type) 104 | 105 | Types::FixedWidth.increment_ref_count!(arg.memory) if arg.class < Types::FixedWidth 106 | 107 | arg 108 | } 109 | end, 110 | retval_mapper: if Types::Type.subclass?(crystalruby_type) 111 | lambda { |arg| 112 | if arg.is_a?(Types::Type) && !arg.is_a?(crystalruby_type) 113 | arg = crystalruby_type.new(arg.memory) 114 | end 115 | arg = crystalruby_type.new(arg) unless arg.is_a?(Types::Type) 116 | 117 | Types::FixedWidth.decrement_ref_count!(arg.memory) if arg.class < Types::FixedWidth 118 | 119 | crystalruby_type.anonymous? ? arg.native : arg 120 | } 121 | # Strings in Crystal are UTF-8 encoded by default 122 | elsif crystalruby_type.equal?(:string) 123 | ->(arg) { arg.force_encoding("UTF-8") } 124 | end, 125 | convert_crystal_to_lib_type: ->(expr) { convert_crystal_to_lib_type(expr, crystalruby_type) }, 126 | convert_lib_to_crystal_type: ->(expr) { convert_lib_to_crystal_type(expr, crystalruby_type) } 127 | } 128 | end 129 | 130 | def ffi_type(type) 131 | case type 132 | when Symbol then type 133 | when Class 134 | if type < Types::FixedWidth 135 | :pointer 136 | elsif type < Types::Primitive 137 | type.ffi_type 138 | end 139 | end 140 | end 141 | 142 | def lib_type(type) 143 | if type.is_a?(Class) && type < Types::FixedWidth 144 | "Pointer(::UInt8)" 145 | elsif type.is_a?(Class) && type < Types::Type 146 | C_TYPE_MAP.fetch(type.ffi_type) 147 | else 148 | C_TYPE_MAP.fetch(type) 149 | end 150 | rescue StandardError 151 | raise "Unsupported type #{type}" 152 | end 153 | 154 | def error_value(type) 155 | if type.is_a?(Class) && type < Types::FixedWidth 156 | "Pointer(::UInt8).null" 157 | elsif type.is_a?(Class) && type < Types::Type 158 | ERROR_VALUE.fetch(type.ffi_type) 159 | else 160 | ERROR_VALUE.fetch(type) 161 | end 162 | rescue StandardError 163 | raise "Unsupported type #{type}" 164 | end 165 | 166 | def crystal_type(type) 167 | if type.is_a?(Class) && type < Types::Type 168 | type.anonymous? ? type.native_type_expr : type.inspect 169 | else 170 | CRYSTAL_TYPE_MAP.fetch(type) 171 | end 172 | rescue StandardError 173 | raise "Unsupported type #{type}" 174 | end 175 | 176 | def convert_lib_to_crystal_type(expr, type) 177 | if type.is_a?(Class) && type < Types::Type 178 | expr = "#{expr}.not_nil!" unless type.nil? 179 | type.pointer_to_crystal_type_conversion(expr) 180 | elsif type == :void 181 | "nil" 182 | else 183 | "#{C_TYPE_CONVERSIONS.convert(type, :from, expr)}.not_nil!" 184 | end 185 | end 186 | 187 | def convert_crystal_to_lib_type(expr, type) 188 | if type.is_a?(Class) && type < Types::Type 189 | type.crystal_type_to_pointer_type_conversion(expr) 190 | else 191 | C_TYPE_CONVERSIONS.convert(type, :to, expr) 192 | end 193 | end 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /lib/crystalruby/reactor.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | module CrystalRuby 4 | # The Reactor represents a singleton Thread responsible for running all Ruby/crystal interop code. 5 | # Crystal's Fiber scheduler and GC assume all code is run on a single thread. 6 | # This class is responsible for multiplexing Ruby and Crystal code onto a single thread. 7 | # Functions annotated with async: true, are executed using callbacks to allow these to be interleaved 8 | # without blocking multiple Ruby threads. 9 | module Reactor 10 | module_function 11 | 12 | class SingleThreadViolation < StandardError; end 13 | 14 | class StopReactor < StandardError; end 15 | 16 | @op_count = 0 17 | @single_thread_mode = false 18 | 19 | REACTOR_QUEUE = Queue.new 20 | 21 | # Invoke GC every 100 ops 22 | GC_OP_THRESHOLD = ENV.fetch("CRYSTAL_GC_OP_THRESHOLD", 100).to_i 23 | # Or every 0.05 seconds 24 | GC_INTERVAL = ENV.fetch("CRYSTAL_GC_INTERVAL", 0.05).to_f 25 | # Or if we've gotten hold of a reference to at least 100KB or more of fresh memory since last GC 26 | GC_BYTES_SEEN_THRESHOLD = ENV.fetch("CRYSTAL_GC_BYTES_SEEN_THRESHOLD", 100 * 1024).to_i 27 | 28 | # We maintain a map of threads, each with a mutex, condition variable, and result 29 | THREAD_MAP = Hash.new do |h, tid_or_thread, tid = tid_or_thread| 30 | if tid_or_thread.is_a?(Thread) 31 | ObjectSpace.define_finalizer(tid_or_thread) do 32 | THREAD_MAP.delete(tid_or_thread) 33 | THREAD_MAP.delete(tid_or_thread.object_id) 34 | end 35 | tid = tid_or_thread.object_id 36 | end 37 | 38 | h[tid] = { 39 | mux: Mutex.new, 40 | cond: ConditionVariable.new, 41 | result: nil, 42 | thread_id: tid 43 | } 44 | h[tid_or_thread] = h[tid] if tid_or_thread.is_a?(Thread) 45 | end 46 | 47 | # We memoize callbacks, once per return type 48 | CALLBACKS_MAP = Hash.new do |h, rt| 49 | h[rt] = FFI::Function.new(:void, [:int, *((rt == :void) ? [] : [rt])]) do |tid, ret| 50 | THREAD_MAP[tid][:error] = nil 51 | THREAD_MAP[tid][:result] = ret 52 | THREAD_MAP[tid][:cond].signal 53 | end 54 | end 55 | 56 | ERROR_CALLBACK = FFI::Function.new(:void, %i[string string string int]) do |error_type, message, backtrace, tid| 57 | error_type = error_type.to_sym 58 | is_exception_type = Object.const_defined?(error_type) && Object.const_get(error_type).ancestors.include?(Exception) 59 | error_type = is_exception_type ? Object.const_get(error_type) : RuntimeError 60 | error = error_type.new(message) 61 | error.set_backtrace(JSON.parse(backtrace)) 62 | raise error unless THREAD_MAP.key?(tid) 63 | 64 | THREAD_MAP[tid][:error] = error 65 | THREAD_MAP[tid][:result] = nil 66 | THREAD_MAP[tid][:cond].signal 67 | end 68 | 69 | def thread_conditions 70 | THREAD_MAP[Thread.current] 71 | end 72 | 73 | def await_result! 74 | mux, cond, result, err = thread_conditions.values_at(:mux, :cond, :result, :error) 75 | cond.wait(mux) unless result || err 76 | result, err, thread_conditions[:result], thread_conditions[:error] = thread_conditions.values_at(:result, :error) 77 | if err 78 | combined_backtrace = err.backtrace[0..(err.backtrace.index { |m| 79 | m.include?("call_blocking_function") 80 | } || 2) - 3] + caller[5..-1] 81 | err.set_backtrace(combined_backtrace) 82 | raise err 83 | end 84 | 85 | result 86 | end 87 | 88 | def halt_loop! 89 | raise StopReactor 90 | end 91 | 92 | def stop! 93 | return unless @main_loop 94 | 95 | schedule_work!(self, :halt_loop!, :void, blocking: true, async: false) 96 | @main_loop.join 97 | @main_loop = nil 98 | CrystalRuby.log_info "Reactor loop stopped" 99 | end 100 | 101 | def start! 102 | @op_count = 0 103 | @main_loop ||= Thread.new do 104 | @main_thread_id = Thread.current.object_id 105 | CrystalRuby.log_debug("Starting reactor") 106 | CrystalRuby.log_debug("CrystalRuby initialized") 107 | while true 108 | handler, *args, lib = REACTOR_QUEUE.pop 109 | send(handler, *args, lib) 110 | @op_count += 1 111 | invoke_gc_if_due!(lib) 112 | end 113 | rescue StopReactor 114 | rescue => e 115 | CrystalRuby.log_error "Error: #{e}" 116 | CrystalRuby.log_error e.backtrace 117 | end 118 | end 119 | 120 | def invoke_gc_if_due!(lib) 121 | schedule_work!(lib, :gc, :void, blocking: true, async: false, lib: lib) if lib && gc_due? 122 | end 123 | 124 | def gc_due? 125 | now = Process.clock_gettime(Process::CLOCK_MONOTONIC) 126 | 127 | # Initialize state variables if not already set. 128 | @last_gc_time ||= now 129 | @op_count ||= 0 130 | @last_gc_op_count ||= @op_count 131 | @last_mem_check_time ||= now 132 | 133 | # Calculate differences based on ops and time. 134 | ops_since_last_gc = @op_count - @last_gc_op_count 135 | time_since_last_gc = now - @last_gc_time 136 | 137 | # Start with our two “cheap” conditions. 138 | due = (ops_since_last_gc >= GC_OP_THRESHOLD) || (time_since_last_gc >= GC_INTERVAL) || Types::Allocator.gc_bytes_seen > GC_BYTES_SEEN_THRESHOLD 139 | 140 | if due 141 | # Update the baseline values after GC is scheduled. 142 | @last_gc_time = now 143 | # If we just did a memory check, use that value; otherwise, fetch one now. 144 | @last_gc_op_count = @op_count 145 | Types::Allocator.gc_hint_reset! 146 | true 147 | else 148 | false 149 | end 150 | end 151 | 152 | def start_gc_thread!(lib) 153 | Thread.new do 154 | loop do 155 | schedule_work!(lib, :gc, :void, blocking: true, async: false, lib: lib) if gc_due? 156 | sleep GC_INTERVAL 157 | end 158 | end 159 | end 160 | 161 | def thread_id 162 | Thread.current.object_id 163 | end 164 | 165 | def yield!(lib: nil, time: 0.0) 166 | schedule_work!(lib, :yield, :int, async: false, blocking: false, lib: lib) if running? && lib 167 | nil 168 | end 169 | 170 | def invoke_async!(receiver, op_name, *args, thread_id, callback, lib) 171 | receiver.send(op_name, *args, thread_id, callback) 172 | yield!(lib: lib, time: 0) 173 | end 174 | 175 | def invoke_blocking!(receiver, op_name, *args, tvars, _lib) 176 | tvars[:error] = nil 177 | begin 178 | tvars[:result] = receiver.send(op_name, *args) 179 | rescue StopReactor 180 | tvars[:cond].signal 181 | raise 182 | rescue => e 183 | tvars[:error] = e 184 | end 185 | tvars[:cond].signal 186 | end 187 | 188 | def invoke_await!(receiver, op_name, *args, lib) 189 | outstanding_jobs = receiver.send(op_name, *args) 190 | yield!(lib: lib, time: 0) unless outstanding_jobs == 0 191 | end 192 | 193 | def schedule_work!(receiver, op_name, *args, return_type, blocking: true, async: true, lib: nil) 194 | if @single_thread_mode || (Thread.current.object_id == @main_thread_id && op_name != :yield) 195 | unless Thread.current.object_id == @main_thread_id 196 | raise SingleThreadViolation, 197 | "Single thread mode is enabled, cannot run in multi-threaded mode. " \ 198 | "Reactor was started from: #{@main_thread_id}, then called from #{Thread.current.object_id}" 199 | end 200 | invoke_gc_if_due!(lib) 201 | return receiver.send(op_name, *args) 202 | end 203 | 204 | tvars = thread_conditions 205 | tvars[:mux].synchronize do 206 | REACTOR_QUEUE.push( 207 | case true 208 | when async then [:invoke_async!, receiver, op_name, *args, tvars[:thread_id], CALLBACKS_MAP[return_type], lib] 209 | when blocking then [:invoke_blocking!, receiver, op_name, *args, tvars, lib] 210 | else [:invoke_await!, receiver, op_name, *args, lib] 211 | end 212 | ) 213 | return await_result! if blocking 214 | end 215 | end 216 | 217 | def running? 218 | @main_loop&.alive? 219 | end 220 | 221 | def init_single_thread_mode! 222 | @single_thread_mode ||= begin 223 | @main_thread_id = Thread.current.object_id 224 | true 225 | end 226 | end 227 | end 228 | end 229 | -------------------------------------------------------------------------------- /lib/crystalruby/types/type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Can create complex structs in Ruby 4 | # E.g. 5 | # Types::MyType.new({a: 1, b: [2,3, {four: "five"}}) 6 | # 7 | # Can read complex structs in Crystal. 8 | # Expose Ruby setters to Crystal. 9 | # - These accepts pointers and *copy* the values from them (so that they are governed by Ruby GC) 10 | # - Safe to GC on Crystal side after call to setter 11 | # 12 | # All Structs are memory managed in Ruby. 13 | # Parameters are temporarily stored in memory *before* being passed, then nilled (might be GC'd if we don't hold on to it elsewhere) 14 | # Internal pointers are stored in complex types. 15 | # Cannot create new structs in Crystal. 16 | # All information passed from Crystal back to Ruby is either: 17 | # - Direct memory access (for primitives) 18 | # - Using setters + copy 19 | # 20 | # What if: 21 | # - We ask Crystal to implement allocation og memory, and implement all of the setters 22 | # - It stores the pointers to the memory in an address indexed hash 23 | # - Ruby will reference count using AutoPointer 24 | # - If reference count is 0, clear out hash in Crystal (Trigger GC) 25 | # - Don't like this because, types can't stand alone in Ruby AND crosses FFI bridge many times to allocate complex struct. 26 | 27 | require "forwardable" 28 | 29 | module CrystalRuby 30 | InvalidCastError = Class.new(StandardError) 31 | 32 | module Types 33 | class Type 34 | # TODO: Replace with pthread primitives and share 35 | # with Crystal 36 | ARC_MUTEX = CrystalRuby::ArcMutex.new 37 | 38 | include Allocator 39 | extend Typemaps 40 | 41 | class << self 42 | attr_accessor :typename, :ffi_type, :memsize, :convert_if, :inner_types 43 | end 44 | 45 | def_delegators :@class, :primitive?, :cast!, :type, :typename, :memsize, :refsize, 46 | :ffi_type, :error, :inner_type, :inner_keys, :inner_types, 47 | :write_mixed_byte_slices_to_uint8_array, :data_offset, :size_offset, 48 | :union_types 49 | 50 | attr_accessor :value, :memory, :ffi_primitive 51 | 52 | def initialize(_rbval) 53 | @class = self.class 54 | raise error if error 55 | end 56 | 57 | def self.finalize(_memory) 58 | ->(_) {} 59 | end 60 | 61 | def self.inspect_name 62 | (name || "#{typename}").to_s.gsub(/^CrystalRuby::Types::[^::]+::/, "") 63 | end 64 | 65 | def self.union_types 66 | [self] 67 | end 68 | 69 | def self.valid? 70 | true 71 | end 72 | 73 | def self.native_type_expr 74 | if !inner_types 75 | "::#{typename}" 76 | elsif !inner_keys 77 | "::#{typename}(#{inner_types.map(&:native_type_expr).join(", ")})" 78 | else 79 | "::#{typename}(#{inner_keys.zip(inner_types).map { |k, v| "#{k}: #{v.native_type_expr}" }.join(", ")})" 80 | end 81 | end 82 | 83 | def self.named_type_expr 84 | if !inner_types 85 | "::#{name || typename}" 86 | elsif !inner_keys 87 | "::#{name || typename}(#{inner_types.map(&:named_type_expr).join(", ")})" 88 | else 89 | "::#{name || typename}(#{inner_keys.zip(inner_types).map { |k, v| "#{k}: #{v.named_type_expr}" }.join(", ")})" 90 | end 91 | end 92 | 93 | def self.valid_cast?(raw) 94 | raw.is_a?(self) || convert_if.any? { |type| raw.is_a?(type) } 95 | end 96 | 97 | def self.[](*value) 98 | is_list_type = ancestors.any? { |a| a < CrystalRuby::Types::Array || a < CrystalRuby::Types::Tuple } 99 | new(is_list_type ? value : value.first) 100 | end 101 | 102 | def self.anonymous? 103 | name.nil? || name.start_with?("CrystalRuby::Types::") 104 | end 105 | 106 | def self.crystal_class_name 107 | name || named_type_expr.split(",").join("_and_") 108 | .split("|").join("_or_") 109 | .split("(").join("_of_") 110 | .gsub(/[^a-zA-Z0-9_]/, "") 111 | .split("_") 112 | .map(&:capitalize).join << "_#{type_digest[0..6]}" 113 | end 114 | 115 | def self.base_crystal_class_name 116 | crystal_class_name.split("::").last 117 | end 118 | 119 | def value(native: false) 120 | @value 121 | end 122 | 123 | def native 124 | value(native: true) 125 | end 126 | 127 | def self.type_digest 128 | Digest::MD5.hexdigest(native_type_expr.to_s) 129 | end 130 | 131 | def self.nested_types 132 | [self, *(inner_types || []).map(&:nested_types)].flatten.uniq 133 | end 134 | 135 | def self.pointer_to_crystal_type_conversion(expr) 136 | anonymous? ? "#{crystal_class_name}.new(#{expr}).native_decr" : "#{crystal_class_name}.new_decr(#{expr})" 137 | end 138 | 139 | def self.crystal_type_to_pointer_type_conversion(expr) 140 | anonymous? ? "#{crystal_class_name}.new(#{expr}).return_value" : "#{expr}.return_value" 141 | end 142 | 143 | def self.template_name 144 | typename || superclass.template_name 145 | end 146 | 147 | def self.type_defn 148 | unless Template.const_defined?(template_name) && Template.const_get(template_name).is_a?(Template::Renderer) 149 | raise "Template not found for #{template_name}" 150 | end 151 | 152 | Template.const_get(template_name).render(binding) 153 | end 154 | 155 | def self.numeric? 156 | false 157 | end 158 | 159 | def self.primitive? 160 | false 161 | end 162 | 163 | def self.variable_width? 164 | false 165 | end 166 | 167 | def self.fixed_width? 168 | false 169 | end 170 | 171 | def self.cast!(value) 172 | value.is_a?(Type) ? value.value : value 173 | end 174 | 175 | def ==(other) 176 | value(native: true) == (other.is_a?(Type) ? other.value(native: true) : other) 177 | end 178 | 179 | def nil? 180 | value.nil? 181 | end 182 | 183 | def coerce(other) 184 | [other, value] 185 | end 186 | 187 | def inspect 188 | value.inspect 189 | end 190 | 191 | def self.from_ffi_array_repr(value) 192 | anonymous? ? new(value).value : new(value) 193 | end 194 | 195 | def inner_value 196 | @value 197 | end 198 | 199 | # Create a brand new copy of this object 200 | def deep_dup 201 | self.class.new(native) 202 | end 203 | 204 | # Create a new reference to this object. 205 | def dup 206 | self.class.new(@memory) 207 | end 208 | 209 | def method_missing(method, *args, &block) 210 | v = begin 211 | native 212 | rescue StandardError 213 | super 214 | end 215 | if v.respond_to?(method) 216 | hash_before = v.hash 217 | result = v.send(method, *args, &block) 218 | if v.hash != hash_before 219 | self.value = v 220 | v.equal?(result) ? self : result 221 | else 222 | result 223 | end 224 | else 225 | super 226 | end 227 | end 228 | 229 | def self.bind_local_vars!(variable_names, binding) 230 | variable_names.each do |name| 231 | define_singleton_method(name) do 232 | binding.local_variable_get("#{name}") 233 | end 234 | define_method(name) do 235 | binding.local_variable_get("#{name}") 236 | end 237 | end 238 | end 239 | 240 | def self.each_child_address(pointer); end 241 | 242 | def item_size 243 | inner_types.map(&:memsize).sum 244 | end 245 | 246 | def total_memsize 247 | memsize 248 | end 249 | 250 | # For non-container ffi_primitive non-named types, 251 | # just use the raw FFI type, as it's much more efficient 252 | # due to skipping Arc overhead. 253 | def self.ffi_primitive_type 254 | respond_to?(:ffi_primitive) && anonymous? ? ffi_primitive : nil 255 | end 256 | 257 | def self.crystal_type 258 | lib_type(ffi_type) 259 | end 260 | 261 | def self.|(other) 262 | raise "Cannot union non-crystal type #{other}" unless other.is_a?(Class) && other.ancestors.include?(Type) 263 | 264 | CrystalRuby::Types::TaggedUnion(*union_types, *other.union_types) 265 | end 266 | 267 | def self.validate!(type) 268 | unless type.is_a?(Class) && type.ancestors.include?(Types::Type) 269 | raise "Result #{type} is not a valid CrystalRuby type" 270 | end 271 | 272 | raise "Invalid type: #{type.error}" unless type.valid? 273 | end 274 | 275 | def self.inner_type 276 | inner_types.first 277 | end 278 | 279 | def self.subclass?(type) 280 | type.is_a?(Class) && type < Types::Type 281 | end 282 | 283 | def self.type_expr 284 | if !inner_types 285 | inspect_name 286 | elsif !anonymous? 287 | name 288 | elsif inner_keys 289 | "#{inspect_name}(#{inner_keys.zip(inner_types).map { |k, v| "#{k}: #{v.inspect}" }.join(", ")})" 290 | else 291 | "#{inspect_name}(#{inner_types.map(&:inspect).join(", ")})" 292 | end 293 | end 294 | 295 | def self.inspect 296 | type_expr 297 | end 298 | end 299 | end 300 | end 301 | -------------------------------------------------------------------------------- /lib/crystalruby/adapter.rb: -------------------------------------------------------------------------------- 1 | module CrystalRuby 2 | module Adapter 3 | # Use this method to annotate a Ruby method that should be crystallized. 4 | # Compilation and attachment of the method is done lazily. 5 | # You can force compilation by calling `CrystalRuby.compile!` 6 | # It's important that all code using crystallized methods is 7 | # loaded before any manual calls to compile. 8 | # 9 | # E.g. 10 | # 11 | # crystallize :int32 12 | # def add(a: :int32, b: :int32) 13 | # a + b 14 | # end 15 | # 16 | # Pass `raw: true` to pass Raw crystal code to the compiler as a string instead. 17 | # (Useful for cases where the Crystal method body is not valid Ruby) 18 | # E.g. 19 | # crystallize :int32, raw: true 20 | # def add(a: :int32, b: :int32) 21 | # <<~CRYSTAL 22 | # a + b 23 | # CRYSTAL 24 | # end 25 | # 26 | # Pass `async: true` to make the method async. 27 | # Crystal methods will always block the currently executing Ruby thread. 28 | # With async: false, all other Crystal code will be blocked while this Crystal method is executing (similar to Ruby code with the GVL) 29 | # With async: true, several Crystal methods can be executing concurrently. 30 | # 31 | # Pass lib: "name_of_lib" to compile Crystal code into several distinct libraries. 32 | # This can help keep compilation times low, by packaging your Crystal code into separate shared objects. 33 | # @param returns The return type of the method. Optional (defaults to :void). 34 | # @param [Hash] options The options hash. 35 | # @option options [Boolean] :raw (false) Pass raw Crystal code to the compiler as a string. 36 | # @option options [Boolean] :async (false) Mark the method as async (allows multiplexing). 37 | # @option options [String] :lib ("crystalruby") The name of the library to compile the Crystal code into. 38 | # @option options [Proc] :block An optional wrapper Ruby block that wraps around any invocations of the crystal code 39 | def crystallize(returns = :void, raw: false, async: false, lib: "crystalruby", &block) 40 | (self == TOPLEVEL_BINDING.receiver ? Object : self).instance_eval do 41 | @crystallize_next = { 42 | raw: raw, 43 | async: async, 44 | returns: returns, 45 | block: block, 46 | lib: lib 47 | } 48 | end 49 | end 50 | 51 | # Alias for `crystallize` 52 | alias crystalize crystallize 53 | 54 | # Exposes a Ruby method to one or more Crystal libraries. 55 | # Type annotations follow the same rules as the `crystallize` method, but are 56 | # applied in reverse. 57 | # @param returns The return type of the method. Optional (defaults to :void). 58 | # @param [Hash] options The options hash. 59 | # @option options [Boolean] :raw (false) Pass raw Crystal code to the compiler as a string. 60 | # @option options [String] :libs (["crystalruby"]) The name of the Crystal librarie(s) to expose the Ruby code to. 61 | def expose_to_crystal(returns = :void, libs: ["crystalruby"]) 62 | (self == TOPLEVEL_BINDING.receiver ? Object : self).instance_eval do 63 | @expose_next_to_crystal = { 64 | returns: returns, 65 | libs: libs 66 | } 67 | end 68 | end 69 | 70 | # Define a shard dependency 71 | # This dependency will be automatically injected into the shard.yml file for 72 | # the given library and installed upon compile if it is not already installed. 73 | def shard(shard_name, lib: "crystalruby", **opts) 74 | CrystalRuby::Library[lib].require_shard(shard_name, **opts) 75 | end 76 | 77 | # Use this method to define inline Crystal code that does not need to be bound to a Ruby method. 78 | # This is useful for defining classes, modules, performing set-up tasks etc. 79 | # See: docs for .crystallize to understand the `raw` and `lib` parameters. 80 | def crystal(raw: false, lib: "crystalruby", &block) 81 | inline_crystal_body = if respond_to?(:name) 82 | Template::InlineChunk.render( 83 | { 84 | module_name: name, 85 | body: SourceReader.extract_source_from_proc(block, raw: raw), 86 | mod_or_class: is_a?(Class) && self < Types::Type ? "class" : "module", 87 | superclass: is_a?(Class) && self < Types::Type ? "< #{crystal_supertype}" : "" 88 | } 89 | ) 90 | else 91 | SourceReader.extract_source_from_proc(block, raw: raw) 92 | end 93 | 94 | CrystalRuby::Library[lib].crystallize_chunk( 95 | self, 96 | Digest::MD5.hexdigest(inline_crystal_body), 97 | inline_crystal_body 98 | ) 99 | end 100 | 101 | # This method provides a useful DSL for defining Crystal types in pure Ruby 102 | # MyType = CRType{ Int32 | Hash(String, Array(Bool) | Float65 | Nil) } 103 | # @param [Proc] block The block within which we build the type definition. 104 | def CRType(&block) 105 | TypeBuilder.build_from_source(block, context: self) 106 | end 107 | 108 | private 109 | 110 | # We trigger attaching of crystallized instance methods here. 111 | # If a method is added after a crystallize annotation we assume it's the target of the crystallize annotation. 112 | # @param [Symbol] method_name The name of the method being added. 113 | def method_added(method_name) 114 | define_crystallized_method(instance_method(method_name)) if should_crystallize_next? 115 | expose_ruby_method_to_crystal(instance_method(method_name)) if should_expose_next? 116 | super 117 | end 118 | 119 | # We trigger attaching of crystallized class methods here. 120 | # If a method is added after a crystallize annotation we assume it's the target of the crystallize annotation. 121 | # @note This method is called when a method is added to the singleton class of the object. 122 | # @param [Symbol] method_name The name of the method being added. 123 | def singleton_method_added(method_name) 124 | define_crystallized_method(singleton_method(method_name)) if should_crystallize_next? 125 | expose_ruby_method_to_crystal(singleton_method(method_name)) if should_expose_next? 126 | super 127 | end 128 | 129 | # Helper method to determine if the next method added should be crystallized. 130 | # @return [Boolean] True if the next method added should be crystallized. 131 | def should_crystallize_next? 132 | defined?(@crystallize_next) && @crystallize_next 133 | end 134 | 135 | # Helper method to determine if the next method added should be exposed to Crystal libraries. 136 | # @return [Boolean] True if the next method added should be exposed. 137 | def should_expose_next? 138 | defined?(@expose_next_to_crystal) && @expose_next_to_crystal 139 | end 140 | 141 | # This is where we extract the Ruby method metadata and invoke the Crystal::Library functionality 142 | # to compile a stub for the Ruby method into the Crystal library. 143 | def expose_ruby_method_to_crystal(method) 144 | returns, libs = @expose_next_to_crystal.values_at(:returns, :libs) 145 | @expose_next_to_crystal = nil 146 | 147 | args, source = SourceReader.extract_args_and_source_from_method(method) 148 | returns = args.delete(:returns) if args[:returns] && returns == :void 149 | args[:__yield_to] = args.delete(:yield) if args[:yield] 150 | src = <<~RUBY 151 | def #{method.name} (#{(args.keys - [:__yield_to]).join(", ")}) 152 | #{source} 153 | end 154 | RUBY 155 | 156 | owner = method.owner.singleton_class? ? method.owner.attached_object : method.owner 157 | owner.class_eval(src) 158 | unless method.is_a?(UnboundMethod) && method.owner.ancestors.include?(CrystalRuby::Types::Type) 159 | owner.instance_eval(src) 160 | end 161 | method = owner.send(method.is_a?(UnboundMethod) ? :instance_method : :method, method.name) 162 | 163 | libs.each do |lib| 164 | CrystalRuby::Library[lib].expose_method( 165 | method, 166 | args, 167 | returns 168 | ) 169 | end 170 | end 171 | 172 | # We attach crystallized class methods here. 173 | # This function is responsible for 174 | # - Generating the Crystal source code 175 | # - Overwriting the method and class methods by the same name in the caller. 176 | # - Lazily triggering compilation and attachment of the Ruby method to the Crystal code. 177 | # - We also optionally prepend a block (if given) to the owner, to allow Ruby code to wrap around Crystal code. 178 | # @param [Symbol] method_name The name of the method being added. 179 | # @param [UnboundMethod] method The method being added. 180 | def define_crystallized_method(method) 181 | CrystalRuby.log_debug("Defining crystallized method #{name}.#{method.name}") 182 | 183 | returns, block, async, lib, raw = @crystallize_next.values_at(:returns, :block, :async, :lib, :raw) 184 | @crystallize_next = nil 185 | 186 | args, source = SourceReader.extract_args_and_source_from_method(method, raw: raw) 187 | 188 | # We can safely claim the `yield` argument name for typing the yielded block 189 | # because this is an illegal identifier in Crystal anyway. 190 | args[:__yield_to] = args.delete(:yield) if args[:yield] 191 | 192 | returns = args.delete(:returns) if args[:returns] && returns == :void 193 | 194 | CrystalRuby::Library[lib].crystallize_method( 195 | method, 196 | args, 197 | returns, 198 | source, 199 | async, 200 | &block 201 | ) 202 | end 203 | end 204 | end 205 | 206 | Module.prepend(CrystalRuby::Adapter) 207 | BasicObject.prepend(CrystalRuby::Adapter) 208 | BasicObject.singleton_class.prepend(CrystalRuby::Adapter) 209 | --------------------------------------------------------------------------------