Ruby FFI provides a nice feature for conveniently defining and using enums. Enums are a way of assigning integer values to symbols.
You should strongly consider using enums instead of defining integer constants in your modules. See the "Example: days of the week" section below to see the difference and read about the advantages of enums.
Enum syntax
Within library modules (modules with extend FFI::Library
), you can use the enum
command to conveniently define enums. There are three basic forms for the command:
- Unnamed enum group:
enum syms
- Example:
enum [:a, :b, :c]
- Example:
- Alternate syntax:
enum *syms
(does the same thing as above)- Example:
enum :a, :b, :c
- Example:
- Named enum group:
enum name, syms
- Example:
enum :letters, [:a, :b, :c]
- Example:
(For more complex forms, see the Other ways to define enums section below.)
By default, the first symbol in the enum group maps to value 0, and each symbol after that goes up by one. So in the example above, :a
means 0, :b
means 1, and :c
means 2. But you can also explicitly assign values for any (or all) of the symbols by giving its number value as the next item in the list:
enum <code>:letters</code>, [:a, 1, <code>:b</code>, <code>:c</code>, <code>:y</code>, 25, :z]
In this example, :a
means 1 and :y
means 25. The other symbols don't have explicit values, so each symbol's value is implicitly one higher than the previous value in the list. So, :b
means 2 (because it comes after :a
, which is explicitly 1), :c
means 3, and :z
means 26 (because it comes after :y
, which is explicitly 25).
Named groups versus unnamed groups
(To be written. Explain situations when you would use a named group or an unnamed group. What are the pros and cons of each?)
Example: days of the week
Imagine a C library called "libweek", with a header file like this:
// The Day enum:
enum Day {
SUNDAY = 1,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
};
// A function that takes an argument of the Day enum type:
int is_work_day( enum Day day_of_week );
Here is how you might translate it into Ruby FFI, if you didn't know about enums:
# Example using integer constants
module Week
extend FFI::Library
ffi_lib "week"
SUNDAY = 1
MONDAY = 2
TUESDAY = 3
WEDNESDAY = 4
THURSDAY = 5
FRIDAY = 6
SATURDAY = 7
attach_function :is_work_day, [ :uint8 ], :int
end
# How you would call the function:
Week.is_work_day( Week::MONDAY )
But there is a better way to do it, using the power of enums:
# Example using enums
module Week
extend FFI::Library
ffi_lib "week"
enum :day, [:sunday, 1,
:monday,
:tuesday,
:wednesday,
:thursday,
:friday,
:saturday ]
attach_function :is_work_day, [ :day ], :int
end
# How you would call the function:
Week.is_work_day( :monday )
# This is also allowed, in case you need to use integers:
Week.is_work_day( 2 )
You can see that this way feels more elegant and has a better style. Here are some of the advantages to doing it this way:
- Consistent with the Ruby idiom of using symbols instead of integer constants.
- Doesn't pollute the module namespace with unnecessary constants.
- It's easier and cleaner to use
:monday
thanWeek::MONDAY
when calling the function. - The function definition is more descriptive:
:day
is more meaningful than:uint8
.
Other ways to define enums
In addition to the "enum" command, there are some other ways to define enums:
- As a typedef:
typedef enum(:a, :b, :c), :letters
- This does the same thing as
enum :letters, [:a, :b, :c]
- This does the same thing as
- Assign to a constant (or variable):
LettersEnum = enum(:a, :b, :c)
Assigning to a constant is useful if you want to use the enum as a field type in a struct, or want to have easy access to the Enum object later:
# Assigning an enum to a constant so you can
# use it as a struct field type
module Week
extend FFI::Library
ffi_lib "week"
Day = enum( :sunday, 1,
:monday,
:tuesday,
:wednesday,
:thursday,
:friday,
:saturday )
class WeeklyReminder < FFI::Struct
layout :hour, :uint8,
:minute, :uint8,
:weekday, Day # <------------
end
attach_function :is_work_day, [ Day ], :int
end
Defining Ruby functions that use enums
If you want to use enums as arguments to pure Ruby functions and want to allow both symbol and integer values to be passed, you will need to add some more code.
require 'ffi'
module Week
extend FFI::Library
ffi_lib "week/Debug/week"
Day = enum(
:sunday, 1,
:monday,
:tuesday,
:wednesday,
:thursday,
:friday,
:saturday)
attach_function :is_work_day, [ Day ], :int
def self.is_monday(day)
# Compare to both enum value and enum symbol
return true if (day == Day[:monday] or day == :monday)
end
def self.is_tuesday(day)
# Convert day to integer before use
day = Day[day] unless Day.symbols.include? day
# Now, use day as integer
return true if day == Day[:tuesday]
end
end
# How you would call the function:
p Week.is_work_day(:monday)
# This is also allowed, in case you need to use integers:
p Week.is_work_day(2)
p Week.is_monday(:monday) # This works
p Week.is_monday(2) # This also works
p Week.is_tuesday(:tuesday) # This works
p Week.is_tuesday(3) # This also works
Enums as constants
Sometimes your enums may be assigned values and could represent individual bits to be set by being OR'd (|
) together. This presents a problem, because FFI normally attempts to resolve enums as Symbols in Ruby-land, which don't like to behave as Integers.
This helper method allows you to reference enum values as virtual constants.
module ExampleLibrary
# . . .
# our example enums, which are bitwise values
enum :VariousBits, [
:ONE_BIT, 0x01,
:TWO_BIT, 0x02,
:FOUR_BIT, 0x04,
:EIGHT_BIT, 0x08,
:SIXTEEN_BIT, 0x10
]
# Allows enums to be used as virtual constants. This gets invoked whenever
# the "fake" constant is encountered. It's a little slower, however, since
# we rely on Ruby catching it.
def ExampleLibrary.const_missing( sym )
# look up the value of the symbol via FFI's method to do so
value = enum_value( sym )
# if no such enum exists, raise an exception using the default
# behavior of this method
return super unless value
# return the value of the enum
value
end
end
You can then make use of this new feature like so:
class ExampleHelper
# . . .
# A constant representing all bits set
ALL_BITS = ExampleLibrary::ONE_BIT |
ExampleLibrary::TWO_BIT |
ExampleLibrary::FOUR_BIT |
ExampleLibrary::EIGHT_BIT |
ExampleLibrary::SIXTEEN_BIT
end