The ANSI C Open Community Version 0.9 Application Program Interface

Richard C. Waters and David B. Anderson

This document is available as "http://www.merl.com/opencom/opencom-c-api.html".
This version of the document was created on February 19, 1997.

Copyright 1997, MERL - A Mitsubishi Electric Research Lab. All rights reserved.


1 introduction

This document describes the Open Community Version 0.9 Application Program Interface (API) as exported to ANSI C. It documents each publicly available object class and function.

This is a machine generated document created from a database of information. It exists in both paper and HTML form. Other documents describe the API exported to other programming language environments.

The API also includes the following ordinary data structures:

Finally, the API includes the following shared object classes:

1.1 Reading This Document

This document is a reference manual rather than a tutorial. In general, each topic is only discussed once and therefore any order of reading the sections in this document will not be quite right, because every section can be best understood only after having read many other sections.

The document is organized around the various classes enumerated above. At the top level, there is a section for each class. Within these sections, there are subsections corresponding to each instance variable and method.

There are two ways to look up instance variables and functions in this document. By using the table of contents, you can look them up by the name of the class. By using the quick reference index, you can look them up alphabetically by the name of the instance variable or method.

There are several quite different kinds of classes listed above: , passive data structures, and shared objects that are the basis of communication between processes.

The passive data structures (e.g., spTransform) are pieces of data that are stored in shared objects or used in intermediate computations. In the C interface, these items are not objects, but rather just simple vectors and structures.

The shared objects consist of the class sp and its subclasses. These classes receive specialized treatment by the system. In particular, instances of the shared classes are communicated between processes. Applications can define new shared classes using a Java interface supported by a special Java preprocessor.

Most of this document consists of descriptions of the shared classes. The first line of each section for a shared class shows the declaration used in the Java interface to define the class. This is followed by a brief discussion of the class and a listing of the externally available shared instance variables and functions in the class. Subsections of the section for a class describe each of its instance variables and functions in detail.

The first line of a subsection discussing an instance variable of a shared object shows the declaration used in the Java interface to define the variable. This line includes both the type of the variable in Java (in the middle) and the type in C (in a comment at the end of the line). From the perspective of this document, only the C type is relevant. The variable declaration line is followed by a general discussion of the instance variable and the various access functions available for the variable.

The first line of a subsection discussing a function is the ANSI C signature of the function. This is followed by a summary of what the function does.

The names of C functions in the API are derived using the following naming scheme. If a class spK contains a method M, then the corresponding C function has the name spKM. Since every shared class name begins with the letters ``sp'', every C function in the API begins with the letters ``sp''.

The sections on non-shared classes have much the same format as the sections on shared classes.

At various places in this document, topics of interest are discussed at length in subsections. One reason for gathering information into a separate section is so that it can be referred to from many places in the document.

1.2 System Overview

Above all else, the system provides a convenient architecture for implementing multi-user interactive environments. This architecture is centered on a world model that mediates all interaction. Figure 1 illustrates the application programming model. It shows five applications interacting through the world model.

Applications do not communicate directly with each other, but rather only with the world model. This allows applications to be written without thinking about how communication is achieved. An application does exactly the same things when it is interacting with an application running in shared memory on the same machine as it does when interacting with an application connected via the Internet.

Figure 1: The programming model.

The world model is not a scene graph, but rather an object-oriented database that does not consider one kind of content to be any more important than another. In particular, we believe that audio information and autonomous behavior are at least as important as visual information and should not be limited by constraints inherited from visual rendering.

The world model specifies what objects exist in the virtual world, where they are, what they look like, and what sounds they are making. The world model does not contain historical information, but rather just a snapshot of what the virtual world is like at the current moment. As the virtual world changes second by second, the world model changes.

The emphasis in the design of the world model is on the term database, not object oriented. The objects have methods associated with them, but by far the dominant operations consist of reading and writing data stored in the instance variables of world model objects.

Applications observe the virtual world by retrieving data from the world model. Applications affect the virtual world by adding, removing, and modifying objects in the world model. To avoid readers/writers conflicts, each object in the world model has one process as its owner and only the owning process can modify it. However, the ownership of an object can be transferred from one process to another.

By itself, the system does not cause objects to persist over time. An object exists only so long as the process that owns it runs. To have persistent objects, an application must provide persistent processes that accept ownership of these objects. These processes could make use of a persistent file format for objects to provide efficient long term support for infrequently visited parts of a virtual world.

1.2.1 Scalability

Application programmers are encouraged to think in terms of Figure 1. However, it would not work well to use a centralized architecture when actually implementing the system. Rather, the system operates as shown in Figure 2. To provide low latency interaction with the world model, the world model is replicated so that a copy resides in each application process. Messages sent over a computer network linking the processes are used to propagate changes from one world model copy to another.

Figure 2: The communication model.

A central feature of the system is that it is designed to be scalable to a large number of users (e.g., thousands) interacting in real time. Two key features support this: providing only approximate equality of local world model copies and dividing the world model into chunks each of which is communicated only to the small group of users that are actually interested in it, rather than to all the users of the world model.

Distributed databases typically require that all local copies of the database must agree exactly on the information in the database. However, this requires object locking and handshaking that is incompatible with real-time interaction if there are more than a very small number of users. In contrast, the focus here is on real-time interaction at all costs, providing only approximate equality between world model copies.

The primary way in which world model copies are only approximately equal is that different users observe things as occurring at slightly different times. We call this a relativity model of communication and is actually not unlike the real world. When you hear sounds from distant sources, you do not hear the sound that is being made now, but rather the sound that was made seconds ago. As a result, people in different locations do not hear the same things at the same time. How great the differences are depends on how far apart the sound sources and people are.

Similarly, when an application process finds out about a world model change, it is not finding out about a change that is happening now, but rather about one that happened some time ago. How long ago depends on the network distance between the two processes. In general, this distance is not more than a couple hundred milliseconds and does not lead to world model differences that are unduly large.

Having only approximate equality of world model copies allows real-time interaction, but does not of itself prevent the computation required to maintain each local world model copy from growing in proportion to the total number of simultaneous users of a virtual world. To prevent this, the world model is broken up into many small chunks called regions and information about a given region is communicated only to the small number of users that are near enough to that region to be interested in it. Each region is associated with a separate communication channel so that processes that are not interested in a region do not have to expend any processing ignoring it. This allows the system to scale based solely on the maximum number of users that are gathered in any one region, rather than on the total number of users in the virtual world.

1.2.2 Servers

A significant feature of Figure 2 is that it does not contain a central process. To minimize latency, and prevent bottlenecks, the primary communication used by the system is peer-to-peer rather than passing through centralized processes. However, centralized server processes are used for five key services. The way these servers interact with applications is illustrated in Figure 3.

Figure 3: Servers.

In the lower right of the figure is the session manager. It handles the connection of new users to an on-going session and their eventual disconnection. Its workload is proportional only to the number of users that enter or leave the session in a given minute, not the total number of connected users.

One or more servers are included in a Spline session to act as: user servers, region servers, beacon servers, and contact points. For flexibility, there is only one kind of server process, which can act in any of the above roles, or several at once.

The purpose of a user server is to support users with slow network connections (e.g., modems) that do not allow them to operate as first class peers. When acting in this role, a server intercepts all communication to and from the user. The message traffic to the user is compressed to take maximum advantage of the bandwidth available. As part of this, audio streams are combined and localized before sending them to the user. Servers are replicated as needed so that no one server has to support more users than it can handle. Two servers acting in this capacity are shown in Figure 3.

A region server maintains a record of everything in a given region. When the locus of attention of a user process enters a new region, the appropriate region server is queried in order to obtain initial information about the state of objects in the region. After this initial download, the user process obtains further incremental information by peer-to-peer communication. Responsibility for regions is parceled out among a set of servers, which is made large enough that no one server is responsible for a larger piece of the virtual world than it can easily handle.

Because the world model copy in an application process only contains information about the objects in the regions the process is attending to, there has to be an explicit mechanism for locating far away objects in the virtual world. This is done by having the servers provide a name service that makes it possible to locate specialized objects called beacons by name no matter where they are in the virtual world. When a user process wants to locate a particular beacon object, it consults the appropriate beacon server in order to find the beacon. As with regions, responsibility for the beacon name space is parceled out among a set of servers, which is made large enough that no one server is responsible for more beacons than it can easily handle.

To simplify the communication between user processes and the various servers it has to interact with, each user process has a server assigned to it that acts as a sole contact point for the process. Every message from a user process (or user server acting on its behalf) that requests a service, is sent to the contact point. This allows the user process to always operate as if there was only one server. The various complexities that arise when servers are replicated are handled by the contact point, which decides where to route the messages it receives from user processes.

The session manager monitors the servers in a session and if it detects that one has crashed, restarts it. This is facilitate by the fact that the user processes and the session manager are the only sources of information. The various servers merely act as repositories of information obtained from elsewhere and can straightforwardly be reinitialized if they need to be restarted.

