Skip to content

Commit

Permalink
159: use Roslyn instead of Reflection.Emit for dynamically generating…
Browse files Browse the repository at this point in the history
… assemblies
  • Loading branch information
daren-thomas committed Sep 22, 2024
1 parent a4b45d4 commit 933456c
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 166 deletions.
7 changes: 1 addition & 6 deletions PythonConsoleControl/PythonConsole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -381,12 +381,7 @@ void ExecuteStatements()
}
else
{
ObjectHandle wrapexception = null;
GetCommandDispatcher()(() => scriptSource.ExecuteAndWrap(commandLine.ScriptScope, out wrapexception));
if (wrapexception != null)
{
error = "Exception : " + wrapexception.Unwrap().ToString() + "\n";
}
GetCommandDispatcher()(() => scriptSource.Execute(commandLine.ScriptScope));
}
}
catch (ThreadAbortException tae)
Expand Down
2 changes: 2 additions & 0 deletions PythonConsoleControl/PythonConsoleControl.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<Configurations>Debug;Release</Configurations>
<PublishSingleFile>true</PublishSingleFile>
<TargetFramework>net8.0-windows</TargetFramework>
<PublishTrimmed>false</PublishTrimmed>
<EnableComHosting>true</EnableComHosting>
<UseWPF>true</UseWPF>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>latest</LangVersion>
Expand Down
40 changes: 6 additions & 34 deletions RevitPythonShell/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Xml.Linq;
using Autodesk.Revit;
using Autodesk.Revit.UI;
Expand Down Expand Up @@ -252,41 +253,12 @@ private static void AddUngroupedCommands(string dllfullpath, RibbonPanel ribbonP
/// </summary>
private static void CreateCommandLoaderAssembly(XDocument repository, string dllfolder, string dllname)
{
var assemblyName = new AssemblyName { Name = dllname + ".dll", Version = new Version(1, 0, 0, 0) };
var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave, dllfolder);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("CommandLoaderModule", dllname + ".dll");
var dllPath = Path.Combine(dllfolder, $"{dllname}.dll");
IDictionary<string, string> classNamesToScriptPaths = GetCommands(repository)
.ToDictionary(command => $"Command{command.Index}", command => command.Source);

foreach (var command in GetCommands(repository))
{
var typebuilder = moduleBuilder.DefineType("Command" + command.Index,
TypeAttributes.Class | TypeAttributes.Public,
typeof(CommandLoaderBase));

// add RegenerationAttribute to type
var regenerationConstrutorInfo = typeof(RegenerationAttribute).GetConstructor(new Type[] { typeof(RegenerationOption) });
var regenerationAttributeBuilder = new CustomAttributeBuilder(regenerationConstrutorInfo, new object[] {RegenerationOption.Manual});
typebuilder.SetCustomAttribute(regenerationAttributeBuilder);

// add TransactionAttribute to type
var transactionConstructorInfo = typeof(TransactionAttribute).GetConstructor(new Type[] { typeof(TransactionMode) });
var transactionAttributeBuilder = new CustomAttributeBuilder(transactionConstructorInfo, new object[] { TransactionMode.Manual });
typebuilder.SetCustomAttribute(transactionAttributeBuilder);

// call base constructor with script path
var ci = typeof(CommandLoaderBase).GetConstructor(new[] { typeof(string) });

var constructorBuilder = typebuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[0]);
var gen = constructorBuilder.GetILGenerator();
gen.Emit(OpCodes.Ldarg_0); // Load "this" onto eval stack
gen.Emit(OpCodes.Ldstr, command.Source); // Load the path to the command as a string onto stack
gen.Emit(OpCodes.Call, ci); // call base constructor (consumes "this" and the string)
gen.Emit(OpCodes.Nop); // Fill some space - this is how it is generated for equivalent C# code
gen.Emit(OpCodes.Nop);
gen.Emit(OpCodes.Nop);
gen.Emit(OpCodes.Ret); // return from constructor
typebuilder.CreateType();
}
assemblyBuilder.Save(dllname + ".dll");
var externalCommandAssemblyBuilder = new ExternalCommandAssemblyBuilder();
externalCommandAssemblyBuilder.BuildExternalCommandAssembly(dllPath, classNamesToScriptPaths);
}

