SFIDL Documentation

Stefan Westerfeld

Document revised: Tue Feb 21 00:28:59 2006

This document gives an overview over sfidl, which is used in BEAST for two purposes:

  1. Describe the API in a language independant fashion, to make it possible to access the BSE functionality from C, C++, Scheme and possibly other languages.
  2. Provide an easy way to fully define the API of plugin modules, specifying all necessary information to generate a useful GUI automatically.

Table of Contents

1 What is SFI?

1.1 Overview

To provide an API to more than one programming language, we first specify the API in a language independant fashion. Therefore, we use an .idl file, which contains a description what a class or procedure looks like.

Then, it is possible to provide an implementation in C or C++.

Finally, for different languages, code to access that implementation can be generated automatically.

1.2 Design goals

We tried to accomplish a number of different design goals during the SFI design.

  1. SFI should act as strict isolation between the (realtime) BSE core, containing the implementation, and the user. The core should run either in a seperate thread, or in a seperate process.
  2. Adding functionality to the implementation (such as new methods, new classes, new procedures, new procedure parameters, new record fields) should generally not affect a client's ability to talk to the core. That is, updating the core to a newer version should not force recompilation of the client code.
  3. Access to the BSE code should be possible from various programming languages.
  4. Writing plugins should be as easy as possible.

1.3 The sfidl command line utility

sfidl is the interface definition language compiler for SFI, a manual page is available as sfidl(1), and the IDL file format is detailed in the next sections.

2 The .idl files

2.1 The basics: comments and namespaces.

The syntax of .idl files is similar to C++ and/or CORBA IDL. In the following sections the elements will be described. First of all, you need to know that everything needs to be declared within the scope of a namespace. This avoids collisions.

The syntax for namespace is

// IDL Code
namespace Foo {
/* declarations go here */
sequence {			// example sequence of integer values
  Sfi::Int ints;
} IntSeq;
/* more declarations go here */
}

Namespaces can also be nested. There is an additional using keyword, which can be used to avoid using the namespace qualifier "::" to refer to something from a different namespace:

// IDL Code
namespace Bar {
Foo::IntSeq get_even_numbers (Sfi::Int bound);	// needed to refer to namespace Foo::
using namespace Foo;				// adds Foo:: to namespace search list
IntSeq get_prime_numbers (Sfi::Int bound);	// IntSeq is found in Foo:: automatically
}

As you see, primitive types (like Int) are declared within the Sfi namespace. C and C++ style comments work as usually.

2.2 Data types: primitive and composite.

2.2.1 Simple primitive types

SFI provides a number of predefined primitive data types. These are

  • void
    no value (this is only used for return values of procedures/methods which have no return value)
  • Sfi::Bool
    a boolean value, which can be either true or false
  • Sfi::Int
    a 32bit signed integer
  • Sfi::Num
    a 64bit signed integer
  • Sfi::Real
    a 64bit double floating point value
  • Sfi::String
    a character string
  • Sfi::BBlock
    a block of bytes (optimized for bulk data transfer)
  • Sfi::FBlock
    a block of 32bit floating point values (optimized for bulk data transfer)
  • Sfi::Rec
    a special type, which can hold any record (for records see the Composite Types below)

2.2.2 Choices

In addition to these simple primitive types, it is possible to define a choice as follows:

// IDL Code
namespace Foo {
choice WaveForm {
  WAVE_FORM_SINE,
  WAVE_FORM_SAW,
  WAVE_FORM_RECT
};
}

This means that a value of type WaveForm can hold one of these choices. Extra information can be supplied with each choice value (both optional):

  • A number that will be used by core language binding implementations (e.g. if in C or C++ the choice is implemenated as an enum type),
  • A user readable string, which can be used in GUIs to describe the value of the choice to the user (this string also is translatable).

// IDL Code
namespace Foo {
choice WaveForm {
  WAVE_FORM_NONE = (Neutral, "No waveform"),
  WAVE_FORM_SINE = (1,       "Sine wave"),
  WAVE_FORM_SAW  = (2,       "Sawtooth wave"),
  WAVE_FORM_RECT = (3,       "Rectangle wave")
};
}

