Skip to content

Rewrite Loader into C# #2647

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 45 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
0ecc3de
feat: add pyRevitAssemblyBuilder project to the Loader solution
romangolev Apr 10, 2025
723821c
feat: make assembly put dlls in a correct folder, scanning all availa…
romangolev Apr 11, 2025
355f92e
feat: implement recursive UI builder
romangolev Apr 11, 2025
847c246
fix: UIManagerServite to handle stacked pulldowns correctly
romangolev Apr 14, 2025
fc104aa
feat: update build dlls
romangolev Apr 14, 2025
f659f49
feat: replace DI architecture with plain intantiating of services. re…
romangolev Apr 16, 2025
c916a4a
feat: implement pyRevitExtensionParser csproject
romangolev Apr 18, 2025
aa4fc9d
feat: update dlls
romangolev Apr 18, 2025
aea0787
feat: change unique id to mimic pythonic one, add logic to CommandTyp…
romangolev Apr 18, 2025
41a5955
feat: change assembly generation module to Roslyn
romangolev Apr 20, 2025
2fc4643
feat: mvp c# loader
romangolev Apr 21, 2025
81efe9a
fix: move enums to parser, simplify code by removing wraps
romangolev Apr 23, 2025
3886911
undate binaries
romangolev Apr 23, 2025
038fd01
Switch for .net core Desktop connector API
jmcouffin Apr 11, 2025
e93db34
fix: is_schedule
leyarx Apr 11, 2025
ae30736
Update pyrevit-hosts.json 2026.0.1
jmcouffin Apr 14, 2025
bb06cdc
Update Directory.Build.targets
jmcouffin Apr 14, 2025
987570d
upd
nodatasheet Feb 8, 2024
4ead9b0
add Associated to Others to select_family_parameters
nodatasheet Apr 12, 2025
b6dda98
cleanup
nodatasheet Apr 12, 2025
2972ffc
Update __init__.py
jmcouffin Apr 14, 2025
0ba9785
Adding exitscript option to SelectFromList from pyrevit.forms
iorhanV Apr 10, 2025
52b9424
Update __init__.py - exitscritp handeled in show method
jmcouffin Apr 14, 2025
7f5eadb
Bump golang.org/x/crypto
dependabot[bot] Apr 14, 2025
3fb7b20
fixes commit f81f483
jmcouffin Apr 16, 2025
db6510d
Update __init__.py
jmcouffin Apr 16, 2025
736cfed
Update go.mod mongodb driver
jmcouffin Apr 16, 2025
6ccc8c1
Update go.mod
jmcouffin Apr 16, 2025
8ae818e
missing comma in the docs for CommandSwitchWindow
jonatanjacobsson Apr 17, 2025
4e98317
feat: add pyRevitAssemblyBuilder project to the Loader solution
romangolev Apr 10, 2025
80f6ccf
feat: make assembly put dlls in a correct folder, scanning all availa…
romangolev Apr 11, 2025
1d6da91
feat: implement recursive UI builder
romangolev Apr 11, 2025
fa8fc2b
fix: UIManagerServite to handle stacked pulldowns correctly
romangolev Apr 14, 2025
ba7d564
feat: update build dlls
romangolev Apr 14, 2025
6964633
feat: replace DI architecture with plain intantiating of services. re…
romangolev Apr 16, 2025
46bdaa9
feat: implement pyRevitExtensionParser csproject
romangolev Apr 18, 2025
0546001
feat: update dlls
romangolev Apr 18, 2025
cffdb8a
feat: change unique id to mimic pythonic one, add logic to CommandTyp…
romangolev Apr 18, 2025
b686515
feat: change assembly generation module to Roslyn
romangolev Apr 20, 2025
33b3d75
feat: mvp c# loader
romangolev Apr 21, 2025
18ecf76
fix: move enums to parser, simplify code by removing wraps
romangolev Apr 23, 2025
e8e445a
undate binaries
romangolev Apr 23, 2025
4acf0a9
feat: add a switcher in settings to be able to toglle between python …
romangolev Apr 24, 2025
84ee64b
update dlls
romangolev Apr 24, 2025
1c90cac
merge conflict
romangolev Apr 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added bin/netcore/engines/IPY342/Lokad.ILPack.dll
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified bin/netcore/engines/IPY342/pyRevitLoader.dll
Binary file not shown.
Binary file modified bin/netcore/engines/IPY342/pyRevitRunner.dll
Binary file not shown.
Binary file added bin/netfx/engines/IPY342/Lokad.ILPack.dll
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added bin/netfx/engines/IPY342/System.ValueTuple.dll
Binary file not shown.
Binary file not shown.
Binary file modified bin/netfx/engines/IPY342/pyRevitLoader.dll
Binary file not shown.
Binary file modified bin/netfx/engines/IPY342/pyRevitRunner.dll
Binary file not shown.
2 changes: 1 addition & 1 deletion dev/Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<PyRevitExtDir>$(PyRevitRootDir)\extensions</PyRevitExtDir>

