Skip to content

Commit 933456c

Browse files
committed
159: use Roslyn instead of Reflection.Emit for dynamically generating assemblies
1 parent a4b45d4 commit 933456c

File tree

9 files changed

+203
-166
lines changed

9 files changed

+203
-166
lines changed

PythonConsoleControl/PythonConsole.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -381,12 +381,7 @@ void ExecuteStatements()
381381
}
382382
else
383383
{
384-
ObjectHandle wrapexception = null;
385-
GetCommandDispatcher()(() => scriptSource.ExecuteAndWrap(commandLine.ScriptScope, out wrapexception));
386-
if (wrapexception != null)
387-
{
388-
error = "Exception : " + wrapexception.Unwrap().ToString() + "\n";
389-
}
384+
GetCommandDispatcher()(() => scriptSource.Execute(commandLine.ScriptScope));
390385
}
391386
}
392387
catch (ThreadAbortException tae)

PythonConsoleControl/PythonConsoleControl.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
<Configurations>Debug;Release</Configurations>
44
<PublishSingleFile>true</PublishSingleFile>
55
<TargetFramework>net8.0-windows</TargetFramework>
6+
<PublishTrimmed>false</PublishTrimmed>
7+
<EnableComHosting>true</EnableComHosting>
68
<UseWPF>true</UseWPF>
79
<PlatformTarget>x64</PlatformTarget>
810
<LangVersion>latest</LangVersion>

RevitPythonShell/App.cs

Lines changed: 6 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Linq;
66
using System.Reflection;
77
using System.Reflection.Emit;
8+
using System.Text;
89
using System.Xml.Linq;
910
using Autodesk.Revit;
1011
using Autodesk.Revit.UI;
@@ -252,41 +253,12 @@ private static void AddUngroupedCommands(string dllfullpath, RibbonPanel ribbonP
252253
/// </summary>
253254
private static void CreateCommandLoaderAssembly(XDocument repository, string dllfolder, string dllname)
254255
{
255-
var assemblyName = new AssemblyName { Name = dllname + ".dll", Version = new Version(1, 0, 0, 0) };
256-
var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave, dllfolder);
257-
var moduleBuilder = assemblyBuilder.DefineDynamicModule("CommandLoaderModule", dllname + ".dll");
256+
var dllPath = Path.Combine(dllfolder, $"{dllname}.dll");
257+
IDictionary<string, string> classNamesToScriptPaths = GetCommands(repository)
258+
.ToDictionary(command => $"Command{command.Index}", command => command.Source);
258259

259-
foreach (var command in GetCommands(repository))
260-
{
261-
var typebuilder = moduleBuilder.DefineType("Command" + command.Index,
262-
TypeAttributes.Class | TypeAttributes.Public,
263-
typeof(CommandLoaderBase));
264-
265-
// add RegenerationAttribute to type
266-
var regenerationConstrutorInfo = typeof(RegenerationAttribute).GetConstructor(new Type[] { typeof(RegenerationOption) });
267-
var regenerationAttributeBuilder = new CustomAttributeBuilder(regenerationConstrutorInfo, new object[] {RegenerationOption.Manual});
268-
typebuilder.SetCustomAttribute(regenerationAttributeBuilder);
269-
270-
// add TransactionAttribute to type
271-
var transactionConstructorInfo = typeof(TransactionAttribute).GetConstructor(new Type[] { typeof(TransactionMode) });
272-
var transactionAttributeBuilder = new CustomAttributeBuilder(transactionConstructorInfo, new object[] { TransactionMode.Manual });
273-
typebuilder.SetCustomAttribute(transactionAttributeBuilder);
274-
275-
// call base constructor with script path
276-
var ci = typeof(CommandLoaderBase).GetConstructor(new[] { typeof(string) });
277-
278-
var constructorBuilder = typebuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[0]);
279-
var gen = constructorBuilder.GetILGenerator();
280-
gen.Emit(OpCodes.Ldarg_0); // Load "this" onto eval stack
281-
gen.Emit(OpCodes.Ldstr, command.Source); // Load the path to the command as a string onto stack
282-
gen.Emit(OpCodes.Call, ci); // call base constructor (consumes "this" and the string)
283-
gen.Emit(OpCodes.Nop); // Fill some space - this is how it is generated for equivalent C# code
284-
gen.Emit(OpCodes.Nop);
285-
gen.Emit(OpCodes.Nop);
286-
gen.Emit(OpCodes.Ret); // return from constructor
287-
typebuilder.CreateType();
288-
}
289-
assemblyBuilder.Save(dllname + ".dll");
260+
var externalCommandAssemblyBuilder = new ExternalCommandAssemblyBuilder();
261+
externalCommandAssemblyBuilder.BuildExternalCommandAssembly(dllPath, classNamesToScriptPaths);
290262
}
291263

