-
Notifications
You must be signed in to change notification settings - Fork 10
Code guidelines and workarounds
Anyolite is designed to take work out of binding Crystal functions to Ruby. However, the way Ruby and Crystal work are more different than their language similarities might show. Therefore, it is necessary to provide certain information to Anyolite in order for it to generate correct bindings which are compliant with the rest of the code.
Anyolite tries to get as many information as possible and provides annotations and options, where the automatic information collection fails. Nonetheless, certain coding guidelines can help in minimizing the amount of annotations needed.
Anyolite does not support every Crystal time as a return value or argument value for bindings. Therefore, it is necessary to avoid using the following types:
-
Symbol
(will be casted toString
if not possible otherwise) Slice
Proc
-
Class
andModule
Other types are possible to use, but do not have their full functionality:
-
Pointer
(passed as weak references without type checks) -
Array
(passed as copy) -
Hash
(passed as copy) -
String
(passed as copy) -
Char
(will be casted toString
and back)
Some specific types like Time
are also not directly supported (yet), but could technically be wrapped.
Safe types with full functionality are:
-
Int
and subtypes -
Float
and subtypes Bool
Nil
- User-defined instances of wrapped structs and enums
- User-defined instances of wrapped classes
- Unions of supported types
Anyolite can cast the return type of a Crystal function to Ruby at runtime, so it does not need to be specified. However, argument types given to Crystal need full specification.
For example, the function
def print(something)
# ...
end
could not be wrapped to Anyolite in this state. Providing all possible arguments helps:
def print(something : String | Int | Float)
# ...
end
Default values are possible, but still need full type specification:
def print(something : String | Int | Float = "Default string")
# ...
end
Another limitation of Anyolite is that default arguments (and also argument types in more nested types) need their full module/class path written out, even if the definition is in the same context:
class Dummy
SOME_CONSTANT = "Hello World"
def greeter(text : String = SOME_CONSTANT)
# Anyolite will trigger an error here!
end
def greeter(text : String = Dummy::SOME_CONSTANT)
# This is okay.
end
end
This limitation is due to the fact that Anyolite uses macros for most of its methods, so the definition context is not known at the point where the macro function is called. For the argument types, the context and therefore full path can be determined in many cases, but for default arguments, this is not possible anymore (imagine a default argument like some_func(other_func(2 * weird_constant + 3))
).
It is possible that future versions of Anyolite will do a better job of determining the full path of types and default arguments, but for now, the safest way is to specify the full path, if in doubt.
Generic types are possible to bind with Anyolite, but not out of the box. You need to specify each single specified generic type at compiletime. For example:
module MyModule
struct Vector(T)
@x : T
@y : T
def initialize(@x : T, @y : T)
end
end
end
Then, there are two thing to do: Anyolite needs to know that Vector
is a generic struct with the generic type T
. Secondly, Anyolite needs to be informed of each used generic type. If we want to have vectors of Float32
, Int32
and String
, the following code will give Anyolite all needed information:
module MyModule
@[Anyolite::SpecifyGenericTypes([T])]
struct Vector(T)
@x : T
@y : T
def initialize(@x : T, @y : T)
end
end
alias VectorFloat32 = Vector(Float32)
alias VectorInt32 = Vector(Int32)
alias VectorString = Vector(String)
end
The obvious tradeoff here is that an alias needs to be created for each specified generic type, but these can be added to existing modules without problems. Each specified generic type will then represent a single class in Ruby with all methods.
Sometimes it is not possible to follow the guidelines (for example if you want to bind somebody else's library with Anyolite), so some additional work needs to be done in order to provide all necessary information to Anyolite. Some specific cases are covered in the following sections.
If you wrap a function, Anyolite will use the function definition if there is no other information provided. If the function definition is not sufficent enough, you need to provide the information manually. This is possible using annotations in two ways. Take the following function definition as an example:
class World
def generate(width, height, name)
# Generation routines...
end
end
To wrap this method, you can either annotate the generate
method itself or the World
class:
# Instance method annotation
class World
@[Anyolite::Specialize([width, height, name], [width : UInt32, height : UInt32, name : String])
def generate(width, height, name)
# Generation routines...
end
end
# Class annotation
@[Anyolite::SpecializeInstanceMethod("generate", [width, height, name], [width : UInt32, height : UInt32, name : String])
class World
def generate(width, height, name)
# Generation routines...
end
end
If you are unable to annotate the function directly, the class annotation is a good alternative (at the cost of slightly longer code), which does exactly the same as the method annotation. A analogous annotation is also available for class methods (see the Anyolite documentation for a full list of annotations).
It is also possible to change argument types (like from a union to a single type) or specify default argument using these annotations. The latter scenario is relevant if you want to add full paths to argument types and default arguments, for example.
Currently, Anyolite does generate keyword methods in most cases (except for function names ending with a symbol, to make operators less complicated). Since these keywords need to be specified explicitly in the Ruby code, this might be unwanted behavior.
Anyolite provides simple annotations to prevent keyword usage:
# Instance method annotation
class World
@[Anyolite::WrapWithoutKeywords]
def generate(width : UInt32, height : UInt32, name : String)
# Generation routines...
end
end
# Class annotation
@[Anyolite::WrapWithoutKeywordsInstanceMethod("generate")]
class World
def generate(width : UInt32, height : UInt32, name : String)
# Generation routines...
end
end
It is also possible to give an additional integer argument to the annotations, in which case it specifies the number of arguments which are not wrapped as keyword arguments. For example, take the function
@[Anyolite::WrapWithoutKeywords(2)]
def generate(width : UInt32, height : UInt32, name : String = "default")
end
as an example. The annotation will prevent the width
and height
arguments from being keyword arguments, but still wrap the name
argument as a keyword.
If you want to remove keyword methods for a whole class or module, annotate it with Anyolite::NoKeywordArgs
.
Finally, it is also possible to add the keyword behavior as default (for operator methods or if you used the Anyolite::NoKeywordArgs
annotation on the class or module), using the Anyolite::ForceKeywordArg
annotations.
Sometimes, it is necessary to rename a function or remove it altogether from the wrapping routines. The following (totall realistic) example shows how to do this:
@[Anyolite::ExcludeInstanceMethod("crash_the_game")]
@[Anyolite::RenameInstanceMethod("stupid_name", "nice_name")]
class Enemy
def crash_the_game
end
@[Anyolite::Exclude]
def delete_everything
end
def stupid_name
end
@[Anyolite::Rename("polite_name")]
def vulgar_name
end
end
It can also be done with constants (using Anyolite::RenameConstant
and Anyolite::ExcludeConstant
on the class) and operator functions in the same way.
If you want to rename a class or module, the annotations Anyolite::RenameClass
and Anyolite::RenameModule
can be used similarly to Anyolite::RenameConstant
, while Anyolite::ExcludeConstant
works on classes and modules per default.
Another typical Crystal feature is the ability to overload functions with different argument types. This is not directly possible in Ruby, but there are some things you can do to mimic this behavior.
If you wrap multiple overloads of a function, only one of them (depending on their appearance in the code) will be selected (and a warning will show), so you need to specify one single function.
For example, you can do the following:
class Spellbook
@[Anyolite::Specialize]
def get_spell(page : UInt32, row : UInt32)
# ...
end
def get_spell(name : String)
# ...
end
end
This will exclude the second method (and all others with that name) from wrapping. You can once again also use Anyolite::SpecializeInstanceMethod
for the specialization, in which case you need to provide the function name and a list of its arguments as arguments. If the argument list of a function should be empty, just use nil
instead of a list of arguments.
In the case of sufficiently similar functions, you can also cheat a bit:
@[Anyolite::SpecializeInstanceMethod("display", [content : Int32], [content : Int32 | String])
class Textboard
def display(content : Int32)
end
def display(content : String)
end
end
This is completely valid, since both union types are actually valid argument types, and Anyolite is only referring to the method name instead of the actual method. As long as all possibilities of the final annotation argument are covered by functions, overloading can be simulated in that way (otherwise you will most likely encounter an error).
Some functions might return a type not allowed in Anyolite. If you are not able to modify the function accordingly, Anyolite provides the Anyolite::ReturnNil
, Anyolite::ReturnNilInstanceMethod
and Anyolite::ReturnNilClassMethod
annotations (similar to the annotations in the sections above), which will change the return type of the respective function to a simple nil
.
Beginner topics:
Advanced topics:
Modifications of Anyolite:
Other useful links: