ClearScript icon indicating copy to clipboard operation
ClearScript copied to clipboard

ClearScript Excessive COM Type Library Loading

Open matvdl opened this issue 8 months ago • 9 comments

ClearScript Version: 7.5 .NET Version: 4.6.2

Issue: We are integrating ClearScript into our .NET application, which heavily utilizes COM interop. After adding our primary COM object using AddHostObject, any access from script triggers extensive and repeated calls to TypeLibConverter.ConvertTypeLibToAssembly

These repeated type library conversions cause significant delays (minutes), and Visual Studio's Modules window shows COM libraries loading multiple times.

We've already tried:

  1. Using HostItemFlags.DirectAccess — No improvement
  2. Switching to AddCOMObject instead of AddHostObject — No improvement
  3. Ensuring Embed Interop Types is set to false — No improvement
  4. Wrapping objects explicitly using Marshal.CreateWrapperOfType helps initially, but associated COM object accesses trigger the issue again.
  5. We have tried in both the legacy JScript engine and V8 engine - does not seem to make a difference.

Questions: Is there a recommended approach or setting in ClearScript to avoid repeated COM type library loading? It's taking around 5m to load the info and finally return.

Any guidance would be greatly appreciated—thanks!

matvdl avatar Mar 10 '25 05:03 matvdl

Hi @matvdl,

As far as we can tell, ClearScript doesn't invoke ConvertTypeLibToAssembly directly. Could you post a stack trace?

Thanks!

ClearScriptLib avatar Mar 10 '25 06:03 ClearScriptLib

