Garuda/Eagle in starpack
Not logged in

Garuda/Eagle in starpack

(1.2) Originally by Yusuke Yamasaki (yyamasak) with edits by mistachkin on 2021-09-13 23:58:45 from 1.1 [link] [source]

(This is the rewrite of my post to comp.lang.tcl. I thought it was a not good place to post this kind of a question.)

Is it possible for a starpack-wrapped Tcl/Tk application to use external .NET assemblies using Garuda? I use the latest build (1.0.7900.33333, 2021-08-23).

My Tcl/Tk application is wrapped into a starpack with ActiveState TclApp. I wanted to check if Garuda can work with my app.

Ideally, I want to embed Eagle.dll and Garuda.dll into my starpack but at first, I just installed Eagle/Garuda binaries to the lib folder under the working directory and inserted the path to the head of ::auto_path.

The purpose to use Garuda is to load an external managed assembly from the local sub-folder. The assembly is dependent on several DLLs in the same folder. I keep them out of my starpack.

+ lib\
  + Garuda-1.0\
    + dotnet.tcl
    + Eagle.dll
    + Garuda.dll
    + helper.tcl
    + pkgIndex.tcl
+ GRPCRemoteClient\
  + Google.Protobuf.dll
  + Grpc.Core.Api.dll
  + Grpc.Core.dll
  + grcp_csharp_ext.x86.dll
  + GRPCRemoteClient.dll (This is the DLL to be loaded from MyApp.tcl)
  + System.Buffers.dll
  + System.Memory.dll
  + System.Numerics.Vectors.dll
  + System.Runtime.CompilerServices.Unsafe.dll
+ MyApp.exe (MyApp.tcl wrapped into exe.)

MyApp.tcl is the main script. It loads Garuda. The eagle command loads GRPCRemoteClient/GRPCRemoteClient.dll and the classes and the methods are used. The other DLLs in GRPCRemoteClient folder are loaded by GRPCRemoteClient.dll.

If MyApp.tcl is not wrapped into a starkit, it works well. However, if it is wrapped, it can't create any instance of classes defined inside GRPCRemoteClient.dll, although it seemed to load GRPCRemoteClient.dll successfully.

I followed the instruction in this article and added METHOD_PROTOCOL_V1R2 flag before loading Garuda package.

https://wiki.tcl-lang.org/page/Garuda

# MyApp.tcl

set exehome [pwd]
set auto_path [linsert $auto_path 0 [file join $::exehome lib]]

namespace eval ::Garuda {
    variable methodFlags 0x40; # METHOD_PROTOCOL_V1R2
}

package require Garuda

eagle {
    object invoke System.Reflection.Assembly LoadFrom ./GRPCRemoteClient/GRPCRemoteClient.dll
}

# => {GRPCRemoteClient, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null} c199bf34-6a72-4256-8e1c-5857959232ad 293
# => Seemed successful.

eagle {
    object create -alias GRPCRemote.GRPCRemoteClient "127.0.0.1" 50051
}

# => {type "GRPCRemote.GRPCRemoteClient" not found} {expected type value but got "GRPCRemote.GRPCRemoteClient"}
# => Failed.

(2) By mistachkin on 2021-09-13 16:38:51 in reply to 1.0 [link] [source]

I think you'll want to use the [object load] sub-command instead of manually
invoking the Assembly class, i.e. instead of using:

object invoke System.Reflection.Assembly LoadFrom ./GRPCRemoteClient/GRPCRemoteClient.dll 

Use something like:

object load -import -loadtype File ./GRPCRemoteClient/GRPCRemoteClient.dll

(3) By Yusuke Yamasaki (yyamasak) on 2021-09-14 01:19:32 in reply to 2 [link] [source]

A plain .tcl run in the same way as before. The wrapped exe raised an exception.

MyApp.tcl

set auto_path [linsert $auto_path 0 [file join [pwd] lib]]
package require Garuda
eagle {
    object load -import -loadtype File ./GRPCRemoteClient/GRPCRemoteClient.dll
}
# => {GRPCRemoteClient, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null} c199bf34-6a72-4256-8e1c-5857959232ad 291

MyApp.exe

