Using PML.NET
- Last UpdatedOct 28, 2024
- 8 minute read
The diagram below shows how AVEVA modules may be customized using PML.NET. A number of .NET API's are available which allow access to the current database session, drawlist, geometry and other functionality. Users are able to write their own managed code which accesses the AVEVA module via these C# API's. It is not possible to directly call PML from C#. However there is an event mechanism which allows PML to subscribe to events raised from C# (these are shown in dashed lines below). Events are also raised when the database changes and can be subscribed to from C# (also shown in dashed lines). In this example the external C# assemblies share the same database session as Design i.e. they run in the same process and therefore see the same data.

Limitations
-
Only .NET classes which are marked as PMLNetCallable and adhere to certain rules can be called from PML (these rules are described later)
-
Module switching does not persist .NET objects. Core PML objects defined in FORTRAN or C++ are not persisted either.
-
Passing other PML system/user objects to .NET, for example: DIRECTION, ORIENTATION, … is not possible. It is possible to pass database references to .NET either as an array or String. It is also possible to pass an existing instance of a PML.NET Proxy to .NET.
-
It is not possible to call directly PML objects from .NET. The only way to call PML from .NET is via events.
-
It is not possible to enter 'partial' namespaces as you might in C# and expect them to be concatenated.
For example: the sequence:
USING NAMESPACE ‘Aveva.Core’
!netobj = object namespace.NETObject ( )
will give an error.
Objects and Namespaces
In order to identify and load a particular class definition the PML proxy class is assumed to have the same name as the .NET class (PML callable classes must be case independent). This class name is passed to the PMLNetEngine which creates the .NET instance.
For example:
!a = object netobject()
creates an instance of the .NET class netobject. To specify in which assembly this class is defined and resolve any name clashes the user needs to also specify the namespace in which the .NET class is defined using the following syntax:
USING NAMESPACE <string>
Where <string> is the namespace in which the class is defined.
For example:
'Aveva.Core.PMLNetExample'
The namespace is considered to be case independent. This namespace will remain current until it goes out of scope, for example: at end of macro. When the user types:
!netobj = object NetObject ( )
then all namespaces in the current scope will be searched to find a match. In this example, if 'Aveva.Core. PMLNetExample' is not currently in scope, then the error:
(46,87) PML: Object definition for NETOBJECT could not be found.
will be raised.
Object Names
Object names can consist of any alpha but not numeric characters (this restriction is imposed by PML). They are treated as case-independent. However, it is no longer necessary to define them in upper case - any mixture of upper and lower case letters will have the same effect.
Query Methods
The query methods on an object have been enhanced as follows:
(a) Querying an object will show the namespace name as well as the object name:
For example:
q var !x
< Aveva.Core.NAMESPACE.NETOBJECT>
Aveva.Core.NAMESPACE.NETOBJECT
(b) There is a new query method to list all the methods of an object (including constructors)
Q METH/ODS
For example:
q meth !x
<Aveva.Core.NAMESPACE.NETOBJECT>Aveva.Core.NAMESPACE.NETOBJECT
NETOBJECT ( )
NETOBJECT (REAL)
ADD (REAL)
REMOVE(REAL)
ASSIGN(Aveva.Core.NAMESPACE.NETOBJECT)
DOSOMETHING(REAL, REAL, REAL)
Note:
Query methods will not list the methods on objects of type ANY, even though such methods
are available on all objects.
(c) A new query:
Q NAMESP/ACES
lists the namespaces currently in scope.
Global Method
There is a new global method on all objects:
.methods()
which returns a list of the methods on the object as an array of strings.
For example:
!arr = !x.methods()
q var !x
returns:
<ARRAY>
[1] <STRING> 'NETOBJECT ( )'
[2] <STRING> 'NETOBJECT (REAL)'
[3] <STRING> 'ADD (REAL)'
[4] <STRING> 'REMOVE(REAL)'
[5] <STRING> 'ASSIGN(Aveva.Core.NAMESPACE.NETOBJECT)'
[6] <STRING> 'DOSOMETHING(REAL, REAL, REAL)'
Importing an assembly
Before an instance of a .NET object can be instantiated the assembly containing the class definition must be loaded. This is done using the IMPORT syntax as follows
IMPORT <string>
Where <string> is the case-independent name of the assembly
Method Arguments
Only PML variables of the following types may be passed to methods on .NET classes. In the table below the PML variable type is in the left column and the .NET equivalent variable type is in the right column. Data is marshalled in both directions between PML and .NET by the PMLNetEngine.
|
PML |
.NET |
|---|---|
|
REAL |
double |
|
STRING |
string |
|
BOOLEAN |
bool |
|
ARRAY (these can be sparse and multi-dimensional) |
Hashtable - The Key must be numeric (integer of double) to match the PML Array index which is always REAL. An error will be raised during execution if this is not the case. |
|
OBJECT Any existing PML.NET instance |
PMLNetCallable class |
Arguments to PML Proxy methods are passed by reference so can be in/out parameters (in .NET output arguments must be passed by reference).
Value Semantics
PML Gadgets and DB elements have reference semantics when they are copied whereas all other objects have value semantics when they are copied. This is controlled by the Assign() method on the .NET class. So, for example if the Assign() method here copies the value then
!a = object netobject()
!a.val(1)
!b = !a
!b.val(2)
then
q var !a.val() returns 1
and
q var !b.val() returns 2
for example, !a and !b do not point to the same object.
In order to perform either a shallow or deep copy of the member data inside the .NET class the Assign() method must be defined on the .NET class (see rules). This is analogous to overriding the operator "=" in C++.
Method Overloading
Overloading of methods is supported for all variable types in PML so a .NET Proxy can be created from a .NET class which has overloaded methods.
Custom Attributes
The custom attribute [PMLNetCallable()] is used to describe the PML interface for a .NET class. This metadata allows the PML callable assemblies to be self-describing. This clearly defines the class and allows an assembly to expose a subset of its public interface to PML. The PMLNetEngine uses this metadata to decide which .NET class definitions can be created in PML. Reflection is used to load an assembly and create PML class definitions. All classes and methods for which PML Proxy class definitions will be created must be marked as PMLNetCallable. The assembly itself must also be marked as PMLNetCallable.
So, a PML callable .NET class in C# looks like this:
[PMLNetCallable()]
namespace PMLNet
{
[PMLNetCallable ()]
public class PMLNetExample
{
[PMLNetCallable ()]
public PMLNetExample()
{
}
[PMLNetCallable ()]
public void DoSomething(double x, double y, double z)
{
z = x + y;
}
}
}
This class has a default constructor and a single method. Both the constructor and method are marked as PMLNetCallable along with the class itself.
The assembly itself must also be marked as PMLNetCallable. This is normally done in the AssemblyInfo file as follows
using Aveva.Core.PMLNet;
[assembly: PMLNetCallable()]
Private Data and Properties
In PML there is no concept of private members or methods - everything is public. Access to public data in .NET must be via properties or get/set accessor methods. Properties in .NET class are defined as get and set methods in PML. So, for example the following PMLNetCallable property in .NET
[PMLNetCallable()]
public double Val
{
get
{
return mval;
}
set
{
mval = value;
}
}
would have the following Proxy methods in PML
REAL VAL()
VAL(REAL)
Scope
PML variables are of two kinds: global and local. Global variables last for a whole session (or until you delete them). A local variable can be used only from within one PML function or macro. The lifetime of the .NET instance is controlled by the scope of the PML proxy.
Instantiation
Classes can have any number of overloaded constructors but must have a default constructor which is marked as PMLNetCallable. The PML Proxy constructor instantiates an instance of the underlying .NET class. When the proxy goes out of scope the destructor destroys the underlying .NET instance.
ToString() Method
The string() method is available on all PML objects. For a .NET Proxy this will call the ToString() method on the .NET instance. If the ToString() method is overridden on the .NET class then this will be called.
Method Names
PML is case independent, so it is not possible to have MyMethod() and MYMETHOD() in .NET. PML will report non-unique object/method names.
Double Precision
Doubles are used in PML.NET to store reals and ints so doubles must be used in .NET (integers are not available in PML)
Events
Events on PML.NET objects may be subscribed to from PML. A PML callback on a particular instance may be added to an event on another PML.NET instance. Events are defined by a .NET component by associating the delegate PMLNetEventHandler with the event. This delegate has the signature
__delegate void PMLNetEventHandler(ArrayList __gc *args);
Where args is an array of event arguments of any PML.NET type (see table of valid types). The following code associates this delegate with an event
[PMLNetCallable()]
public class PMLNetExample
{
[PMLNetCallable()]
public event PMLNetDelegate.PMLNetEventHandler PMLNetExampleEvent;
[PMLNetCallable()]
public PMLNetExample ()
{
}
[PMLNetCallable()]
public void Assign(PMLNetExample that)
{
}
[PMLNetCallable()]
public void RaiseExampleEvent()
{
ArrayList args = new ArrayList();
args.Add("PMLNetExampleEvent ");
args.Add("A");
if (PMLNetExampleEvent!= null)
PMLNetExampleEvent(args);
}
}
This event can then be caught in Programmable Macro Language (PML) by adding an appropriate callback to the instance raising the event
!n = object pmlnetexample()
!c = object netcallback()
!handle = !n.addeventhandler('pmlnetexampleevent', !c, 'callback')
Where
-
!n is the PML.NET instance on which the event will be raised
-
!c is the instance of a PML object with a method callback() with the appropriate arguments
At some later time the event handler may be removed
!n.removeeventhandler('pmlnetexampleevent', !handle)
where !handle is the handle of the PML.NET delegate returned by addeventhandler().
Netcallback is a PML object defined as
define method .callback(!array is ARRAY)
!args = 'NETCALLBACK object ' + !array[0] + !array[1]
$P $!args
endmethod
Error Handling
Exception handling is placed around the Invoke method to handle .NET method invocation exceptions like TargetException, ArgumentException etc. The result of catching such an exception is to ultimately return a PMLError object from PMLNetProxy::Invoke() which results in a PML exception (1000,n) being thrown where 1000 is the module number for PML.NET. .NET can throw its own PML exceptions. The exception to throw is PMLNetException. For example
throw new PMLNetException(1000, 1, "PMLNetExample Exception");
This may then be handled inside a PML macro i.e.
handle(1000,1)
…
endhandle
Any other exception within the loaded assembly itself is caught by the global exception handler inside the AVEVA module.
Rules for Calling .NET
Certain rules must be adhered to when defining a .NET class which is PML callable. These are enforced by the PMLNetEngine when an assembly is imported. They are
-
PML callable assemblies must be marked as PMLNetCallable and reside in the %AVEVA_DESIGN_EXE% directory, subdirectory of the application or UNC path.
-
Only classes may be PML Callable (this excludes Structures, Interfaces, Enums, …).
-
A PML callable class must be marked as PMLNetCallable.
-
A PML callable method must be marked as PMLNetCallable.
-
A PML callable method can only pass valid argument types (see table of types).
-
PML callable classes and methods must be public.
-
PML callable methods with default arguments cannot be defined.
-
PML callable class and method names must be case independent.
-
PML callable classes must have an Assign() method.
-
PML callable classes must have a public default constructor which is PMLNetCallable.
If these rules are not adhered to then errors are reported to the trace log when the assembly is loaded and a PML class definition will not be created. If the class definition has not been defined then the following PML error will result
(46,87) PML: Object definition for XXX could not be found.
Tracing
In order to output trace to a log file and the console window add the following lines to the exe's config file
<system.diagnostics>
<switches>
<add name="PMLNetTraceSwitch" value="4" />
</switches>
</system.diagnostics>
<appSettings>
<add key="PMLNetTraceLog" value="C:\temp\PMLNetTrace.log" />
</appSettings>
This will create the file PMLNetTrace.log in C:\temp and log all the valid class definitions as they are imported.