Objects, classes and attributes
Contents
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.