<RevitDevDir>$(PyRevitDevDir)\libs\Revit</RevitDevDir>
<IronPythonStdLibDir>$(PyRevitDevDir)\libs\IronPython\</IronPythonStdLibDir>
<IronPythonStdLibDir>$(PyRevitDevDir)\libs\IronPython</IronPythonStdLibDir>
</PropertyGroup>

<PropertyGroup Condition="$(UseDefaultBin) == 'true'">
Expand Down
3 changes: 2 additions & 1 deletion dev/pyRevitLoader/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PropertyGroup Condition="'$(MSBuildProjectName)' != 'pyRevitAssemblyBuilder'">
<UseRevit>true</UseRevit>
<UseWpf>true</UseWpf>
<UseWindowsForms>true</UseWindowsForms>
<UseAssemblyBuilder>true</UseAssemblyBuilder>
<TargetFrameworks>net48;net8.0-windows</TargetFrameworks>

<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
Expand Down
14 changes: 9 additions & 5 deletions dev/pyRevitLoader/Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<None Remove="obj\**" />
</ItemGroup>

<ItemGroup Condition="$(UseRunner)=='' OR $(UseRunner)==False">
<ItemGroup Condition="$(UseLoader)==True">
<Compile Include="..\Source\PyRevitLoaderApplication.cs" Link="Source\PyRevitLoaderApplication.cs"/>
<Compile Include="..\Source\ScriptExecutor.cs" Link="Source\ScriptExecutor.cs"/>
</ItemGroup>
Expand All @@ -17,7 +17,7 @@
<Compile Include="..\Source\ScriptExecutor.cs" Link="Source\ScriptExecutor.cs"/>
</ItemGroup>

<ItemGroup Condition="$(IronPythonVersion.Contains('PR'))">
<ItemGroup Condition="'$(MSBuildProjectName)' != 'pyRevitAssemblyBuilder' AND $(IronPythonVersion.Contains('PR'))">
<Reference Include="IronPython" HintPath="$(PyRevitEnginesDir)\$(IronPythonVersion)\pyRevitLabs.IronPython.dll"/>
<Reference Include="IronPython.Modules" HintPath="$(PyRevitEnginesDir)\$(IronPythonVersion)\pyRevitLabs.IronPython.Modules.dll"/>
<Reference Include="IronPython.SQLite" HintPath="$(PyRevitEnginesDir)\$(IronPythonVersion)\pyRevitLabs.IronPython.SQLite.dll"/>
Expand All @@ -26,15 +26,19 @@
<Reference Include="Microsoft.Scripting" HintPath="$(PyRevitEnginesDir)\$(IronPythonVersion)\pyRevitLabs.Microsoft.Scripting.dll"/>
</ItemGroup>

<ItemGroup Condition="!$(IronPythonVersion.Contains('PR'))">
<ItemGroup Condition="'$(MSBuildProjectName)' != 'pyRevitAssemblyBuilder' AND !$(IronPythonVersion.Contains('PR'))">
<PackageReference Include="IronPython" Version="$(Version)"/>
</ItemGroup>

<ItemGroup>
<Reference Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'" Include="Microsoft.CSharp"/>
</ItemGroup>

<ItemGroup>

<ItemGroup Condition="'$(UseAssemblyBuilder)' == 'true'">
<ProjectReference Include="..\pyRevitAssemblyBuilder\pyRevitAssemblyBuilder.csproj" />
</ItemGroup>

<ItemGroup Condition="'$(MSBuildProjectName)' != 'pyRevitAssemblyBuilder'">
<EmbeddedResource Include="$(IronPythonStdLibDir)\$(IronPythonStdLib)"/>
</ItemGroup>

Expand Down
72 changes: 70 additions & 2 deletions dev/pyRevitLoader/Source/PyRevitLoaderApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
using System.Reflection;
using Autodesk.Revit.UI;
using Autodesk.Revit.Attributes;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using pyRevitAssemblyBuilder.AssemblyMaker;
using pyRevitAssemblyBuilder.SessionManager;
using pyRevitAssemblyBuilder.Shared;
using pyRevitAssemblyBuilder.Startup;

