Objects, classes and attributes

From PROSE Programming Language - Wiki
Jump to: navigation, search

PAL Tutorial - Objects, classes and attributes

In the current release, only the PROSE Assembly Language (PAL) is available. So be aware, it's very low-level programming at this time. To learn more about the PROSE Programming Language, visit http://prose.sourceforge.net.

It is suggested you begin with the following articles before attempting this tutorial.

A full list of available tutorials for the PROSE Assembly Language can be found on the tutorials index page.

This tutorial discusses objects, classes and attributes.

Objects, schemas and modules

PROSE represents objects in the nexus using an LDAP-type schema. Objects can be given classes, which describe the capabilities of each object (esp. which attributes can be assigned to them). For example, the psContainer class is used to mark an object that is used chiefly for structural purposes, like a directory in a filesystem. When the psContainer class is assigned to an object, the object must also be assigned a pn attribute and may optionally be given a description attribute too. The available classes are listed in the ps_classes(5) man page.

Objects also possess attributes. An attribute has both a type and a value, so for example the description attribute is used for descriptive information and may have one or more values which contain the actual text of the description. The available attributes are listed in the ps_attributes(5) man page.

A set of strict rules are defined that describe which attributes may be used by which object classes. These attribute types will only then be available to objects in the nexus that have been assigned the appropriate classes. These rules are defined in a schema. However, at this time the PROSE engine ignores most schema rules, although does enforce the SINGLE-VALUE flag which indicates that an attribute may not be given multiple values. Also, read-only attributes are also enforced, as is the restriction that an object must have one and only one STRUCTURAL class assigned to it. Other schema rules will be enforced in a later release of the PROSE engine.

The PROSE schema is described in files that are the same format as an LDAP schema file. It is an extensible schema, and is currently read once at start-up. Any changes to the schema files will be picked up the next time you execute prose. The default location of the schema files is /usr/local/share/prose/schema, although a different location for the schema directory may be supplied via the prose -s option or the PROSE_SCHEMADIR environment variable.

Here are some example attribute and class definitions from the PROSE core schema file 00core.schema:

    attributetype ( 1.3.6.1.4.1.23780.1.2.1 NAME ( 'pn' 'psName' )
            DESC 'Name of PROSE node'
            SYNTAX 1.3.6.1.4.1.23780.1.3.1 )

    attributetype ( 2.5.4.13 NAME 'description'
            DESC 'RFC2256: descriptive information'
            EQUALITY caseIgnoreMatch
            SUBSTR caseIgnoreSubstringsMatch
            SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1024} )

    objectclass ( 2.5.6.1 NAME 'top'
            DESC 'RFC2256: top'
            STRUCTURAL )

    objectclass ( 1.3.6.1.4.1.23780.1.1.1 NAME 'psContainer'
            DESC 'PROSE node metadata'
            SUP top STRUCTURAL
            MUST pn
            MAY description )

The pn attribute is actually a virtual attribute. All nodes in the nexus have this attribute, and it contains the name of the object. So, for example, the top-level node .prose will have a pn attribute of prose, while the .prose.sys.io object will have a pn attribute of io.

The top class is an implied class. All nodes in the nexus have this class.

Attributes are given a syntax, which identifies the module and code entry-point that handles requests to read or modify the attribute value. By default, modules are located in /usr/local/share/prose/modules and described in a collection of .conf files. A different location for the module directory may be supplied via the prose -m option or the PROSE_MODULEDIR environment variable.

The core module is described in the ps_core.conf file, and it is in this file that syntaxes and module entry points are described. Here are some example definitions in the core module configuration file:

    name="ps_core" rev=1 desc="PROSE core support routines" cltrig="top";

    rule="caseIgnoreMatch" oid="2.5.13.2" type="equality";
    rule="caseExactMatch" oid="2.5.13.5" type="equality";
    rule="caseIgnoreOrderingMatch" oid="2.5.13.3" type="ordering";
    rule="caseIgnoreSubstringsMatch" oid="2.5.13.4" type="substr";

    syntax="1.3.6.1.4.1.1466.115.121.1.15"
            entrypoint="dirstring" desc="Directory String"
            attach_rule="caseIgnoreMatch"
            attach_rule="caseExactMatch"
            attach_rule="caseIgnoreSubstringsMatch"
            attach_rule="caseIgnoreOrderingMatch";

