Understanding the Synergy .NET assembly API

The Synergy .NET assembly API enables your code to take advantage of .NET features from a non-.NET environment. Using the .NET assembly API, you can load a .NET assembly; instantiate types defined in that assembly; and communicate with the methods, properties, fields, and events of those objects. By adding .NET components to your application, you can extend its capabilities with a wide variety of interactive and multimedia effects.

The Synergy .NET assembly API methods belong to one of the following classes:

Important

We recommend that you use this API for reference only, and instead use the gennet utility (with .NET Core or .NET 5 and higher) or gennet40 utility (with .NET Framework) to generate wrappers if you want to have natural Synergy syntax for interoperation with .NET. Using gennet/gennet40 will also enable your syntax to move almost seamlessly to Synergy .NET in the future. See gennet/gennet40 utility for more information.

Requirements

The .NET assembly API supports Microsoft .NET (.NET Framework, .NET Core, and .NET 5 and higher).

.NET must be installed on each computer that uses this API. This means that if the Synergy application that uses the .NET assembly API is installed in a shared configuration, .NET must be installed on each client machine. You can obtain .NET updates through Windows Update. If you use Windows Server Update Services, you can force this update to be applied to all computers that need it.

The Synergy .NET assembly API has the following additional requirements and restrictions:

Callback routines

A callback routine in .NET is called a delegate. A delegate is a Synergy DBL routine that is called whenever a specific event is raised by .NET. A delegate must be created with the DotNetDelegate constructor and bound to the event using the AddDelegateToEvent method. You can write to arguments in callback routines if the delegate defines them as writable. The .NET parameters for delegates determine the type of argument passed to the Synergy method. (See DotNetObject.AddDelegateToEvent for syntax and details on the conversion rules.)

Note

A callback routine cannot perform a STOP chain or throw an exception that can be caught back in the Synergy caller. The runtime will generate an $ERR_NOCHAIN error if a callback routine attempts to perform a STOP chain. Unpredictable results can occur if a callback routine throws an exception.

You can add and remove event handlers for code generated by the gennet/gennet40 utility using the ADDHANDLER and REMOVEHANDLER statements, respectively.

Be aware of synchronicity with your callback routines. The .NET Common Language Runtime (CLR) may fire many simultaneous events as the user moves the mouse, for instance. While your program is in a callback routine, it can be interrupted and the same callback called recursively. Ensure all callback routines are re-entrant, and don’t use writable global data.

Error handling

The DotNetException class represents an exception raised by .NET. Any exception thrown by the .NET assembly code triggers a DotNetException exception, which references and wraps the .NET exception so you can catch it and then look at the native .NET exception properties using the .NET assembly API.

The DotNetException class consists of the following members:

Object member

Description

Message

The text of the Message property of the original .NET exception.

NativeException

A public property of type DotNetObject. It contains a reference to the original exception raised by .NET.

If an exception is thrown (or a runtime error is generated that is not caught by either a TRY-CATCH or ONERROR statement) while executing code in a Synergy delegate, the exception will unwind the stack up to the point where .NET code is executing. The interop layer throws a .NET exception that can be caught by the .NET assembly code, allowing it to handle the exception or rethrow it back to Synergy. When possible, the exception object will have accurate Synergy line information for the error location, as well as a stack trace.

When crossing the Synergy/.NET boundary, the interop layer attempts to convert to a native exception type (for example, System.Exception, which exists in both Synergy and .NET). When a conversion is not possible, .NET will throw a dynamically generated exception that inherits from System.Exception. The interop layer converts .NET exceptions to a Synergex.Synnet.DotNetException when they cannot be converted to a native Synergy runtime exception.

Note

Avoid throwing an exception across a boundary, especially a boundary where .NET is handling Windows messages to execute code. Failure to trap errors may cause the program to crash with a segmentation fault or stack overflow.

Using .NET assemblies referenced by the Synergy .NET assembly API

Whenever the version or strong name of an assembly (or a dependent assembly) changes, the Synergy wrappers must be regenerated and recompiled. We recommend that you not use auto-incrementing version numbers on assemblies used with the .NET assembly API. The version should change whenever any of the method signatures or existing interfaces are changed. The version does not have to change if new methods or interfaces are added, but you must regenerate your wrappers to take advantage of these new entities. This is because the wrappers generated by gennet/gennet40 load assemblies either from a Synergy environment variable location or from the .NET display name, and specific versions of the assemblies and wrappers will need to change if these locations or versions change.

We also strongly recommend the following:

If you are...

We recommend that you...

Not going to place your assemblies in the same location as your .dbr files

Put these assemblies in the .NET global assembly cache (GAC) so the generated wrappers will work on any system. (Note that .NET Core and .NET 5 and higher don’t have a GAC.)

Distributing software, rather than creating an in-house solution

Strong name your assemblies.

Using shared installations for your .dbr files and assemblies

Use the environment variable form instead of putting them in the GAC, and add a local policy to allow them to load.

Note

You cannot use dbr.exe.config to set your .NET settings, because Microsoft does not support an app.exe.config file with hosted .NET applications.

If an assembly is loaded into gennet/gennet40 as a dependency and later at runtime has one of its types loaded before the original loading assembly, it will not be able to find the DLL outside of the GAC or currently running dbr.exe directory. If an assembly dependency fails to load from the GAC or the current gennet/gennet40 directory, gennet/gennet40 will try to load from each of the directories from which it has successfully loaded other assemblies. This list of directories is processed in order of appearance. If an attempt is made to load a duplicate assembly (with the same display name but a different file path), the DLL will not be found, and a “Could not load assembly” error ($ERR_NOLOAD) will be generated.