set auto_path [linsert $auto_path 0 [file join [pwd] lib]]
package require Garuda
eagle {
    object load -import -loadtype File ./GRPCRemoteClient/GRPCRemoteClient.dll
}
# => System.Reflection.ReflectionTypeLoadException: Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more information.

   at System.Reflection.RuntimeModule.GetTypes(RuntimeModule module)

   at System.Reflection.RuntimeModule.GetTypes()

   at System.Reflection.Assembly.GetTypes()

   at Eagle._Components.Public.Interpreter.AddObjectNamespaces(Assembly assembly, Boolean nonPublic, MatchMode mode, String pattern, Boolean noCase, Result& error)

I have no idea how to write try-catch statement in Garuda to look into LoaderExceptions property.

(4) By mistachkin on 2021-09-14 01:48:17 in reply to 3 [link] [source]

From the error message, it seems that .NET is unable to locate the other DLLs needed for the primary one.

One thing to try is to temporarily move the GRPCRemoteClient files into the same directory as the EXE.

(5) By mistachkin on 2021-09-14 02:28:35 in reply to 4 [link] [source]

Here is something to try adding before the [object load]:

set appDomain [object invoke -alias AppDomain CurrentDomain]

proc assemblyResolve { sender e } {
  #
  # HACK: This assumes that each assembly is named using the pattern:
  #
  #       "<baseAssemblyName>.dll"
  #
  #       This may not be correct for all assemblies being loaded.
  #       Of course, this script procedure is free to simply use a
  #       list of hard-coded assembly name to file name mappings.
  #
  set fileName [file join \
      [file dirname [file dirname [lindex [info assembly] end]]] \
      GRPCRemoteClient [appendArgs [$e Name] [info sharedlibextension]]]

  puts stdout $fileName

  return [object load -loadtype File $fileName]
}

$appDomain -marshalflags +DynamicCallback add_AssemblyResolve assemblyResolve

(6) By Yusuke Yamasaki (yyamasak) on 2021-09-14 12:50:08 in reply to 5 [link] [source]

Since [info assembly] returned the path to Eagle.dll, I used a global variable which has the folder of the target DLLs.

MyApp.exe

proc assemblyResolve { sender e } {
  global dll_dir

  regexp {^([^,]+),.+$} [$e Name] -> name
  log "name = $name"

  set fileName [file join $dll_dir $name.dll]
  log "fileName=$fileName"

  set type [object load -loadtype File $fileName]
  log "type=$type"

  return $type
}

proc load_type {dll_path} {
  set type [object load -import -loadtype File $dll_path]
  log "type=$type"
  return $type
}

proc call_tcl {script} {
  tcl eval [tcl primary] $script
}

proc log {msg} {
  call_tcl [list puts $msg]
}

proc get_client {} {
  set host "127.0.0.1"
  set port 50051
  object create -alias GRPCRemote.GRPCRemoteClient $host $port
}

set appDomain [object invoke -alias AppDomain CurrentDomain]
$appDomain -marshalflags +DynamicCallback add_AssemblyResolve assemblyResolve

set dll_dir [file normalize "GRPCRemoteClient"]
set dll_file "GRPCRemoteClient.dll"
set dll_path [file join $dll_dir $dll_file]

if {[catch {load_type $dll_path} err]} {
  log $errorInfo
}

if {[catch {get_client} client]} {
  log $errorInfo
}

The AssemblyResolve delegate loaded two dependent assemblies but finally eagle could not load all the assemblies needed for GRPCRemoteClient.dll to work.

log

name = Google.Protobuf
fileName=C:/Users/yusuke/Documents/devel/IAS/PerkinElmer/Syngistix SDK v2/210914/Garuda-Syngistix/GRPCRemoteClient/Google.Protobuf.dll
type={Google.Protobuf, Version=3.12.0.0, Culture=neutral, PublicKeyToken=a7d26565bac4d604} e280b797-c0cd-438d-9770-77b6d396d79e 316
name = Grpc.Core.Api
fileName=C:/Users/yusuke/Documents/devel/IAS/PerkinElmer/Syngistix SDK v2/210914/Garuda-Syngistix/GRPCRemoteClient/Grpc.Core.Api.dll
type={Grpc.Core.Api, Version=2.0.0.0, Culture=neutral, PublicKeyToken=d754f35622e28bad} 480c6bff-dbc7-4d31-9c4e-de5aa2eda1e1 340
type={GRPCRemoteClient, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null} c199bf34-6a72-4256-8e1c-5857959232ad 299
type={GRPCRemoteClient, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null} c199bf34-6a72-4256-8e1c-5857959232ad 299
{type "GRPCRemote.GRPCRemoteClient" not found} {expected type value but got "GRPCRemote.GRPCRemoteClient"}

    while executing

