Getting EnvDTE.DTE instance outside Visual Studio IDE - c#

I am creating a project automation tool in Visual Studio 2013 where I have my own project template and I am trying to add it to an existing solution programatically.I am using the following code in a console application.
EnvDTE.DTE dte = (EnvDTE.DTE)Marshal.GetActiveObject("VisualStudio.DTE.12.0");
string solDir = dte.Solution.FullName;
solDir=solDir.Substring(0, solDir.LastIndexOf("\\"));
dte.Solution.AddFromTemplate(path, solDir+"\\TestProj", "TestProj", false);
It is working when I run the application from Visual Studio IDE. But when I try to run the exe from command prompt, I get the following exception.
Unhandled Exception: System.Runtime.InteropServices.COMException: Operation unav
ailable (Exception from HRESULT: 0x800401E3 (MK_E_UNAVAILABLE))
at System.Runtime.InteropServices.Marshal.GetActiveObject(Guid& rclsid, IntPtr reserved, Object& ppunk)
at System.Runtime.InteropServices.Marshal.GetActiveObject(String progID)
at ProjectAutomation.Console.Program.Main(String[] args)
I want to know whether there is any way to get the active EnvDTE.DTE instance outside Visual Studio IDE .?

Automating an existing Visual Studio instance from an external tool to modify a loaded solution is a bad idea. If you use GetActiveObject(...) and there are two Visual Studio instances launched, how do you know that the correct instance is returned? And what if the user or Visual Studio is doing something with the solution when the user launches the external tool? There are two better approaches:
1) Use an external tool to automate a new Visual Studio instance, load the desired solution and modify it. This can be done even with the VS instance not visible. To create a new instance the proper code is:
System.Type type = Type.GetTypeFromProgID("VisualStudio.DTE.12.0");
EnvDTE.DTE dte = (EnvDTE.DTE) System.Activator.CreateInstance(type);
dte.MainWindow.Visible = true;
...
2) Use a Visual Studio extension, such as a macro (VS 2010 or lower), add-in (VS 2013 or lower) or package (any VS version) to provide a menu item or button toolbar that, when clicked, modifies the currently loaded solution. This prevent the "busy" scenario because if VS is busy the menu item or toolbar button can't be clicked (unless the "busy" operation is asynchronous).