As shown in Figure 3, the system utilizes a hybrid communication model in which client/server communication is primarily point-to-point but server/server communication is peer-to-peer. In addition, users with sufficiently fast network connections (e.g., users on corporate intranets) can interact directly with each other and the servers using peer-to-peer communication.

1.2.3 Application Processes

The structure of a single application process is shown in Figure 4. The dashed box at the top of the figure shows how the application itself fits into the picture.

Figure 4: An application process.

The foundation of the system is the inter-process communication module shown at the bottom of Figure 4. It provides all the processing necessary to maintain approximate consistency between the world model copies associated with a group of communicating processes, sending messages describing changes in the world model caused by the local application and receiving messages from other processes about changes made remotely. The network interface specifies the format of these messages. Any process that obeys this interface can interoperate.

The messages sent are of three kinds, corresponding to three kinds of data in the world model: small rapidly changing objects, large slowly changing objects, and continuous streams of data. An important feature of the system is that it includes an efficient scheme for synchronizing these different kinds of data.

The most prevalent kind of object in the world model is small things that can change rapidly. For example, an object representing something in the virtual world (e.g., a chair) requires only a small description---i.e., to specify its position and orientation, whether it is contained in some other object, and which appearance should be used when displaying it. The features of small objects can be changed very rapidly.

Messages describing changes in small objects are sent using User Datagram Protocol (UDP) messages. This allows them to be communicated very rapidly. The objects must be small enough so that a message describing one will fit in one UDP message.

Graphic models, recorded sounds, and behaviors are represented using large objects. These objects are identified by Universal Resource Locators (URLs) and communicated using standard World Wide Web protocols. Standard formats are used (e.g., VRML for graphic models, WAVE for sounds, and Java for behaviors) so that standard tools can be used. There is no limitation on the size of the large objects, but several seconds can be required to communicate one. Fortunately, since these objects change infrequently, this latency can generally be masked by preloading the objects before they need to be used.

The final kind of object in the world model corresponds to continuous streams of data such as sound captured by a microphone. These streams are communicated in small chunks using UDP messages. At the moment, video streams are not supported. When they are, they will be communicated in a similar fashion, but of necessity using larger messages.

A central feature of the system is that using the various messaging approaches above, every kind of data in the world model can be communicated between processes. Therefore, applications can modify and extend every aspect of a virtual world. Furthermore, while it is often advantageous to prestore data for an application to use (i.e., by delivering it on a CD-ROM) this is not necessary. Once started, an application will fetch everything it needs that has not been prestored.

1.2.4 The API

The Application Program Interface (API) described in this document consists primarily of operations for creating/deleting objects in the world model and reading/writing instance variables in these objects. The application support module (see Figure 4) contains various facilities that make application writing easier. For the convenience of application writers, multiple APIs are provided. The principle APIs being in ANSI C and Java.

1.2.5 Rendering

Figure 5 shows the system being used to support an application that interacts with a human user. The primary feature of the figure is that three applications are used in this situation. The main application (in the dashed box) presents an interface to the user.

Figure 5: Typical configuration supporting a user.

Visual and audio rendering modules are provided; however rather than being tightly integrated with the system, they are separate applications interfaced to the system. They use the same API as other applications and do not have to be tightly coupled with the main application.

One advantage of the loose coupling of renderers is that the renderers interacting with a person can run in separate processes from the main application. This allows them to operate on separate machines in situations where maximum performance is required. However, the primary mode of operation is for them to run in the same process with the main application, sharing a single copy of the world model as shown in Figure 5.

The greatest advantage of the loose coupling between rendering and the system core is that the system is not tied to any one renderer. Rather, the API is designed to be easily interfaced to almost any renderer. Default renderers are supplied with the system, but it is expected that demanding applications will switch to renderers that are tuned to the task at hand.

Consider visual rendering as an example. In the world model, objects can have positions, orientations, and appearances. The visual renderer creates a scene graph by combining the appearances associated with the objects that are near enough to be seen and renders the scene graph from the vantage point specified by the application. The system itself does nothing with the graphic models that describe the appearances of objects. The only thing that matters is that the visual renderer being used can load them.

Audio rendering is supported in a similar fashion. The world model contains objects representing sources of sounds. These objects specify the places where the sounds are located. They can either be point sources or diffuse ambient sources. Sound to be played through these objects can be captured live from a microphone or prestored in recorded sound objects. The audio renderer creates an audio scene graph by combining the sounds associated with sound source objects that are near enough to be heard and renderers this from the vantage point specified by the application. The system itself does nothing with sound encodings. The only thing that matters is that the audio renderer being used can decode them.

1.2.6 Supporting Simulations

From the earliest days of work on the system, we paid close attention to supporting interaction with computer simulations as well as people. The way this is done is shown in Figure 6.

Figure 6: Typical configuration supporting a simulation.

A simulation operates just like any other application using the same API to interact with the world model. However, no visual or audio rendering is needed, because there is no person to see or hear it. Complex simulations, intelligent agents, and large persistent databases can all be directly connected to a virtual world. Large powerful computers without support for graphics or sound can be used to manage shared content.

It is important to note that it is a great deal easier to say that a simulation interacts with the world model just like any other application than it is to make it possible for a simulation to effectively interact with the world model. The reason for this is that applications supporting a user have a human being in the loop and simulations do not.

For example, it is typically easy for a person to look at an avatar and determine which way the avatar is facing, based on a rendered image. However, it verges on impossible for a program to tell where the face of an avatar is by looking at a list of polygons. To deal with this kind of problem, the system has been designed to make information such as which way an avatar is facing easily accessible to programs. Specifically, Spline's world model contains an object class spAvatar that is used to identify avatars as opposed to other kinds of objects and by convention the center of the coordinate system for an object is at the center of the object, the Y axis is up, and objects face done the negative Z axis

1.3 Atomic Data

Much of the data used in the API is simple atomic data of 64 bits or less in length such as boolean values, integers, and floating point numbers. This data is copied whenever it is passed to or returned from a function.

The following paragraphs briefly describe each kind of atomic data used in the API. Some of these types of data are described at greater length in other sections.

Boolean values represented using the type spBoolean. These are used to indicate True and False values.

16-bit integers represented using the type short. These are used when low range integers are adequate.

32-bit integers represented using the type long. These are used for representing most integer values.

32-bit floating point numbers represented using the type float. In keeping with standard graphics practice, these are used to represent most non-integer values. They have limited precision, but are compact.

32-bit unique names represented using the type spName. The system dynamically generates names that are unique across all the processes participating in a session. These names are used both to identify shared objects and to identify the processes that own the objects.

IP addresses/port pairs represented using the type spAddress. These are used to represent network addresses. They contain two pieces of information, a 32-bit IP host address and a 16-bit port number. In C an spAddress is a struct containing the host address, followed by the port number. followed by 16 unused bits to pad the total field out to two full words.

32-bit time durations in milliseconds represented using the type spDuration. Whenever an API function takes a time duration as an argument, this argument is in terms of milliseconds. For compactness, 32-bit integers are used. This allows a range of approximately plus or minus two weeks. (This does not impose any limitation on the lifetime of a session in the virtual world, but rather only limits the maximum difference between two times that can be represented.)

World model view masks represented using the type spMask. These bit masks are used to control the visibility of shared objects in the world model.

Audio formats represented using the type spFormat. These values are used to represent audio encodings and sample rates.

1.4 Pointer Data

In addition to atomic data, the API makes use of several different kinds of pointer data. As discussed at length in the next section, the most important kind of pointer data is pointers to shared objects. However, several other kinds of pointer data are used as well.

A key feature of the API is that pointer data is always passed into and returned from functions by reference rather than copying. This promotes efficiency, but means you must pay close attention to the following.

1- The system never alters data that is passed to it via a pointer unless this document explicitly states otherwise. This means you can depend on the value remaining the same unless you change it yourself.

2- The system never retains a pointer passed to it beyond the time the function that was given the pointer returns. (If the system needs to keep any of the data, it copies it.)

3- When a pointer is passed to you, you must never alter the data pointed to unless it is explicitly stated that you should, because the system depends on the data not being altered. In particular, you must never free something referred to by a pointer unless you yourself allocated the storage.

4- It is allowed that you retain pointers returned by an API function, but only until the next time spWMUpdate is called. The reason for this is that the system assumes that during calls on spWMUpdate, it can free any storage it has allocated. The only exception to this is certain shared objects. If you want to save data across a call on spWMUpdate, you must copy it.

The following paragraphs briefly describe each kind of pointer data used by the system. Many of these types of data are described at greater length in other sections.

A UTF8 ASCII string represented using the type char *. This is a null-terminated string. If the characters are all 7-bit ASCII characters, then this is an entirely ordinary string. If extended characters are used, then special escape sequences are present.

