HomeForumSourceResearchGuide
<< back to guide home

A Dana component provides and requires a set of interfaces. These interfaces are connected to other components to form a complete system. In this section we describe how interfaces and implemented and used, including the use of multiple primary interfaces and secondary interfaces, and how inheritance works in Dana.

Provided interfaces (objects)

Each provided interface declared by a component is a source of instantiable objects (of that interface type) for other components. For example:

component provides App {
    int number
    
    int App:main(AppParam params[])
        {
        return 0
        }
    }

This component offers a single interface of type App. Other components can instantiate objects of type App from this component. Each such instance has its own separate copy of any global instance variables, in this case the integer variable number.

All instance variables in Dana are inherently "private" and can never be accessed from outside the component that declares them.

When providing an interface, a component must implement all functions declared in that interface. An interface function is implemented by declaring a function whose name starts with the interface type name, followed by a colon, and has the same name as one of the functions in the interface definition. This is exemplified by the function App:main in the above code. The parameter list and return type must match those of the corresponding function definition in the interface type.

The objects that are instantiated from a given provided interface can have additional (private) interfaces. This is done using brackets as follows:

component provides App(AdaptEvents) {
    int number
    
    void AdaptEvents:active()
        {
        }
    
    int App:main(AppParam params[])
        {
        return 0
        }
    }

In this example, any objects instantiated from the App interface of this component also have a second interface AdaptEvents. These secondary interfaces are only available to access if you have the IDC of the loaded component, and can be used by system management processes to inform objects about or query objects for various pieces of information. More than one additional interface can be implemented by an object; the list of such interfaces is separated by commas within the brackets, i.e. App(x, y, z).

Static variables

As well as instance variables, a component can declare "class" variables which are shared between all instances of all objects in the component. These are simply declared as follows:

component provides App {
    static int instanceCount
    int number
    
    int App:main(AppParam params[])
        {
        return 0
        }
    }

In this example, the variable instanceCount is shared between all instances of App from this component.

Using multiple provided interfaces

Most components declare just one provided interface, following a single-responsiblity model.

A component can, however, declare more than one provided interface in rare cases where this is useful. Each provided interface is a separate source of instantiable objects, each with its own set of private instance variables. When using more than one provided interface, a component must use implementation blocks to indicate where the scopes for instance variables start and end. For example, if we define a new interface type in a file resources/Map.dn:

interface Map {
    void start()
    void work(App a)
}

...we could then define a component in a file Test.dn as:

 component provides App, Map {
     implementation App {
         int number
         
         int App:main(AppParam params[])
             {
             return 0
             }
         }
     
     implementation Map {
         void Map:start()
             {
             }
         
         void Map:work(App a)
             {
             }
         }
     }

This component provides two interfaces, App and Map, each of which is its own instantiable source of objects for other components. Each instance of type App has its own private instance variable number, while instances of type Map have no instance variables. Each of these provided interfaces can still have secondary interfaces as described above; these secondary interfaces do not need to be re-stated in the implementation block headers (declaring the primary interface of each provided interface is sufficient).

There are two main design patterns in which the use of multiple provided interfaces are beneficial. One is the case where we need a "control" interface at the component level to configure some static variable values which help define how the component behaves (static variables are declared outside of implementation blocks and are shared between all instances of all provided interfaces). The other is for cases where instances of two different interfaces have some entanglement between then respective internal instance state; for example, the TCPServerSocket and TCPSocket interfaces must both be implemented by the same component, because the accept function needs to operate on the internal state of both instances combined.

For this second design pattern, objects of different types that have been instantiated from the same component have a special privilege in that they can access the private instance variables of each other. In the above example, if an instance of Map is given a reference to an instance of App via the work function, and that instance of App is from this same component, the instance of Map can then access App's private instance variable number by using the normal dot notation, e.g. a.number. See the net.TCP component in Dana's standard library for a real example use of this.

A component can check if it is the implementer of a given object instance using the implements notation, i.e:

if (implements a)
    {
    //...
    }

Required interfaces

A required interface indicates that a component depends on some functionality from another component. Each required interface is a source of instantiable objects, for example:

component provides App requires Orange {
    int number
    
    int App:main(AppParam params[])
        {
        Orange x = new Orange()
        Orange y = new Orange()
        return 0
        }
    }

As a convenience, required interfaces can also be given a "handle" if only a single instance is needed and if the required interface in question does not declare a constructor function. For example:

component provides App requires io.Output out {
    int number
    
    int App:main(AppParam params[])
        {
        out.println("Some text output")
        return 0
        }
    }

This is a shorthand notation which is the same as declaring and instantiating the instance as a global instance variable. As with provided interfaces, a component may declare more than one required interface, separating each required interface type with a comma, i.e. requires io.Output out, data.IntUtil, data.StringUtil.

In your own projects you will create plenty of components that depend on each other to create a working system, but Dana also features a large standard library of ready-made APIs which can be referenced by including them in your list of required interfaces - see our API pages for the list.

Semantic variants

Dana is designed to support the existence of multiple implementation variants of the same interface, and adaptation between these variants at runtime. In this case all of the implementations are expected to be equivalent in the task that they perform.

Sometimes, however, different component implementations would naturally have the same interface but perform semantically different tasks to other implementations of that interface - such as image file parsers that read different image formats, or encoding components that encode and decode data into different standards like Base 64 or UTF-16. In these cases it does not make sense to adapt between different implementations because they would break the program.

For this kind of design pattern, Dana supports semantic variants of both provided and required interfaces. A component can declare a provided interface with a semantic variant as follows:

component provides Encoder:base64  {
    
    char[] Encoder:encode(char content[])
        {
        //...
        }
    }

A component can then require such a specific semantic variant using the same notation on a required interface:

component provides App requires Encoder:base64 encoder  {
    
    }

Here the component will be connected to the default implementation of an Encoder interface with the base64 semantic.

For semantic variant implementations to be automatically discovered by Dana's runtime linker, components should use file names in the format Interface.variant.dn, compiled to Interface.variant.o. The above component would be saved in a file Encoder.base64.dn, and compiled to the file Encoder.base64.o.