First published on MSDN on Jul 22, 2018
In the
previous post
we have seen how to use the Package Support Framework in a Desktop Bridge app to solve common issues without having to change the code.
In that post we have assumed you don't have access anymore to the code of the application, so we have used a manual approach to unpack the package, add the required files by the Package Support Framework and then repack it.
In this post, instead, we'll see how to do the same with Visual Studio, which gives you access to a better debugging experience and it makes easier to write your own runtime fixes.
Adding the runtime fixes project
We're going to start from a Visual Studio solution which contains our Win32 application. In my case, it's the project of the WPF app I've used in the previous post to demonstrate how to handle an application which tries to read or write files in the installation folder.
Let's start by adding a project that will contain our runtime fixes. We're going to leverage C++, since it allows to intercept low level Windows APIs in an easier way. Additionally, the Package Support Framework includes a set of C++ headers which helps to create new runtime fixes.
Right click on your Visual Studio solution and look for the template under
Visual C++ -> Windows Desktop -> Dynamic-Link Library (DLL)
.
Do you remember the NuGet package we have downloaded in the previous post to get access to the Package Support Frameworks executables and libraries? The same package contains the files we need to implement the fixes, so we need to add it to this new project. Right click on it, choose
Manage NuGet packages
, look for the package called
Microsoft.PackageSupportFramework
and install it.
If you want to leverage one of the existing runtime fixes, you can open the
bin
folder of the NuGet package (see
the previous post
to discover where to find it) and copy one or more DLL inside the project.
For example, let's say that, like in the previous post, we need to implement the file redirection fix, in order to make sure that our application is still able to update the app.json file, even if the original code performs this operation in the installation folder. Look for the
FileRedirectionShim.dll
file for your application's architecture (
FileRedirectionShim32.dll
for x86 or
FileRedirectionShim64.dll
for x64) and copy it inside your project. Then right click on it in Visual Studio, choose
Properties
and make sure
Content
is set to
True
.
The shim launcher
The next step is to define a new project, which will host our runtime fixes and the shim launcher. For this purpose we need to use the template under
Visual C++ -> General -> Empty project
.
Also in this case, the first step to do is to right click on it in Visual Studio, choose
Manage NuGet packages
and install the
Microsoft.PackageSupportFramework
package.
The next step is to add a reference to the previous project we have created with the fixes. Right click again on the project, choose
Add reference
and look for the project we have created in the previous step.
Once you have added the reference, click on it (under the
References
section), choose
Properties
and make sure the following properties are correctly set:
-
Copy Local: True
-
Copy local Satellite Assemblies: True
-
Reference Assembly output: True
-
Link Library Dependencies: False
-
Use Library Dependency Inputs: False
This project won't contain any actual code. It will be used as a gateway for the shim launcher and our runtime fixes. As such, we need to change the Target Name of the project to execute the shim launcher provided by the NuGet package instead of the output of the project. To do this, we need to right click on the project, choose
Properties
and, under
General
, set
Target Name
to
ShimLauncher32
(if it's a 32 bit application) or
ShimLauncher64
(if it's a 64 bit application).
The Windows Application Packaging Project
The last step shouldn't be a surprise, since we're working with an application packaged with the Desktop Bridge. We need to add a Windows Application Packaging Project, in order to take our Win32 application and package it using the AppX format (MSIX in the future).
You can find it under
Windows Universal -> Windows Application Packaging Project
.
The difference compared to the standard approach is that, this time, we won't have to add a reference only to the Win32 app, but also to the shim launcher project. As such, right click on
Applications
and choose:
-
The project which contains your Win32 app
-
The project which hosts the shim launcher we have created in the previous step
For example, this is how my sample project looks like:
Right click on the shim launcher's project and choose
Set as entry point
. It will be the starting point of our application.
If you know how, under the hood, the Windows Application Packaging Project works, you will realize that however there's a mismatch between the package we're creating and the one we have created in the
previous post
.
As a reminder, here is how the layout of the package we have created the last time looks like:
As you can see, the main application is stored inside a subfolder (
PSFDemo
), while the Package Support Framework files are stored in the root of the package.
However, when we use the Windows Application Packaging Project, the outcome is a package where all the referenced applications are stored inside a subfolder (the
PSFDemo
one). In our scenario, instead we need that the output of the shim launcher project is stored in the package's root.
In order to achieve this we need to tweak a bit the configuration of the packaging project. Right click on it and choose
Edit
project name
.wapproj
. This will open the XML representation of the project.
Move to the end and add the following lines:
<Target Name="PSFRemoveSourceProject" AfterTargets="ExpandProjectReferences" BeforeTargets="_ConvertItems">
<ItemGroup>
<FilteredNonWapProjProjectOutput Include="@(_FilteredNonWapProjProjectOutput)">
<SourceProject Condition="'%(_FilteredNonWapProjProjectOutput.SourceProject)'=='PSFDemo.Launcher'" />
</FilteredNonWapProjProjectOutput>
<_FilteredNonWapProjProjectOutput Remove="@(_FilteredNonWapProjProjectOutput)" />
<_FilteredNonWapProjProjectOutput Include="@(FilteredNonWapProjProjectOutput)" />
</ItemGroup>
</Target>
Make sure to replace
PSFDemo.Launcher
with the name of your shim launcher's project.
Setting up the Package Support Framework
The last step is to configure the Package Support Framework. In fact, we have set up the shim launcher as starting point, but we haven't instructed it which is the application to launch or which runtime fixes to apply.
The way to do this is the same we have seen
in the previous post
. We need a
config.json
file in the package root. To fulfill this requirement, we need to add it directly in the Windows Application Packaging Project:
The way the configuration file works is the same regardless of how you have created your package, so I won't explain again how to create it. You can copy and paste the same one we have created in the previous post:
{
"applications": [
{
"id": "PSFDemo",
"executable": "PSFDemo/PSFDemo.exe",
"workingDirectory": "PSFDemo/"
}
],
"processes": [
{
"executable": "PSFDemo",
"shims": [
{
"dll": "FileRedirectionShim.dll",
"config": {
"redirectedPaths": {
"packageRelative": [
{
"base": "PSFDemo/",
"patterns": [
".*\\.json"
]
}
]
}
}
}
]
}
]
}
Testing our work
We're done! The advantage of this approach compared to the one we've seen
in the previous post
is that we don't have anymore to deal with manual packaging and signing of our application. We can just set the Windows Application Packaging Project as startup and press F5 in Visual Studio to deploy our application and test it.
If we did everything correctly, the outcome should be the same of the previous post. Our application will start and we'll be able to read and update the configuration file, despite it's stored inside the installation folder.
Create your own runtime fix
So far, we have used the runtime fixes project only to host existing ones, like the file redirection shim. However, we can use it also to host our own runtime fix.
Let's see how to create a very simple one, using the guidance shared by
the official documentation
.
The first thing we need to do is to enable support for the latest standard of the C++ language. The Package Support Framework, in fact, is based on the C++ 17 standard which, however, isn't enabled by default in Visual Studio.
Right click on the runtime fix project, choose
Properties
and, under,
Language
, set
C++ Language Standard
to
ISO C++ 17 Standard
.
Please note!
Make sure to set the
Configuration
dropdown to
All configurations
and the
Platform
dropdown to
All Platforms
before making the change. This way you make sure that the correct C++ standard is used regardless of the CPU architecture and configuration mode you use.
Now you can expand the
Source files
section and open the CPP file with the same name of your project. This is how a runtime fix looks like:
#include "stdafx.h"
#define SHIM_DEFINE_EXPORTS
#include <shim_framework.h>
using namespace std::literals;
// Intercept and customize MessageBox calls
auto MessageBoxWImpl = &::MessageBoxW;
int WINAPI MessageBoxWShim(
_In_opt_ HWND hwnd,
_In_opt_ LPCWSTR message,
_In_opt_ LPCWSTR /*caption*/,
_In_ UINT type)
{
return MessageBoxWImpl(hwnd, message, L"Package Support Framework", type);
}
DECLARE_SHIM(MessageBoxWImpl, MessageBoxWShim);
First, you need to specify which is the API you want to override. In this code, we're changing the behavior of the
MessageBoxW
function, which is invoked by Windows when you want to display a message box to the user.
Then you need to declare a new function which will be invoked in replacement of the existing API. In this case, we have called it
MessageBoxWShim
. This function takes the original API and simply sets a fixed title for the message box (Package Support Framework).
In the end, we need to use a function provided by the Package Support Framework, called
DECLARE_SHIM
, to specify which is the original API we want to override (
MessageBoxWImpl
) and the replacement method (
MessageBoxWShim
).
Now let's use this API in our application, so that we can see the behavior. Let's add a message box in our WPF app. It will be displayed to the user when the operation to read the configuration file has been completed. This is the updated code of the method invoked when you click on the
Read configuration file
button:
private void OnReadFile(object sender, RoutedEventArgs e)
{
string filePath = $"{Environment.CurrentDirectory}/app.json";
string json = File.ReadAllText(filePath);
Config config = JsonConvert.DeserializeObject<Config>(json);
AppName.Text = config.AppName;
Version.Text = config.Version;
MessageBox.Show("Configuration has been read successfully", "PSFDemo");
}
Now run again the application and read the configuration file. You should see the following message popping up:
As you can see, the title of the box is
PSFDemo
, which is the value we have specified in the code of the WPF app. This is expected. In fact, we have created the runtime fix, but we haven't configured the shim launcher to use it.
Let's return back to the
config.json
file inside the Windows Application Packaging Project and let's add the definition of a new shim below the file redirection one:
{
"applications": [
{
"id": "PSFDemo",
"executable": "PSFDemo/PSFDemo.exe",
"workingDirectory": "PSFDemo/"
}
],
"processes": [
{
"executable": "PSFDemo",
"shims": [
{
"dll": "FileRedirectionShim.dll",
"config": {
"redirectedPaths": {
"packageRelative": [
{
"base": "PSFDemo/",
"patterns": [
".*\\.json"
]
}
]
}
}
},
{
"dll" : "PSFDemo.Fixups.dll"
}
]
}
]
}
We have added a new
dll
entry with the name of the DLL generated by our runtime fixes' project.
Now let's run the packaged application again and read the configuration file:
As you can notice our runtime fix has kicked in! Despite, in code, we have specified
PSFDemo
as title of the box, the displayed title is
Package Support Framework
, which is the fixed string we have specified in the runtime fix.
Configuring your runtime fix
Like the file redirection runtime fix supports a way to configure which writing operations we want to redirect, we can allow developers to configure also our own runtime fix.
We can implement it by using the
ShimQueryCurrentDllConfig()
API, which returns the JSON structure of the
config
section in the
config.json
file.
For example, let's say that we want developers to be able to customize the title displayed in the message box. As such, we add a new
title
property under the
config
section of our shim:
{
"dll": "PSFDemo.Fixups.dll",
"config": {
"title": "Sample title"
}
We can use the following code to retrieve it:
if (auto configRoot = ::ShimQueryCurrentDllConfig())
{
auto& config = configRoot->as_object();
if (auto titleValue = config.try_get("title"))
{
auto title = titleValue->as_string().wstring();
//parse the title
}
}
The
configRoot
property is populated with the content of the specific
config
section of our shim. In our case, it contains a property called
title
, which we try to retrieve using the
try_get()
method.
In order to parse the title we need a bit more code, because the APIs we're using to parse the JSON returns the value of the
title
property using a specific C++ 17 class (
wstring_view
), while the
MessageBoxWImpl
API requires a
LPCWSTR
object to represent the string.
This is the full implementation of our shim:
#include "stdafx.h"
#define SHIM_DEFINE_EXPORTS
#include <shim_framework.h>
#include <winrt/Windows.Foundation.h>
#include <string_view>
using namespace std::literals;
std::wstring s2ws(const std::string& s)
{
int len;
int slength = (int)s.length() + 1;
len = MultiByteToWideChar(CP_ACP, 0, s.c_str(), slength, 0, 0);
wchar_t* buf = new wchar_t[len];
MultiByteToWideChar(CP_ACP, 0, s.c_str(), slength, buf, len);
std::wstring r(buf);
delete[] buf;
return r;
}
// Intercept and customize MessageBox calls
auto MessageBoxWImpl = &::MessageBoxW;
int WINAPI MessageBoxWShim(
_In_opt_ HWND hwnd,
_In_opt_ LPCWSTR message,
_In_opt_ LPCWSTR /*caption*/,
_In_ UINT type)
{
if (auto configRoot = ::ShimQueryCurrentDllConfig())
{
auto& config = configRoot->as_object();
if (auto titleValue = config.try_get("title"))
{
//parse the title
auto title = titleValue->as_string().wstring();
std::string myTitle = winrt::to_string(title);
std::wstring temp = s2ws(myTitle);
LPCWSTR result = temp.c_str();
return MessageBoxWImpl(hwnd, message, result, type);
}
}
return MessageBoxWImpl(hwnd, message, L"Package Support Framework", type);
}
DECLARE_SHIM(MessageBoxWImpl, MessageBoxWShim);
The
s2ws
is a helper method which helps us to convert the
wstring_view
into a
wstring
one. This type, in fact, supports an extension method called
c_str()
, which is able to convert the string into a
LPCWSTR
object.
Then we have replaced the fixed title
Package Support Framework
with the value from our configuration file.
If we compile and run the application again, this time, instead of the fixed string
Package Support Framework
we will see the one we have specified in the config file as title of the message box:
Wrapping up
In this blog post we have seen another approach to implement the Package Support Framework in our Desktop Bridge application. Compared to the approach we have seen
in the previous post
there are more steps to do, but it's easier to debug potential issues and to create new runtime fixes.
I strongly encourage you to read the full
documentation
. You will learn also some advanced debugging techniques in case your runtime fix isn't behaving as expected.
You can find the sample project used in this post on
GitHub
.
Happy packaging!