Result IExternalApplication.OnShutdown(UIControlledApplication application)
Expand Down
181 changes: 91 additions & 90 deletions RevitPythonShell/RevitCommands/DeployRpsAddinCommand.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Windows.Forms;
using System.Xml.Linq;
using Autodesk.Revit.Attributes;
using Autodesk.Revit.UI;
using Microsoft.CodeAnalysis;
using RpsRuntime;
using TaskDialog = Autodesk.Revit.UI.TaskDialog;

namespace RevitPythonShell.RevitCommands
{
Expand All @@ -23,11 +25,45 @@ namespace RevitPythonShell.RevitCommands
[Regeneration(RegenerationOption.Manual)]
public class DeployRpsAddinCommand: IExternalCommand
{
private const string FileHeaderTemplate = """
using Autodesk.Revit.Attributes;
using RevitPythonShell.RevitCommands;
#nullable disable
""";

private const string ExternalCommandTemplate = """
using Autodesk.Revit.Attributes;
using RevitPythonShell.RevitCommands;
#nullable disable
[Regeneration]
[Transaction]
public class CLASSNAME : RpsExternalCommandBase
{
}
""";

private const string ExternalApplicationTemplate = """
using Autodesk.Revit.Attributes;
using RevitPythonShell.RevitCommands;
#nullable disable
[Regeneration]
[Transaction]
public class CLASSNAME : RpsExternalApplicationBase
{
}
""";

private string _outputFolder;
private string _rootFolder;
private string _addinName;
private XDocument _doc;

[UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "<Pending>")]
Result IExternalCommand.Execute(ExternalCommandData commandData, ref string message, Autodesk.Revit.DB.ElementSet elements)
{
try
Expand All @@ -46,8 +82,8 @@ Result IExternalCommand.Execute(ExternalCommandData commandData, ref string mess
// copy static stuff (rpsaddin runtime, ironpython dlls etc., addin installation utilities)
CopyFile(typeof(RpsExternalApplicationBase).Assembly.Location); // RpsRuntime.dll

var ironPythonPath = Path.GetDirectoryName(this.GetType().Assembly.Location);
CopyFile(Path.Combine(ironPythonPath, "IronPython.dll")); // IronPython.dll
var ironPythonPath = Path.GetDirectoryName(GetType().Assembly.Location);
CopyFile(Path.Combine(ironPythonPath!, "IronPython.dll")); // IronPython.dll
CopyFile(Path.Combine(ironPythonPath, "IronPython.Modules.dll")); // IronPython.Modules.dll
CopyFile(Path.Combine(ironPythonPath, "Microsoft.Scripting.dll")); // Microsoft.Scripting.dll
CopyFile(Path.Combine(ironPythonPath, "Microsoft.Scripting.Metadata.dll")); // Microsoft.Scripting.Metadata.dll
Expand All @@ -68,7 +104,7 @@ Result IExternalCommand.Execute(ExternalCommandData commandData, ref string mess
catch (Exception exception)
{

TaskDialog.Show("Deploy RpsAddin", "Error deploying addin: " + exception.ToString());
TaskDialog.Show("Deploy RpsAddin", $"Error deploying addin: {exception}");
return Result.Failed;
}
}
Expand All @@ -84,8 +120,6 @@ Result IExternalCommand.Execute(ExternalCommandData commandData, ref string mess
/// </summary>
private void CopyIcons()
{
HashSet<string> copiedIcons = new HashSet<string>();

foreach (var pb in _doc.Descendants("PushButton"))
{
CopyReferencedFileToOutputFolder(pb.Attribute("largeImage"));
Expand All @@ -108,7 +142,7 @@ private void CopyExplicitFiles()
{
foreach (var xmlFile in _doc.Descendants("Files").SelectMany(f => f.Descendants("File")))
{
var source = xmlFile.Attribute("src").Value;
var source = xmlFile.Attribute("src")!.Value;
var sourcePath = GetRootedPath(_rootFolder, source);

if (!File.Exists(sourcePath))
Expand All @@ -122,7 +156,7 @@ private void CopyExplicitFiles()
File.Copy(sourcePath, Path.Combine(_outputFolder, fileName));

// remove path information for deployment
xmlFile.Attribute("src").Value = fileName;
xmlFile.Attribute("src")!.Value = fileName;
}
}

Expand Down Expand Up @@ -166,7 +200,7 @@ private string GetAddinXmlPath()
dialog.CheckPathExists = true;
dialog.Multiselect = false;
dialog.DefaultExt = "xml";
dialog.Filter = "RpsAddin xml files (*.xml)|*.xml";
dialog.Filter = @"RpsAddin xml files (*.xml)|*.xml";

dialog.ShowDialog();
return dialog.FileName;
Expand All @@ -180,63 +214,76 @@ private string GetAddinXmlPath()
/// </summary>
private void CreateAssembly()
{
var assemblyName = new AssemblyName { Name = _addinName + ".dll", Version = new Version(1, 0, 0, 0) }; // FIXME: read version from doc
var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave, _outputFolder);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("RpsAddinModule", _addinName + ".dll");

string dllPath = Path.Combine(_outputFolder, $"{_addinName}.dll");

StringBuilder sourceCode = new StringBuilder();
sourceCode.Append(FileHeaderTemplate);
sourceCode.Append(ExternalApplicationTemplate.Replace("CLASSNAME", _addinName));

List<ResourceDescription> resources = new List<ResourceDescription>();

foreach (var xmlPushButton in _doc.Descendants("PushButton"))
{
string scriptFileName;
if (xmlPushButton.Attribute("src") != null)
{
scriptFileName = xmlPushButton.Attribute("src").Value;
scriptFileName = xmlPushButton.Attribute("src")!.Value;
}
else if (xmlPushButton.Attribute("script") != null) // Backwards compatibility
{
scriptFileName = xmlPushButton.Attribute("script").Value;
scriptFileName = xmlPushButton.Attribute("script")!.Value;
}
else
{
throw new ApplicationException("<PushButton/> tag missing a src attribute in addin manifest");
}

var scriptFile = GetRootedPath(_rootFolder, scriptFileName); // e.g. "C:\projects\helloworld\helloworld.py" or "..\helloworld.py"
var newScriptFile = Path.GetFileName(scriptFile); // e.g. "helloworld.py" - strip path for embedded resource
var className = "ec_" + Path.GetFileNameWithoutExtension(newScriptFile); // e.g. "ec_helloworld", "ec" stands for ExternalCommand

var scriptStream = File.OpenRead(scriptFile);
moduleBuilder.DefineManifestResource(newScriptFile, scriptStream, ResourceAttributes.Public);
var scriptFilePath = GetRootedPath(_rootFolder, scriptFileName); // e.g. "C:\projects\helloworld\helloworld.py" or "..\helloworld.py"
var embeddedScriptFileName = Path.GetFileName(scriptFilePath); // e.g. "helloworld.py" - strip path for embedded resource
var className = "ec_" + Path.GetFileNameWithoutExtension(embeddedScriptFileName); // e.g. "ec_helloworld", "ec" stands for ExternalCommand

var resourceDescription = new ResourceDescription(
embeddedScriptFileName,
() => new FileStream(scriptFilePath, FileMode.Open, FileAccess.Read),
isPublic: true);
resources.Add(resourceDescription);

// script has new path inside assembly, rename it for the RpsAddin xml file we intend to save as a resource
xmlPushButton.Attribute("src").Value = newScriptFile;

var typeBuilder = moduleBuilder.DefineType(
className,
TypeAttributes.Class | TypeAttributes.Public,
typeof(RpsExternalCommandBase));
xmlPushButton.Attribute("src")!.Value = embeddedScriptFileName;

AddRegenerationAttributeToType(typeBuilder);
AddTransactionAttributeToType(typeBuilder);

typeBuilder.CreateType();
sourceCode.Append(ExternalCommandTemplate.Replace("CLASSNAME", className));
}

// add StartupScript to addin assembly
if (_doc.Descendants("StartupScript").Count() > 0)
if (_doc.Descendants("StartupScript").Any())
{
var tag = _doc.Descendants("StartupScript").First();
var scriptFile = GetRootedPath(_rootFolder, tag.Attribute("src").Value);
var newScriptFile = Path.GetFileName(scriptFile);
var scriptStream = File.OpenRead(scriptFile);
moduleBuilder.DefineManifestResource(newScriptFile, scriptStream, ResourceAttributes.Public);

var scriptFilePath = GetRootedPath(_rootFolder, tag.Attribute("src")!.Value);
var embeddedScriptFileName = Path.GetFileName(scriptFilePath);

var resourceDescription = new ResourceDescription(
embeddedScriptFileName,
() => new FileStream(scriptFilePath, FileMode.Open, FileAccess.Read),
isPublic: true);
resources.Add(resourceDescription);

// script has new path inside assembly, rename it for the RpsAddin xml file we intend to save as a resource
tag.Attribute("src").Value = newScriptFile;
tag.Attribute("src")!.Value = embeddedScriptFileName;
}

AddRpsAddinXmlToAssembly(_addinName, _doc, moduleBuilder);
AddExternalApplicationToAssembly(_addinName, moduleBuilder);
assemblyBuilder.Save(_addinName + ".dll");
resources.Add(new ResourceDescription($"{_addinName}.xml", () =>
{
var stream = new MemoryStream();
using (var writer = new StreamWriter(stream))
{
writer.Write(_doc.ToString());
writer.Flush();
}
stream.Position = 0;
return stream;
}, isPublic: true));

DynamicAssemblyCompiler.CompileAndSave(sourceCode.ToString(), dllPath, resources.ToArray());
}

/// <summary>
Expand All @@ -258,52 +305,6 @@ private static string GetRootedPath(string sourceFolder, string possiblyRelative
return possiblyRelativePath;
}

/// <summary>
/// Adds a subclass of RpsExternalApplicationBase to make the assembly
/// work as an external application.
/// </summary>
private void AddExternalApplicationToAssembly(string addinName, ModuleBuilder moduleBuilder)
{
var typeBuilder = moduleBuilder.DefineType(
addinName,
TypeAttributes.Class | TypeAttributes.Public,
typeof(RpsExternalApplicationBase));
AddRegenerationAttributeToType(typeBuilder);
AddTransactionAttributeToType(typeBuilder);
typeBuilder.CreateType();
}

/// <summary>
/// Adds the [Transaction(TransactionMode.Manual)] attribute to the type.
/// </summary>
private void AddTransactionAttributeToType(TypeBuilder typeBuilder)
{
var transactionConstructorInfo = typeof(TransactionAttribute).GetConstructor(new Type[] { typeof(TransactionMode) });
var transactionAttributeBuilder = new CustomAttributeBuilder(transactionConstructorInfo, new object[] { TransactionMode.Manual });
typeBuilder.SetCustomAttribute(transactionAttributeBuilder);
}

/// <summary>
/// Adds the [Transaction(TransactionMode.Manual)] attribute to the type.
/// </summary>
/// <param name="typeBuilder"></param>
private void AddRegenerationAttributeToType(TypeBuilder typeBuilder)
{
var regenerationConstrutorInfo = typeof(RegenerationAttribute).GetConstructor(new Type[] { typeof(RegenerationOption) });
var regenerationAttributeBuilder = new CustomAttributeBuilder(regenerationConstrutorInfo, new object[] { RegenerationOption.Manual });
typeBuilder.SetCustomAttribute(regenerationAttributeBuilder);
}

private void AddRpsAddinXmlToAssembly(string addinName, XDocument doc, ModuleBuilder moduleBuilder)
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write(doc.ToString());
writer.Flush();
stream.Position = 0;
moduleBuilder.DefineManifestResource(addinName + ".xml", stream, ResourceAttributes.Public);
}

/// <summary>
/// Creates a subfolder in rootFolder with the basename of the
/// RpsAddin xml file and returns the name of that folder.
Expand All @@ -314,7 +315,7 @@ private void AddRpsAddinXmlToAssembly(string addinName, XDocument doc, ModuleBui
/// </summary>
private string CreateOutputFolder()
{
var folderName = string.Format("{0}_{1}", "Output", _addinName);
var folderName = $"Output_{_addinName}";
var folderPath = Path.Combine(_rootFolder, folderName);

if (Directory.Exists(folderPath))
Expand All @@ -323,7 +324,7 @@ private string CreateOutputFolder()
Directory.Delete(folderPath, true);
}

Directory.CreateDirectory(folderPath, Directory.GetAccessControl(_rootFolder));
Directory.CreateDirectory(folderPath);
return folderPath;
}
}
Expand Down
Loading

0 comments on commit 933456c

Please sign in to comment.