There is no guarantee what number the interfacing code using the choice in C and C++ will see for a particular WaveForm (i.e. WAVE_FORM_SINE could be 3 in client code). This makes it possible to add choice values or change their number even after the client got compiled. (Internally the communication is done by passing the choice value as string).

There is one exception: the Neutral keyword indicates that the number should be 0 for both, client code and implementation. In C/C++, this allows writing

// C++ Code
  Foo::WaveForm wave = osc->get_wave_form();
  if (!wave)
    printf ("No wave form selected.\n");

2.2.3 Composite types

More complex data types can be constructed from these simple data types. There are two composite types: records and sequences.

Sequences are used for a sequence of multiple values of the same data type. The syntax is:

// IDL Code
namespace Foo {
sequence IntSeq {
  Sfi::Int ints;  // the contained type
};
}

A sequence may then hold 0, 1 or N values of the same type. The name "ints" here is only used for the C language, where ints contains the actual data and n_ints contains the number of items in the sequence.

// C Code
  FooIntSeq *seq = foo_int_seq_new();
  foo_int_seq_resize (seq, 2);
  seq->ints[0] = seq->ints[1] = 42;
  g_assert (seq->n_ints == 2)

Records can be used for a sequence of values of different data types (they behave similar to structures in C/C++).

// IDL Code
namespace Foo {
using namespace Sfi;

record PartNote {
  // a human readable string describing what the record does (optional)
  Info     blurb = "Part specific note representation";

  // the record fields
  Int      id;
  Int      tick;
  Int      duration;
  Int      note;
  Int      fine_tune;
  Real     velocity;
  Bool     selected;
};
}

In opposition to sequences, NULL (or nil or whatever is adequate for the language you are using) is a possible value for records, that is, a record value either contains all of the above fields, or it is a NULL pointer.

For sequences, NULL is not a valid value (you can always use empty sequences there).

Parameter specifications: Note that you can, and probably should add parameter specifications for the fields in a sequence and a record. However, we describe parameter specifications seperately below.

2.2.4 Prototyping

In some cases you know that a certain type will be defined later, but you can't give it's definition already. Perhaps this occurs because two types, often classes, need each other in their definition. Perhaps this also occurs because you want seperate things in seperate .idl files in a special way.

You can use prototypes in this case, for all types the IDL compile understands.

namespace Foo {
choice   SomeChoice;
sequence SomeSequence;
record   SomeRecord;
class    SomeClass;

record Test {
  SomeChoice   a;
  SomeSequence b;
  SomeRecord   c;
  SomeClass    d;
};
}

2.3 Classes and procedures

2.3.1 Classes

IDL defined APIs consist mostly of classes. SFI supports single inheritance, so a set of simple classes might look like this (not taken from the BSE class hierarchy):

// IDL Code
namespace Foo {
using namespace Sfi;

class AudioObject {
  void play ();
  void stop ();
};

class Sample : AudioObject {
};

class Song : AudioObject {
  Sample load_sample (String filename);
  void   insert_sample (Sample sample, Real position, Real speed);
};
}

As far as this example goes, we have only added the bare minimum. It is however possible (and probably desirable) to add defaults and documentation to the methods, as demonstrated in this simple example:

// IDL Code
#include <bse/bse.idl>

namespace Foo {
using namespace Bse; // for Bse::STANDARD
// ... like above ...

class Song : AudioObject {
  Sample
  load_sample (Sample filename)
  {
    // _() is used as i18n markup for translatable strings
    Info blurb = _("This loads a sample for further use into the song");

    In  filename = (_("Sample Filename"), _("The name of the sample file"), "", STANDARD);
    Out sample   = (_("Sample"), _("The newly loaded sample"), STANDARD);
  };
  // ... like above ...
};
}

Return and argument values referring to instances of classes (in this example samples) can be NULL. So load_sample in this example would probably return a valid sample, if the sample file could be loaded successfully, and NULL otherwise.

