Skip to content

Setting up a project

Philipp Schulz edited this page Nov 2, 2023 · 9 revisions

Essentially, there are three common scenarion, in which Anyolite might be used:

Depending on which scenario, the integration of Anyolite might become trivial or a bit more complicated (in descending order). Make sure to read these in order, since understanding the trivial scenario is helpful for understanding the more complex ones.

Using Anyolite in a completely new project

Here, the integration of Anyolite is quite trivial, since the code can be designed with Anyolite in mind. There are a few guidelines to respect this task (see Code guidelines and workarounds), but these are quite forgiving and easy to fulfill.

At the beginning of the development stage, all you need to do is to include Anyolite in the shard.yml file. Using shards install will then install Anyolite automatically.

Requiring Anyolite is then trivial:

require "anyolite"

To actually use Anyolite, you need to create an interpreter context. There is a special class for this, but it is recommended to use a block:

Anyolite::RbInterpreter.create do |rb|
  # Insert code here
end

If you don't want to use a block, you can alternatively use:

rb = Anyolite::RbInterpreter.new

# You can omit this unless you need regular expressions in mruby
Anyolite::HelperClasses.load_all(rb)

# Insert code here

rb.close

Then, the interpreter context will be stored in the rb variable, which you can pass around.

At this point, the code will not do anything yet. The interpreter needs something to do. Usually, you want to wrap some modules and classes. For example, you have a module or class named GameData, which you want to expose to the interpreter:

Anyolite::RbInterpreter.create do |rb|
  Anyolite.wrap(rb, GameData)
end

If you respect the code guidelines, this should work out of the box. Otherwise, you might get warnings or errors (in severe cases). If you want a more verbose output, you can add a verbose: true keyword argument to Anyolite.wrap.

You now have full bindings to your Crystal module in Ruby, with just these few lines!

But even then, you still need to do something with it. If you have a script file called test.rb in a scripts directory relative to your main project directory, you can execute it by calling:

rb.load_script_from_file("scripts/test.rb")

from inside the interpreter block. The full Crystal code then looks like this:

require "anyolite"

# GameData definition

Anyolite::RbInterpreter.create do |rb|
  Anyolite.wrap(rb, GameData)
  rb.load_script_from_file("scripts/test.rb")
end

This should be the template for your project. It is recommended to put most of your other code inside this block to avoid generating the bindings each time you want to execute a script (unless you want to reset the state of your script completely each time).

The way you handle scripts is up to you. Usually, you want to either execute a single script defining your program flow or multiple scripts at runtime, which execute simple tasks (even single codelines are completely possible). Anyolite does not restrict you there. Just use whatever approach seems best for your project.

Using Anyolite in your existing project

This section assumes that you have an existing project you want to integrate Anyolite into and you have no problem with modifying your code. If you want to avoid any modification to your old code, you might want to read the following section as well.

First, you should think about where in the code you want to open the interpreter block, where you want to call scripts and what modules and classes you want to expose to these.

Calling Anyolite.wrap with all modules will probably not work immediately, so make sure to modify all your code according to the Anyolite code guidelines. If the number of warnings and errors is too overwhelming, try to avoid wrapping all modules all at once and try wrapping them one at a time. Writing the compiler output to a log file can also help.

There are various annotations you can use to modify the way Anyolite wraps functions and classes, so usually there is rarely a reason to change a function completely. If your code already is used in many other projects, changing its core behavior might break these, so it is generally a better idea to add helper functions for scripting. For example, take this class and method:

class Storage
  @container : Hash(Symbol, String)

  # Constructors and other functions...

  def get_by_symbol(sym : Symbol)
    if @container[sym]?
      @container[sym]
    else
      raise "Invalid symbol: #{sym}"
    end
  end
end

Due to the symbol argument, this can not be wrapped directly to Ruby. A possible workaround would be:

@[Anyolite::ExcludeInstanceMethod("get_by_symbol")
class Storage

  # ...

  def helper_for_get_by_symbol(str : String)
    found_key = nil
    @container.each_key {|key| found_key = key if key.to_s == str}
    if found_key
      get_by_symbol(found_key)
    else
      Anyolite.raise_key_error("Invalid key: #{str}")
    end
  end
end

Note that this does only work with symbols actually contained inside @container. Adding new symbols could be implemented with a symbol table (if all symbols are known at compiletime), but for optimal interoperability, using strings is much more flexible (see code guidelines).

However, if it is no problem to change your core codebase, reworking it to support Anyolite comfortably is usually the easiest solution (and spares you from using a large set of annotations).

Using Anyolite with somebody else's project

If you want to bind a Crystal module into a Ruby interpreter without changing it, Anyolite provides a set of annotations and options to help you. Take the following (completely useless) class as an example:

class TextScrambler
  @scramble_text : String

  def initialize(scramble_text)
    @scramble_text = scramble_text
  end

  def scramble(this_text, number = 1)
    result = ""
    this_text.chars.each {|c| result = result + c + @scramble_text * number}
    result
  end
end

Most prominently, the function arguments have no type restriction. For binding these to Ruby, Anyolite needs to know the types at compiletime, though. Since you are not able to change this class, and want to avoid rewriting it, you can reopen it and use annotations (see code guidelines for more explanations and options):

@[Anyolite::SpecializeInstanceMethod("initialize", [scramble_text], [scramble_text : String])
@[Anyolite::SpecializeInstanceMethod("scramble", [this_text, number = 4], [this_text : String, number : UInt32 = 1])
class TextScrambler
end

This way, Anyolite knows how to cast the function arguments properly. Do not worry if you specify the argument types wrong, since the Crystal compiler will let you know if Anyolite would perform an illegal cast.

Performing these specializations for a large codebase might seem tedious, but Anyolite still saves much of your time by performing most of the internal conversions between Ruby and Crystal. Usually, a large portion of the functions (and other things like structs or enums) can still be wrapped without modification, so make sure to inspect the code before, so you can estimate how much time you might need.