Create managed Tests for native code
In the old days of code development, the developer would do several steps repeatedly:
1. edit the code
2. Save
3. Compile
4. Link
5. Deploy (if necessary)
6. Start (or switch to) the debugger
7. Start the app under the debugger.
8. Examine the code behavior changes with breakpoints and other debugger windows.
This is quite tedious. Visual Studio does a lot to reduce these steps: Hit F5 and VS will automatically save, compile, link, deploy (usually), and start the debugger.
But still, often it takes many repro steps or a long time to get the application to the target code.
I really like TDD: Test Driven Development. Typically, the projects I work on consist of a Visual Studio Solution with multiple projects, and multiple project types. TDD allows me to execute the changed code much faster, and the code can be built in Release mode (so it’s much faster) and I don’t have to debug it: I just monitor the test log or test result. I spend a lot less time in the debugger or reproducing the steps to get to the code (Open a menu->Create a new Invoice, navigate to a line, enter a customer, etc.). The debugger is still able to step through the code, but it’s not really running the application, but just the target code.
Plus, with TDD, when I’m done, the tests are the best defense mechanism for the code not breaking from another developer (or myself) changing the code later. The tests are also a great way for a new team member to get familiar with the code base and how it works.
When I have a task to create some native code (C++) to add to the solution, I want to add tests of that native code to the existing C# or VB Test Projects. However, doing so requires a little fiddling to get it working. For example, if the test invokes the C++ code via P/Invoke (using DllImport) , then that code will be loaded into the test execution engine (e.g. “VsTest.executionEngine.x86.exe”)
When I’m doing TDD, I want to quickly change the code and run the very fast test again. Can’t do that with TDD because the test execution process still has the DLL loaded, so you get an error message indicating that the code can’t be modified because it’s in use.
You might think you could use a separate AppDomain to solve the problem, but that is cumbersome and works for managed DLLs only.
Try out the code sample below. It uses LoadLibrary and FreeLibrary to load the native DLL directly, and allows you to change the code without needing to kill the execution process or restart VS.
The sample target C++ code has a method called Return3 that just returns the integer 3. (I initially wanted to play around with regular expressions in C++).
First, create a C# Test project:
File->New->project->Visual C# ->Test->Unit Test Project
Name it TestRegEx
Build the project (Ctrl-Shift-B)
Test->Window->Test Explorer
This shows the default method “TestMethod1”, which we can run and it passes.
Building results in the test being shown in Test->Windows->TestExplorer
Now let’s add the C++ project:
File->Add->New Project-> Visual C++->Win32 ->Win32 Project
Name it CppRegEx
In the wizard, choose Application Type: Dll, Finish.
Paste in this code in CppRegEx.cpp:
extern "C" int __declspec(dllexport) __stdcall Return3()
{
return 3;
}
In the CppRegEx project’s Post Build Event ((Right Click on CppRegEx project, Properties. For all configurations, Debug, Active, Release, or any you may have added) we want to copy the built native binary to where the test code can find it:
xcopy /dy "$(TargetPath)" "$(SolutionDir)\TestRegEx"
Because the command uses macros, it works for Debug and Release builds.
Now build and the correct Dll will be copied to the Test folder.
Now we need to add the C++ DLL as an item to the Test project that gets deployed, so it gets copied to the target directory. Right Click on the TestRegEx project->Add Existing Item, navigate to the build DLL in the TestRegEx folder, then change the Properties->Copy To OutputDirectory ->Copy If newer.
Below is the full sample for the C++ target code and for C# Test project.
Try to modify the code and run the test. (Ctrl-R +T runs the test under the cursor, Ctrl-R + L repeats the last test run)
Now if you change the C++ code the change is reflected in the test. (I’ve found that I have to manually invoke a C++ build (Ctrl-Shift-B) first. You can see the CPP file being compiled in the Output Window (Build panel))
I use this technique to try out various C++ code fragments
You can also debug into the native code: For the test project->Properties->Debug-> Enable Native Code Debugging.
<C++ code>
// CppRegEx.cpp : Defines the exported functions for the DLL application.
//
#include "stdafx.h"
#include <regex>
extern "C" int __declspec(dllexport) __stdcall Return3()
{
return 3;
}
using namespace std;
extern "C" int __declspec(dllexport) __stdcall ReturnRegEx(WCHAR *pString, WCHAR *pRegEx)
{
wcmatch match;
wstring str(pString);
wregex reg(pRegEx);
auto res = regex_match(pString, match, reg);
return (int)res;
}
extern "C" int __declspec(dllexport) __stdcall ReturnRegExMany(int nIter, WCHAR *pString, WCHAR *pRegEx)
{
int retval = 0;
wcmatch match;
wstring str(pString);
wregex reg(pRegEx);
for (int i = 0; i < nIter; i++)
{
auto res = regex_match(pString, match, reg);
retval = (int)res;
}
return retval;
}
</C++ code>
<C# Test Code>
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Runtime.InteropServices;
using System.IO;
namespace TestRegEx
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
using (var x = new DynamicDllLoader("CppRegEx.dll"))
{
var res = NativeMethods.Return3();
Assert.AreEqual(res, 3, "length not equal " + res.ToString());
}
}
[TestMethod]
public void TestRegEx()
{
using (var x = new DynamicDllLoader("CppRegEx.dll"))
{
var str = "igdasdf.dll";
str = str.ToLowerInvariant();
var regex = @"(^asdf(.*)|^fdsa (.*)|^fdasd(.*)|^fddd(.*))\.dll";
var resNative = NativeMethods.ReturnRegEx(str, regex);
var resManaged = System.Text.RegularExpressions.Regex.Match(str, regex);
var resM = resManaged.Success ? 1 : 0;
Assert.AreEqual(resNative, resM, "result not equal " + resNative.ToString());
Assert.AreEqual(resNative, 1, "is false");
}
}
[TestMethod]
public void TestRegExLots()
{
using (var x = new DynamicDllLoader("CppRegEx.dll"))
{
var str = "igdasdf.dll";
str = str.ToLowerInvariant();
var regex = @"(^igd(.*)|^amd(.*)|^ati(.*)|^nv(.*))\.dll";
var res = NativeMethods.ReturnRegExMany(10000, str, regex);
//for (int i = 0; i < 1; i++)
//{
// for (int j = 0; j < 10000; j++)
// {
// /*
// var resManaged = System.Text.RegularExpressions.Regex.Match(str, regex);
// /*/
// var resNative = NativeMethods.ReturnRegEx(str, regex);
// //*/
// }
//}
}
}
}
static class NativeMethods
{
[DllImport("CppRegEx.dll")]
public static extern int Return3();
[DllImport("CppRegEx.dll", CharSet = CharSet.Unicode)]
public static extern int ReturnRegEx(string somestring, string regex);
[DllImport("CppRegEx.dll", CharSet = CharSet.Unicode)]
public static extern int ReturnRegExMany(int iter, string somestring, string regex);
}
/// <summary>
/// we want to load and free a native dll from a particular location.
/// see https://blogs.msdn.com/calvin_hsia/archive/2008/10/28/9020745.aspx
/// </summary>
public class DynamicDllLoader : IDisposable
{
private IntPtr _handleDll;
public DynamicDllLoader(string fullPathDll)
{
if (!File.Exists(fullPathDll))
{
throw new FileNotFoundException("couldn't find " + fullPathDll);
}
_handleDll = LoadLibrary(fullPathDll);
if (_handleDll == IntPtr.Zero)
{
throw new InvalidOperationException(
string.Format("couldn't load {0}. Err {1} ",
fullPathDll,
System.Runtime.InteropServices.Marshal.GetLastWin32Error()
)
);
}
}
private void UnloadDll()
{
if (_handleDll != IntPtr.Zero)
{
var res = 0;
int nTries = 0;
while ((res = FreeLibrary(_handleDll)) != 0)
{
if (++nTries == 3)
{
throw new InvalidOperationException("Couldn't free library. # tries = " + nTries.ToString());
}
}
_handleDll = IntPtr.Zero;
}
}
public void Dispose()
{
UnloadDll();
}
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr LoadLibrary(string dllName);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int FreeLibrary(IntPtr handle);
}
}
</C# Test Code>