Clarion and C#.NET Interop without COM
by Unknown user
Clarion has always had the capability of calling functions and procedures from non-Clarion DLLs. Documentation on the mechanics of how to do this is available within LanguageReference.pdf in the Clarion installation docs directory and does a fair job of explaining how to do things – as long as you are calling Windows API functions or functions in other non-.NET DLLs.
While the .NET frameworks have become a standard means of programming for Windows, the DLLs produced by .NET languages cannot be called from Clarion in the traditional way.
When searching the Internet (and other areas such as newsgroups), I found very little information on this topic. A handful of articles were available, none of which had a complete, tested, documented methodology.
After gleaning as much information as possible from the few articles available, searching the various Clarion newsgroups for answers, and expending many hours of trial and error, I acquired enough information to describe the process. In this article I will show the specific details of how to accomplish this from the Clarion side (using Clarion 9.1) as well as from the .NET side (using C# and Visual Studio 2012). You can apply the technique to other versions of Clarion and to other .NET languages.
As with any software development, there are many approaches and solutions to a problem. Developing this methodology is no different. There are many possible approaches, but this one attempts to stay with "Clarion norms" in terms of calling conventions, primarily using strings. Also, I use this methodology in a large scale real world application (approximately 2000 procedures across 85 DLLs).
Creating/Preparing a C#.NET DLL for use by Clarion
.NET DLLs contain “managed code”, which cannot be used directly by non-.NET applications because of differences in calling conventions.To create a .NET DLL that can be used by Clarion, the functions (which are actually class methods) need to be exported as “unmanaged” assemblies. Once an unmanaged DLL is created, it can then be used by Clarion just like any other non-Clarion DLL. The steps below outline the process of creating an “unmanaged” DLL:
- Create a C#.NET DLL/assembly as you normally would.
- Write the method(s) you wish to call from Clarion.
- Select the processor type to compile for (X86, i64, etc) – you must select a specific processor.
- Import the RGiesecke.DllExport NUGET package into the C# project
- This package is what allows for automated creation of unmanaged exports
- Add a “using” clause to the assembly (see source lines 6/7)
- Add DLLExport code/tags in front of each function to be exported (see source lines 14,21,29). These tags:
- Set the calling convention to stdcall
- Set the name of the function (as Clarion will see it)
- Parameters may need some special handling:
- INT (LONG) and BYTE types can be passed natively
- Strings take special handling – they must be passed/received as BSTRINGS
- Use the MarshalAs BSTR attribute on any passed or returned strings (see source lines 22,23,30,31)
- Build the C#.NET solution.
- The DLL is ready to use, but the .LIB file will need to be created as “Clarion-friendly”.
As a side note, unmanaged exported functions can call other (managed) functions within the C# assembly. For example, you could create a JSON API to do conversions to/from JSON format. Many times it’s much easier to create a .NET assembly to handle a specific task than to try and handle that task directly in Clarion. With these methodologies you can quickly create the .NET code, export it, and write a Clarion function to call the code just as if it were written as a Clarion DLL.
First, create a new class library:
The default CPU is "any".
Edit the CPU...
and change it to x86:
Add Robert Giesecke's UNmanagedExports library using NuGet:
You can also do this from the Package Manager console with
Install-Package UnmanagedExports
Write the source for the DLL, using the DllExport attribute:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Runtime.InteropServices; using RGiesecke.DllExport; namespace CMagLib { public class CMagClass { [DllExport("Add2", CallingConvention = CallingConvention.StdCall)] public static int Add2(int arg1, int arg2) { return arg1 + arg2; } [DllExport("SubTxt2_3", CallingConvention = CallingConvention.StdCall)] [return: MarshalAs(UnmanagedType.BStr)] public static string SubTxt2_3([MarshalAs(UnmanagedType.BStr)] string arg1) { return arg1.Substring(1, 2); } [DllExport("RetText2", CallingConvention = CallingConvention.StdCall)] [return: MarshalAs(UnmanagedType.BStr)] public static string RetText2([MarshalAs(UnmanagedType.BStr)] string arg1, [MarshalAs(UnmanagedType.BStr)] string arg2) { return arg2; } } }
Using a C#.NET DLL in Clarion
After creating the C#.NET DLL with the appropriate exported functions, it’s time to use it in Clarion. The process is outlined below:
- Create a Clarion-friendly .LIB file using LibMaker (see the Clarion help). However the shipping LibMaker won't work - go to the Fushisoft site and downoad Brahn Partridge's version.
- Create a new Clarion app as an EXE – this app will call the C# DLL
- Copy the C# DLL and LIB file (from step 1) to the root of your new app’s folder. You will also need RGiesecke.DllExport.Metadata.dll from the C# project.
- Add the C# DLL/LIB file to the Clarion solution
- In the Global Embeds, Inside Program Map
- Add a module statement that references the C# DLL
- Add the procedure declarations using:
- ,NAME() attribute
- ,PASCAL, RAW, DLL(True) calling convention attributes
- Declare any STRINGs as BSTRINGS, whether passed or received
- Parameter types are required, parameter names are not
- Create a Clarion procedure that will call the C# DLL
- The C# procedure is called like any other Clarion procedure would be called
- Passing and receiving LONG and BYTE data types requires no special handling
- When using strings, they must be passed/received as BSTRINGs, which are relatively undocumented in Clarion:
- To declare a BSTRING variable, no “size” specification is given
- BSTRINGs are assigned data from Clarion strings using bvar = clip(strvar)
- BSTRING data can be converted to a string using strvar = bvar
Here's Brahn Patridge's Libmaker with the C# DLL loaded, before clicking Save as:
Adding the lib to the project:
SDI vs. MDI applications and Recursive Calls to the C# DLL
SDI applications can call the functions in a C# DLL multiple times without issue, but MDI applications that use an application frame and START() procedures from that frame must handle repeated calls to the C# DLL a bit differently.
Methods exported from a C# DLL may only be called once per thread
To handle this:
- Create a Clarion procedure that calls the C# DLL
- Call the procedure by using START() – the process will automatically kill/close the thread when it’s done processing
Defining the procedures in the Clarion map:
module('CMagLib.dll') Add2 procedure(long, long), long, name('Add2'), pascal,raw,dll(true) SubTxt2_3 procedure(bstring), bstring, name('SubTxt2_3'), pascal,raw,dll(true) RetText2 procedure(bstring, bstring), bstring, name('RetText2'), pascal,raw,dll(true) end
Calling the C# DLL from Clarion:
Main PROCEDURE d byte num1 long num2 long retsum long strtxt1 string(100) strtxt2 string(100) bstr1 bstring bstr2 bstring bretstr bstring CODE message('Add 2 Numbers') loop d = 1 to 5 num1 = d num2 = d * 2 retsum = Add2(num1, num2) message(num1 & ' + ' & num2 & ' = ' & retsum, 'Add 2 Numbers') end message('Pass String|Return Characters 2-3','String Params') strtxt1 = 'Message' bstr1 = clip(strtxt1) bretstr = SubTxt2_3(bstr1) message('String = ' & bstr1 & '|Return String = ' & bretstr, 'Return Chars (2-3)') message('Pass 2 Strings|Return 2nd String','String Params') strtxt1 = 'Test' strtxt2 = 'Message' bstr1 = clip(strtxt1) bstr2 = clip(strtxt2) bretstr = RetText2(bstr1, bstr2) message('String1 = ' & bstr1 & '|String2 = ' & bstr2 & '|Return String = ' & bretstr, 'Return 2nd String')
When I was using the test application in this article, calling .NET functions multiple times worked without issue. However, this methodology failed when I put it into a large, multi-DLL system. In the test application, some calls are made to the DLL and the results are displayed in a message window. No START() function is used - just a direct call to the DLL.
The specific scenario that failed, along with its solution, is as follows:
The C#.NET DLL uses a set of controls from the text messaging service Plivo. The exported C# function (let’s call it SendTextMsg) is used to send text messages in a “one-off” mode, as well as in a batch.
The SendTextMsg function was incorporated into a large-scale, multi-DLL system (1600+ procedures over 90+ DLLs and 20+ EXEs). The system incorporates a main frame and most calls from the frame are STARTed(), which runs the process on a new thread.
An example:
Call MainFrame --> START(PatientList) --> SendTextMsg, then return to PatientList. At this point, if another text is sent, the system GPFs. However, if you back out of the PatientList and reopen it, another text can be sent. This tells me that an external C#.NET DLL may only be called once per thread. To correct for the GPF I created a wrapper procedure that calls the .NET DLL (call it wrapSendTextMsg). When sending a text I call START(wrapSendTextMsg, paramsGrp), where paramsGrp is a data group formatted with the appropriate parameters needed by the wrapSendTextMsg procedure.
As in the earlier example, here's the new call chain:
Call MainFrame --> START(PatientList) --> START(SendTextMsg). This solved the problem and also made the process asynchronous in that the user is back to the PatientList without having to wait for the text message to be sent. Also, this allows for a looped process to be created where a batch of text messages could be sent from a STARTed procedure.
Note
If a batch process were created as an SDI app, none of this applies as there is no multi-threading in an SDI app.
Hopefully this information is as useful to you as it has been to me. Although the steps outlined above are for calling C#.NET assemblies from Clarion, the reverse can also be done. In a follow up article I will give the details on how to go the other direction (C#.NET assemblies calling Clarion DLLs), as well as how to handle calling DLL functions that include windows for display, entry and interaction.