previous up next
4.1 The choice of Java and an embedded Scheme interpreter

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
 
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.

previous up next