We have chosen the Java language for the development of our
environment. In addition, we embedded a Scheme interpreter into the
environment. In this section we will argument this choice. First, let
us underline the advantageous of an embedded interpreter (see also
[BS95] for a discussion on the subject.)
The advantages of an embedded interpreter can be grouped in four
sections:
- High-level language: Interpreters generally provide an
accessible syntax that reduces the learning curve and
facilitates the reading and coding of programs. The language
is more concise and allows to express simple programs rapidly.
- Portability: The interpreter hides machine specific
features. Interpreted languages are therefore machine
independent.
- Development environment: Interpreters allow to extend
the behavior of the application in ways that were not
programmed by the developer. In addition, they promote a short
programming cycle, in general a read-eval-print loop, in which
the compilation phase is invisible. They provide a better
error handling than in traditional languages, and handle
memory management implicitly.
- Distribution: Due to the textual format, concise syntax,
and dynamic compilation, source code and data structures can
easily be sent over the network.
A distinction has to be made between an extendible and an embeddable interpreter. We will say an interpreter is extendible if it provides the means to extend its primitives. To
this extend it provides a foreign interface. This is a
programming interface that allows the following:
- call objects implemented in a language other than the
language used by the interpreter,
- insert new primitives to the interpreter: define new
functional objects in the interpreted language to access
functions provided by an external library.
In our project the use of an interpreter that is extendible is
important to integrate new synthesis modules into the environment.
An interpreter is embeddable if it is possible to evaluate an
expression of the interpreter language from another language.
Embeddable interpreters are generally extendible. When we use
``embeddable interpreter'' we assume the interpreter is extendible as
well.
The use of an embeddable interpreter will prove to be important:
external libraries can invoke the evaluation of complex interpreted
functions. For example, the response of the system to low-level events
can be programmed by the user in a high-level language and is by no
means fixed by the environment.
However, there are a couple of drawbacks to the use of interpreters:
- Performance
- Complexity
- Different memory management
- Incompatible object systems.
The performances offered by interpreted languages are still below
those offered by traditional languages.
In certain cases it can be a complex development task to embed an
interpreter into an environment. In addition, the interpreter forces
the user to pick up programming, something s/he may not be acquainted
with. This can be help by supplementing the environment with graphical
editors (as in the case of PatchWork and OpenMusic). However, such an
interface constitutes a research project of its own [Ago98].
The expert user who wants to get the best of the system will have to
invest the time to master the language.
The interpreter uses a garbage collected memory heap; external
libraries written in traditional languages use explicit memory. To
define new primitives in a foreign language, a new type is defined,
accompanied with a set of functions to print and construct these
objects. Additional functions have to be written for the coordination
between the garbage collection and the native memory
management. Imagine an external object refers to an object of the
interpreter and that this reference is the only link to the
object. Since there is no link from the interpreter's data structures
to the object, the storage space of this object will be reclaimed
incorrectly. The external libraries must either help the garbage
collector during the tracing or explicitly tell the interpreter when
objects are used and released. This places a burden on the developer.
In addition, the interpreter typically uses an object system that is
distinct from the object system used by the external libraries. This
fact, combined with the separate memory strategy, makes it impossible
the share objects between the two subsystems seamlessly.
Let us illustrate these problems with a concrete example. We
experimented for a while with the Extension Language Kit (Elk,
[LC94]), which is also used for the Foo and Modalys
environments. The first problem was that Elk is not multi-threaded.
This problem can be overcome, but is not trivial. Second, if we want
Elk objects to be referred to by our code we should use a single memory
strategy. We have therefore put Elk on top of Boehm's collector for C
[Boe93]. This solved the memory management problem but still
left us with two other problems. First, it is difficult to make
Boehm's collector real-time. Read and write barriers are hard to
implement for C code. In general virtual memory protection is used as
read or write barrier, but this strategy cannot be made real-time.
Second, we still have two separate object systems. This means that the
root of Elk's type system has nothing to do with the root of the
type system in the external library. This prevents a transparent use
of objects, unless we recode the external library.
A really integrated, seamless solution would require the following steps:
- 1.
- Write a portable runtime layer,
- 2.
- Define the structures for the garbage collector,
- 3.
- Define a class hierarchy,
- 4.
- Write an interpreter in this framework,
- 5.
- Develop external libraries in the framework.
This project clearly requires a lot of work and is very difficult to
realize.
A solution to the problem is available since the introduction of the
Java platform by Sun Microsystems. The Java platform is a combination
of a virtual machine, an object-oriented language, and rich set of
standard libraries [GJS96,GM95]. In addition, a Scheme interpreter, called
Kawa, is available. The Scheme language is a small but powerful,
Lisp-style language [KCR98]. The Kawa implementation is
entirely written in the Java language and provides a foreign function
interface to call Java objects from within Scheme. Since all the
Scheme primitives are implemented as Java objects, they can easily
migrate to the external libraries. Kawa achieves good performances by
compiling Scheme expressions to Java byte code which is then
immediately executed on the Java virtual machine
[Bot98].
The inconveniences of this solution are reduced efficiency compared to
traditional languages. The performance of Java is below that of C. The
efficiency is better with the use of just-in-time compilers (JIT) and
is still improving. In its current state, Java does not offer
real-time guarantees. However, Sun announced real-time extensions
late 1998. Because of the high interest in Java for embedded systems a
lot of effort is put on real-time Java.
The advantage of this solution is the single memory management
strategy and a single object system. In addition, we benefit from
Java's portability, standard libraries, and improved error handling
over traditional languages. Libraries written in C, for example
existing signal processing libraries, can still be called thru the
Java Native Interface (JNI).
|
|
Figure 4.1:
The global view of the environment.
|
The global view of the environment is given in figure 4.1. The Java classes of our project are
grouped in a package called Varèse, after the French-American
composer. We have written an interface in Scheme to call our classes
from within the Scheme interpreter and to use Scheme primitives in the
Varèse environment. In the following sections we discuss some of
the basic concepts that underlie our system's architecture.