Microsoft & .NETVisual C#Custom MSBuild Task Development with Visual C++

Custom MSBuild Task Development with Visual C++

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

The introduction of MSBuild in Visual C++ 2010 opens up the C++ compilation and linking process to a high degree of customization and extension. Prior to MSBuild, the build process could only be extended with pre- and post-build steps that offered limited flexibility and were decoupled from the rest of the build process. In contrast, MSBuild allows fine-tuned, integrated extension, and any new-format Visual C++ project file (vcxproj) can be customized to complete whatever task is required during a build.

In my last Using Visual C++.NET article, MSBuild basics were covered, including Tasks, which are the building-blocks of the build process. Tasks take items and properties to complete some action, and tasks are chained together in targets, with targets being the entry point into a MSBuild file. MSBuild and Visual C++ ship with a large number of predefined tasks, but in the spirit of MSBuild extensibility, custom task development is extremely easy with helper classes present in the MSBuild assemblies. The remainder of this article will cover the process of building a custom MSBuild task in C++/CLI and then using this task in a Visual C++ 2010 project.

Developing the Hard Link Task

The NT File System (NTFS) supports a feature known as hard links, which allows multiple directory entries to map to the same underlying file. Hard links have a number of advantages over simply copying a file, with the main advantage being that, because all directory entries point to the same physical file, an update to the file is automatically available to all consumers of the file regardless of which directory entry the file is accessed through. Using hard links also conserves disk space, as multiple copies of the same file are not scattered randomly across a drive.

MSBuild does not ship with a task for creating hard links, and as there is no managed API for creating hard links, the exercise of creating a custom MSBuild task to create hard links is ideally suited to the power of C++/CLI in seamlessly bridging the native and managed worlds.

The Windows SDK function to create a hard link is quite simple:

BOOL WINAPI CreateHardLink(
   __in       LPCTSTR lpFileName,
   __in       LPCTSTR lpExistingFileName,
   __reserved LPSECURITY_ATTRIBUTES lpSecurityAttributes
);

The LPSECURITY_ATTRIBUTES parameter must be NULL when CreateHardLink is called, so only the two parameters need to be passed from the MSBuild custom task to the Windows SDK function are the source and destination file name. Without the two parameters, calling CreateHardLink is not possible, so it is important that the custom task has these properties passed in. Custom MSBuild tasks can designate that a task property as mandatory with the use of the Required attribute, and these requirements lead to the following C++/CLI class definition:

public ref class HardLinkTask:
   public Microsoft::Build::Utilities::Task
{
public:
   virtual bool Execute() override;

   [Microsoft::Build::Framework::Required]
   property String^ SourceFileName;

   [Microsoft::Build::Framework::Required]
   property String^ TargetFileName;
};

The Microsoft::Build::Utilities::Task base class is used to simplify the process of creating the custom task. The actual requirement for a custom Task is the implementation of the Microsoft::Build::Framework::ITask interface, but this requires a fair amount of boiler-plate code to be written to integrate the task into the build process, and Microsoft::Build::Utilities::Task implements these standard interface methods, leaving only the Execute method of the custom task to be implemented. Execute is the method called by the build process when the task executes, and a Boolean value is returned by Execute to indicate whether the task has been successful.

To implement Execute, the managed System::String properties of the task need to be converted to LPCTSTR variables, the CreateHardLink function needs to be called, and any error information needs to be logged using the standard MSBuild logging infrastructure so error conditions can be diagnosed by the end-users of the task. With these requirements, a simple implementation of Execute would look something like the following:

using namespace System::Runtime::InteropServices;
using namespace msclr::interop;

bool HardLinkTask::Execute()
{
   marshal_context mc;

   if (CreateHardLink(
      mc.marshal_as<LPCWSTR>(TargetFileName),
      mc.marshal_as<LPCWSTR>(SourceFileName), NULL)){
         Log->LogMessage(String::Format(
            "Hard link successfully created from {0} to {1}",
            SourceFileName, TargetFileName));
         return true;
   }
   else{
      int errorHresult = Marshal::GetHRForLastWin32Error();
      Exception^ ex = Marshal::GetExceptionForHR(errorHresult);
      Log->LogErrorFromException(ex);
      return false;
   }
}

The C++/CLI marshalling library (covered previously in this article) is used for the string conversion, and System::Runtime::InteropServices::Marshal is used to convert from a Win32 error code to a .NET exception in the case of an error. The .NET exception then can be passed directly to the Microsoft::Build::Utilities::Task.Log helper object for display to the end user. The preceding two code snippets are all that is required to define and implement a custom task.

Using the HardLinkTask

To add the HardLinkTask to a MSBuild project file, the task first needs to be imported so that it is in scope for the build process. This is accomplished through the UsingTask element:

<UsingTask TaskName="HardLinkTask"
   AssemblyName = "HardLinkTask, Version=1.0.0.0,
   Culture=neutral, PublicKeyToken=a912dca8b5fb92f0"/>

MSBuild uses the standard .NET assembly loading infrastructure, which means that a Task-implementing assembly can be specified in a vcxproj build file using either a path or using a fully qualified name for a GAC-housed assembly. In the example above, a GAC-located is used.

Tasks cannot be called directly from the MSBuild command line, and need to be nested within a Target element. A Target can contain multiple Tasks, but for the demo purposes, the HardLinkTask will be the only task in the Target:

<Target Name="CreateHardLink">
   <HardLinkTask
      SourceFileName="$(HardLinkSource)"
      TargetFileName="$(HardLinkTarget)"/>
</Target>

The final piece of the puzzle is to add a PropertyGroup element to store the properties that need to be passed into the task:

<PropertyGroup>
   <HardLinkSource>C:source.txt</HardLinkSource>
   <HardLinkTarget>C:destination.txt</HardLinkTarget>
</PropertyGroup>

The target then can be executed by invoking MSBuild from the command line and using the /t switch to specify the target name:

msbuild MyApp.vcxproj /t:CreateHardLink

Conclusion

MSBuild custom tasks allow any arbitrary managed code to be executed as part of the build process. C++/CLI is the managed language that offers the easiest access to native functionality, and as many custom MSBuild tasks will be written to access functionality only available through native APIs, C++/CLI is a great language for custom task development.

The Windows SDK CreateHardLink function allows hard links to be created to a file. Hard links are an additional directory entry to an existing file, and allow a virtual copy operation to take place. Creating a hard-link MSBuild task in C++/CLI is a simple matter of capturing two properties to store the source and destination file name, converting these to the appropriate string data type for calling CreateHardLink, and calling this method during the tasks Execute method. Once the task is completed and compiled, it can be added to an MSBuild file target ready for employment during the build process.

About the Author

Nick Wienholt is an independent Windows and .NET consultant based in Sydney. He is the author of Maximizing .NET Performance and co-author of A Programmers Introduction to C# 2.0 from Apress, and specialises in system-level software architecture and development, with a particular focus of performance, security, interoperability, and debugging.

Nick is a keen and active participant in the .NET community. He is the co-founder of the Sydney Deep .NET User group and writes technical articles for Australian Developer Journal, ZDNet, Pinnacle Publishing, CodeGuru, MSDN Magazine (Australia and New Zealand Edition) and the Microsoft Developer Network. An archive of Nick’s SDNUG presentations, articles, and .NET blog is available at www.dotnetperformance.com. In recognition of his work in the .NET area, he was awarded the Microsoft Most Valued Professional Award from 2002 through 2007.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories