123456789_123456789_123456789_123456789_123456789_

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:

(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:

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:

Other ways to define enums

In addition to the "enum" command, there are some other ways to define enums:

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