A syntax may be associated with a number of rules. At this time PROSE ignores the rules, but in the future they will be used to modify the behaviour of the given syntax.

The schema, the modules, syntaxes and rules are mentioned here only as an overview. You do not need to understand these concepts for day-to-day use of PROSE or PAL. Schema files and modules should not need modification for normal use of PROSE. It only becomes necessary to supply modified schemas or new modules when extending the functionality of PROSE beyond that designed in the core product - an option open to the advanced programmer, and a feature that is beyond the scope of this tutorial.

Examining some basic objects

Using the obj/dump command, one can examine some basic objects in the nexus. The topmost node is called .prose and can be displayed with the following code:

    ._init
    func/def     [main], &[.main]
    local/rtn

    .main
    obj/dump     ![.prose]
    func/rtn

The output will look much like this:

    .prose:objectClass=psContainer
    .prose:objectClass=top
    .prose:objectClass=psRoot
    .prose:pn=[prose]
    .prose:psVersion=[0.11.0]
    .prose:psEnableExtraDebug=[FALSE]
    .prose:psWithMemWatch=[FALSE]

From this you will see that the root of the nexus is an object that contains both the psContainer and psRoot classes as well as the psVersion attribute. It is therefore possible to determine the version of the running PROSE engine by looking for the psVersion attribute in the topmost node, like this:

    attr/test ![.prose], [psVersion], [0.11.0]

This instruction will set the SCMP register to 1 if the version is 0.11.0, otherwise the SCMP register will be set to 0.

Running obj/dump ![-] dumps the module root (this is the root of the current executing module). If your bytecode file was called example.pro the output would look like this:

    .prose.code.default:objectClass=psContainer
    .prose.code.default:objectClass=top
    .prose.code.default:objectClass=psModule
    .prose.code.default:pn=[default]
    .prose.code.default:psObjectFileId=[example.pro]

From this you will see that the module root uses the psModule class as well as the psObjectFileId attribute to identify that a) it is the root of a module and b) the name of the bytecode files which supplied the code for the module. There might be more than one file installing different functions within the module, as in the following example:

    .prose.code.default:objectClass=psContainer
    .prose.code.default:objectClass=top
    .prose.code.default:objectClass=psModule
    .prose.code.default:pn=[default]
    .prose.code.default:psObjectFileId=[example1.pro]
    .prose.code.default:psObjectFileId=[example2.pro]
    .prose.code.default:psObjectFileId=[example3.pro]

Here you will see that the psObjectFileId attribute is a multi-value attribute, i.e. it is permitted to have more than one value if necessary. Attributes that can only ever have one value are called single-value attributes.

Now run reg/dump PCTX at the top of a function. This displays the context root which is used to determine relative paths, i.e. when your path does not start with a dot. By default this register is set to point to the executing function when the function is first called, but the PCTX register can be changed to point to anything, as in the following example:

    ._init
    func/def     [main], &[.main]
    func/def     [main.sub], &[.sub]
    local/rtn

    .main
    reg/dump     PCTX
    reg/load     PCTX, ![sub]
    reg/dump     PCTX
    func/rtn

    .sub
    func/rtn

In the above example, two functions are defined. One will be set-up at .prose.code.default.main and the other in .prose.code.default.main.sub. When the main function is called, the reg/dump instruction will show you where it is pointing. Then it is changed to point to sub underneath the current position and dumped again. The output from this example is:

    register: PCTX
    type: PSUNIT_TYPE_NODE (0x81)
    root: 0x839170 (global) nrefs: 4
    .prose.code.default.main

    register: PCTX
    type: PSUNIT_TYPE_NODE (0x81)
    root: 0x839170 (global) nrefs: 2
    .prose.code.default.main.sub

Notice the difference between using reg/dump and obj/dump when debugging registers. The reg/dump instruction will display the type and value of a register. The obj/dump instruction displays information about the underlying object that the register is pointing to.

Add the following to the top of the previous example. This defines a data segment called Module, which is a reserved keyword used for redefining the name of the module:

    ~Module
    EQUS {[loft.utils]}