Sure - here is the call-stack

 	[Managed to Native Transition]	
 	mscorlib.dll!System.Reflection.RuntimeAssembly.GetType(string name, bool throwOnError, bool ignoreCase)	Unknown
 	mscorlib.dll!System.Runtime.InteropServices.TypeLibConverter.TypeResolveHandler.ResolveEvent(object sender, System.ResolveEventArgs args)	Unknown
 	mscorlib.dll!System.AppDomain.OnTypeResolveEvent(System.Reflection.RuntimeAssembly assembly, string typeName)	Unknown
 	[Native to Managed Transition]	
 	[Managed to Native Transition]	
 	mscorlib.dll!System.Runtime.InteropServices.TypeLibConverter.ConvertTypeLibToAssembly(object typeLib, string asmFileName, System.Runtime.InteropServices.TypeLibImporterFlags flags, System.Runtime.InteropServices.ITypeLibImporterNotifySink notifySink, byte[] publicKey, System.Reflection.StrongNameKeyPair keyPair, string asmNamespace, System.Version asmVersion)	Unknown
 	mscorlib.dll!System.Runtime.InteropServices.ImporterCallback.ResolveRef(object TypeLib)	Unknown
 	mscorlib.dll!System.Runtime.InteropServices.TypeLibConverter.TypeResolveHandler.ResolveRef(object typeLib)	Unknown
 	[Native to Managed Transition]	
 	[Managed to Native Transition]	
 	mscorlib.dll!System.Runtime.InteropServices.TypeLibConverter.ConvertTypeLibToAssembly(object typeLib, string asmFileName, System.Runtime.InteropServices.TypeLibImporterFlags flags, System.Runtime.InteropServices.ITypeLibImporterNotifySink notifySink, byte[] publicKey, System.Reflection.StrongNameKeyPair keyPair, string asmNamespace, System.Version asmVersion)	Unknown
 	mscorlib.dll!System.Runtime.InteropServices.Marshal.GetTypeForITypeInfo(System.IntPtr piTypeInfo)	Unknown
 	ClearScript.Core.dll!Microsoft.ClearScript.Util.COM.TypeInfoHelpers.GetManagedType.AnonymousMethod__0(System.Guid _)	Unknown
 	mscorlib.dll!System.Collections.Concurrent.ConcurrentDictionary<System.Guid, System.Type>.GetOrAdd(System.Guid key, System.Func<System.Guid, System.Type> valueFactory)	Unknown
 	ClearScript.Core.dll!Microsoft.ClearScript.Util.COM.TypeInfoHelpers.GetManagedType(System.Runtime.InteropServices.ComTypes.ITypeInfo typeInfo)	Unknown
 	ClearScript.Core.dll!Microsoft.ClearScript.Util.ObjectHelpers.GetTypeForTypeInfo(System.Runtime.InteropServices.ComTypes.ITypeInfo typeInfo)	Unknown
 	ClearScript.Core.dll!Microsoft.ClearScript.Util.ObjectHelpers.GetTypeOrTypeInfo(object value)	Unknown
 	ClearScript.Core.dll!Microsoft.ClearScript.HostItem.Wrap(Microsoft.ClearScript.ScriptEngine engine, object obj, System.Type type, Microsoft.ClearScript.HostItemFlags flags)	Unknown
 	ClearScript.Windows.Core.dll!Microsoft.ClearScript.Windows.Core.WindowsScriptEngine.MarshalToScriptInternal(object obj, Microsoft.ClearScript.HostItemFlags flags, System.Collections.Generic.HashSet<System.Array> marshaledArraySet)	Unknown
 	ClearScript.Windows.Core.dll!Microsoft.ClearScript.Windows.Core.WindowsScriptEngine.MarshalToScript(object obj, Microsoft.ClearScript.HostItemFlags flags)	Unknown
 	ClearScript.Core.dll!Microsoft.ClearScript.ScriptEngine.MarshalToScript(object obj)	Unknown
 	System.Core.dll!System.Linq.Enumerable.WhereSelectArrayIterator<System.__Canon, System.__Canon>.MoveNext()	Unknown
 	System.Core.dll!System.Linq.Buffer<object>.Buffer(System.Collections.Generic.IEnumerable<object> source)	Unknown
 	System.Core.dll!System.Linq.Enumerable.ToArray<object>(System.Collections.Generic.IEnumerable<object> source)	Unknown
 	ClearScript.Core.dll!Microsoft.ClearScript.ScriptEngine.MarshalToScript(object[] args)	Unknown
 	ClearScript.Windows.Core.dll!Microsoft.ClearScript.Windows.Core.WindowsScriptItem.SetProperty.AnonymousMethod__15_0((Microsoft.ClearScript.Windows.Core.WindowsScriptItem self, string name, object[] args) ctx)	Unknown
 	ClearScript.Windows.Core.dll!Microsoft.ClearScript.Windows.Core.WindowsScriptEngine.ScriptInvoke.AnonymousMethod__60_0((Microsoft.ClearScript.Windows.Core.WindowsScriptEngine self, System.Action<(Microsoft.ClearScript.Windows.Core.WindowsScriptItem, string, object[])> action, (Microsoft.ClearScript.Windows.Core.WindowsScriptItem, string, object[]) arg) ctx)	Unknown
 	ClearScript.Core.dll!Microsoft.ClearScript.ScriptEngine.ScriptInvokeInternal<(System.__Canon, System.__Canon, (System.__Canon, System.__Canon, System.__Canon))>(System.Action<(System.__Canon, System.__Canon, (System.__Canon, System.__Canon, System.__Canon))> action, (System.__Canon, System.__Canon, (System.__Canon, System.__Canon, System.__Canon)) arg)	Unknown
 	ClearScript.Core.dll!Microsoft.ClearScript.ScriptEngine.ScriptInvoke<(Microsoft.ClearScript.Windows.Core.WindowsScriptEngine, System.Action<(Microsoft.ClearScript.Windows.Core.WindowsScriptItem, string, object[])>, (Microsoft.ClearScript.Windows.Core.WindowsScriptItem, string, object[]))>(System.Action<(Microsoft.ClearScript.Windows.Core.WindowsScriptEngine, System.Action<(Microsoft.ClearScript.Windows.Core.WindowsScriptItem, string, object[])>, (Microsoft.ClearScript.Windows.Core.WindowsScriptItem, string, object[]))> action, (Microsoft.ClearScript.Windows.Core.WindowsScriptEngine, System.Action<(Microsoft.ClearScript.Windows.Core.WindowsScriptItem, string, object[])>, (Microsoft.ClearScript.Windows.Core.WindowsScriptItem, string, object[])) arg)	Unknown
 	ClearScript.Windows.Core.dll!Microsoft.ClearScript.Windows.Core.WindowsScriptEngine.ScriptInvoke<(Microsoft.ClearScript.Windows.Core.WindowsScriptItem, string, object[])>(System.Action<(Microsoft.ClearScript.Windows.Core.WindowsScriptItem, string, object[])> action, (Microsoft.ClearScript.Windows.Core.WindowsScriptItem, string, object[]) arg)	Unknown
 	ClearScript.Windows.Core.dll!Microsoft.ClearScript.Windows.Core.WindowsScriptItem.SetProperty(string name, object[] args)	Unknown
>	dotNETCode.dll!EvaluatorJS.get_compiledInstance() Line 73	Basic
 	dotNETCode.dll!EvaluatorJS.Eval(String script) Line 313	Basic
 	TestdotNetFunctions.exe!TestdotNetFunctions.ScriptEngineManagerJScript.RunEvaluateJsScriptJScript() Line 131	Basic
 	TestdotNetFunctions.exe!TestdotNetFunctions.Module1.Main() Line 18	Basic

