Class: CrystalRuby::Library

Inherits:
Object
  • Object
show all
Includes:
Config, Typemaps
Defined in:
lib/crystalruby/library.rb

Constant Summary collapse

CR_COMPILE_MUX =

CR_ATTACH_MUX and CR_COMPILE_MUX are used to only allow a single FFI compile or attach operation at once to avoid a rare scenario where the same function is attached simultaneously across two or more threads.

Mutex.new
CR_ATTACH_MUX =
Mutex.new

Constants included from Typemaps

Typemaps::CRYSTAL_TYPE_MAP, Typemaps::C_TYPE_CONVERSIONS, Typemaps::C_TYPE_MAP, Typemaps::ERROR_VALUE, Typemaps::FFI_TYPE_MAP

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Config

#config

Methods included from Typemaps

#build_type_map, #convert_crystal_to_lib_type, #convert_lib_to_crystal_type, #crystal_type, #error_value, #ffi_type, #lib_type

Constructor Details

#initialize(name) ⇒ Library

A Library represents a single Crystal shared object. It holds code as either methods (invokable from Ruby and attached) or anonymous chunks, which are just raw Crystal code.



32
33
34
35
36
37
38
39
# File 'lib/crystalruby/library.rb', line 32

def initialize(name)
  self.name = name
  self.methods = {}
  self.exposed_methods = {}
  self.chunks = []
  self.shards = {}
  initialize_library!
end

Instance Attribute Details

#chunksObject

Returns the value of attribute chunks.



13
14
15
# File 'lib/crystalruby/library.rb', line 13

def chunks
  @chunks
end

#codegen_dirObject

Returns the value of attribute codegen_dir.



13
14
15
# File 'lib/crystalruby/library.rb', line 13

def codegen_dir
  @codegen_dir
end

#exposed_methodsObject

Returns the value of attribute exposed_methods.



13
14
15
# File 'lib/crystalruby/library.rb', line 13

def exposed_methods
  @exposed_methods
end

#lib_dirObject

Returns the value of attribute lib_dir.



13
14
15
# File 'lib/crystalruby/library.rb', line 13

def lib_dir
  @lib_dir
end

#methodsObject

Returns the value of attribute methods.



13
14
15
# File 'lib/crystalruby/library.rb', line 13

def methods
  @methods
end

#nameObject

Returns the value of attribute name.



13
14
15
# File 'lib/crystalruby/library.rb', line 13

def name
  @name
end

#root_dirObject

Returns the value of attribute root_dir.



13
14
15
# File 'lib/crystalruby/library.rb', line 13

def root_dir
  @root_dir
end

#shardsObject

Returns the value of attribute shards.



13
14
15
# File 'lib/crystalruby/library.rb', line 13

def shards
  @shards
end

#src_dirObject

Returns the value of attribute src_dir.



13
14
15
# File 'lib/crystalruby/library.rb', line 13

def src_dir
  @src_dir
end

Class Method Details

.[](name) ⇒ Object



22
23
24
25
26
27
# File 'lib/crystalruby/library.rb', line 22

def self.[](name)
  @libs_by_name[name] ||= begin
    CrystalRuby.initialize_crystal_ruby! unless CrystalRuby.initialized?
    Library.new(name)
  end
end

.allObject



18
19
20
# File 'lib/crystalruby/library.rb', line 18

def self.all
  @libs_by_name.values
end

.chunk_storeObject



269
270
271
# File 'lib/crystalruby/library.rb', line 269

def self.chunk_store
  @chunk_store ||= []
end

Instance Method Details

#attach!Object



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/crystalruby/library.rb', line 234

def attach!
  CR_ATTACH_MUX.synchronize do
    lib_file = self.lib_file
    lib_methods = methods
    lib_methods.values.reject(&:ruby).each(&:attach_ffi_func!)
    singleton_class.class_eval do
      extend FFI::Library
      ffi_lib lib_file
      %i[yield init].each do |method_name|
        singleton_class.undef_method(method_name) if singleton_class.method_defined?(method_name)
        undef_method(method_name) if method_defined?(method_name)
      end
      attach_function :init, %i[string pointer pointer], :void
      attach_function :yield, %i[], :int
      lib_methods.each_value.select(&:ruby).each do |method|
        attach_function :"register_#{method.name.to_s.gsub("?", "q").gsub("=", "eq").gsub("!", "bang")}_callback", %i[pointer], :void
      end
    end

    if CrystalRuby.config.single_thread_mode
      Reactor.init_single_thread_mode!
    else
      Reactor.start!
    end

    Reactor.schedule_work!(self, :init, name, Reactor::ERROR_CALLBACK, Types::Type::ARC_MUTEX.to_ptr, :void,
                           blocking: true, async: false)
    methods.values.select(&:ruby).each(&:register_callback!)
  end