Various aspects of valid filenames are specified through the assignment of a parameter specification to filename. Parameter specifications are described in detail in the section PARAM SPECS.

2.3.2 Signals

Signals provide the possibility for objects (instances of classes) to emit events. A user can connect to the signal, and whenever a signal gets emitted, his callback gets called. In our example it would be possible to add two signals to AudioObject, one where it emits the current position regularily while playing, and one that gets emitted when it is done playing.

// IDL Code
namespace Foo {
class AudioObject {
  void   play ();
  void   stop ();

  signal position_changed (Real new_position);
  signal done_playing ();
};
}

2.3.3 Properties

Another important element commonly found in classes are properties. Usually, they are logically grouped to improve readability in GUIs.

// IDL Code
namespace Bse { namespace Contrib {
class Balance : Bse::Effect {
  group _("Audio Input") {
    Real  alevel1;	// volume of audio input 1
    Real  alevel2;	// volume of audio input 2
  };
  group _("Control Input") {
    Real  clevel1    = Perc (_("Input 1 [%]"), _("Attenuate the level of control input 1"), 100, STANDARD);
    Real  clevel2    = Perc (_("Input 2 [%]"), _("Attenuate the level of control input 2"), 100, STANDARD);
  };
  group _("Output Panning") {
    Real  lowpass    = Frequency (_("Lowpass [Hz]"),
                                  _("Lowpass filter frequency for the control signal"),
                                  100, 100, 1000, STANDARD);
    Real  obalance   = (_("Output Balance"), 
                        _("Adjust output balance between left and right"),
                        0, -100, 100, 10, STANDARD);
  };
};
} } // Bse::Contrib

Although the above example isn't comprehensive in this regard, properties can be of any IDL type. The aforementioned groups come with translatable names, and properties may support a good chunk of definitions besides just their type and name.

In the example, alevel1 and alevel2 use the simplest form possible to define a property, just the types and names are given. Displaying a property like this in GUIs will provide unattractive results, to say the least.

For this reason, sfidl allows type specific constructors, and by convention these constructors expect a translatable label and a translatable description as first two arguments and an option string as last argument. The constructors are provided by the sfidl language bindings, so their availability differs dependant on that. In the above example, the percentage and frequency constructors, both of type Real and provided by BSE, are used and expect these arguments:
  1. a translatable label,
  2. a translatable description,
  3. a default value (for Perc() within 0 .. 100, for Freuquency() within minimum and maximum),
  4. a minimum value (Freuquency() only),
  5. a maximum value (Freuquency() only),
  6. an option argument.
Finally, obalance is an example for leaving out the constructor name, in this case the constructor name is assumed to be the type name, so the following two lines are fully equivalent:
    Real obalance = (_("Output Balance"), _("Blurb"), 0, -100, 100, 10, STANDARD);
    Real obalance = Real (_("Output Balance"), _("Blurb"), 0, -100, 100, 10, STANDARD);

More on property constructors provided by BSE can be found as part of the plugin development guide in the property section and the property option section.

2.3.4 Streams

For classes that do audio processing (module objects), it is necessary to specify streams, which will transport the audio data into the module, and the resulting audio data out of it. Generally there are three types of streams:

  • IStreams contain input audio signal for the module
  • OStreams contain output audio signal from the module
  • JStreams contain 0, 1 or more input audio signals to the module

namespace Arts {
class Compressor : Bse::Effect {
  // ...
  IStream invalue   = (_("Audio In"), _("Audio input"));
  OStream outvalue  = (_("Audio Out"), _("Compressed audio output"));
};
}

As you see, the syntax is quite straight forward, containing a variable name and the specification of the translatable user visible strings, which contain the name of the stream, and a blurb describing what it does.

2.3.5 Procedures

Procedures can be thought of as methods-without-a-class. A classic example is:

namespace Bse {
Real note_to_freq (Int note, Int fine_tune);
}

which converts a midi note to a frequency, using a given fine tune. The same syntactic elements for specifying more details (parameter speccifications, Info strings) that are valid for methods can be used here as well.

3 The language bindings