"object create -alias GRPCRemote.GRPCRemoteClient $host $port"

    (procedure "get_client" line 4)

    invoked from within

"get_client"

    ("catch" body line 1)

(7) By mistachkin on 2021-09-14 15:25:39 in reply to 6 [link] [source]

There are several troubleshooting steps that may be useful when dealing with
this kind of error in an Eagle script.

First, adding the -verbose option to the **object create** or **object invoke**
sub-command calls.  This should give more clues about what Eagle actually did
try to resolve.

Second, check that the all the needed assemblies are loaded using something
like this:

    foreach assembly [object assemblies] { puts stdout $assembly }

Third, since you used the -import option, check resolution namespace list to
make sure it includes the desired namespaces:

    foreach namespace [object namespaces] { puts stdout $namespace }

Also, could you provide me a link where I can download the Google RPC client
binaries you are using so that I can help figure out how the type resolution
is failing?

(8) By Yusuke Yamasaki (yyamasak) on 2021-09-15 04:56:43 in reply to 7 [link] [source]

Thank you for the advice. I looked into the details by your instruction.

I sent the necessary files to your mail address.

Because the wrapped script failed but the unwrapped script was successful, I compared the differences.

Before explaining it, there should be a special consideration for GRPC behavior. I'm not sure of the detailed reason but if I set AssemblyResolve delegate to my unwrapped script version, an exception is thrown saying UnityEngine.dll is missing. I don't have UnityEngine.dll The reason is explained in this post. https://github.com/grpc/grpc/issues/20251

Anyway, I had to remove the AssemblyResolve delegate. Only the wrapped script has AssemblyResolve delegate.

The unwrapped script loads all the necessary assemblies when they are needed (lazy loading?). I checked it by calling [object assemblies]. But it was not enough. I checked the loaded DLLs by LockHunter. https://lockhunter.com/

At this I concluded that the following assemblies under GRPCRemoteClient folder are loaded to wish.exe (which sources the unwrapped script.)

Google.Protobuf.dll
Grpc.Core.dll
Grpc.Core.Api.dll
GRPCRemoteClient.dll
System.Memory.dll
System.Runtime.CompilerServices.Unsafe.dll
grpc_csharp_ext.x86.dll

The last one grpc_csharp_ext.x86.dll ‚Äčis a native DLL not a managed DLL.

So I tried somehow to load the same DLLs from the wrapped script. The following two assemblies are loaded by AssemblyResolve delegate.

Google.Protobuf.dll
Grpc.Core.Api.dll

LockHunter says only the following assemblies were loaded.

Google.Protobuf.dll
Grpc.Core.Api.dll
GRPCRemoteClient.dll

In order to load the rest of the assemblies, I explicitly called [object load -loadtype File $dll_path]. I could load them.

Grpc.Core.dll
System.Memory.dll
System.Runtime.CompilerServices.Unsafe.dll

Regarding the DLLs loaded to the memory, the difference between the unwrapped and wrapped versions is only one.

grpc_csharp_ext.x86.dll

This native DLL was not loaded. This DLL is referenced by Grpc.Core.dll (I grepped the binary data as text.) But it seems this managed assembly doesn't load it. For now, I believe this is the problem.

I also tried copying all DLLs to the same folder as the executable but the result was the same.

(9) By Yusuke Yamasaki (yyamasak) on 2021-09-15 05:14:27 in reply to 7 [link] [source]

Regarding the UnityEngine problem that I mentioned in the last message, it will be solved by returning a null object from the AssemblyResolve delegate. Is it possible in Eagle?