292264
Result IExternalApplication.OnShutdown(UIControlledApplication application)

RevitPythonShell/RevitCommands/DeployRpsAddinCommand.cs

Lines changed: 91 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
34
using System.IO;
45
using System.Linq;
5-
using System.Reflection;
6-
using System.Reflection.Emit;
6+
using System.Text;
77
using System.Windows.Forms;
88
using System.Xml.Linq;
99
using Autodesk.Revit.Attributes;
1010
using Autodesk.Revit.UI;
11+
using Microsoft.CodeAnalysis;
1112
using RpsRuntime;
13+
using TaskDialog = Autodesk.Revit.UI.TaskDialog;
1214

1315
namespace RevitPythonShell.RevitCommands
1416
{
@@ -23,11 +25,45 @@ namespace RevitPythonShell.RevitCommands
2325
[Regeneration(RegenerationOption.Manual)]
2426
public class DeployRpsAddinCommand: IExternalCommand
2527
{
28+
private const string FileHeaderTemplate = """
29+
using Autodesk.Revit.Attributes;
30+
using RevitPythonShell.RevitCommands;
31+
32+
#nullable disable
33+
""";
34+
35+
private const string ExternalCommandTemplate = """
36+
using Autodesk.Revit.Attributes;
37+
using RevitPythonShell.RevitCommands;
38+
39+
#nullable disable
40+
41+
[Regeneration]
42+
[Transaction]
43+
public class CLASSNAME : RpsExternalCommandBase
44+
{
45+
}
46+
""";
47+
48+
private const string ExternalApplicationTemplate = """
49+
using Autodesk.Revit.Attributes;
50+
using RevitPythonShell.RevitCommands;
51+
52+
#nullable disable
53+
54+
[Regeneration]
55+
[Transaction]
56+
public class CLASSNAME : RpsExternalApplicationBase
57+
{
58+
}
59+
""";
60+
2661
private string _outputFolder;
2762
private string _rootFolder;
2863
private string _addinName;
2964
private XDocument _doc;
3065

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

49-
var ironPythonPath = Path.GetDirectoryName(this.GetType().Assembly.Location);
50-
CopyFile(Path.Combine(ironPythonPath, "IronPython.dll")); // IronPython.dll
85+
var ironPythonPath = Path.GetDirectoryName(GetType().Assembly.Location);
86+
CopyFile(Path.Combine(ironPythonPath!, "IronPython.dll")); // IronPython.dll
5187
CopyFile(Path.Combine(ironPythonPath, "IronPython.Modules.dll")); // IronPython.Modules.dll
5288
CopyFile(Path.Combine(ironPythonPath, "Microsoft.Scripting.dll")); // Microsoft.Scripting.dll
5389
CopyFile(Path.Combine(ironPythonPath, "Microsoft.Scripting.Metadata.dll")); // Microsoft.Scripting.Metadata.dll
@@ -68,7 +104,7 @@ Result IExternalCommand.Execute(ExternalCommandData commandData, ref string mess
68104
catch (Exception exception)
69105
{
70106

71-
TaskDialog.Show("Deploy RpsAddin", "Error deploying addin: " + exception.ToString());
107+
TaskDialog.Show("Deploy RpsAddin", $"Error deploying addin: {exception}");
72108
return Result.Failed;
73109
}
74110
}
@@ -84,8 +120,6 @@ Result IExternalCommand.Execute(ExternalCommandData commandData, ref string mess
84120
/// </summary>
85121
private void CopyIcons()
86122
{
87-
HashSet<string> copiedIcons = new HashSet<string>();
88-
89123
foreach (var pb in _doc.Descendants("PushButton"))
90124
{
91125
CopyReferencedFileToOutputFolder(pb.Attribute("largeImage"));
@@ -108,7 +142,7 @@ private void CopyExplicitFiles()
108142
{
109143
foreach (var xmlFile in _doc.Descendants("Files").SelectMany(f => f.Descendants("File")))
110144
{
111-
var source = xmlFile.Attribute("src").Value;
145+
var source = xmlFile.Attribute("src")!.Value;
112146
var sourcePath = GetRootedPath(_rootFolder, source);
113147

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

124158
// remove path information for deployment
125-
xmlFile.Attribute("src").Value = fileName;
159+
xmlFile.Attribute("src")!.Value = fileName;
126160
}
127161
}
128162

@@ -166,7 +200,7 @@ private string GetAddinXmlPath()
166200
dialog.CheckPathExists = true;
167201
dialog.Multiselect = false;
168202
dialog.DefaultExt = "xml";
169-
dialog.Filter = "RpsAddin xml files (*.xml)|*.xml";
203+
dialog.Filter = @"RpsAddin xml files (*.xml)|*.xml";
170204

171205
dialog.ShowDialog();
172206
return dialog.FileName;
@@ -180,63 +214,76 @@ private string GetAddinXmlPath()
180214
/// </summary>
181215
private void CreateAssembly()
182216
{
183-
var assemblyName = new AssemblyName { Name = _addinName + ".dll", Version = new Version(1, 0, 0, 0) }; // FIXME: read version from doc
184-
var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave, _outputFolder);
185-
var moduleBuilder = assemblyBuilder.DefineDynamicModule("RpsAddinModule", _addinName + ".dll");
186-
217+
string dllPath = Path.Combine(_outputFolder, $"{_addinName}.dll");
218+
219+
StringBuilder sourceCode = new StringBuilder();
220+
sourceCode.Append(FileHeaderTemplate);
221+
sourceCode.Append(ExternalApplicationTemplate.Replace("CLASSNAME", _addinName));
222+
223+
List<ResourceDescription> resources = new List<ResourceDescription>();
224+
187225
foreach (var xmlPushButton in _doc.Descendants("PushButton"))
188226
{
189227
string scriptFileName;
190228
if (xmlPushButton.Attribute("src") != null)
191229
{
192-
scriptFileName = xmlPushButton.Attribute("src").Value;
230+
scriptFileName = xmlPushButton.Attribute("src")!.Value;
193231
}
194232
else if (xmlPushButton.Attribute("script") != null) // Backwards compatibility
195233
{
196-
scriptFileName = xmlPushButton.Attribute("script").Value;
234+
scriptFileName = xmlPushButton.Attribute("script")!.Value;
197235
}
198236
else
199237
{
200238
throw new ApplicationException("<PushButton/> tag missing a src attribute in addin manifest");
201239
}
202240

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

245+
var resourceDescription = new ResourceDescription(
246+
embeddedScriptFileName,
247+
() => new FileStream(scriptFilePath, FileMode.Open, FileAccess.Read),
248+
isPublic: true);
249+
resources.Add(resourceDescription);
250+
210251
// script has new path inside assembly, rename it for the RpsAddin xml file we intend to save as a resource
211-
xmlPushButton.Attribute("src").Value = newScriptFile;
212-
213-
var typeBuilder = moduleBuilder.DefineType(
214-
className,
215-
TypeAttributes.Class | TypeAttributes.Public,
216-
typeof(RpsExternalCommandBase));
252+
xmlPushButton.Attribute("src")!.Value = embeddedScriptFileName;
217253

218-
AddRegenerationAttributeToType(typeBuilder);
219-
AddTransactionAttributeToType(typeBuilder);
220-
221-
typeBuilder.CreateType();
254+
sourceCode.Append(ExternalCommandTemplate.Replace("CLASSNAME", className));
222255
}
223256

224257
// add StartupScript to addin assembly
225-
if (_doc.Descendants("StartupScript").Count() > 0)
258+
if (_doc.Descendants("StartupScript").Any())
226259
{
227260
var tag = _doc.Descendants("StartupScript").First();
228-
var scriptFile = GetRootedPath(_rootFolder, tag.Attribute("src").Value);
229-
var newScriptFile = Path.GetFileName(scriptFile);
230-
var scriptStream = File.OpenRead(scriptFile);
231-
moduleBuilder.DefineManifestResource(newScriptFile, scriptStream, ResourceAttributes.Public);
232-
261+
var scriptFilePath = GetRootedPath(_rootFolder, tag.Attribute("src")!.Value);
262+
var embeddedScriptFileName = Path.GetFileName(scriptFilePath);
263+
264+
var resourceDescription = new ResourceDescription(
265+
embeddedScriptFileName,
266+
() => new FileStream(scriptFilePath, FileMode.Open, FileAccess.Read),
267+
isPublic: true);
268+
resources.Add(resourceDescription);
269+
233270
// script has new path inside assembly, rename it for the RpsAddin xml file we intend to save as a resource
234-
tag.Attribute("src").Value = newScriptFile;
271+
tag.Attribute("src")!.Value = embeddedScriptFileName;
235272
}
236273

237-
AddRpsAddinXmlToAssembly(_addinName, _doc, moduleBuilder);
238-
AddExternalApplicationToAssembly(_addinName, moduleBuilder);
239-
assemblyBuilder.Save(_addinName + ".dll");
274+
resources.Add(new ResourceDescription($"{_addinName}.xml", () =>
275+
{
276+
var stream = new MemoryStream();
277+
using (var writer = new StreamWriter(stream))
278+
{
279+
writer.Write(_doc.ToString());
280+
writer.Flush();
281+
}
282+
stream.Position = 0;
283+
return stream;
284+
}, isPublic: true));
285+
286+
DynamicAssemblyCompiler.CompileAndSave(sourceCode.ToString(), dllPath, resources.ToArray());
240287
}
241288

242289
/// <summary>
@@ -258,52 +305,6 @@ private static string GetRootedPath(string sourceFolder, string possiblyRelative
258305
return possiblyRelativePath;
259306
}
260307

261-
/// <summary>
262-
/// Adds a subclass of RpsExternalApplicationBase to make the assembly
263-
/// work as an external application.
264-
/// </summary>
265-
private void AddExternalApplicationToAssembly(string addinName, ModuleBuilder moduleBuilder)
266-
{
267-
var typeBuilder = moduleBuilder.DefineType(
268-
addinName,
269-
TypeAttributes.Class | TypeAttributes.Public,
270-
typeof(RpsExternalApplicationBase));
271-
AddRegenerationAttributeToType(typeBuilder);
272-
AddTransactionAttributeToType(typeBuilder);
273-
typeBuilder.CreateType();
274-
}
275-
276-
/// <summary>
277-
/// Adds the [Transaction(TransactionMode.Manual)] attribute to the type.
278-
/// </summary>
279-
private void AddTransactionAttributeToType(TypeBuilder typeBuilder)
280-
{
281-
var transactionConstructorInfo = typeof(TransactionAttribute).GetConstructor(new Type[] { typeof(TransactionMode) });
282-
var transactionAttributeBuilder = new CustomAttributeBuilder(transactionConstructorInfo, new object[] { TransactionMode.Manual });
283-
typeBuilder.SetCustomAttribute(transactionAttributeBuilder);
284-
}
285-
286-
/// <summary>
287-
/// Adds the [Transaction(TransactionMode.Manual)] attribute to the type.
288-
/// </summary>
289-
/// <param name="typeBuilder"></param>
290-
private void AddRegenerationAttributeToType(TypeBuilder typeBuilder)
291-
{
292-
var regenerationConstrutorInfo = typeof(RegenerationAttribute).GetConstructor(new Type[] { typeof(RegenerationOption) });
293-
var regenerationAttributeBuilder = new CustomAttributeBuilder(regenerationConstrutorInfo, new object[] { RegenerationOption.Manual });
294-
typeBuilder.SetCustomAttribute(regenerationAttributeBuilder);
295-
}
296-
297-
private void AddRpsAddinXmlToAssembly(string addinName, XDocument doc, ModuleBuilder moduleBuilder)
298-
{
299-
var stream = new MemoryStream();
300-
var writer = new StreamWriter(stream);
301-
writer.Write(doc.ToString());
302-
writer.Flush();
303-
stream.Position = 0;
304-
moduleBuilder.DefineManifestResource(addinName + ".xml", stream, ResourceAttributes.Public);
305-
}
306-
307308
/// <summary>
308309
/// Creates a subfolder in rootFolder with the basename of the
309310
/// RpsAddin xml file and returns the name of that folder.
@@ -314,7 +315,7 @@ private void AddRpsAddinXmlToAssembly(string addinName, XDocument doc, ModuleBui
314315
/// </summary>
315316
private string CreateOutputFolder()
316317
{
317-
var folderName = string.Format("{0}_{1}", "Output", _addinName);
318+
var folderName = $"Output_{_addinName}";
318319
var folderPath = Path.Combine(_rootFolder, folderName);
319320

320321
if (Directory.Exists(folderPath))
@@ -323,7 +324,7 @@ private string CreateOutputFolder()
323324
Directory.Delete(folderPath, true);
324325
}
325326

326-
Directory.CreateDirectory(folderPath, Directory.GetAccessControl(_rootFolder));
327+
Directory.CreateDirectory(folderPath);
327328
return folderPath;
328329
}
329330
}

0 commit comments

Comments
 (0)