You can see in the modules window where it loads libraries multiple times

Image

matvdl avatar Mar 10 '25 06:03 matvdl

Sure - here is the call-stack [...]

Hmm, we aren't seeing anything unusual or incorrect in the stack. Given an unknown COM object (one without managed type information), ClearScript has limited options for discovering its capabilities. In the worst case – that is, if no relevant interop assembly is loaded or registered – one must be generated at runtime.

You can see in the modules window where it loads libraries multiple times

The differences in naming suggest that those assemblies are not the same. Our guess would be that one is the native COM DLL, one is the generated interop assembly, and one might be a JIT compiled version of the latter. In any case, the runtime should reuse previously generated interop assemblies; if that's not happening, it isn't clear why.

Can you say anything about the COM API in question? Is it relatively large? Does it have an interop assembly registered? Does your .NET application interact with it directly, or does all interaction originate in script code? How are you instantiating the primary COM object for handoff to ClearScript?

Thanks!

ClearScriptLib avatar Mar 10 '25 12:03 ClearScriptLib

Thanks for the reply!

More Context:

The COM API involved is extensive, referencing approximately 10 other COM objects.

We already have generated and registered interop assemblies (Interop.*.dll) for all these COM libraries. These are explicitly referenced by our .NET application.

We have also found that if we continue to execute the code, many modules with the same name get repeatedly loaded. Image

Our .NET application directly interacts extensively with these COM objects. Typically, we instantiate the COM objects via CreateObject (late-bound) (sometimes using the New keyword, but this makes no difference regarding this issue) in VB.NET and immediately cast them to strongly-typed interop types. If we use AddHostObject with something that looks like __ComObject, the issue is triggered (even when it is declared as the underlying type—for example, kWatchServer.usrUser).

We explicitly pass these wrapped objects to ClearScript using Marshal.CreateWrapperOfType (creating a kWatchServer.usrUserClass wrapper), which successfully prevents issues on the initial object. However, accessing any associated COM objects (item.ClassObject.Server) re-triggers full type-library conversion. (item is the base usrUser object, while ClassObject returns itself (usrUser) but is declared as an Object, which causes it to be late-bound.)

Key issue:

Despite using pre-built interop assemblies, ClearScript still invokes repeated calls to ConvertTypeLibToAssembly each time a COM object's property returns another COM object (e.g., when a property returns itself as Object).

Is there a way to get ClearScript to only use IDispatch rather than requiring full knowledge of all objects? Is there something else I need to do to make the system aware of the interop DLLs?

matvdl avatar Mar 11 '25 22:03 matvdl

Hi @matvdl,

Thanks for providing additional information!

The COM API involved is extensive, referencing approximately 10 other COM objects.

Ah, so hiding the whole thing behind a strongly typed .NET façade isn't something you'd want to do, right? 😁

We already have generated and registered interop assemblies (Interop.*.dll) for all these COM libraries.

Would you mind describing how – that is, where in the registry – the interop assemblies are registered? For example, ClearScript looks for HKCR\TypeLib\{LIBID}\{Major}.{Minor}\PrimaryInteropAssemblyName. Perhaps there's another place it should look?

We have also found that if we continue to execute the code, many modules with the same name get repeatedly loaded.

That's very surprising, as Marshal.GetTypeForITypeInfo checks for an existing assembly before building another one. By any chance, are you using multiple application domains? In any case, ClearScript wouldn't even make that call if it found an interop assembly in the registry.

We explicitly pass these wrapped objects to ClearScript using Marshal.CreateWrapperOfType (creating a kWatchServer.usrUserClass wrapper), which successfully prevents issues on the initial object.

That shouldn't be necessary. If you have a strongly typed COM object on the .NET side, you can use AddRestrictedHostObject to expose it with type information. However, as you've discovered, if a COM property or method returns an unknown COM object, you'll run into the same issue.

Is there a way to get ClearScript to only use IDispatch rather than requiring full knowledge of all objects?

Ironically, that might already work on modern .NET platforms, which don't support Marshal.GetTypeForITypeInfo. On .NET Framework, ClearScript always attempts to acquire strong type information via ITypeInfo. We could add an option to bypass that method, but you'd have to wait for the next release.