Workbench support

Professional Series Workbench facilitates the use of .NET assemblies in your applications as long as you use gennet/gennet40 (rather than the Synergy .NET API) to generate your wrappers. Including the files generated by gennet/gennet40 in a separate tag file allows the tag information for the .NET assemblies to be used across multiple projects. See “Tagging imported classes” in the Workbench Help for the most efficient way to tag your classes in Workbench.

UI Toolkit support

To integrate .NET Windows Forms into UI Toolkit, you must use a special Toolkit container window. A Toolkit container window has the basic characteristics of a Toolkit window (character-based increments, placement order, maintained environment, and title manipulation, for example), yet it has the display and operational characteristics of a .NET container (load controls, get/set property values, call methods, bind callback routines, and so forth).

Debugging

The .NET assembly API includes a debugging system to enable you to analyze problems in your code. To enable debugging, set the SYNNET_DEBUG environment variable to 1 before the first assembly is loaded.

When debugging is enabled, debug messages are logged using the OutputDebugString Windows API function. You can view these messages using Visual Studio, other compatible C-level debuggers, or the debugview program from technet.microsoft.com/en-us/sysinternals/bb896647.

Debug messages are logged when any of the following occur:

If the InnerException property is non-null, the same information is recursively output for the InnerException.

When the Synergy runtime shuts down, the debug output includes a count of objects deleted at shutdown. If this count is nonzero, objects were not released by the application.

Sample programs

Example 1

main
record
    asm         ,@DotNetAssembly
    obj         ,@DotNetObject
    obj2        ,@DotNetObject
    obj3        ,@DotNetObject
    itmp        ,i4
endrecord
proc
    open(1,o,'tt:')
    asm = new DotNetAssembly("mscorlib")

    ;Constructor with parameter
    ;DateTime(Int64)  initializes a new instance of the DateTime structure to
    ;  a specified number of ticks.
    obj = new DotNetObject(asm, "System.DateTime", 34567)

    ;Call example, capitalization is important in object names
    writes(1, (string)obj.Call("ToString"))

    ;Call example with parameter, (void) is  to prevent a level 4 compiler warning
    ;AddHours  adds the specified number of hours to the value of this instance.
    (void)obj.Call("AddHours", 5.5)

    ;Static call example with parameter
    obj2 = (DotNetObject)((DotNetObject)asm).Call("System.DateTime.Parse",
    &      "13 March 1986")

    ;Get property
    writes(1, %string((int)obj.GetProperty("Hour")))
    ;Get static property
    obj = (DotNetObject)((DotNetObject)asm).GetProperty("System.DateTime.Now")
    ;No boxing form
    obj.GetProperty("Hour", itmp)
    writes(1, %string(itmp))

    obj3 = new DotNetObject(asm, "System.Collections.BitArray", (int)102293);
    writes(1, %string((int)obj3[1]))
endmain

Example 2

main
record
    asm                 ,@DotNetAssembly
    sys                 ,@DotNetAssembly
    form                ,@DotNetObject
    button              ,@DotNetObject
    form_OnLoad         ,@DotNetDelegate
    button_OnClick      ,@DotNetDelegate
    callbackObject      ,@ns1.cls1
    controls            ,@DotNetObject
endrecord

proc
    open(1,o,'tt:')
    ;Display name-based loading
    asm = new DotNetAssembly("System.Windows.Forms, Version=2.0.0.0, Culture=neutral, 
    &  PublicKeyToken=b77a5c561934e089")
    sys = new DotNetAssembly("mscorlib")
    callbackObject = new ns1.cls1()
    form = new DotNetObject(asm, "System.Windows.Forms.Form")
    button = new DotNetObject(asm, "System.Windows.Forms.Button");
    button_OnClick = new DotNetDelegate(sys, "System.EventHandler",  ^NULL,
    &                "ns1.cls1.static_meth1")
    form_OnLoad = new DotNetDelegate(sys, "System.EventHandler", callbackObject, "meth2")
    
    button.AddDelegateToEvent("Click", button_OnClick)
    form.AddDelegateToEvent("Load", form_OnLoad)
    
    button.SetProperty("Text", "button1")
    form.SetProperty("Text", "form1")
    controls = (DotNetObject)((DotNetObject)form.GetProperty("Controls")).Call("Add", button)
    
    ;(void) suppresses the level 4 warning about not using the return value
    (void)((DotNetObject)asm).Call("System.Windows.Forms.Application.Run", form)
    
    ;This call isn’t strictly required, as the references are cleaned up 
    ; automatically when the delegate goes out of scope.
    button.RemoveDelegateFromEvent("Click", button_OnClick)
    form.RemoveDelegateFromEvent("Load", form_OnLoad)
endmain
namespace ns1
    class cls1
        public static method static_meth1, void
        sender, @DotNetObject
        e, @DotNetObject
        endparams

        proc
            writes(1, "In meth1 callback")
            mreturn
        endmethod

        public method meth2, void
        sender, @DotNetObject
        e, @DotNetObject
        endparams

        proc
            writes(1, "In meth2 callback")
            mreturn
        endmethod
    endclass
endnamespace