Objects
From the patch programmer point of view, an object is the elementary
unit he use to build up a MAX program; they are the equivalent to
basic language statement and definitions in a standard programming
language.
In order to execute this program, jMax, as its predecessors,
use an object oriented interpreter; each statement/definition is
represented with an C object implemented using what we call the FTS
message system, that have features that resemble from some point of
view some of the characteristic of some object oriented languages
(in the old time when you called a generic function by sending
a message to an object).
"Max" messages are mapped to messages in messages to these objects,
and executed by executing these objects methods.
This cover primitive jMax objects like "int" or "float"; but currently
the jMax language is a lot richer, including the definition of new objects
in terms of the language itself (template and old abstractions), variables
and expressions and so on.
So when a user type an object description in an object box, the result
is not always the creation of a single object, but actually can include
much more work, like loading files, redefining other objects and so on;
so there are two meaning in the system for creating an object,
one is to process a language level object description and do all the needed
work to add this statement to the program (implemented by the C function
fts_eval_object_description
, and actually instantiating an instance of a
class of the FTS message system (implemented by the C function
fts_object_new
); these two functions are documented
below.
Changing and deleting an object require also that all effects tied to
variables are propagated in the right ways, so also these functionalities
need to be explained.
One non-user visible mechanism that jMax define is the object doctor,
essentially a macro processor for object description where macros are defined
as C functions, and can do invisible or visible transformation on objects; quite
usefull, for example, to keep compatibility between different version of objects
by automatically translating the old object syntax in the new syntax.
Creating an FTS message system instance
It is done by the fts_object_new
function.
The object creation is pretty straighforward; including a call to the
class discrimination mechanism to find the right class, allocating and
initializing the system part of the object, adding it to the container
patcher, and calling the object initialization (the init method).
Things worth to note:
- In jMax, objects know in which patcher they are; they can
refer to the context of the patcher for various purposes, notably
variable handling.
- Patcher are not handled as standard objects, and always assigned to the
same class; see the patcher chapter for details.
- The init message is alway sent with all the checks active; see the
message chapter for details on optional run time checks.
- While the object include in its state its description (i.e. the
text given by the user parsed as a list of atoms) this function do not
initialize it; the description is seen has an high level functionalities,
and is handled in the language level object creation.
Creating a jMax object
The language work is done in the fts_eval_object_description
function;
given a description, a number of things in the right order need to be
done.
- Variable preparation
If the object description define a variable ("foo : const 10")
syntax, the variable need to be defined, in order to inform the
variable propagation algorithm that we are adding a new variable in
the local context; this action can have quite a number of side
effects; note that the variable is defined before the object is
created, so we don't have a value for the variable; the variable will
be set later; this mechanism is needed to prevent loops in variable
value propagation; see the chapter on variables for details.
- Doctor
We check now if there is a object doctor (a C coded macro) registered
on the class name of this object; we do it here to give the macro a chance
to change the object before the expression evaluation; but, since also
the class name can be the result of an expression evaluation, we will need
to retry this after the expression evaluation.
See the paragraph on object doctors for details on what a doctor can actually
do and how.
- Expression evaluation
Taken away the variable definition (if any) the rest of the
description is evaluated by the expression evaluation; every
expression is subtituted by its value; in the process, the expression
evaluator produce a list of the variable used in this evaluation, and
a list of name/value pairs corresponding to the assignement syntax at
the end of the description (corresponding to named variables for
templates and patchers); see the expression chapter for details
on expression evaluation.
- Doctor (II)
We re-check for doctors after the expression evaluation,
for the reasons explained above.
- Explicit templates
We check if the class name correspond to an explicitly declared
template, i.e. currently declared with the tcl macro template
.
Doing this before trying a standard FTS class allow an explictly declared
template to overwrite a C written class, for example a library class.
- Explicit Abstractions
If we hav'nt created the object yet, we check if the class name correspond to an explicitly declared
abstraction, i.e. currently declared with the tcl macro
abstraction
. Doing this before trying a standard FTS
class allow an explictly declared template to overwrite a C written
class, for example a library class.
abstractions are the Max 0.26 and FTS 1.5 abstraction
mechanism; jMax can read old abstractions, but cannot save new
ones.
- Standard FTS objects
If we have not created the object yet, the fts_object_new
function is called, to build
a standard FTS object if the right class exists; see above.
- Template on a declared template Path
If we have not created the object yet, we check if a template with the correct
name exists in one of the template path declared (currently template path
are declared with the templateDirectory
tcl command).
Note that template on a template path do not overwrite standard FTS class;
the reason is a matter of loading perfomance: if a template on a template path
would overwrite an FTS class, we had to look for the template file before
looking for the FST class for all the objects instantiations,
with a fenomenal degradation in loading performance (a file system call
for every template path for every object loaded).
- Abstractions on a declared abstraction Path
If we have not created the object yet, we check if a abstraction with the correct
name exists in one of the abstraction path declared (currently abstraction path
are declared with the abstractionDirectory
tcl command).
As for templates, an abstraction on an abstraction path do not overwrite standard FTS class.
- Error object
If we have not created the object yet, create a fake error object; an error object
is a special object that work as a place holder for all the connections, and have a special
error properties for user interface purposes; see the Error object chapter for details.
- Check the variable value
In case the description define a variable, check if the object can actually
define a variable, i.e. do export a state
property; if it cannot,
substitute the object with an error object.
- Set the object description
If the previous object creation process have not set the object description,
assign it; for example, a doctor can have translated on the fly an object to another
one, producing also the new description, so in this case we should not set the
original one.
- Register itself as user of the used variables
Register the created object as user of the variable used in the expression evaluation;
this links will allow the object to be redefined if some of these variables change values.
- Handle the assignements
Get the expression assignements and assign them to internal variables
in case of patchers or templates, or to object properties in case of
standard objects.
- Restore of the variable value
Now that the object exists, the state property value can be assigned
to the defined variable (if any); this action will have as effect the redefinition
of all the objects depending on this variable.
Of course, the code have also to deal with a number of error
checks in between all this phases.
Deleting an Object
Deleting an object (function fts_object_delete
is a multiple
phases operation; first, we reset
the object (function
fts_object_reset
), i.e. we take it away from the variable
propagation network; if this object define a variable, it is
undefined, and all the objects depending on it are recomputed; and the
object is removed from the user list of the variables referred during
its instantiation.
Second, the delete
message is sent to the object, so that
the object can free its resources.
Third the object is removed
from the list having property changes (function
fts_object_reset_changed
); to keep this list consistent
(otherwise, deleting an object having a pending update would cause a
crash).
Finally the object is freed (function fts_object_free
)
all its connections are deleted, system structures are updated
(patcher object, id table and so on), its private data structure are
freed. Deleting an object can optionally be done without destroying
the client counterpart, if any; this feature is important when
redefining/recomputing an object, because we want to leave the object
identity intact on the client side.
Redefining an Object
In the message system, we talk about two slightly different concepts,
recomputing an object, and redefining an object.
To recompute (function fts_object_recompute
) an object
means to redefine it with the same description, taking care of
communicating the modification to the client, because the recomputing
is always a server initiative, tied to variable propagation;
recomputing is used when a variable the object depend on change value;
its implementation release the object edited data (value of the data
property), i.e. close any editor opened on the object "content", redefine
the object using the object description as new definition, and finally
upload the kernel properties to the client if needed.
To redefine an object (function fts_object_redefine
)
means to substitute it with a new one, with potentially a different
description in the patcher, without telling the other objects or the
client; connections are moved to the new one and id are kept.
Redefining patchers is done with a different strategy, to avoid loosing
their content; see below for details.
The object redifinition algorithm is the following:
- If the new object description define the same variable as the old
one the variable is suspended: essentially this means that while the
variable name does still exists in its scope, any access to its value is
illegal; suspending a variable is an essential concept in the variable
propagation mechanism, see the relative chapter.
- If the new object keep the same id, update the object table
to reflect the new binding of the id.
- As during an object delete, remove the old object from the
variable propagation structure.
- Prepare for data trasfer if needed, send the delete message otherwise.
- Build the new object, using the standard
fts_eval_object_description
function.
- Fix the internal structures of the loading vm: if we are loading
a jMax file, the vm may refer the object being redefined (in
the object table); the references are transparently converted to the
new object.
- If the new object one is an error object, set its number of inlets and outlets
to be equal to the ones of the old object.
- Copy standard properties (like x and y position) from the old object to the new one.
- Offer the two objects a chance to exchange data by means of a
redefining
message, only if the old object defined a method for the release
message.
- Move all the connections from the old object to the new one, keeping their client
identity if any.
- Complete the deleting of the old object, by calling
fts_object_free
.
Doctors
An object doctor is a macro expansor for object definition, written as a C function;
a specific object doctor is bound to a class name, and invoked during the object creation
process.
We way identify three kinds of doctors:
- Invisible doctors.
- An invisible
doctor produce a macro expansion that is not visible to the user (and
so not persistent); the expanded object keeps the same description as
the original; an object doctor of this kind usually produce the
expanded object description starting from the original description,
create the new object by calling
fts_eval_object_description
and then
remove the object description of the created object using the
fts_object_reset_description
; this will cause the object
creation function to install the original description on the object;
the argument
is actually implemented as an object doctor
producing a const
object.
- Visible doctor.
- A visible doctor produce a user visible and
persistent transformation of an object; this trasformation is usefull
in case like automatically updating a patch to a different syntax, in
case of object changes. An example of use if for example the
automatic translation of an old version of the comment object to the
current version.
-
- Special purpose
-
A doctor have a lot of freedom in trapping the object creation mechanism;
freedom that can be quite usefull, and of course dangerous, in some special
situation; for example, an object doctor can instantiate an object calling
directly
fts_make_objecy
, to avoid recursive calls to himself
and expression evaluation; this technique is used to transform old instances
of exp to the new syntax, prevening any interaction with the new expression
mechanism.
Note: there is a conceptual bug that should be fixed in the
way doctors work: as said above, a doctor can be called before or
after the evaluation of the expressions defining the object; but, the
doctor function have no way to know if the expressions has been
evalutated or not, so it is not clear if the
fts_eval_object_description
function or the
fts_object_new
should be called; calling the first can
produce inespected results when the result of the previous evaluation
is still an expression; calling the second can fail because the
expressions have not been evaluated; the doctor mechanism should be
refined to give more information to the doctor function .
Patchers
Patchers are very special objects in FTS; their main characteristic is
that they can dynamically change of class (not metaclass), so they can
change dynamically their number of inlets and outlets; in general a
patcher cannot be redefined by reinstantiation, in order to avoid
loosing their content; also, patchers represent contexts in
different case, like update handling and variable resolution; all this
require a dedicated support right in the kernel of the message system.
Patchers Inlet and Outlets
Main role of a patcher is to present a group object as a single entity
in the parent patcher; in order to allow to communicate with its content,
the patcher have inlets and outlets, that are mapped to inlet and outlet
objects inside the patcher.
This mapping is handled by the patcher itself; the inlet/outlet object
are not a lot more than placeholders for connections, rerouting,
together with the patcher object itself, messages from or to the
outside of the patcher.
A patcher is always instantiated without inlets and outlets; then,
adding inlets and outlets objects will automatically change its number
of inlet and outlet; a new inlet/outlet object can be instantiated
with a given position, or can automatically require the next available
position; changing the number of inlets or outlets dynamically require
from one side finding out the new class instance, on the other side change
the object system structures to reflect the new number of inlets/outlets;
this work is done in functions
fts_patcher_redefine_number_of_inlets
and
fts_patcher_redefine_number_of_outlets
.
When edited, inlet and outlet objects are not redefined; they are just repositioned,
and the patcher updated.
Inlet and outlet objects require special handling for loading a .pat file;
they are loaded inactive, in the sense they are stored in the
patcher but the patcher is not redefined, and only when the whole
patcher has been read, the inlets and outlets are analized and sorted
starting from their x
position and really positioned in the
patcher.
Patcher Variables
As objects, patchers can now define a variable; the variable is bound
to the patcher itself, i.e. to a variable scope; this is made to allow
composed name and inter-scope access for variables; note that the
variable value propagation engine do not yet support this feature.
Also, in order to parametrize the patcher content, a patcher can define
a number of internal variables; the array variable args, that include
a number of positional arguments, and a number of keyword argument.
The variable are defined and handled with techniques similar to those
of objects.
Patcher Redefinitions
Patchers have a special redefine strategy: since we don't want its content
to be lost; also, note that changing the description of a patcher
do not change its number of inlets/outlets, that is instead changed by adding
inlets/outlet objects inside the patcher; the patcher redefinition algorithm (function
fts_patcher_redefine
) is the following:
- Set the patcher description to the new decription.
- Handle variable definition: if the patcher defined a variable before
the redefinition, and do not define the same variable now, undefine the old variable;
otherwise suspend it; define the new one if any.
- Suspend the old argument variables.
- Eval the patcher arguments expression.
- Restore the argument variables, either positional or keyword, using
the result of the expression evaluation.
- Register the patcher as user of the variables accessed during
the expression evaluation.
- Undefine all the variable that are still suspended, i.e. old key and
positional arguments that have not been redefined.
- Restore the variable the patcher define, if any.
- Inform the UI of the changes.
Patchers and templates, abstractions, error objects
Given their ability to change number of inlets, patchers are used to
implement abstractions, templates and error objects; this means that
all they integrate the house keeping code for template instances, for
example.
Actually, a template or an abstraction consists precisely of a
patcher; the argument passing mechanism of templates is actually the
same mechanism that allow a patcher to define internal variables; the
template specific code regard only the mapping of names to files, the
loading of those files and the automatic reinstantiation of templates
during editing; see the relative chapter for details.
Error objects need to emulate input/output behaviour of other objects;
this is why they are actually implemented with patchers; a internal
flag prevent some of the operation to be done on a patcher that is actually
an error object.
Content Editing
In order to export the patcher content to a client editor,
patchers define a data property, that is a patcherdata
used to export the content of a patcher to the application layer; actually,
a patcher editor is an editor of patcherdata; this features can be blocked
with a special property, to make the patcher not editable.
Other features
Patchers directly support a number of extra features:
- Load bangs: the load init message is automatically propagated to its content;
the mechanism guarantee that the inner patchers are initialized before the standard
objects.
- Support for find: look for atoms occurencies in a patcher content,
and recursively in all its subpatchers.
- Support for find error: look for error objects in its content and
recusively in all its subpatchers.
- Deleting an patcher: patchers can recursively delete their content.
- Update control: a patcher keep track of open editors, and this is used to enable/disable
updates from its content.
- Support for blips, i.e. context dependent messages usually shown in
the first ancersto open patcher status line.
Properties
Object properties are a system, and not a language, feature; essentially consists
of associating to each object a dynamic associative list mapping a symbol to a single
atom value, so that each part
of the system can use properties as block-notes during computation
about objects; they are currently used to implement all the language features
different from the object description; for example, position and color (since we
have a visual language, these are language and not ui feature).
Properties, together with object description, are the basic FTS mechanism
for persistency: only escription and properties of an object are saved to
a file; properties are also used to represent those dynamical characteristic
of an user interface object that are affect the interface at run time;
see the update mechanism description.
Properties are stored/retrieved at three level: explicitly at the object level,
explicitly at the class level, and implicitly thru class daemons; a daemon
is a function that can override the standard system behaviour for setting/getting
a property.
When getting the property from an object, the system first check if the property
has been explictly set on the object, then check the class property as a default
value, and if not yet found, check if there is daemon available to compute this
property on the fly; also when setting an object property the system check if
there is a daemon for setting the property; daemons can be also define for
the remove property operation.
The property implementation is straightforward: they are stored in an associative
list for each object, or in the class, and the class have a unique daemon list with tags
for the action type.
Currently there is no way to define global daemons, i.e. deamons that are applied for
all the objects for a given property; this can come handy for example for some system wide
or UI relative property (like for example a property like is_dsp
)
Currently, the set of properties of an object save to a patcher file is hardwired;
the set of properties uploaded to the client for editing is fixed, but each object can
define some more with a special method. This is not very correct; each object should
define its own persistent and uploaded properties; in this way the kernel handle
a group of properties that are actually dependent on the objects.
The FTS kernel define a number of properties; the following table include
all the properties the kernel use; some of the properties are applicable to
all objects, or may just to one or two.
Kernel Defined Properties
Property Name | Meaning | Automatically Uploaded | Saved |
---|
name | Name of the object. Used to in converting .pat explode | N | N |
ninlets | Number of object inlets | Y | Y |
noutlets | Number of object outlets | Y | Y |
x | object x position | Y | Y |
y | object y position | Y | Y |
width | object width | Y | Y |
height | object height | Y | Y |
wx | object window x position | Y | Y |
wy | object window y position | Y | Y |
wh | object window height | Y | Y |
ww | object window width | Y | Y |
font | Object font | Y | Y |
fontSize | Object font size | Y | Y |
color | Object Color (for buttons) | Y | Y |
comment | Comment Text (comment objects) | Y | Y |
layer | z position of an object | Y | Y |
max_value | Max value (for sliders) | Y | Y |
min_value | Min value (for sliders) | Y | Y |
error | If not zero, this is object have an error | Y | N |
error_description | textual description of an error | Y | N |
no_upload | if set, the object cannot be uploaded | N | N |
state | If set, its value can be assigned
to the variable defined by this object | N | N |
patcher_type | the type of a patcher
(template/abstraction/standard patcher) | Y | N |
data | if set, its value will be edited
when the object is double clicked
its value must be an fts_data_t | N | N |
filename | The file a patcher has been loaded from
currently used for autosave | N | N |
Error Objects
With the term error objects we actually identify two different things:
a normal object that is, in its own code, signalling an initialization error to the user by becoming
red, and a real error object, i.e. a place holder for an object that the message system
could not instantiate at all for some reason.
In the first case, we just have an object that set the error
and error_description
properties properly, possibly using the
fts_object_set_error
function.
The error object system define also a call (fts_recompute_errors
and fts_do_recompute_errors
) to try to recompute all the objects
with errors; this is done when some big environmental changes happen (for
example, a new template directory is added), to give a chance to fix
configuration errors without reloading the edited patches.
Related Files
-
objects.c
-
objects.h
-
doctor.c
-
doctor.h
-
patcher.c
-
patcher.h
-
properties.c
-
properties.h
-
errobj.c
-
errobj.h