Here's a summary:

  • We'd love to make your scenario work, but we need to reproduce the issue to test any fix. Would it be possible to post a small sample that demonstrates the problem?
  • In the meantime, the best way forward might be to understand why ClearScript isn't finding your interop assemblies. You might be able to make it work by changing how they're registered.
  • Additionally, if you're OK with JScript or VBScript as opposed to V8, HostItemFlags.DirectAccess should work. It just has no effect with V8.

Cheers!

ClearScriptLib avatar Mar 12 '25 19:03 ClearScriptLib

Thanks for the reply and apologies for the delay in responding.

I’m very keen to get this working, as the performance gains would be significant for our application. We’ve moved most of our code to .NET, but we still need to handle these legacy DLLs for a while longer.

Thanks again for the guidance on strong naming and creating a Primary Interop Assembly (PIA) to avoid repeated ConvertTypeLibToAssembly calls. I understand that .NET generally requires a strongly named assembly for a “true” PIA.

I checked the registry at the expected location (HKCR\TypeLib{LIBID}\1.0), and it appears there are no entries (e.g., PrimaryInteropAssemblyName) registered. This indicates that we currently have only simple interop assemblies rather than fully registered PIAs.

Given our scenario—where we have a large COM library returning nested objects (some of which are late-bound)—is strongly naming + registering a PIA the only reliable way to stop ClearScript and .NET from regenerating interop assemblies at runtime?

Or is there a simpler workaround, such as:

  1. Forcing ClearScript to use raw IDispatch (so it doesn’t need deep type info)?
  2. Relying on a non-primary interop?
  3. Some other setting or flag in ClearScript that could bypass the full reflection?

I just want to confirm if there’s no alternative approach we might have missed if we aren’t quite ready to fully commit to strong naming and GAC registration.

Thanks again for all your help!

matvdl avatar Mar 19 '25 09:03 matvdl

Hi @matvdl,

Thanks for the reply and apologies for the delay in responding.

Sure thing, and no problem at all! 😊

Given our scenario—where we have a large COM library returning nested objects (some of which are late-bound)—is strongly naming + registering a PIA the only reliable way to stop ClearScript and .NET from regenerating interop assemblies at runtime?

Without modifying ClearScript or migrating off .NET Framework, that's likely – and because we don't have a test case, we can't verify that it would work for you. A test case might also reveal why interop assemblies are being generated redundantly, potentially leading to another solution.

Forcing ClearScript to use raw IDispatch (so it doesn’t need deep type info)?

ClearScript does have that capability, but on .NET Framework, it's used only if Marshal.GetTypeForITypeInfo fails. We could provide a flag that bypasses that call, but (a) you'd have to wait for the next ClearScript release, and (b) we still couldn't verify that it would work for you. However, we might be able to give you a modified ClearScript.Core.dll to test.

Relying on a non-primary interop?

That's something we'll look into, but whether it would work for you is even less clear than with other options. As far as we're aware, non-primary interop assemblies are only registered for CLSIDs, so ClearScript could find them only for objects that support IProvideClassInfo.

Some other setting or flag in ClearScript that could bypass the full reflection?

Unfortunately, we aren't aware of any such setting in the current release.

Thanks!

ClearScriptLib avatar Mar 19 '25 16:03 ClearScriptLib

By the way, if you're interested in a quick-and-dirty solution that just might work, you could try live-patching ClearScript to bypass the call to Marshal.GetTypeForITypeInfo.

First, add the Lib.Harmony package to your project.

Next, add the following class somewhere within reach of your application's initialization code:

// using HarmonyLib;
internal static class ClearScriptPatcher {
    public static void Patch() {
        var type = typeof(ScriptEngine).Assembly.GetType("Microsoft.ClearScript.Util.COM.TypeInfoHelpers");
        var method = type.GetMethod("GetManagedType", BindingFlags.Public | BindingFlags.Static);
        var prefix = typeof(ClearScriptPatcher).GetMethod(nameof(GetManagedTypePrefix), BindingFlags.NonPublic | BindingFlags.Static);
        new Harmony(typeof(ClearScriptPatcher).FullName).Patch(method, prefix);
    }
    private static bool GetManagedTypePrefix(out Type __result, TypeInfo typeInfo) {
        __result = null;
        return false;
    }
}

Finally, make the following call once at startup – before using any ClearScript APIs:

ClearScriptPatcher.Patch();

Please let us know if that works. Thanks!

ClearScriptLib avatar Mar 19 '25 18:03 ClearScriptLib

Hi @matvdl,

Friendly reminder: Please let us know if that worked for you. If it did, we'll add a way to bypass that call without patching.

Thanks!

ClearScriptLib avatar Mar 25 '25 16:03 ClearScriptLib