An immutable spFixedAscii string containing no more than 500 characters represented using the type spFixedAscii. This is the same as the above, but is limited in length so that it can fit into a single UDP message. It is used for most shared character data in shared objects. This data must be specified when the object is initially created and cannot be altered afterward.

A 17-element position, orientation, and scaling vector represented using the type spTransform. This structure is used as the primary representation of the position and orientation of objects.

A 3-element vector represented using the type spVector. Vectors containing three values are used for several different purposes.

A 4-element quaternion represented using the type spQuaternion. Quaternions are one way of representing rotations.

A 4x4 transformation matrix represented using the type spMatrix. Following standard graphics practice, the position, orientation, and scaling of objects can be represented using a 4x4 matrix.

A locally defined operation represented using the class spFn. The API makes heavy use of callback functions and the mapping of functions over objects. For convenience, a single type is used to represent functional arguments in all these situations.

An interaction window represented using the type spWindow. Exactly what type this is depends on the execution environment.

An opaque pointer represented using the type void *. In a number of places, the system records pointers that are not part of the external interface and are not intended to be manipulated by applications.

1.5 Shared Objects

The central kind of data in the API consists of shared objects. These objects are stored in a world model and shared between the participants in a session. (Other data is shared only if it is stored in a shared variable of a shared object.) The key feature of shared objects is instance variables. They can have methods associated with them as well; however, the system makes relatively little use of these methods. Rather, shared objects are essentially passive, principally just representing a database of information.

1.5.1 Accessors

All interaction with instance variables in shared objects is via access functions, rather than via direct access to the instance variables or structure fields. In the external API, the number of access functions available depends on the visibility of the instance variable in the API. The following discusses the accessors in detail for an instance variable V in a shared class spK containing data of type T.

The following accessor obtains the value of the instance variable V for an object. It reports an error if the object from which the data is being obtained is not an instance of (a subclass of) the class spK.

T spKGetV(sp Object)

The following accessor is available for setting the value of V. A set accessor is available in the external API only if the external API allows the variable to be set. The Set accessor reports an error if the object to be modified is not an instance of (a subclass of) the class spK, if the object has been removed from the world model, or if the value being stored is a shared object that has been removed. If V is an instance variable that is shared between the processes in a session, then the Set accessor reports an error if the owner of the object is not equal to the current value of spWMGetMe.

void spKSetV(sp Object, T Value)

A few instance variables in shared objects are constant in nature in that they cannot be altered after an object is initially created. For example, this is true for variables whose values have the C type spFixedAscii. If a shared instant variable is not constant in nature, then the following accessor is available for obtaining the value of V prior to a change. (Specifically, the accessor obtains the value V had at the end of the last call on spWMUpdate.) The primary intended use of these accessors is in callback tests.

The accessor reports an error if the object from which the data is being obtained is not an instance of (a subclass of) the class spK. Obtaining an old value can take substantially more time than obtaining the corresponding current value.

T spKGetOldV(sp Object)

In the interest of saving space in this document, the discussions of individual instance variables in shared objects merely list which accessors are available with reference to this section for greater details.

It should be noted that for a given class spK, the accessors above are available not just for the variables directly defined in the class, but also for the variables that are inherited. For example, if spK inherits a variable V from a superclass spJ and the accessor spJSetV is available, then the accessor spKSetV is also available. In the interest of brevity, these inherited accessors are not explicitly listed in this documentation.

1.5.2 Referring To

Instance variables that point to shared objects are handled specially in a number of ways. This is necessary due to the partial and asynchronous way that the world model is copied between processes.

A very important feature of the API is that the local world model copy in a given process does not contain everything in the entire virtual world, but rather only a subset of the objects that are of local interest. As discussed elsewhere, the determination of subsets is based primarily on regions.