Replace the reg/dump instructions with obj/dump, assemble and re-execute. The result should be:

    .prose.code.loft.utils.main:objectClass=psContainer
    .prose.code.loft.utils.main:objectClass=top
    .prose.code.loft.utils.main:objectClass=psFunction
    .prose.code.loft.utils.main:pn=[main]
    .prose.code.loft.utils.main:psObjectFileRd=[example.pro]
    .prose.code.loft.utils.main:psObjectCodeRef=[0x000b]
    .prose.code.loft.utils.main:psModuleRoot=[.prose.code.loft.utils]
    .prose.code.loft.utils.main.sub:objectClass=psContainer
    .prose.code.loft.utils.main.sub:objectClass=top
    .prose.code.loft.utils.main.sub:objectClass=psFunction
    .prose.code.loft.utils.main.sub:pn=[sub]
    .prose.code.loft.utils.main.sub:psObjectFileRd=[example.pro]
    .prose.code.loft.utils.main.sub:psObjectCodeRef=[0x0014]
    .prose.code.loft.utils.main.sub:psModuleRoot=[.prose.code.loft.utils]

Here you see two objects, both functions. Function objects are identified by the psFunction class. They contain the psObjectFileRd, psObjectCodeRef and psModuleRoot attributes, which identify the binary file in which the function is defined, the code address within that bytecode where the function code begins and the module root for the function.

If you were to disassemble the bytecode using prism -D30 -d example you will see how the code addresses used by the psObjectCodeRef attributes tally with the assembly instructions:

    $ prism -D30 -d example
    << Instruction code: 0x00001c >>
    << Code labels: 0x000035 >>
    << Code addresses: 0x00004a >>
    << Text segment: 0x000051 >>
    << Data labels: 0x000075 >>
    << Data addresses: 0x000080 >>
    << Data segment: 0x000085 >>
    ~Module
    EQUS { [loft.utils] }

    0x0000 : ._init
    0x0000 :   func/def      [main], &[.main]
    0x0005 :   func/def      [main.sub], &[.sub]
    0x000a :   local/rtn
    0x000b : .main
    0x000b :   obj/dump      PCTX
    0x000d :   reg/load      PCTX, ![sub]
    0x0011 :   obj/dump      PCTX
    0x0013 :   func/rtn
    0x0014 : .sub
    0x0014 :   func/rtn

Running obj/dump ![+] dumps the instance container. This is where objects relating to a particular instance of a function reside. As functions may be called multiple times (recursively), each time it is called a new instance container is automatically created. Walkers are an example type of structure that are stored in the instance container. So let's create a couple of walkers before doing the dump:

    ._init
    func/def     [main], &[.main]
    local/rtn

    .main
    reg/load     P0, (![.prose])
    reg/load     P1, (![.prose.code])
    obj/dump     ![+]
    func/rtn

The output of the above program will be:

    .prose.code.default.main._i0#0:objectClass=psContainer
    .prose.code.default.main._i0#0:objectClass=top
    .prose.code.default.main._i0#0:objectClass=psInstance
    .prose.code.default.main._i0#0:pn=[_i0#0]
    .prose.code.default.main._i0#0:psWalker=[.prose]
    .prose.code.default.main._i0#0:psWalker=[.prose.code]

The name of the instance container is _i0#0 where the first 0 is the thread ID (always 0 at this time as the PROSE engine is not yet multi-threaded), and the second 0 is the instance ID which will be incremented for each recursive call to the same function.

Creating, modifying and deleting objects

Objects may be created and modified either by direct edits or by means of an edit buffer. Edit buffers are used when more than one operation is to be performed on an object. Commands are inserted into the buffer that will add, modify or delete classes and attributes. The edit buffer is then committed against an existing object, or a request is made to create a new object with the edit buffer. It is at the time the edit buffer is committed that the commands in the edit buffer are processed.

Edit buffers may be created from scratch or created based upon an existing object. Edit buffers themselves may also be cloned if multiple objects are to be created with similar properties.

Use the obj/def instruction to create a new edit buffer. This would be used when defining a new object. Then use class/add to add new object classes to the buffer. Use attr/add to add attributes with single values or attr/mvadd to add multi-value attributes. At this time you may then optionally use the obj/clone instruction to create a cloned edit buffer. Finally you will use obj/commit to create your new object. This is demonstrated in the following example:

    ._init
    func/def     [main], &[.main]
    local/rtn

    .main
    obj/def      P0
    class/add    P0, [psContainer]
    attr/mvadd   P0, [description], [Description value 1], [Description value 2]
    obj/commit   [myobject], P0
    obj/dump     ![myobject]
    func/rtn