I found an alternative to GetActiveObject, here, where Kiril explains how to enumerate the ROT. There are other examples on MSDN.
Since some SO users don't like links here are the details:
Enumerate all of the processes, named devenv.exe.
Show a list of main window titles. (I strip "Microsoft Visual Studio" off the end)
Ask the user which one they want to use.
Use the process.Id to find an object in the ROT, which I believe is the OP's question. This is done by enumerating the ROT using IEnumMoniker.Next(), which returns monikers and process id's (in the case of VS).
Having found the moniker. Cast the running object to a DTE and off you go.
COM, ROT and Moniker sounded too complex for me, so I was happy to see that the heavy lifting had already been done at the link above.
I had the example working in a couple of minutes. It worked the first time I stepped through with the debugger. But at full speed, I needed to add some sleeps or retries, because it is easy to get an exception from HRESULT: 0x8001010A (RPC_E_SERVERCALL_RETRYLATER))
Also, I replaced the exact match with a regex that tolerates other versions of VS:
Regex monikerRegex = new Regex(#"!VisualStudio.DTE\.\d+\.\d+\:" + processId, RegexOptions.IgnoreCase);
The issue where VS may be busy, with an open dialog, or compiling many projects is common to many applications you might try to force feed key strokes or COM requests. If you get an error retry for a few seconds. Finally, pop up a message box if needed.

Some SO users don't like links because links get broken ;)
Took me over an hour to write my version so might as well post it here. Requires references to envdte and envte80 (from add references / assemblies / extensions). Provides static method for opening a C# file in Visual Studio or Notepad++ as a backup and optionally navigate to a specific line.
using System;
using System.Diagnostics;
using EnvDTE80;
using System.Runtime.InteropServices.ComTypes;
using System.Runtime.InteropServices;
namespace whatever
{
public static class CsFile
{
public static void Open(string fileName, int? lineNr = null)
{
try
{
OpenFileInVisualStudio(fileName, lineNr);
}
catch
{
try
{
OpenFileInNotePadPlusPlus(fileName, lineNr);
}
catch
{
// Woe is me for all has failed. Somehow show an error.
}
}
}
public static void OpenFileInVisualStudio(string fileName, int? lineNr = null)
{
DTE2 dte = null;
TryFor(1000, () => dte = GetDteByName("VisualStudio.DTE"));
if (dte == null) throw new Exception("Visual Studio not running?");
dte.MainWindow.Activate();
TryFor(1000, () => dte.ItemOperations.OpenFile(fileName));
if (lineNr.HasValue) TryFor(1000, () => ((EnvDTE.TextSelection)dte.ActiveDocument.Selection).GotoLine(lineNr.Value, true));
}
public static void OpenFileInNotePadPlusPlus(string fileName, int? lineNr = null)
{
if (lineNr.HasValue) fileName += " -n" + lineNr.Value.ToString();
Process.Start(#"C:\Program Files (x86)\Notepad++\notepad++.exe", fileName);
}
private static void TryFor(int ms, Action action)
{
DateTime timeout = DateTime.Now.AddMilliseconds(ms);
bool success = false;
do
{
try
{
action();
success = true;
}
catch (Exception ex)
{
if (DateTime.Now > timeout) throw ex;
}
} while (!success);
}
static DTE2 GetDteByName(string name)
{
IntPtr numFetched = Marshal.AllocHGlobal(sizeof(int));
IRunningObjectTable runningObjectTable;
IEnumMoniker monikerEnumerator;
IMoniker[] monikers = new IMoniker[1];
IBindCtx bindCtx;
Marshal.ThrowExceptionForHR(CreateBindCtx(reserved: 0, ppbc: out bindCtx));
bindCtx.GetRunningObjectTable(out runningObjectTable);
runningObjectTable.EnumRunning(out monikerEnumerator);
monikerEnumerator.Reset();
while (monikerEnumerator.Next(1, monikers, numFetched) == 0)
{
IBindCtx ctx;
CreateBindCtx(0, out ctx);
string runningObjectName;
monikers[0].GetDisplayName(ctx, null, out runningObjectName);
if (runningObjectName.Contains(name))
{
object runningObjectVal;
runningObjectTable.GetObject(monikers[0], out runningObjectVal);
DTE2 dte = (DTE2)runningObjectVal;
return (dte);
}
}
return null;
}
[DllImport("ole32.dll")]
private static extern int CreateBindCtx(uint reserved, out IBindCtx ppbc);
}
}

Related

Can I programmatically attach a running .NET 6 process to a running instance of Visual Studio debugger?

I'm debugging an IPC logic where one .NET 6 processes is launching another, and I can't get to debug both within the same Visual Studio 2022 instance.
When the child process hits DebugBreak(true) as below:
[Conditional("ATTACH_DEBUGGER")]
public static void DebugBreak(bool condition)
{
if (condition)
{
if (!System.Diagnostics.Debugger.IsAttached)
{
System.Diagnostics.Debugger.Launch();
System.Diagnostics.Debugger.Break();
}
}
}
I get prompted to launch a new instance of VS2022 to debug it:
However, I want to debug it within the existing VS instance, were I'm already debugging the parent process.
Is that possible at all? Is there a VS config setting I might be missing?
Current workaround: attaching manually, while putting the child process in the waiting state using WaitForDebugger() below:
[Conditional("ATTACH_DEBUGGER")]
public static void WaitForDebugger()
{
using var process = Process.GetCurrentProcess();
Console.WriteLine($"Waiting for debugger, process: {process.ProcessName}, id: {process.Id}, Assembly: {Assembly.GetExecutingAssembly().Location}");
while (!System.Diagnostics.Debugger.IsAttached)
{
// hit Esc to continue without debugger
if (Console.KeyAvailable && Console.ReadKey(intercept: true).KeyChar == 27)
{
break;
}
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.Beep(frequency: 1000, duration: 50);
}
}
I have a very similar setup where the following steps achieve this. However, this is with .NET Framework so I cannot guarantee it will work with .NET Core. (UPDATE: see below for .NET Core approach; see end for link to complete code samples on GitHub.)
First, identify a point in your first process (say, FirstProcess) where you want to attach the debugger to the second process (SecondProcess). Obviously SecondProcess will need to be running by this point (as SecondProcess.exe, say).
Open Solution Explorer and navigate to References for the relevant project in FirstProcess. Right-click, search for "env" and add two references, EnvDTE (v.8.0.0.0) and EnvDTE80 (v.8.0.0.0).
Add the following methods to the class in FirstProcess from where you want to attach:
private static void Attach(DTE2 dte)
{
var processName = "SecondProcess.exe";
var processes = dte.Debugger.LocalProcesses;
// Note: Depending on your setup, consider whether an exact match is required instead of using .IndexOf()
foreach (var proc in processes.Cast<EnvDTE.Process>().Where(proc => proc.Name.IndexOf(processName) != -1))
{
proc.Attach();
}
}
private static DTE2 GetCurrent()
{
// Note: "16.0" is for Visual Studio 2019; you might need to tweak this for VS2022.
var dte2 = (DTE2)Marshal.GetActiveObject("VisualStudio.DTE.16.0");
return dte2;
}
Visual Studio should now prompt you to add the following references to your class:
using System.Runtime.InteropServices;
using EnvDTE80;
Finally, insert the following line at the point in FirstProcess where you wish to attach the debugger to SecondProcess:
Attach(GetCurrent());
If you manage to get this working, please feel free to edit this answer with any changes needed for the .NET Core environment.
UPDATE - for .NET Core:
For .NET Core there are two problems to overcome:
The DTE references are not available in .NET Core. However, they can
be added as NuGet packages. You will need to add two NuGet
packages, envdte and envdte80 (both by Microsoft with 2M+
downloads), to FirstProcess.
The method Marshal.GetActiveObject() is not available in .NET
Core. To resolve this you can get the source code from Microsoft
(here) and add it manually; this has already been done in
the code sample below.
What follows is a complete working .NET Core code sample for FirstProcess. This correctly attaches programmatically to SecondProcess after startup.
namespace FirstProcess
{
using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security;
using System.Threading;
using EnvDTE80;
class Program
{
private const string FileName = "C:\\Users\\YourUserName\\source\\repos\\TwoProcessesSolution\\SecondProcess\\bin\\Debug\\net5.0\\SecondProcess.exe";
private const string ProcessName = "SecondProcess";
static void Main(string[] args)
{
var childProcess = Process.Start(new ProcessStartInfo(FileName));
Attach(GetCurrent());
while (true)
{
// Your code here.
Thread.Sleep(1000);
}
childProcess.Kill();
}
private static void Attach(DTE2 dte)
{
var processes = dte.Debugger.LocalProcesses;
// Note: Depending on your setup, consider whether an exact match is required instead of using .IndexOf()
foreach (var proc in processes.Cast<EnvDTE.Process>().Where(proc => proc.Name.IndexOf(ProcessName) != -1))
{
proc.Attach();
}
}
private static DTE2 GetCurrent()
{
// Note: "16.0" is for Visual Studio 2019; you might need to tweak this for VS2022.
var dte2 = (DTE2)Marshal2.GetActiveObject("VisualStudio.DTE.16.0");
return dte2;
}
public static class Marshal2
{
internal const String OLEAUT32 = "oleaut32.dll";
internal const String OLE32 = "ole32.dll";
[System.Security.SecurityCritical] // auto-generated_required
public static Object GetActiveObject(String progID)
{
Object obj = null;
Guid clsid;
// Call CLSIDFromProgIDEx first then fall back on CLSIDFromProgID if
// CLSIDFromProgIDEx doesn't exist.
try
{
CLSIDFromProgIDEx(progID, out clsid);
}
// catch
catch (Exception)
{
CLSIDFromProgID(progID, out clsid);
}
GetActiveObject(ref clsid, IntPtr.Zero, out obj);
return obj;
}
//[DllImport(Microsoft.Win32.Win32Native.OLE32, PreserveSig = false)]
[DllImport(OLE32, PreserveSig = false)]
[ResourceExposure(ResourceScope.None)]
[SuppressUnmanagedCodeSecurity]
[System.Security.SecurityCritical] // auto-generated
private static extern void CLSIDFromProgIDEx([MarshalAs(UnmanagedType.LPWStr)] String progId, out Guid clsid);
//[DllImport(Microsoft.Win32.Win32Native.OLE32, PreserveSig = false)]
[DllImport(OLE32, PreserveSig = false)]
[ResourceExposure(ResourceScope.None)]
[SuppressUnmanagedCodeSecurity]
[System.Security.SecurityCritical] // auto-generated
private static extern void CLSIDFromProgID([MarshalAs(UnmanagedType.LPWStr)] String progId, out Guid clsid);
//[DllImport(Microsoft.Win32.Win32Native.OLEAUT32, PreserveSig = false)]
[DllImport(OLEAUT32, PreserveSig = false)]
[ResourceExposure(ResourceScope.None)]
[SuppressUnmanagedCodeSecurity]
[System.Security.SecurityCritical] // auto-generated
private static extern void GetActiveObject(ref Guid rclsid, IntPtr reserved, [MarshalAs(UnmanagedType.Interface)] out Object ppunk);
}
}
}
Note 1: This is for Visual Studio 2019 and .NET Core 5. I would expect this to work for VS2022 and .NET Core 6 with a single change to the Visual Studio version as annotated above.
Note 2: You will need to have only one instance of Visual Studio open (though if this isn't possible, the code is probably fixable quite easily).
Downloadable demo
Complete working examples for both .NET Framework (v4.7.2) and .NET Core (v5.0) are available on GitHub here: https://github.com/NeilTalbott/VisualStudioAutoAttacher

C# SAP GUI 7.4 Scripting existing Application/Connection/Session

I am trying to write a C# program that will automate a user's input into the SAP GUI (currently on version 7400.3.11.3364) using SAP's scripting API. I've done similar things in the past using VBA, but I'm struggling to get it working exactly how I want it to in C#. My end goal is to have a method that opens SAP (if it isn't already running), returns the GuiApplication object of SAP, and leaves SAP open after my program ends. I currently have it working in VBA, and I believe I had it working correctly in C# on a previous project when we were on SAP GUI version 7.3, but I'm not 100% sure
Here is the VBA Function I use:
Public Function GetSapApp() as GuiApplication
Dim Start as Date
If GetObject("SAPGUI") Is Nothing Then
Start = Now()
Shell "C:\Program Files (x86)\SAP\FrontEnd\SAPgui\saplogon.exe"
Do Until Not GetObject("SAPGUI") Is Nothing Or Now > (Start + TimeValue("00:01:00"))
DoEvents
Loop
If GetObject("SAPGUI") Is Nothing Then
MsgBox "Unable to detect SAP Scripting API. Please contact the developer."
End If
Set GetSapApp = GetObject("SAPGUI").GetScriptingEngine
End Function
And here are the different C# methods I've found while googling:
Below is what I'm currently using (the idea was inspired by this: https://www.codeproject.com/Questions/829500/How-do-I-connect-Csharp-to-SAP-GUI), but there are a couple issues with it. It doesn't seem to detect SAP if it is already open. Also, after my program runs, this SAP isn't detectable by any of our VBA macros.
private static GuiApplication GetSapApp()
{
try
{
object SapGuilRot = new CSapROTWrapper().GetROTEntry("SAPGUI");
return SapGuilRot.GetType().InvokeMember("GetScriptingEngine", System.Reflection.BindingFlags.InvokeMethod, null, SapGuilRot, null) as GuiApplication;
}
catch (Exception e)
{
try
{
string SapPath = "C:\\Program Files (x86)\\SAP\\FrontEnd\\SAPgui\\saplogon.exe";
if (File.Exists(SapPath))
{
System.Diagnostics.Process.Start(SapPath);
DateTime StartTime = DateTime.Now;
object SapGuilRot = new CSapROTWrapper().GetROTEntry("SAPGUI");
while (SapGuilRot == null && 30 >= (DateTime.Now - StartTime).TotalSeconds)
{
SapGuilRot = new CSapROTWrapper().GetROTEntry("SAPGUI");
}
return SapGuilRot.GetType().InvokeMember("GetScriptingEngine", System.Reflection.BindingFlags.InvokeMethod, null, SapGuilRot, null) as GuiApplication;
}
else
{
return null;
}
}
catch
{
return null;
}
}
}
Here is another option I've tried (per this answer on another SO post: https://stackoverflow.com/a/14205520/5134861), but it has the same first issue as above (doesn't detect if SAP is currently running), the SAP session that gets opened when you call the OpenConnection method looks different and then closes when my program is done running.
private static GuiApplication GetSapApp()
{
return (GuiApplication)System.Activator.CreateInstance(Type.GetTypeFromProgID("SapGui.ScriptingCtrl.1"));
}
And here is the last option I've tried, but I get a Cannot create ActiveX component error, despite having varified sapfewse.ocx is registered.
private static GuiApplication GetSapApp()
{
object sap = Microsoft.VisualBasic.Interaction.GetObject("SAPGUI", "");
return sap.GetType().InvokeMember("GetScriptingEngine", System.Reflection.BindingFlags.InvokeMethod, null, sap, null) as GuiApplication;
}
Any help and/or suggestions would be greatly appreciated.
Thanks in advance!
I've used:
Microsoft.VisualBasic.Interaction.GetObject("SAPGUISERVER", "");

COM+ activation on a remote server with partitions in C#

I want to access partitioned COM+ applications on a remote server.
I have tried this:
using COMAdmin
using System.Runtime.InteropServices;
_serverName = myRemoteServer;
_partionName = myPartionName;
_message = myMessage;
ICOMAdminCatalog2 catalog = new COMAdminCatalog();
catalog.Connect(_serverName);
string moniker = string.Empty;
string MsgInClassId = "E3BD1489-30DD-4380-856A-12B959502BFD";
//we are using partitions
if (!string.IsNullOrEmpty(_partitionName))
{
COMAdminCatalogCollection partitions = catalog.GetCollection("Partitions");
partitions.Populate();
string partitionId = string.Empty;
foreach (ICatalogObject item in partitions)
{
if (item.Name == _partitionName)
{
partitionId = item.Key;
break;
}
}
if (!string.IsNullOrEmpty(partitionId) )
{
moniker = $"partition:{partitionId}/new:{new Guid(MsgInClassId)}";
try
{
var M = (IMsgInManager)Marshal.BindToMoniker(moniker);
M.AddMsg(_message);
}
catch (Exception ex)
{
throw new Exception($"We can not use: {_partitionName} with Id {partitionId}. {ex.ToString()}");
}
}
else
{
throw;
}
}
else
//we don't have partitions and this will work
{
Type T = Type.GetTypeFromCLSID(new Guid(MsgInClassId), _serverName, true);
var M = (IMsgInManager)Activator.CreateInstance(T);
M.AddMsg(_message);
}
}
So when we are local on the (remote) machine, partitions are working with the moniker and Marshal.BindToMoniker.
But when I try do the same remotely from my machine, I get an error from
Marshal.BindToMoniker that Partitons is not enabled. Because on my machine partitions is not enabled.
Message = "COM+ partitions are currently disabled. (Exception from HRESULT: 0x80110824)"
How can I use Marshal.BindToMoniker to run on the remote server.
Is it something I can add to the moniker string i.e.
moniker = $"server:_server/partition:{partitionId}/new:{new Guid(MsgInClassId)}"
My questions is very simular to this:
COM+ object activation in a different partition
tl;dr
According to MS documentation there is a way to do this by setting the pServerInfo in BIND_OPTS2 structure for binding the moniker. Unfortunately this is not working for the COM class moniker.
see:
https://msdn.microsoft.com/en-us/library/windows/desktop/ms694513(v=vs.85).aspx
where it says for *pServerInfo:
COM's new class moniker does not currently honor the pServerInfo flag.
But maybe just try your scenario and at some future time it might be supported (or already is and documentation is wrong).
also see:
http://thrysoee.dk/InsideCOM+/ch11c.htm
where it also says in the footnote it does not work for class moniker: http://thrysoee.dk/InsideCOM+/footnotes.htm#CH1104
Theory and suggested solution if it was supported in c#
Disclaimer: I couldn't test the code as I don't have a test setup. This is off the top of my head. A bit pseudo code.
To do this you would have to code the COM/Moniker calls yourself. For this you could look at the source of microsofts implementation as a starting point.
There BindToMoniker is implemented like:
public static Object BindToMoniker(String monikerName)
{
Object obj = null;
IBindCtx bindctx = null;
CreateBindCtx(0, out bindctx);
UInt32 cbEaten;
IMoniker pmoniker = null;
MkParseDisplayName(bindctx, monikerName, out cbEaten, out pmoniker);
BindMoniker(pmoniker, 0, ref IID_IUnknown, out obj);
return obj;
}
CreateBindCtx, MkParseDisplayName and BindMoniker are OLE32.dll functions.
IBindCtx has methods to change the binding context. For this you call IBindCtx.GetBindContext(out BIND_OPTS2) and change the settings to what you need. Then set the new binding context with IBindCtx.SetBindContext(BIND_OPTS2). So essentially your own version of code would look something like this (pseudo code):
public static Object BindToMoniker(String monikerName)
{
Object obj = null;
IBindCtx bindctx = null;
CreateBindCtx(0, out bindctx);
BIND_OPTS2 bindOpts;
bindOpts.cbStruct = Marshal.SizeOf(BIND_OPTS2);
bindctx.GetBindOptions(ref bindOpts);
// Make your settings that you need. For example:
bindOpts.dwClassContext = CLSCTX_REMOTE_SERVER;
// Anything else ?
bindOpts.pServerInfo = new COSERVERINFO{pwszName = "serverName"};
bindctx.SetBindOptions(ref bindOpts);
UInt32 cbEaten;
IMoniker pmoniker = null;
MkParseDisplayName(bindctx, monikerName, out cbEaten, out pmoniker);
BindMoniker(pmoniker, 0, ref IID_IUnknown, out obj);
return obj;
}
As said, unfortunately this code is not possible to write in C# out of the box. Even the OLE32.dll method declarations CreateBindCtx, MkParseDisplayName and BindMoniker are privately declared in Marshal.cs so you will have to declare them in your project again.
But we are lucky with the IBindCtx declaration using a BIND_OPTS2 and the BIND_OPTS2 structure definition itself. They are declared in Microsoft.VisualStudio.OLE.Interop (interesting declarations in this namespace anyway). So you can try using them because inside the Marshal object and marshal.cs only the BIND_OPTS structure is used. I don't know if this is part of the framework and redistributable (I doubt it) but for testing this should be good enough. If it works these things can be declared again in your own solution.
Some Info on the used functions:
BindMoniker
CreateBindCtx
MkParseDisplayName
BIND_OPTS2
The remote COM needs to be accessed by Queue or DCOM. You need to export the application proxy on the server when accessing by DCOM. And install the proxy in the client PC.
The COM activation type must be configured as "Server Application" to export application proxy.
After installing application proxy, the client can directly call
moniker = $"new:{new Guid(MsgInClassId)}";
try
{
var M = Marshal.BindToMoniker(moniker);
}
For the partition, it's designed to show each user with own application set. If the current partition is associated to the user, the partition needs not to be written in codes.

C# Attempted to read or writer protected memory

I write down a class to record results to xml files.When this multi-threads debug,it's get "attempt to read or write protected memory..." error.Why does the error display?
static private readonly object recordLock = new object();
private readonly object createLock = new object();
public static XMLHandle GetXMLHandle()
{
if (xmlHandle == null)
{
lock (lockHelper)
{
if (xmlHandle == null)
xmlHandle = new XMLHandle();
}
}
return xmlHandle;
}
public void RecordXML(string filePath, string fileName, string content, bool HaveLink)
{
System.Threading.Monitor.Enter(recordLock);<<======here
//...
System.Threading.Monitor.Exit(recordLock);
}
May be an invalid code generated by xml serializer?
Also VS debugger sometimes crashes on multithread debugging and you can do only few things to workaround it.
Ensure that you are not debugging an optimized binary (in Project Properties - Build - Optimize code).
Try switching between 32-64 bit.
Try toggling "Enable native code debugging".
Try toggling "Enable VS hosting process". I encountered a debugger problem myself and this fixed it.
Try switching between .NET frameworks (especially 3.5 and 4.0+).
Update your Visual Studio.
3 and 4 are located in the Debug tab of Project Properties.

System.AccessViolationException in C# interface to Swi-prolog

I am new here and I hope that i will find a solution for my problem. The background of the problem is as follows:
I am trying to build an expert system that constitute a C# front-end which is interacting with Swi-prolog.
I have downloaded SwiPlCs.dll (A CSharp class library to connect .NET languages with Swi-Prolog)
And added a reference to it in a Visual Studio project(Win form app) that I have created to test if I can query prolog from c# (I followed the example used in the documentation found here).
It worked fine.
Then, in a more complicated scenario, I have built a WCF service that will act as an intermediary layer between Swi-Prolog and C# client application (it consumes the service).
The service is hosted in IIS 7.0.
For the sake of simplicity, lets say my service contains three methods.
The first method initializes the prolog engine, consults prolog source file then queries the file.
The second method performs another query.
The third method calls PlCleanup().
Method#1:
public void LaunchAssessment()
{
Dictionary<string, string> questions = new Dictionary<string, string>();
#region : Querying prolog using SwiPlCs
try
{
if (!PlEngine.IsInitialized)
{
String[] param = { "-q" };
PlEngine.Initialize(param);
PlQuery.PlCall("consult('D:/My FYP Work/initialAssessment')");
using (var q = new PlQuery("go(X, Y)"))
{
foreach (PlQueryVariables v in q.SolutionVariables)
{
questions.Add("name", v["X"].ToString());
questions.Add("age", v["Y"].ToString());
}
}
}
}
catch (SbsSW.SwiPlCs.Exceptions.PlException exp)
{
throw new FaultException<PrologFault>(new PrologFault(exp.Source), exp.MessagePl);
}
#endregion
Callback.PoseQuestion(questions, ResponseType.None);
}
Method#2:
public void DetermineAgeGroup(int age)
{
//Determine age group
string age_group = string.Empty;
try
{
using (var query = new PlQuery("age_group(" + age + ", G)"))
{
foreach (PlQueryVariables v in query.SolutionVariables)
age_group += v["G"].ToString();
}
}
catch (SbsSW.SwiPlCs.Exceptions.PlException exp)
{
throw new FaultException<PrologFault>(new PrologFault(exp.Source), exp.MessagePl);
}
//Check whether age_group is found or not
if (string.IsNullOrEmpty(age_group))
{
throw new FaultException<NoSolutionFoundFault>(new NoSolutionFoundFault("No solution found"), "Age specified exceeds the diagnosis range!");
}
else
{
Callback.RespondToUser(age_group, ResponseType.Age);
}
}
Method#3:
public void QuitProlog()
{
if (PlEngine.IsInitialized)
{
PlEngine.PlCleanup();
}
}
The client invokes the first method just fine and a result of the first query is successfully returned. When client tries to call the second method an exception is thrown with message (attempted to read or write protected memory) which causes the application to freeze. I checked the event viewer and this is what I get:
Application: w3wp.exe
Framework Version: v4.0.30319
Description: The process was terminated due to an unhandled exception.
Exception Info: System.AccessViolationException
Stack:
at SbsSW.SwiPlCs.SafeNativeMethods.PL_new_term_ref()
at SbsSW.SwiPlCs.PlQuery..ctor(System.String, System.String)
at SbsSW.SwiPlCs.PlQuery..ctor(System.String)
at PrologQueryService.PrologQueryService.DetermineAgeGroup(Int32)
I also tried to use the interface for a .NET project.
Looking in the official repository of the CSharp interface to SWI-Prolog I noticed that the project is very old and the latest updates do not seem included in the binaries available in the download page of the official website.
Then I did the following steps:
The contrib repository dedicated to .NET indicates that the compatible SWI-Prolog version (at the time of writing) is "8.0.3-1" (look in the README file).
-> Then I uninstalled from my computer the latest stable and installed the indicated one. I got it from the full list of downloads of the old versions at this link.
I cloned the SWI-Prolog/contrib-swiplcs repository, unloaded the incompatible projects from the solution, in my case, since I don't use Visual Studio.
-> I set the target framework to Net Framework 4.8 and recompiled it (you can also do this with standard NET). Beware of some pragma directives defined in the old project file (For example I re-defined _PL_X64 variable via code.
I brought the main unit test methods into a new project with xUnit wiht the appropriate changes.
I set the target to x64, recompiled and rebuilt the tests and the "hello world" example.
It worked!
I was able to use SWI-Prolog both for Net 4.8 and in other Net Core applications (if you make the needed changes in order to target the Net Standard). You should not have any problem in both cases).
This is my fork as a preliminary example.
Finally, I can load a *.pl Prolog file with a program in my C# application and use it to evaluate some business logic rules (example with boolean answer [Permitted/Not-Permitted]):
[Fact]
public void ShouldLoadAProgramAndUseIt()
{
var pathValues = Environment.GetEnvironmentVariable("PATH");
pathValues += #";C:\Program Files\swipl\bin";
Environment.SetEnvironmentVariable("PATH", pathValues);
// Positioning to project folder
var currentDirectory = Directory.GetCurrentDirectory().Split('\\').ToList();
currentDirectory.RemoveAll(r => currentDirectory.ToArray().Reverse().Take(3).Contains(r));
var basePath = currentDirectory.Aggregate((c1, c2) => $"{c1}\\{c2}");
var filePath = $"{basePath}\\prolog_examples\\exec_checker.pl";
String[] param = { "-q", "-f", filePath };
PlEngine.Initialize(param);
try
{
var query = "exutable('2020-08-15',[('monthly', ['2019-12-30', '2020-03-10'])])";
_testOutputHelper.WriteLine($"Query: {query}");
using (var q = new PlQuery(query))
{
var booleanAnswer = q.NextSolution();
_testOutputHelper.WriteLine($"Answer: {booleanAnswer}");
Assert.True(booleanAnswer);
}
query = "exutable('2020-08-15',[('daily', ['2019-12-30', '2020-08-15'])])";
_testOutputHelper.WriteLine($"Query: {query}");
using (var q = new PlQuery(query))
{
var booleanAnswer = q.NextSolution();
_testOutputHelper.WriteLine($"Answer: {booleanAnswer}");
Assert.False(booleanAnswer);
}
}
finally
{
PlEngine.PlCleanup();
}
}
Try to close engine in the end of the first method and initialize it in the second again.
You can check this as the answer to the question unless you object.

Categories