/* Note:
* It is necessary that this code object do not have any references to IronPython.
Expand All @@ -28,7 +34,21 @@ Result IExternalApplication.OnStartup(UIControlledApplication application)

try
{
return ExecuteStartupScript(application);
// we need a UIApplication object to assign as `__revit__` in python...
var versionNumber = application.ControlledApplication.VersionNumber;
var fieldName = int.Parse(versionNumber) >= 2017 ? "m_uiapplication" : "m_application";
var fi = application.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);

var uiApplication = (UIApplication)fi.GetValue(application);

var executor = new ScriptExecutor(uiApplication);
var result = ExecuteStartupScript(application);
if (result == Result.Failed)
{
TaskDialog.Show("Error Loading pyRevit", executor.Message);
}

return result;
}
catch (Exception ex)
{
Expand All @@ -54,6 +74,13 @@ private static void LoadAssembliesInFolder(string folder)
}

private static Result ExecuteStartupScript(UIControlledApplication uiControlledApplication)
{
//TODO: Implement a switcher here to be able to switch between Python/C# loaders
//return ExecuteStartUpPython(uiControlledApplication);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commented code indicates a switcher to toggle between Python and C# loaders, but the implementation currently just calls the C# version. Consider implementing this switch with a configuration option so users can choose which loader to use.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think @sanzoghenzo @dosymep @dosymep, should we keep 2 loaders and some sort of a transition period while migrating? I can add a key with a true/false value to the pyRevitConfig.ini in order to switch between them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that a feature flag is a good idea, it allows is to keep things working the old way while letting beta testers in and ironing out bugs that may (will) pop up.

return ExecuteStartUpCsharp(uiControlledApplication);
}

public static Result ExecuteStartUpPython(UIControlledApplication uiControlledApplication)
{
// we need a UIApplication object to assign as `__revit__` in python...
var versionNumber = uiControlledApplication.ControlledApplication.VersionNumber;
Expand All @@ -76,7 +103,48 @@ private static Result ExecuteStartupScript(UIControlledApplication uiControlledA

return result;
}
public static Result ExecuteStartUpCsharp(UIControlledApplication uiControlledApplication)
{
try
{
var versionNumber = uiControlledApplication.ControlledApplication.VersionNumber;
var fieldName = int.Parse(versionNumber) >= 2017 ? "m_uiapplication" : "m_application";
var fi = uiControlledApplication.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
var uiApplication = (UIApplication)fi.GetValue(uiControlledApplication);

var services = new ServiceCollection();
services.AddLogging(cfg => cfg.AddDebug());

// Add Revit UIApplication to services
services.AddSingleton(uiApplication);

// Add known services
services.AddSingleton<ICommandTypeGenerator, DefaultCommandTypeGenerator>();
services.AddSingleton<IHookManager, DummyHookManager>();
services.AddSingleton<IUIManager, UIManagerService>();
services.AddSingleton<ISessionManager, SessionManagerService>();
services.AddSingleton<IExtensionManager, ExtensionManagerService>();

// Register AssemblyBuilderService with explicit string parameter
services.AddSingleton<AssemblyBuilderService>(sp =>
new AssemblyBuilderService(
sp.GetRequiredService<ICommandTypeGenerator>(),
versionNumber
)
);

var serviceProvider = services.BuildServiceProvider();
var sessionManager = serviceProvider.GetRequiredService<ISessionManager>();
sessionManager.LoadSessionAsync().Wait();

return Result.Succeeded;
}
catch (Exception ex)
{
TaskDialog.Show("Error Starting pyRevit Session", ex.ToString());
return Result.Failed;
}
}
private static string GetStartupScriptPath()
{
var loaderDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
Expand All @@ -90,4 +158,4 @@ Result IExternalApplication.OnShutdown(UIControlledApplication application)
return Result.Succeeded;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System;
using System.IO;
using System.Reflection;
using System.Reflection.Emit;
using pyRevitAssemblyBuilder.Shared;

namespace pyRevitAssemblyBuilder.AssemblyMaker
{
public class AssemblyBuilderService
{
private readonly ICommandTypeGenerator _typeGenerator;
private readonly string _revitVersion;

public AssemblyBuilderService(ICommandTypeGenerator typeGenerator, string revitVersion)
{
_typeGenerator = typeGenerator;
_revitVersion = revitVersion ?? throw new ArgumentNullException(nameof(revitVersion));
}

public ExtensionAssemblyInfo BuildExtensionAssembly(IExtension extension)
{
string extensionHash = GetStableHash(extension.GetHash() + _revitVersion).Substring(0, 16);
string fileName = $"pyRevit_{_revitVersion}_{extensionHash}_{extension.Name}.dll";

string outputDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"pyRevit",
_revitVersion
);
Directory.CreateDirectory(outputDir);

string outputPath = Path.Combine(outputDir, fileName);

var asmName = new AssemblyName(extension.Name)
{
Version = new Version(1, 0, 0, 0)
};

string fileNameWithoutExt = Path.GetFileNameWithoutExtension(outputPath);

#if NETFRAMEWORK
var domain = AppDomain.CurrentDomain;
var asmBuilder = domain.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.RunAndSave, outputDir);
var moduleBuilder = asmBuilder.DefineDynamicModule(fileNameWithoutExt, fileName);
#else
var asmBuilder = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
var moduleBuilder = asmBuilder.DefineDynamicModule(fileNameWithoutExt);
#endif

foreach (var cmd in extension.GetAllCommands())
{
_typeGenerator.DefineCommandType(extension, cmd, moduleBuilder);
}

#if NETFRAMEWORK
asmBuilder.Save(fileName);
#else
var generator = new Lokad.ILPack.AssemblyGenerator();
generator.GenerateAssembly(asmBuilder, outputPath);
#endif

return new ExtensionAssemblyInfo(
name: extension.Name,
location: outputPath,
isReloading: CheckIfExtensionAlreadyLoaded(extension.Name)
);
}

private bool CheckIfExtensionAlreadyLoaded(string extensionName)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
if (asm.GetName().Name == extensionName)
return true;
}
return false;
}

private static string GetStableHash(string input)
{
using (var sha1 = System.Security.Cryptography.SHA1.Create())
{
var hash = sha1.ComputeHash(System.Text.Encoding.UTF8.GetBytes(input));
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Reflection.Emit;
using System.Reflection;
using pyRevitAssemblyBuilder.Shared;

namespace pyRevitAssemblyBuilder.AssemblyMaker
{
public class DefaultCommandTypeGenerator : ICommandTypeGenerator
{
public void DefineCommandType(IExtension extension, ICommandComponent command, ModuleBuilder moduleBuilder)
{
var typeBuilder = moduleBuilder.DefineType(
name: command.UniqueId,
attr: TypeAttributes.Public | TypeAttributes.Class
);

var attrBuilder = new CustomAttributeBuilder(
typeof(System.ComponentModel.DescriptionAttribute).GetConstructor(new[] { typeof(string) }),
new object[] { command.Tooltip }
);
typeBuilder.SetCustomAttribute(attrBuilder);

typeBuilder.CreateType();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace pyRevitAssemblyBuilder.AssemblyMaker
{
public class ExtensionAssemblyInfo
{
public string Name { get; }
public string Location { get; }
public bool IsReloading { get; }

public ExtensionAssemblyInfo(string name, string location, bool isReloading)
{
Name = name;
Location = location;
IsReloading = isReloading;
}

public override string ToString()
{
return $"{Name} ({(IsReloading ? "Reloaded" : "Fresh")}) -> {Location}";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using pyRevitAssemblyBuilder.Shared;
using System.Reflection.Emit;

namespace pyRevitAssemblyBuilder.AssemblyMaker
{
public interface ICommandTypeGenerator
{
void DefineCommandType(IExtension extension, ICommandComponent command, ModuleBuilder moduleBuilder);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Microsoft.Extensions.DependencyInjection;
using pyRevitAssemblyBuilder.AssemblyMaker;

namespace pyRevitAssemblyBuilder.Startup
{
public static class ServiceRegistration
{
public static IServiceCollection AddAssemblyBuilder(this IServiceCollection services)
{
services.AddSingleton<ICommandTypeGenerator, DefaultCommandTypeGenerator>();
services.AddSingleton<AssemblyBuilderService>();
return services;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Reflection;
using System.Reflection.Emit;
using pyRevitAssemblyBuilder.Shared;

namespace pyRevitAssemblyBuilder.AssemblyMaker
{
public class TypeMakerService : ICommandTypeGenerator
{
public void DefineCommandType(IExtension extension, ICommandComponent command, ModuleBuilder moduleBuilder)
{
var typeBuilder = moduleBuilder.DefineType(
name: command.UniqueId,
attr: TypeAttributes.Public | TypeAttributes.Class
);

if (!string.IsNullOrEmpty(command.Tooltip))
{
var attrBuilder = new CustomAttributeBuilder(
typeof(System.ComponentModel.DescriptionAttribute).GetConstructor(new[] { typeof(string) }),
new object[] { command.Tooltip }
);
typeBuilder.SetCustomAttribute(attrBuilder);
}

// Emit a default constructor
typeBuilder.DefineDefaultConstructor(MethodAttributes.Public);

// Create the type (works in .NET Standard 2.0 via CreateTypeInfo)
_ = typeBuilder.CreateTypeInfo();
}
}
}
Loading