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:
- DotNetAssembly, which represents a .NET assembly
- DotNetObject, which represents an instance of a .NET object
- DotNetDelegate, which represents a .NET delegate
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:
- .NET’s bitness must match the address size of the runtime used. (In other words, 32-bit .NET must be used for the 32-bit runtime, and 64-bit .NET must be used for the 64-bit runtime.)
-
On .NET Core, you should publish for x64 or x86, because .NET Core does not have a global assembly cache. Set the SYNNET_CLR environment variable to v6+ to enable the .NET assembly API to take advantage of .NET Core features.
- If you use STOP chains with this API, .NET assemblies will not be unloaded, and if static members are set in those assembly instances, they will still be set if the assembly is accessed in the chained-to program. Chaining does not remove any threads for the loaded assembly. If a .NET method creates any threads, those threads are not shut down on a STOP chain; they are only shut down when the dbr.exe process exits.
- Any UI components that are automated with this interface should operate modally. The API does not expect an application to leave a UI component displayed and simultaneously enable access to Synergy/DE components.
- Synergy code that uses the API cannot be compiled on non-Windows systems.
- The API does not support passing the result of a property call (that is, a .NET property) into a method that accepts it as a by-reference parameter.
- Static methods and fields are not directly supported, except through the assembly object.
- Generic types and methods are not supported.
- Multi-dimensional arrays are not supported. Use an ArrayList instead. (See Dynamic arrays for more information about ArrayLists.)
- If you deploy an application created with the .NET assembly API in a shared configuration, you will need to deal with the challenges of the .NET security implementation, which include intranet security zone settings and Code Access Security Policy (caspol). These security issues apply any time you use a .NET component on a network share—not just .NET components used with Synergy/DE products. See Synergex KnowledgeBase article 1853 for details.
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.)
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.
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. |
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:
- An assembly is loaded, along with its parameters and returned object ID.
- An object or delegate is created, along with its parameters and returned ID.
- Any method or property is invoked, along with its parameters and return value.
- Any field is accessed, along with its value.
- Any delegate is associated with or removed from an event.
- Any error is encountered. The information from the exception is logged and includes the following:
- The typename of the exception (fully qualified)
- The Message property of the exception
- The Source property of the exception
- The StackTrace property of the exception
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