Because only some of the objects in the world model are in the local world model copy at a given moment, it is entirely possible that the local world model might contain an object A that refers to an object B (e.g., A's Parent might be B) and yet the local world model might not contain the object B.

Communication is arranged so that if two objects refer to each other, then in general they are both communicated in the same region. As a result, the kind of inconsistency described above seldom exists for long. However, it is very common for this kind of inconsistency to exist briefly for a variety of reasons.

(1) When the focus of interest moves into a new region, a process inevitably hears about some objects before others. (Because there are places where circularity of references is required, nothing can avoid the fact that this can cause temporary dangling references.)

(2) When new objects are created, a process inevitably hears about some objects before others.

(3) When objects are removed, a process may hear that an object is gone before hearing that other objects have stopped referring to it.

The above situations are handled by essentially putting the burden of a sanitized version of the problem on the application. When an instance variable contains a shared object, the access function that obtains the value (e.g., spGetParent) returns the object referred to only if the object is in the local world model copy. Otherwise it returns Null.

Suppose again that there is an object A in the local world model copy. If spGetParent returns non-Null when applied to A, then the return value is the Parent of A. However, if spGetParent returns Null, then this could mean either that A has no Parent or merely that the Parent of A does not yet exist in the local world model copy. (There is no way to tell the difference between these two things without inspecting details of A that are below the level of the even the internal API.)

Suppose that A does indeed have a Parent B and suppose further that A appears in the local world model before B. When B later appears, this registers in all respects just the same as if the Parent of A changed from Null to B. In particular, if there is a callback watching for such a change, it will be triggered.

The above approach works well because it does not matter to the typical application whether the Parent of A has changed or has merely just become known. An application has to be able to deal with arbitrary changes made by other processes, and doing this properly typically leads to a solution that operates properly in the presence of evolving partial knowledge as well. However, it is important to keep clearly in mind that a process's knowledge of the world model is always partial and therefore, it is never justified to draw more than merely provisional conclusions from the absence of something in the world model, since anything can appear during a call on spWMUpdate.

1.5.3 Removal

A key issue is asynchronous changes in the world model. In particular, objects owned by other processes can appear, change, and disappear from the world model any time spWMUpdate is called. In addition, if there are multiple threads in the local process, then from the perspective of a given thread, other threads can asynchronously create, modify, and remove locally owned objects.

A particularly good example of the problems related to asynchronous changes is what must be done to properly handle the asynchronous removal of objects. To start with, it is important to understand what happens when an object is removed.

First, the IsRemoved bit is set on in the object R being removed and all connections between R and other objects are broken. Specifically, any instance variable in any other object that refers to R is set to Null. In addition, any instance variable of R that refers to any other shared object is set to Null. A result of this is that having an object's Parent removed is treated very much the same as if the object's Parent was simply changed to Null.

After the first stage of removal above, one can still consult all the instance variables of R, with the proviso that the variables that point to other objects have all become Null. (For variables that are shared with other processes, you can consult the old values of the instance variables to find out what they used to point to.)

Second, some time later (exactly how much time later is discussed in detail below) the storage corresponding to object R is freed. After that time, one can no longer access any of the instance variables of R, and it can be disastrous to try. A key purpose of the first step of removal is to ensure that no other shared object can retain a pointer to a freed object. Applications must be sure that they don't either. There is a separation in time between the first and second stages of removal so that applications have time to notice when objects have been removed.

To deal with asynchronous object removal, an application must check on a regular basis whether any objects it is interested in have been removed and respond appropriately before the objects are freed. The system has carefully designed rules about when objects can be removed and freed in order to reduce the number of times an application has to check for an object's continued existence.

Between calls on spWMUpdate, objects are never removed unless they are explicitly removed by the local process. The basic result of this is that it is sufficient for a process to check for the continued existence of an object it is keeping track of once each time after spWMUpdate returns. This checking is done using the accessor spGetIsRemoved. Once an object has been removed, the local process should drop all pointers to it immediately, because they will very soon be invalid. (A particularly good way to do the checking above is to use an spJustRemoved callback on the object in question.)

Objects are never freed except during calls on spWMUpdate. In addition, if an object is removed after the beginning of a call on spWMUpdate it is not freed until the very end of the next call on spWMUpdate. This means that for every object, there will be at least one period between spWMUpdate calls where the object has been removed and not yet freed. This gives the application an opportunity to detect this fact and do something about it. (It also guarantees that the object stays around long enough for any spJustRemoved callbacks to get triggered.)

As a result of the above, C applications need not worry much about objects being asynchronously freed as long as they check that the objects are still in the world model once after each call to spWMUpdate (e.g., with callbacks).

In the Java interface, garbage collection and finalize methods ensure that shared objects are not freed as long as any pointers to them remain. In the C interface the function spWMRegister is used to obtain a somewhat similar level of protection. This can be used to make sure that the object pointed to from a given variable will never be freed and therefore it will always be valid to check whether the object has been removed.

An important special case is worthy of note. If the local process owns an object, then it can rely on the fact that the object will not get removed unless the local process removes it. This obviates the need for a significant amount of spGetIsRemoved checking in typical applications.

If the local process has multiple threads, then several problems arise. First, the other threads in the process can remove anything and so no object access can be considered entirely safe. Second, it is likely that some of the threads will not be synchronized with calls on spWMUpdate. It is difficult for these threads to know when they should check that objects still exist.

The most robust way to avoid problems with threads is to avoid having asynchronous threads manipulate shared objects directly, but rather have them communicate in some other way with the main thread in a process. Barring this, spJustRemoved callbacks are the best way to ensure safety.

1.6 Defining Shared Classes

No matter what API language is being used, shared objects are defined using Java with a few small extensions. (The following discussion assumes a basic understanding of Java.)

The extended Java syntax is supported by a preprocessor called SPOT. In general, SPOT takes in a Java file containing a shared class definition and produces:

SPOT operates in 3 basic modes: C-only, Java-only, and mixed.

In C-only mode, SPOT is used purely to extend the C API. In C-only mode, everything in the definition of a class must be native. (What this means for instance variables is discussed below.) In C-only mode outputs (2) and (3) are unnecessary.

In Java-only mode, SPOT operates purely to extend the Java API. In Java-only mode, classes can not contain any native elements. Things are arranged so that the shared class can be used from Java without having to link anything additional into the system core. In particular, outputs (1) and (3) are unnecessary.

In mixed mode, the class can contain native and non-native elements and all four outputs are produced so that the class can be used to maximum effect in both C and Java. The shared classes described in this document are defined solely using native elements and are processed using the mixed mode of SPOT so that they are available fully in both the Java and C APIs. An appendix at the end of this documentation contains the SPOT input corresponding to the entire API described in this document.

1.6.1 Example

The following example is used throughout the explanation below. It is contrived to illustrate many different features in a small space.

public class spExample extends spThing { //* [+SendToContact]
  shared public float[] Orientation;            //* [spTransform:17]
  shared public/protected spAction Agent;
  native int Timeout;                           //* [spDuration]

  native public static int New(float [] Orientation);
   //* [sp spExampleNew(spTransform:17 Orientation)]

  native public void Initialization();
   //* [void spExampleInitialization(sp Object)]

  native void SetTimeout(int Timeout);
   //* [void spExampleSetTimeout(sp Object, spDuration Timeout)]

  native public final void Setup(int Timeout, spAction Action);
   //* [void spExampleSetup(sp ExampleObj, spDuration Timeout, sp Action)]
}

For the most part, the class definition above is standard Java. The exceptions to this are the keyword `shared', the use of the keyword `native' in an instance variable declaration, the use of the syntax keyword/keyword, and the uses of the //* comments. (spThing and spAction are shared object classes. spTransform and spDuration are types in the C API.)

1.6.2 Shared Classes

A class is a shared class if and only if it is the root shared class sp or extends another shared class. (In the case above, spExample extends the shared class spThing.) The key feature of a shared class is that the objects that are created as instances of the class are shared between the processes participating in a session.

The definition of a shared class must use the keyword `extends' and can use the keywords `public' and/or `abstract'. However, it cannot use any other keywords. In particular, it cannot use the keyword `implements'.

The open brace that begins the body of a shared class definition can be followed by a comment beginning with `//*'. The text in the comment can begin with [+/-SendToRegion +/-SendToContact]. This specifies two key facts about the class: whether it is communicated in the normal region-based way and whether it is communicated directly to the contact point for a process, with `+' signifying yes and `-' signifying no. If either the SendToRegion or the SendToContact specification is omitted, it defaults to the value inherited from the class that is being extended.

The SendToRegion and SendToContact specifications are used in a few crucial places in the definition of the standard shared classes, but it is very unlikely that an application programmer would ever need to use these flags. The reason for this is that for any class that an application programmer is likely to define, the inherited values of these flags will be correct.

1.6.3 Native Instance Variables

The keyword `shared' specifies that an instance variable is shared between processes. When a shared variable is set in one process, the change is automatically reflected in all other processes. In contrast, variables that are not marked as shared exist in each process, but the values of non-shared variables are set separately by each process, with no effect on their values in any other processes.

The keyword `native' specifies that an instance variable is represented in C so that efficient methods exist for accessing it directly from C. (Shared implies native.)

The keyword native by itself is used often when defining the standard classes in the system, and would be used by someone extending the C API. It would never be used by someone who was solely extending the Java API.

Shared and native instance variables cannot be directly referred to as instance variables. (For instance, when using an instance X of the class spExample, one cannot write X.Agent anywhere.) Rather, a shared instance variable Var in the class spK is accessed solely through the use of automatically generated access methods.

Shared and native instance variables cannot redefine any variable in the class being extended. (This is because, for efficiency, the system assumes that none of the instance variables it uses can have their definitions changed.)

If the keyword shared or native is used, one can also use the access control keywords private, protected, and public. If an access control keyword appears, then the Get and Set methods have the specified access control. It is permissible to use the form Keyword/Keyword, in which case the Get method has the access limitations specified by the keyword before the / and the Set method has the access limitations specified by the keyword after the /. (The keyword before or after the / can be omitted with the meaning that the corresponding accessor can be used anywhere in the package.) For example, the Agent instance variable of spExample can be read by everyone, but only written by the methods of the spExample class and its subclasses.

If the keyword shared is used, it is not permissible to use the keywords static or final. However, it is permissible for a native variable to be static or static final.

If a native variable is specified to be static, it is processed by SPOT in the same way as a variable that is not static except that the methods created for accessing the variable are static. The only class in the API that uses native static variables is the class spWM.

If a native variable is specified to be static final, it acts as a constant that is known both to Java and C. It is referred to directly as a variable in both APIs, rather than being accessed via methods. In C native static final variables are #defines. An example of a native static final variable in the API is spDEGREES.

If the keyword shared or native is used, then it is not permissible to have an initialization expression, except for a variable that is native static final. Rather, one uses an Initialization method.

1.6.4 Scalar Types

If the keyword shared or native is used, then the data stored in the variable must be one of only a few permitted types. These types fall into two groups: scalar types and pointer types.

A shared or native instance variable can have as its type any of Java's eight primitive scalar data types (byte, short, int, long, float, double, char, and boolean).

Since each shared and native instance variable is represented in C, it must have a C type as well as a Java type. The following table shows the default C type associated with each of the scalar Java types.

The semicolon ending a shared or native instance variable declaration can be followed by a comment beginning with `//*' that specifies a non-default type to use for the variable in the C API. This specification has the form [type] where the type is a C type (defined separately in a C file). The only restriction on the C type is that it occupy the same amount of storage as the Java type. For instance, an int in Java is represented using 32 bits. Any 32-bit C type can be used in conjunction with it. Whenever they are passed as arguments to functions or passed between C and Java, scalar values are copied.

In general, SPOT does nothing special with individual types. It merely needs to know what types to use in Java and in C and what their sizes are. However, one scalar type gets special treatment.

If the C type of a shared or native instance variable is spBoolean, then the value is stored in a single bit using special internal variables of the class sp. However, once the bits reserved in these variables have been exhausted, then a whole byte is used for an spBoolean value.

1.6.5 Pointer Types

In addition too the eight Java scalar types, a shared or native instance variable can have one of the following pointer types.

As with scalar types, each of the pointer types above must be mapped to a C type. The correspondence is shown in the table below.

In C, all shared objects are referred to by pointers of type sp and no other C type can be specified. All functional arguments are referred to by pointers of type spFn and no other C type can be specified. Note that it is not possible to have a shared or native variable whose Java type is anything other than the types specified above.

For strings and arrays, there is no default C type. Rather, the C type must be specified using a `//*' comment. When specifying a pointer type, this comment begins with [type:size] where type must be a C type that is a pointer (e.g., float*) and where size specifies how many elements there are in the string or array. For instance, in the spExample class, the C type spTransform points to a vector of 17 floats.

When pointer types are passed as arguments to functions, they are passed whenever possible by reference via a pointer. However, when pointer types are passed between C and Java, the data is typically copied so that appropriate data structures can be maintained separately in C and Java.

As with scalar types, SPOT typically does nothing special with individual pointer types. It merely needs to know what types to use in Java and in C and what the memory size in C is. (For ease of allocation and communication, strings and arrays stored in shared or native variables are always stored in-line in the C representation for an object.) However, several pointer types get special treatment.

The shared object types and the type spFn get special treatment so that appropriate objects will be available efficiently in both Java and C. In addition, Strings get special treatment.

A difference between Java and C is that Strings in Java use Unicode characters while strings in C use ASCII characters. To accommodate this, Java strings are encoded as null terminated UTF8 strings when communicated to C. (If a string only contains 7-bit ASCII characters, this encoding leaves the bits unchanged.)

The C type spFixedAscii specifies a string of variable length that can only be set at the moment when a shared object is being created and cannot be changed later. To minimize the memory used for spFixedAscii values, they are placed in a special variable-sized part of an object. (A consequence of this is that it is not possible to obtain the old value of an spFixedAscii variable.)

The type spFixedAscii can only be used for shared variables as opposed to ones that are merely native. There can be at most one spFixedAscii variable in a shared object. Unlike other array-like types, no explicit size can be specified when using the type spFixedAscii.

The total size (in C) of all the shared instance variables must fit in a single UDP message (i.e., be less than 600 bytes or so). A warning is issued if this restriction is violated.

1.6.6 Native Static Final Variables

A shared or native variable has an initializer expression if and only if it is a native static final variable. If the initialization of the value in C needs to be different from the initialization in Java (e.g., because it is not just a numeric constant) then the C initialization can be specified after an `=' sign in the //* comment that specifies the C type of the variable. For example the definition of spDEGREES could be:
native static final public float DEGREES = Math.PI/180.0; //* [float=M_PI/180.0]

1.6.7 Methods

The methods in the definition of a shared class are specified using entirely standard Java code. However, for a method to be available in C, it must be native, or redefine one of a few special methods that the system core uses.

The specification of a native method in a shared class is obligatorily followed by a //* comment containing the signature of the C function that implements the method. The signatures are used by SPOT both to create appropriate C .h files and to produce appropriate stub files linking the Java and C API's.

In order to make it possible to create stubs correctly, types in the C function signature that correspond to arrays must obligatorily contain a specification of their sizes as illustrated below. This is the same convention that is used when specifying native or shared variables that contain arrays. In general, lengths must also be included for string types. However, it can be omitted if the string is null terminated.

  native public static int New(float [] Orientation);
   //* [sp spExampleNew(spTransform:17 Orientation)]

For the generation of stubs, the names of the arguments are used to determine the correspondence between the arguments of the Java and C signatures. The Java and C type of an argument of a native method must be one of the types that are valid for native variables. The corresponding C type of the argument must be one of the C types that are permitted to correspond to the Java type. Automatic conversions are performed when passing values between Java and C.

If a method is not a static method, then the object the method is applied to is passed to the C function via the first argument whose name is not the same as the name of any argument in the Java method. Otherwise, if an argument appears only in the C signature, it is passed the value 0. If an argument appears only in the Java signature it is not be passed to the C function.

In the standard API classes, the convention is followed that if a class spK has a method M, then the name of the corresponding C function is spKM, the arguments are in the same order, and the argument to the C function that receives the object the method is applied to is the first argument.

The following methods are interpreted in special ways. (Note that for all these methods, there can be at most one method with the indicated name in a given shared class definition.)

Initialization - As noted above, you cannot have initialization expressions for native variables. However, you can define a method called Initialization that initializes variables to any values you want. (Note that unlike merely using initialization expressions, this approach allows you to change the initialization of variables that are inherited from an ancestor class.) If no Initialization method is provided, SPOT defines one that does nothing.

New - You can define a static method called New that can be called from C to create an instance of the class. This method can take various arguments and initialize various values. If no New method is provided, then SPOT creates one with no arguments that does nothing other than call spClassNewObj, which in turn calls the Initialization methods for the class and every ancestor class (calling the ancestor methods first). (Operating in Mixed or Java-only mode SPOT generates a creation method for each shared class which calls the New method.)

C - A static method called C is automatically generated by SPOT. This method returns the class object corresponding to the class being defined. You cannot define a method with this name.

ReadData, Function, Predicate, Inside - These four methods are recorded in the class descriptor object (along with the Initialization method) so that they can be called by the system core.

Accessor methods - In general, there is no explicit mention in a shared class definition of the access methods corresponding to shared and native variables because these methods are generated by SPOT. However, If you include an explicit definition of an accessor method, it will override what SPOT would have created. In the definition of spExample above, this is the case for the method SetTimeout.

1.6.8 Example Revisited

Returning to the example above.

public class spExample extends spThing { //* [+SendToContact]
  shared public float[] Orientation;            //* [spTransform:17]
  shared public/protected spAction Agent;
  native int Timeout;                           //* [spDuration]

  native public static int New(float [] Orientation);
   //* [sp spExampleNew(spTransform Orientation)]

  native public void Initialization();
   //* [void spExampleInitialization(sp Object)]

  native void SetTimeout(int Timeout);
   //* [void spExampleSetTimeout(sp Object, spDuration Timeout)]

  native public final void Setup(int Timeout, spAction Action);
   //* [void spExampleSetup(sp ExampleObj, spDuration Timeout, sp Action)]
}

SPOT expects the following C functions to be defined separately:

extern sp spExampleNew(spTransform Orientation);
extern void spExampleInitialization(sp Object);
extern void spExampleSetTimeout(sp Object, spDuration Timeout);
extern void spExampleSetup(sp ExampleObj, spDuration Timeout, sp Action);

Operating in C-only or mixed mode, SPOT generates the following functions:

extern sp spExampleC();
extern spTransform spExampleGetOrientation(sp Object);
extern spTransform spExampleGetOldOrientation(sp Object);
extern void spExampleSetOrientation(sp Object, spTransform Orientation);
extern sp spExampleGetAgent(sp Object);
extern sp spExampleGetOldAgent(sp Object);
extern void spExampleSetAgent(sp Object, sp Agent);
extern spDuration spExampleGetTimeout(sp Object);

SPOT would have generated spExampleNew, spExampleInitialization, and spExampleSetTimeout if they had not been provided by the programmer. The generated spExampleNew would just call spClassNewObj with the class spExample as its argument. The generated spExampleInitialization would do nothing. The generated spExampleSetTimeout would merely set the value of the field.

1.6.8 Non-Shared classes

A Java file input to SPOT can define non-shared classes that are meaningful to Java and will be available in Java. These classes cannot use the keyword shared. However, they can have native static variables, native static final variables, and methods. These three constructs are handled in exactly the same way as in shared classes. In particular, the native variables are accessible in both C and Java, and SPOT automatically creates stubs so that the native methods are available in Java and C.

A Java file input to SPOT can define a non-shared class that does not contain anything that is either native or shared. When that is the case, the class has no relationship whatever to the system. However, anything that is acceptable to Java can be used in the definition of such a class.

A shared class can contain instance variables that are neither shared nor native. Such a variable can be specified in any way that is acceptable to Java. However, the variable will not be accessible from C.

In addition, a shared class can define non-native methods. However, except for the methods Initialization, ReadData, Function, Predicate, and Inside, non-native methods have nothing to do with the system and will not be called by the system or available from C.

2 spWM

The central data structure in the API is the world model. There can be only one world model object at a time. It contains the various shared objects that are communicated among a group of processes. The spWM type does not extend the class sp and does not correspond to an object in the world model.

To start up a process, the first thing one does is create the world model to operate on. When the process wishes to terminate, it should remove the world model. In between, the world model is repetitively updated to reflect changes in the objects in the world model caused by other processes.

The class spWM defines the following instance variables:

The class spWM defines the following functions:

2.1 Me

native public static int Me; //* [spName]

The following access functions are available:

spName spWMGetMe()
void spWMSetMe(spName X)

Processes are identified by 32-bit session-wide unique owner ids of type spName that are assigned by the session manager. The Me variable of the spWM object contains the owner id of the activity currently in control.

When the world model is created, the Me value is set to a main owner id assigned by the session manager. It can be changed at will later. Additional owner ids can be obtained using spWMGenerateOwner.

2.2 MainOwner

native public/ static int MainOwner; //* [spName]

The following access functions are available:

spName spWMGetMainOwner()

The MainOwner variable of the spWM object contains the owner id assigned to the local process by the session manager. It is set when the world model is initially created and cannot be altered later.

2.3 Error

native public static String Error; //* [char *:500]

The following access functions are available:

char * spWMGetError()
void spWMSetError(char * X)

The Error instance variable of an spWM object records the most recent error to have occurred during the evaluation of any API function. When the world model is created the Error value is set to Null. It is changed whenever an error occurs. You can set the Error value back to Null if you want to. However, it cannot be directly set to any other value, but rather only indirectly via spWMReportError. Error strings are limited to being no more than 500 characters long.

2.4 LastError

native public/ static String LastError; //* [char *:500]

The following access functions are available:

char * spWMGetLastError()

The LastError instance variable of an spWM object is identical to the Error instance variable except that it cannot be directly modified by an application, but rather only indirectly via spWMReportError. The LastError value can always be consulted to determine what the most recent error, if any, was.

2.5 Interval

native public/ static spDuration Interval; //* [spDuration]

The following access functions are available:

spDuration spWMGetInterval()

The Interval instance variable of a world model records the time in milliseconds between the ends of the last two calls on spWMUpdate. When the world model is created, the interval is set to zero. After the second and subsequent calls on spWMUpdate, the interval value is updated to reflect the timing of events. It must not be modified by an application.

To be precise, the interval value is updated just before action processing begins and reflects the interval in time between corresponding points in spWMUpdate calls. Note particularly, that the interval is calculated after any waiting that spWMUpdate does in order to achieve the desired interval.

2.6 DesiredInterval

native public static spDuration DesiredInterval; //* [spDuration]

The following access functions are available:

spDuration spWMGetDesiredInterval()
void spWMSetDesiredInterval(spDuration X)

The DesiredInterval instance variable of a world model controls the interval between calls on spWMUpdate as follows. When spWMUpdate is just about to begin processing actions, it determines the elapsed time since the last time actions were processed. If this time is less than the desired interval, then spWMUpdate waits until the interval is reached. If the elapsed time is greater than or equal to the desired interval, spWMUpdate proceeds without waiting. There is nothing that spWMUpdate can do to make processing take less time than it is taking. However, spWMUpdate ensures that the actual interval will not be less than the desired interval. Since there is a good deal of imprecision in the system's timing mechanisms, the system cannot guarantee that the actual interval will be equal to the desired interval; however, as long as the desired interval is long enough to be achievable, the system guarantees that the average error over time will be low.

When the world model is initially created, the desired interval is set to zero. This causes spWMUpdate to run as fast as possible without ever waiting. You can change the desired interval at any time.

It is important to think carefully about how often spWMUpdate gets executed. If it executes too often, time is wasted looking at data that has not changed in any useful way. Alternatively, If spWMUpdate executes infrequently, you will be operating on very stale data much of the time. Further, if spWMUpdate runs very infrequently (e.g., less than once a second or so) some of the information about world model changes will probably be lost as queues and buffers overflow. (Things are arranged, however, so that even if you never call spWMUpdate, the system will not crash.)

2.7 Window

native public static int Window; //* [spWindow]

The following access functions are available:

spWindow spWMGetWindow()
void spWMSetWindow(spWindow X)

The Window instance variable contains the current user interaction window. The exact type of the window object and the operations that can be applied to it depend on the operating environment---In Java they are one thing and in C under Windows95 they are another.

When the world model is first created, the Window is set to Null. The surrounding environment may force a particular value to be used subsequently. For instance, this is the case when using the system as a Netscape plugin. Otherwise, the application is free to create or choose a window to use.

2.8 spWMNew

spWM spWMNew(char * Server, spTransferVector V)

Creates the world model. As part of this, establishes a connection to a session via a session server. A process can only create one world model at a time.

The first argument is a DNS string that identifies the session server to connect to (e.g., "node.myUniv.edu"). If the server address string is Null, then all phases of initialization are performed except contacting the session server. This is useful for some kinds of single-user testing.

A process must create the world model before using any shared object operation. After initialization, the world model contains nothing but definitions of the built-in shared classes and a few internal objects that are not observable by applications.

The transfer vector is used to specify key internal operations (e.g., allocating memory) that may have special definitions that are required by the surrounding environment. In simple applications, Null is an acceptable value for this argument.

2.9 spWMRemove

void spWMRemove()

Eliminates the world model and disconnects the process from the session it is in. Among other things, this causes all of the objects owned by the process to be removed from the world model and ensures that all the other processes in the session are informed of this fact. A process should always call spWMRemove before terminating. Once spWMRemove has been called, a process cannot use any shared object operation unless it first creates a new world model.

2.10 spWMUpdate

void spWMUpdate()

Causes the local world model copy to be updated to reflect changes made by other processes. The fundamental operation of an application process is based on a cycle of: updating the world model (based on information received from other processes); running the application, waiting until the desired update interval has been reached, updating the world model again, and so on.

A number of important things happen each time spWMUpdate is called.

(1) Other processes are notified of changes made in the local world model copy only when spWMUpdate is called. This gives the application control over when other processes see changes. (For instance, one can ensure that several instance variables of an object will change simultaneously).

(2) The world model is brought up to date by processing messages from other processes. It is guaranteed that the world model will not change in any way between calls to spWMUpdate, unless the application makes the changes itself.

(3) Any actions, including Callbacks, in the world model are run. (This is the only time they are run.)

(4) As part of updating the world model, some objects may be removed from the world model.

2.11 spWMGenerateOwner

spName spWMGenerateOwner()

Activities in processes are identified by 32-bit session-wide unique ids of type spName. These owner ids are used to tag the objects created by different activities. The main owner id of a process is assigned by the session manager. Additional ids can be created by using the function spWMGenerateOwner. Each time this function is called it returns an additional owner id.

There are two kinds of owner ids: system and ordinary. spWMGenerateOwner returns ordinary owner ids. System owner ids are only used by the system core; there is no function in the API for creating them.

The purpose for having multiple owner ids in a single process is as the basis for controlling the visibility of objects via world model view masks.

2.12 spWMReportError

void spWMReportError(sp Object, long Code, char * Description)

The API utilizes a uniform approach to error reporting. This centers around the error reporting strings created by spWMReportError. Whenever an error occurs, spWMReportError is called and an error reporting string is stored so that it can be easily retrieved.

An error is created based on the data passed to spWMReportError as follows. The description string becomes the heart of the error report. It should be human readable, containing as much contextual information as possible. The system does not modify the descriptive string and does not retain a pointer to it.

The code is converted to ASCII and appended to the front of the description followed by a blank. The code should be a unique identifier of the error. (The codes for the errors reported by the system are all negative and every different error has its own unique error code. Applications are advised to use positive error codes.) Programs that want to handle errors can tell which error occurred by looking at the error code portion of the report string.

The error string created is stored so that it can be retrieved as the value of spWMGetError and spWMGetLastError.

As an example of the way error reporting is used, consider that a careful program that was not completely sure that the variable X contained an spThing, might retrieve the Transform from X as follows:

  spWMSetError(NULL);
  transform = spThingGetTransform(X);
  if (spWMGetError()) ...

2.13 spWMRegister

void spWMRegister(sp * Pointer)

After a shared object has been removed, the storage associated with it is eventually freed. However, if a pointer is registered using the function spWMRegister, then the system will never free an object that is pointed to by the pointer. This means that it will be safe to save an object in this pointer indefinitely. To stop protection, thereby allowing eventual freeing of the storage, call spWMDeregister.

The system essentially supports a restricted form of garbage collection where the function spWMRegister is used to specify exactly which pointers are taken into account for garbage collection purposes. Note that several threads can each register the same pointer and the pointer will be protected until after all the threads have deregistered it. Note also that it is pointers that are being protected, not objects per se.

2.14 spWMDeregister

void spWMDeregister(sp * Pointer)

Cancels the protection of a pointer started by spWMRegister. It is important to cancel registration when a pointer no longer needs to be protected. This is particularly true if the pointer is stack allocated and the function it is declared in is about to return, rendering the address of the pointer meaningless. Calling spWMRemove cancels all pointer registration.

3 spFn

An important part of the API is functions that map functional arguments over objects. There are three basic kinds of mapping functions.

The type spFn is used for the functional arguments to all the above functions. (A single type is used in all these situations because this is considerably more convenient than having to define a separate type for each purpose.) The spFn type does not extend the class sp and does not correspond to objects in the shared world model. Rather, spFn data is stored in shared objects and used in intermediate computation.

In C, spFn is the following functional type:

typedef spBoolean spFn(sp Object, void * State)
The State argument is used to communicate state information between calls to an spFn. It can be used to accumulate a result. The return value is used for search-like operations. If an spFn ever returns True, then the mapping activity immediately halts. It is essential that an spFn be light weight in the sense that it runs quickly, and must not call spWMUpdate.

The following spFn could be used to count.

spBoolean spCount(sp ignore, void * state) {
  int * count = (int *)state;
  *count = *count+1;
  return FALSE;
}

For instance, the following code counts the number of children of an object X.

int Counter = 0;
spExamineChildren(X, spMaskNORMAL, spCount, &Counter);
Result = Counter;
The value of the count is obtained by observing the value of the state variable Counter after the examination is over.

Note that the state value that is passed to spExamineChildren and then on to the spFn spCount, is passed directly without copying. This is essential so that it can communicate information by side-effect. However, it means that the state must be in existance for the full period of time that spExamineChildren runs. This is not a problem for spExamineChildren, but requires more thought for operations like spExamineBeacons where the operate continues asynchronously for a potentially long period of time.

3.1 spFn Predicates

Another key part of the API is the ability to install callback functions that will automatically be called when certain events occur. Events are defined by predicates that test changes in the shared variables of objects. These predicates are defined using the same type spFn as functions that are mapped over objects.

When an spFn is used as a callback predicate, the return value is interpreted as specifying whether an event has occurred. Specifically, the predicate should return True or False depending on whether it observes that the event it tests for has occurred.

Callback predicates define events in terms of changes to shared instance variables. In particular, they compare the current state of these variables with the state of these variables at the end of the last call on spWMUpdate. (Since callback predicates are evaluated during each call on spWMUpdate, this ensures that any state change will be detected.)

The following shows an example of a simple callback predicate.

spBoolean spChangedParent(sp object, void * ignore) {
  return spThingGetOldParent(object) != spThingGetParent(object);
}

There are several key things to note about callback predicates. First, since callbacks are only applied to objects whose shared variables have changed, callback predicates are not called in situations where there has been no change in the shared instance variables of an object. Therefore, events must involve some change in these variables.

Second, the event generally must focus on changes in the shared instance variables of an object. The API does not provide any way to look at old values of local instance variables.

Third, callback predicates should not modify the object passed to them.

The API includes the following predefined callback predicates. Users can define any other predicates they want.

4 spMask

A fundamental feature of the API is the notion of a view mask for the world model. The purpose of these masks is to control the visibility of objects when using functions like spClassExamine. The spMask type does not extend the class sp and does not correspond to objects in the shared world model. Rather, spMask data is stored in shared objects and used in intermediate computation.

The class spMask defines the following instance variables:

There are several kinds of objects that an application typically should not see. First, they should not see objects that exist purely for the use of the system core and are not part of the external API. Second, they should not see objects that are created by another activity and exist in the local world model copy only because this activity happens to be running as part of the same process, rather than in a separate process. (For instance, an application does not want to see objects owned by other activities that are not communicated to other processes listening in the regions the objects are in.) Note that an application also typically does not want to see objects that have been removed. However, this is no problem because objects that have been removed are not in the local world model copy and therefore are not encountered by an application.

A world model view mask (spMask) is an integer bit mask that controls what objects are viewable. A view mask is created by adding (or or'ing together) the constants above.

Before discussing the exact meaning of these constants, it is useful to consider a simple example. The following expression applies F to every spThing object that is not the private object of some other activity.

spClassExamine(spThingC(), spMaskNORMAL, F, NULL)

In contrast, the following expression applies F to every spThing object that is owned by the current activity. It does not apply F to any objects owned by other activities.

spClassExamine(spThingC(), spMaskMINE, F, NULL)

The following pseudo-code shows the reasoning applied in functions like spExamineChildren to determine whether an object is compatible with a view mask.

boolean CompatibleWithMask(sp object, int mask) {
  if (spGetOwner(object) == spWMGetMe()) {
    return (spMaskMINE & mask)
  }
  else {
    return (spMaskOTHERS & mask) &&
           (~ SystemOwner(spGetOwner(object))) &&
           ( spClassGetSendToRegion(spGetClass(object)))
  }
}

For a view mask to make any objects viewable, at least one of the spMaskMINE and spMaskOTHERS bits must be on. The three view masks constructible using spMaskMINE and spMaskOTHERS that view any objects are all of potential use to applications. The most common single case is spMaskNORMAL which combines spMaskMINE and spMaskOTHERS and views all ordinary objects. This is the default value for spMasks in most situations.

The functions spExamineChildren, spExamineDescendants, spClassExamine, and spClassMonitor all have view mask arguments that limit the objects that are viewed. Actions and callbacks also make use of view masks.

View mask restrictions do not apply to the access functions (such as spGetParent) for obtaining the value of instance variables. This is not a problem because, in general, you cannot get from objects you should see to ones you should not see. Objects with ordinary owners should never point to objects with system owners. Objects that are communicated via regions never point to ones that are not. Note that no object ever points to an object that has been removed.

4.1 Constants

View masks are created by combining the following constants. (In C these constants are #defines.)

As illustrated above, view masks can be created by adding or or'ing these constants together.

5 spTransform

The data type spTransform is used as the fundamental representation of the position, orientation, and scaling of objects. A key advantage of an spTransform is that the various components are easy to understand. The components are also well suited to interpolation. The spTransform type does not extend the class sp and does not correspond to objects in the shared world model. Rather, spTransform data is stored in shared objects and used in intermediate computation.

The class spTransform defines the following instance variables:

The class spTransform defines the following functions:

An spTransform contains the same information as in a VRML transform node. Specifically, an spTransform is a vector of 17 floats representing 5 logical values as follows.

The first three elements are a vector representing the translation. The next four elements are an spRotation vector representing a rotation. The next three elements specify the amount of scaling along the X, Y, and Z axes with the value 1.0 indicating no scaling. The next four elements are an spRotation that is applied before the scaling is performed. The final three elements specify a center point that specifies the center point for both rotations.

As in VRML, the parts of an spTransform act together as follows. Note that because of the ScaleOrientation, the scale value can specify shear as well as scaling. It can be shown that the composition of any two spTransforms can be represented as an spTransform and the inverse of any spTransform can be represented as an spTransform.

(translate by Translation
 (translate by Center
  (rotate by Rotation
   (rotate by ScaleOrientation
    (scale by Scale
     (rotate by -ScaleOrientation
      (translate by -Center
       ...)))))))

By convention, Spline uses a right-hand coordinate system with the Y axis up and objects facing down the negative Z axis. (No assumptions are made about the relationship of the X and Y axes to compass directions.) It should be noted that different graphics modeling languages disagree with each other about these conventions. For instance, some have the Z axis up. The way spVisualDefinition links are specified makes it easy to use any kind of graphic model, without having to modify it.

The origin of the coordinate system for an object should be in the middle of the object unless there is a compelling reason otherwise. This is needed so that the InRadius and OutRadius of spThings will be meaningful. (An example of a compelling reason why the origin of an object would not be in the middle is that the origin of a subpart of an articulated form should be at the pivot point. This makes the mathematics of moving the subpart much easier.)

Any object that has a recognizable up direction should be oriented so that this up direction is parallel to the Y axis and points toward positive Y. Similarly, any object that has a recognizable front should have the front facing toward the negative Z axis. For instance, an object corresponding to the torso of an avatar would have the origin of its coordinate system in the middle of the chest, with the Y axis pointing up toward the head and the negative Z axis pointing out through the front of the torso. These axis conventions are needed both so that it is easy to combine different objects into a single scene and so that simulations that want to interact with objects can find where the tops and fronts of the objects are.

All units are in terms of meters and radians. Radians are used because they are more natural for numerical calculations. However, most people think more easily in degrees. In recognition of this, the following constant is provided.

Using this constant you can specify PI/2 radians (90 degrees) as follows.

90.0f * spDEGREES

In C, an spTransform is the following type.

typedef float * spTransform;

In addition, the following type is available for allocating memory for an spTransform. When calling a function that operates on spTransforms, one can pass in a variable that is either of the type spTransform or spTransformData.

typedef float spTransformData[17];
There are several levels at which you can interact with an spTransform. First, you can alter the underlying vector directly, e.g., using the index constants above. This allows you to do everything that is possible, but nothing easily.

Second, you can use the functions described in the following subsections to operate on spTransform objects. These make it easier to do complex operations. However, they require a clear understanding of transform interactions in order to figure out how to obtain a desired effect. In addition, these operations are static in the sense they call for the computation of particular spTransforms, rather than sequences of transform values over time.

Several conventions are worthy of note about the functions on spTransform objects. None of the functions ever allocates memory. Rather, the control of memory is left entirely up to the application. All modifications are by side-effect. However, to make it easier to create nested expressions the modified value is used as the return value.

A key complexity is that four different representations for rotations are supported. This is necessary because different communities of people are accustomed to different representations and justified because each representation has a situation where it is particularly convenient. The primary rotation representation is spRotation vectors such as those included in an spTransform. The second rotation representation is a vector of 3 Euler angles (in an spVector). The Third rotation representation is a vector of 4 floats representing a quaternion (spQuaternion). The fourth rotation representation is the rotation part of an spMatrix.

5.1 Constants

The following constants are available for directly accessing the various elements of an spTransform. (In C, these constants are #defines.)

For example, you might write.

spTransform P;
P[spTransformX] = P[spTransformY] + 2.0;

5.2 spTransformCopy

spTransform spTransformCopy(spTransform Destination, spTransform Source)

Copies the data from a Source spTransform to another, which is returned.

5.3 spTransformFromIdent

spTransform spTransformFromIdent(spTransform Transform)

Initializes the values in an spTransform so that they specify no translation, rotation, or scaling. That is to say, the spTransform is set to (0,0,0, 0,0,1,0, 1,1,1, 0,0,1,0, 0,0,0). It is important to initialize an spTransform before using it as a source of data, because the operations on spTransforms do not test that transforms are well formed before beginning their operations. In particular, they assume that the rotations in it are well formed.

5.4 spTransformGetTranslation

spVector spTransformGetTranslation(spTransform Transform)

Returns the Translation portion of an spTransform. In C, the spVector returned shares memory with the spTransform.

5.5 spTransformSetTranslation

spTransform spTransformSetTranslation(spTransform Transform, spVector Translation)

Sets the Translation portion of an spTransform. The other parts of the spTransform are not altered.

5.6 spTransformGetRotation

spRotation spTransformGetRotation(spTransform Transform)

Returns the Rotation portion of an spTransform represented as an spRotation. In C, the spRotation returned shares memory with the spTransform.

5.7 spTransformSetRotation

spTransform spTransformSetRotation(spTransform Transform, spRotation Rotation)

Sets the Rotation portion of an spTransform. The other parts of the spTransform are not altered.

5.8 spTransformGetScale

spVector spTransformGetScale(spTransform Transform)

Returns the Scale portion of an spTransform. In C, the spVector returned shares memory with the spTransform.

The three elements of the spVector contain the amount of scale along the X, Y, and Z axes respectively. The value 1.0 indicates no change in scale.

5.9 spTransformSetScale

spTransform spTransformSetScale(spTransform Transform, spVector Vector)

Sets the Scale portion of an spTransform. The other parts of the spTransform are not altered.

5.10 spTransformGetScaleOrientation

spRotation spTransformGetScaleOrientation(spTransform Transform)

Returns the ScaleOrientation portion of an spTransform represented as an spRotation. In C, the spRotation returned shares memory with the spTransform.

5.11 spTransformSetScaleOrientation

spTransform spTransformSetScaleOrientation(spTransform Transform, spRotation R)

Sets the ScaleOrientation portion of an spTransform. The other parts of the spTransform are not altered.

5.12 spTransformGetCenter

spVector spTransformGetCenter(spTransform Transform)

Returns the Center portion of an spTransform. In C, the spVector returned shares memory with the spTransform.

5.13 spTransformSetCenter

spTransform spTransformSetCenter(spTransform Transform, spVector Center)

Sets the Center portion of an spTransform. The other parts of the spTransform are not altered.

6 spVector

The type spVector is a 3-element vector of floats. It is used to represent several distinct things. The spVector type does not extend the class sp and does not correspond to objects in the shared world model. Rather, spVector data is stored in shared objects and used in intermediate computation.

The class spVector defines the following instance variables:

The class spVector defines the following functions:

There are three major uses of vectors. The first is as an ordinary vector in Cartesian coordinates, with the three elements being the X, Y, and Z coordinates respectively. These vectors are used to specify translations, rotation axes, and center points.

The second use of vectors is as a set of Euler angles. In this use, the first element is a rotation around the X axis in radians; the second element is a rotation around the Y axis in radians; and the third element is a rotation around the Z axis in radians. Since rotations do not commute, it is important to realize that these correspond to first rotating around Z, and then rotating around Y, and lastly rotating around X.

The third use of vectors is as a representation for scaling. In this use, the three elements represent the amount of scaling in the X, Y, and Z axes respectively. A value of 1.0 in an element specifies no scaling.

In C, an spVector is the following type.

typedef float * spVector;

In addition, the following type is available for stack allocating memory for an spVector. When calling a function that operates on spVectors, one can pass in a variable that is either of the type spVector or spVectorData.

typedef float spVectorData[3];

Several conventions are worthy of note about the functions on spVectors. None of the functions ever allocates memory. Rather, the control of memory is left entirely up to the application. All modifications are by side-effect. However, to make it easier to create nested expressions the modified vector is used as the return value.

6.1 Constants

The following three constants are provided for accessing the components of an spVector. (In C, these constants are external variables initialized to appropriate values.)

For example, you might write.

spVector V;
V[spVectorX] = P[spVectorY] + 2.0;

The following four constants contain particular vectors that are useful as arguments to various API functions. (In C, these constants are external variables initialized to appropriate values.)

For example, you might write.

spRotation R;
spRotationLookAt(R, spVectorZERO, spVectorAXISX, spVectorAXISY);

6.2 spVectorCopy

spVector spVectorCopy(spVector Destination, spVector Source)

Copies a vector into another.

6.3 spVectorSetFromScalar

spVector spVectorSetFromScalar(spVector Vector, float Scalar)

Sets all three elements of an spVector to be equal to a given Scalar.

6.4 spVectorEquals

spBoolean spVectorEquals(spVector A, spVector B)

Returns True if two vectors are component-by-component equal within a small system-defined tolerance.

6.5 spVectorEqualsDelta

spBoolean spVectorEqualsDelta(spVector A, spVector B, float Tolerance)

Returns True if two vectors are component-by-component equal within a tolerance specified by the user.

6.6 spVectorAdd

spVector spVectorAdd(spVector A, spVector B)

Adds two vectors together and stores the result in the first vector.

6.7 spVectorSubtract

spVector spVectorSubtract(spVector A, spVector B)

Subtracts a second vector from a first vector and stores the result in the first vector.

6.8 spVectorMultiplyByScalar

spVector spVectorMultiplyByScalar(spVector Vector, float Scalar)

Modifies a vector by multiplying each element by a scalar.

6.9 spVectorDivideByScalar

spVector spVectorDivideByScalar(spVector Vector, float Scalar)

Modifies a vector by dividing each element by a scalar.

6.10 spVectorCrossProduct

spVector spVectorCrossProduct(spVector A, spVector B)

Computes the cross product of two vectors and stores it in the first vector.

6.11 spVectorDotProduct

float spVectorDotProduct(spVector A, spVector B)

Computes the dot product of two vectors and stores the result in the first vector.

6.12 spVectorComposeScales

spVector spVectorComposeScales(spVector A, spVector B)

Modifies an spVector by multiplying each element by the corresponding element of another spVector.

6.13 spVectorLength

float spVectorLength(spVector Vector)

Computes the length of a vector. That is to say, the square root of the sum of the squares of the elements.

6.14 spVectorNormalize

spVector spVectorNormalize(spVector Vector)

Converts a vector into a unit length vector pointing in the same direction. That is to say, divides each element by the length of the vector.

7 spRotation

The principle way that rotations are specified is in terms of a rotation axis passing through the origin and a rotation angle about this axis in radians. Typically, the rotation angle is between plus or minus PI. A key advantage of an spRotation is that the various components are easy to understand. In addition, because the components are relatively independent, they are well suited to interpolation. The spRotation type does not extend the class sp and does not correspond to objects in the shared world model. Rather, spRotation data is stored in shared objects and used in intermediate computation.

The class spRotation defines the following instance variables:

The class spRotation defines the following functions:

An spRotation is a vector of 4 floats consisting of two parts as follows.

The first three elements are a vector representing an axis through the origin to rotate about. The last element is an amount of rotation in radians.

In C, an spRotation is the following type.

typedef float * spRotation;

In addition, the following type is available for allocating memory for an spRotation. When calling a function that operates on spRotations, one can pass in a variable that is either of the type spRotation or spRotationData.

typedef float spRotationData[4];

Several conventions are worthy of note about the functions on spRotations. None of the functions ever allocates memory. Rather, the control of memory is left entirely up to the application. All modifications are by side-effect. However, to make it easier to create nested expressions the modified value is used as the return value.

7.1 Constants

The class spRotation includes the following constants for accessing the X, Y, Z, and Angle components of an spRotation. (In C, these constants are #defines.)

For example, you might write.

spRotation R;
R[spRotationX] = A[spRotationY] + 2.0;

7.2 spRotationCopy

spRotation spRotationCopy(spRotation Destination, spRotation Source)

Copies the data from a Source spRotation to another, which is returned.

7.3 spRotationFromIdent

spRotation spRotationFromIdent(spRotation Rotation)

Initializes the values in an spRotation so that they specify no rotation about the Z axis. That is to say, the spRotation is set to (0,0,1,0). It is important to initialize an spRotation before using it as a source of data because the operations on spRotations do not test that rotations are well formed before beginning their operations. In particular, they assume that the axis does not have zero length.

7.4 spRotationGetAxis

spVector spRotationGetAxis(spRotation Rotation)

Returns the Axis portion of an spRotation. In C, the spVector returned shares memory with the spRotation.

7.5 spRotationSetAxis

spRotation spRotationSetAxis(spRotation Rotation, spVector Axis)

Sets the Axis portion of an spTransform. The angle is not altered.

7.6 spRotationGetAngle

float spRotationGetAngle(spRotation Rotation)

Returns the Angle in an spRotation.

7.7 spRotationSetAngle

spRotation spRotationSetAngle(spRotation Rotation, float Angle)

Sets the Angle in an spTransform. The axis is not altered.

7.8 spRotationToQuat

spQuaternion spRotationToQuat(spRotation Rotation, spQuaternion Quat)

Computes the spQuaternion corresponding to an spRotation.

7.9 spRotationFromQuat

spRotation spRotationFromQuat(spRotation Rotation, spQuaternion Quat)

Computes the spRotation corresponding to an spQuaternion.

7.10 spRotationToAngles

spVector spRotationToAngles(spRotation Rotation, spVector Vector)

Computes the Euler angles corresponding to an spRotation.

7.11 spRotationFromAngles

spRotation spRotationFromAngles(spRotation Rotation, spVector Angles)