The output of the above example is:

   .prose.code.default.main.myobject:objectClass=top
   .prose.code.default.main.myobject:objectClass=psContainer
   .prose.code.default.main.myobject:pn=[myobject]
   .prose.code.default.main.myobject:description=[Description value 1]
   .prose.code.default.main.myobject:description=[Description value 2]

Use the obj/edit instruction to create a new edit buffer intended to be used for editing a specific object. Then use the class and attr instructions to add, modify or delete classes or attributes in the buffer. At this time you may then optionally use the obj/clone instruction to create a cloned edit buffer. Finally you will use obj/commit to create your new object. Add the following code to the previous example, immediately before obj/dump:

    obj/edit     P0, ![myobject]
    attr/mvadd   P0, [description], [Description value 3]
    obj/commit   P0

When using obj/edit as above, the obj/commit instruction requires only one argument because a record of which object was being edited is kept in the edit buffer. The output should now be:

    .prose.code.default.main.myobject:objectClass=top
    .prose.code.default.main.myobject:objectClass=psContainer
    .prose.code.default.main.myobject:pn=[myobject]
    .prose.code.default.main.myobject:description=[Description value 1]
    .prose.code.default.main.myobject:description=[Description value 2]
    .prose.code.default.main.myobject:description=[Description value 3]

The obj/clone instruction may also be used to create an edit buffer that will be pre-populated with the commands needed to create another identical object. This edit buffer may then be committed over an existing or new object using obj/commit, which will replay a list of class add and attribute add commands. Add the following code to the previous example, immediately before func/rtn:

    obj/clone    P0, ![myobject]
    obj/commit   [myobject2], P0
    obj/dump     ![myobject2]

The output should now be:

    .prose.code.default.main.myobject:objectClass=top
    .prose.code.default.main.myobject:objectClass=psContainer
    .prose.code.default.main.myobject:pn=[myobject]
    .prose.code.default.main.myobject:description=[Description value 1]
    .prose.code.default.main.myobject:description=[Description value 2]
    .prose.code.default.main.myobject:description=[Description value 3]
    .prose.code.default.main.myobject2:objectClass=top
    .prose.code.default.main.myobject2:objectClass=psContainer
    .prose.code.default.main.myobject2:pn=[myobject2]
    .prose.code.default.main.myobject2:description=[Description value 1]
    .prose.code.default.main.myobject2:description=[Description value 2]
    .prose.code.default.main.myobject2:description=[Description value 3]

Finally, objects are deleted using obj/del. Add the following to the end of the previous example, immediately before func/rtn:

    obj/del      ![myobject], ![myobject2]
    obj/dump     ![myobject], ![myobject2]

You should now receive an error that the object referenced by the final obj/dump cannot be found:

    .prose.code.default.main.myobject:objectClass=top
    .prose.code.default.main.myobject:objectClass=psContainer
    .prose.code.default.main.myobject:pn=[myobject]
    .prose.code.default.main.myobject:description=[Description value 1]
    .prose.code.default.main.myobject:description=[Description value 2]
    .prose.code.default.main.myobject:description=[Description value 3]
    .prose.code.default.main.myobject2:objectClass=top
    .prose.code.default.main.myobject2:objectClass=psContainer
    .prose.code.default.main.myobject2:pn=[myobject2]
    .prose.code.default.main.myobject2:description=[Description value 1]
    .prose.code.default.main.myobject2:description=[Description value 2]
    .prose.code.default.main.myobject2:description=[Description value 3]
    * prose.error.sys.NoEntry: No such entry or object
    * Can't lookup object reference: myobject
    *    at default.main()                      [testobj.pro, addr 0x0039]
    *    in prose.code._tid.0

Resources from this tutorial

Further reading

See the other tutorials available for the PROSE Assembly Language on the tutorials index page.

PROSE is released with detailed manual pages that describe how PAL operates, and how each instruction is used. These manual pages can be read using the man command, for example man pal_intro or man pal_commands, or from the Reference Manual Pages online.