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).
.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:
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; } } } |
After creating the C#.NET DLL with the appropriate exported functions, it’s time to use it in Clarion. The process is outlined below:
Here's Brahn Patridge's Libmaker with the C# DLL loaded, before clicking Save as:
Adding the lib to the project:
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:
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.
|
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.