end

#build!Object



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/crystalruby/library.rb', line 211

def build!
  CR_COMPILE_MUX.synchronize do
    File.write codegen_dir / "index.cr", Template::Index.render(requires: requires)
    unless compiled?
      FileUtils.rm_f(lib_file)

      if shard_dependencies.any? && shards.empty?
        rewrite_shards_file!
      end

      CrystalRuby::Compilation.install_shards!(src_dir)
      CrystalRuby::Compilation.compile!(
        verbose: config.verbose,
        debug: config.debug,
        src: main_file,
        lib: "#{lib_file}.part"
      )
      FileUtils.mv("#{lib_file}.part", lib_file)
      attach!
    end
  end
end

#build_type(type_name, expr) ⇒ Object



148
149
150
151
152
153
154
155
156
157
# File 'lib/crystalruby/library.rb', line 148

def build_type(type_name, expr)
  parts = type_name.split("::")
  typedef = parts[0...-1].each_with_index.reduce("") do |acc, (part, index)|
    acc + "#{"  " * index}module #{part}\n"
  end
  typedef += "#{"  " * parts.size}#{expr}\n"
  typedef + parts[0...-1].reverse.each_with_index.reduce("") do |acc, (_part, index)|
    acc + "#{"  " * (parts.size - 2 - index)}end\n"
  end
end

#compiled?Boolean

Returns:

  • (Boolean)


121
122
123
124
125
126
127
128
# File 'lib/crystalruby/library.rb', line 121

def compiled?
  @compiled ||= File.exist?(lib_file) && chunks.all? do |chunk|
    chunk_data = chunk[:body]
    file_digest = Digest::MD5.hexdigest chunk_data
    fname = chunk[:chunk_name]
    index_contents.include?("#{chunk[:module_name]}/#{fname}_#{file_digest}.cr")
  end && shards_installed?
end

#crystalize_chunk(mod, chunk_name, body) ⇒ Object



113
114
115
# File 'lib/crystalruby/library.rb', line 113

def crystalize_chunk(mod, chunk_name, body)
  write_chunk(mod.respond_to?(:name) ? name : "main", chunk_name, body)
end

#crystalize_method(method, args, returns, function_body, async, &block) ⇒ Object

Generates and stores a reference to a new CrystalRuby::Function and triggers the generation of the crystal code. (See write_chunk)



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/crystalruby/library.rb', line 64

def crystalize_method(method, args, returns, function_body, async, &block)
  CR_ATTACH_MUX.synchronize do
    methods.each_value(&:unattach!)
    method_key = "#{method.owner.name}/#{method.name}"
    methods[method_key] = Function.new(
      method: method,
      args: args,
      returns: returns,
      function_body: function_body,
      async: async,
      lib: self,
      &block
    ).tap do |func|
      func.define_crystalized_methods!(self)
      func.register_custom_types!(self)
      write_chunk(func.owner_name, method.name, func.chunk)
    end
  end
end

#digestObject



265
266
267
# File 'lib/crystalruby/library.rb', line 265

def digest
  Digest::MD5.hexdigest(File.read(codegen_dir / "index.cr")) if File.exist?(codegen_dir / "index.cr")
end

#expose_method(method, args, returns) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/crystalruby/library.rb', line 84

def expose_method(method, args, returns)
  CR_ATTACH_MUX.synchronize do
    methods.each_value(&:unattach!)
    method_key = "#{method.owner.name}/#{method.name}"
    methods[method_key] = Function.new(
      method: method,
      args: args,
      returns: returns,
      ruby: true,
      lib: self
    ).tap do |func|
      func.register_custom_types!(self)
      write_chunk(func.owner_name, method.name, func.ruby_interface)
    end
  end
end

#index_contentsObject



138
139
140
141
142
# File 'lib/crystalruby/library.rb', line 138

def index_contents
  IO.read(codegen_dir / "index.cr")
rescue StandardError
  ""
end

#initialize_library!Object

Bootstraps the library filesystem and generates top level index.cr and shard files if these do not already exist.



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/crystalruby/library.rb', line 43

def initialize_library!
  @root_dir, @lib_dir, @src_dir, @codegen_dir = [
    config.crystal_src_dir_abs / name,
    config.crystal_src_dir_abs / name / "lib",
    config.crystal_src_dir_abs / name / "src",
    config.crystal_src_dir_abs / name / "src" / config.crystal_codegen_dir
  ].each do |dir|
    FileUtils.mkdir_p(dir)
  end
  IO.write main_file, "require \"./#{config.crystal_codegen_dir}/index\"\n" unless File.exist?(main_file)

  return if File.exist?(shard_file)

  IO.write(shard_file, <<~YAML)
    name: src
    version: 0.1.0
  YAML
end

#instantiated?Boolean