Grpc.Core.dll (disassembled code in C#)

    static PlatformApis()
    {
      PlatformID platform = Environment.OSVersion.Platform;
      PlatformApis.isMacOSX = platform == PlatformID.Unix && PlatformApis.GetUname() == "Darwin";
      PlatformApis.isLinux = platform == PlatformID.Unix && !PlatformApis.isMacOSX;
      PlatformApis.isWindows = platform == PlatformID.Win32NT || platform == PlatformID.Win32S || platform == PlatformID.Win32Windows;
      PlatformApis.isNetCore = false;
      PlatformApis.isMono = Type.GetType("Mono.Runtime") != (Type) null;
      Type type = Type.GetType("UnityEngine.Application, UnityEngine");
      if (type != (Type) null)
      {
        PlatformApis.isUnity = true;
        PlatformApis.isUnityIOS = type.GetTypeInfo().GetProperty("platform")?.GetValue((object) null)?.ToString() == "IPhonePlayer";
      }
      else
      {
        PlatformApis.isUnity = false;
        PlatformApis.isUnityIOS = false;
      }
      PlatformApis.isXamarinIOS = Type.GetType("Foundation.NSObject, Xamarin.iOS") != (Type) null;
      PlatformApis.isXamarinAndroid = Type.GetType("Java.Lang.Object, Mono.Android") != (Type) null;
      PlatformApis.isXamarin = PlatformApis.isXamarinIOS || PlatformApis.isXamarinAndroid;
    }

(11) By mistachkin on 2021-09-15 15:08:37 in reply to 9 [link] [source]

Yes, you should be able to simply use "return null" inside the event handler procedure to do this.

(10) By Yusuke Yamasaki (yyamasak) on 2021-09-15 12:37:43 in reply to 7 [link] [source]

I found DLLImport part in Grpc.Core.dll using dotPeek. It doesn't specify the full name of the DLL. ".86.dll" is omitted. I don't know if this is something like a naming rule for AnyCPU platform.

    internal class DllImportsFromSharedLib
    {
      private const string ImportName = "grpc_csharp_ext";

      [DllImport("grpc_csharp_ext")]
      public static extern void grpcsharp_init();

      [DllImport("grpc_csharp_ext")]
      public static extern void grpcsharp_shutdown();
      :

(12.5) By mistachkin on 2021-09-15 15:51:20 edited from 12.4 in reply to 10 [link] [source]

Maybe try renaming the file to remove the ".x86" suffix and make sure that directory is in the PATH?

e.g.:

addToPath C:/full/path/to/dir/bin

I'm looking at the sample code you sent now. It appears that the assembly "GRPCRemoteClient" is not signed with a strong name. This may have impact on the type resolution. The following appears to work:

object create -verbose -alias "GRPCRemote.GRPCRemoteClient, GRPCRemoteClient, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" $host $port

This uses an assembly qualified name.

Using the above change and one additional change to the assemblyResolve procedure, I am able to get the wrapped script working here locally. Here is the modified assemblyResolve procedure:

proc assemblyResolve { sender e } {
  # NOTE: The next [if] was added.  If the assembly name being resolved
  #       should NOT be handled by us, return null.
  if {[$e Name] in [list UnityEngine Xamarin.iOS Mono.Android]} then {return null}

  global dll_dir

  regexp {^([^,]+),.+$} [$e Name] -> name
  log "name = $name"

  set fileName [file join $dll_dir $name.dll]
  log "fileName=$fileName"

  set type [load_type $fileName]
  log "type=$type"

  return $type
}

If you like, I can send you the modified script via email; however, the above (two) changes are apparently all that was required so far.

(13.1) By Yusuke Yamasaki (yyamasak) on 2021-09-15 16:33:23 edited from 13.0 in reply to 12.2 [source]

Finally, the wrapped script could create an instance of GRPCRemoteClient.

These were necessary.

  • AssemblyResolve delegate.
  • Return null when an exception is thrown in AssemblyResolve delegate.
  • Fully qualified assembly name to instantiate GRPCRemoteClient.

These were not necessary.

  • Removing ".x86" from native assembly file name.
  • addToPath $dll_dir
  • Explicitly loading the following dependent assemblies.
    • Grpc.Core.dll
    • System.Memory.dll
    • System.Runtime.CompilerServices.Unsafe.dll

Really, thank you very much! I can proceed to integrate this code to my real application.

Rewrite of assemblyResolve

proc assemblyResolve { sender e } {
	global dll_dir
	
	if {[catch {
		set eName [$e Name]
		if {![regexp {^([^,]+),.+$} $eName -> name]} {
			set name $eName
		}
		log "name = $name"
		
		set dll_path [file join $dll_dir $name.dll]
		log "dll_path=$dll_path"
		
		if {[file exists $dll_path]} {
			set type [load_type $dll_path]
			log "type=$type"
		} else {
			return AssemblyNotFound
		}
	} err]} {
		log $err
		return null
	} else {
		return $type
	}
}

(14) By mistachkin on 2021-09-15 16:15:13 in reply to 13.0 [link] [source]

No problem. Your use case was quite interesting and in-depth.