Returns:

  • (Boolean)


117
118
119
# File 'lib/crystalruby/library.rb', line 117

def instantiated?
  @instantiated
end

#lib_fileObject



105
106
107
# File 'lib/crystalruby/library.rb', line 105

def lib_file
  lib_dir / FFI::LibraryPath.new("#{name}#{config.debug ? "-debug" : ""}", abi_number: digest).to_s
end

#main_fileObject



101
102
103
# File 'lib/crystalruby/library.rb', line 101

def main_file
  src_dir / "#{name}.cr"
end

#register_type!(type) ⇒ Object



144
145
146
# File 'lib/crystalruby/library.rb', line 144

def register_type!(type)
  write_chunk("types", type.crystal_class_name, build_type(type.crystal_class_name, type.type_defn))
end

#require_shard(name, opts) ⇒ Object



173
174
175
176
# File 'lib/crystalruby/library.rb', line 173

def require_shard(name, opts)
  @shards[name.to_s] = JSON.parse(opts.merge("_crystalruby_managed" => true).to_json)
  rewrite_shards_file!
end

#requiresObject



196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/crystalruby/library.rb', line 196

def requires
  Template::Type.render({}) +
    Template::Primitive.render({}) +
    Template::FixedWidth.render({}) +
    Template::VariableWidth.render({}) +
    chunks.map do |chunk|
      chunk_data = chunk[:body]
      file_digest = Digest::MD5.hexdigest chunk_data
      fname = chunk[:chunk_name]
      "require \"./#{chunk[:module_name]}/#{fname}_#{file_digest}.cr\"\n"
    end.join("\n") + shards.keys.map do |shard_name|
                       "require \"#{shard_name}\"\n"
                     end.join("\n")
end

#rewrite_shards_file!Object



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/crystalruby/library.rb', line 178

def rewrite_shards_file!
  dependencies = shard_dependencies

  dirty = @shards.any? do |k, v|
    dependencies[k] != v
  end || (@shards.empty? && dependencies.any?)

  return unless dirty

  if @shards.empty?
    shard_file_contents.delete("dependencies")
  else
    shard_file_contents["dependencies"] = @shards
  end

  self.shard_file_contents = shard_file_contents
end

#shard_dependenciesObject



169
170
171
# File 'lib/crystalruby/library.rb', line 169

def shard_dependencies
  shard_file_contents["dependencies"] ||= {}
end

#shard_fileObject



109
110
111
# File 'lib/crystalruby/library.rb', line 109

def shard_file
  src_dir / "shard.yml"
end

#shard_file_contentsObject



159
160
161
162
163
# File 'lib/crystalruby/library.rb', line 159

def shard_file_contents
  @shard_file_contents ||= YAML.safe_load(IO.read(shard_file))
rescue StandardError
  @shard_file_contents ||= { "name" => "src", "version" => "0.1.0", "dependencies" => {} }
end

#shard_file_contents=(contents) ⇒ Object



165
166
167
# File 'lib/crystalruby/library.rb', line 165

def shard_file_contents=(contents)
  IO.write(shard_file, JSON.load(contents.to_json).to_yaml)
end

#shards_installed?Boolean

Returns:

  • (Boolean)


130
131
132
133
134
135
136
# File 'lib/crystalruby/library.rb', line 130

def shards_installed?
  shard_file_content = nil
  shards.all? do |k, v|
    dependencies ||= shard_file_contents["dependencies"]
    dependencies[k] == v
  end && CrystalRuby::Compilation.shard_check?(src_dir)
end

#write_chunk(module_name, chunk_name, body) ⇒ Object



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/crystalruby/library.rb', line 273

def write_chunk(module_name, chunk_name, body)
  chunks.delete_if { |chnk| chnk[:module_name] == module_name && chnk[:chunk_name] == chunk_name }
  chunk = { module_name: module_name, chunk_name: chunk_name, body: body }
  chunks << chunk
  existing = Dir.glob(codegen_dir / "**/*.cr")

  current_index_contents = index_contents
  module_name, chunk_name, body = chunk.values_at(:module_name, :chunk_name, :body)

  file_digest = Digest::MD5.hexdigest body
  filename = (codegen_dir / module_name / "#{chunk_name}_#{file_digest}.cr").to_s

  unless current_index_contents.include?("#{module_name}/#{chunk_name}_#{file_digest}.cr")
    methods.each_value(&:unattach!)
    @compiled = false
  end

  unless existing.delete(filename)
    FileUtils.mkdir_p(codegen_dir / module_name)
    File.write(filename, body)
  end
  existing.select do |f|
    f =~ /#{config.crystal_codegen_dir / module_name / "#{chunk_name}_[a-f0-9]{32}\.cr"}/
  end.each do |fl|
    File.delete(fl) unless fl.eql?(filename)
  end
end