Java 调用 C# DLL 的加载问题
2024-07-17 23:27:37

Java 调用托管 DLL 的方式

基本上就两种方式:

  1. C# 开发 COM 组件,Java 调用。
  2. 用 C++/CLI 编写 DLL,暴露接口给 Java,内部再调用 C# DLL 的接口,起到桥接的作用。


用 COM 组件的方式简单直接,只是不够便携,需要注册才可使用,注册后 DLL 不可移动文件位置,否则要重新注册,不过也算不上大问题。
用 C++/CLI 方案就繁琐些,需要了解一点 C++/CLI 语法。但是对于 Java 来说就像调用纯 C DLL 一样简单。

Java 加载托管 DLL 的问题

在实践 C++/CLI 方案中,碰到一个让人在意的问题:托管 DLL 放在哪里才能被 Java 加载?
在开发 C++/CLI 工程时,正常做法都会引用 C# 工程:

1
#using "CSharp.dll"

因为只有 using 后才能在代码中调用托管接口,但这产生了一个问题,C++ DLL 在被加载时会启动 CLR,而 CLR 只会从当前进程目录和 GAC 中加载托管 DLL。如果主程序是 C++ 等程序倒不会有问题,因为 exe 和 dll 文件都在一个目录下,自然可以顺利加载。但是到了 Java 这里情况就不同了,比如应用目录是这样:

1
2
3
app.jar
Native.dll
CSharp.dll

因为 Java 应用并不是传统的 exe 可执行文件,它最终是由java.exe作为宿主进程运行的,而java.exe 文件在%JAVA_HOME%\bin目录下,CLR 会从这里加载托管 DLL,这就逼迫我们必须将托管 DLL 放置在%JAVA_HOME%\bin目录下,很有侵入性。


我想到有两种解决方案:

  1. 在开发 C++/CLI 工程时,不要强引用 C# DLL 工程,这样生成的 DLL 文件不会在初始化时去加载托管 DLL。然后在代码中动态加载托管 DLL,大概是这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    String^ currentDir = Path::GetDirectoryName(Assembly::GetExecutingAssembly()->Location);
    String^ dllPath = Path::Combine(currentDir, "CSharp.dll");
    Assembly::LoadFrom(dllPath);

    // 动态查找接口
    static MethodInfo^ GetMethod(String^ name)
    {
    Type^ type = lib->GetType("mynamespae.Demo");
    return type->GetMethod(name);
    }

    这样做的缺点就是在开发时会痛苦,因为无法直接使用托管 DLL 的接口了,完全靠动态加载,显得比较繁琐。好处是能将 DLL 放在 jar 包同一个目录下。

  2. 将托管 DLL 打包到资源中,在加载 C++ DLL 之前先将托管 DLL 释放到%JAVA_HOME%\bin目录,然后再加载 C++ DLL 文件。不过这样会污染 Java 目录。


我个人推荐第一种方式,牺牲一点开发时的便捷性,提高环保性。

相关阅读

实现通过COM组件方式实现java调用C#写的DLL文件的完整demo
Java调用C#的DLL的坑
使用 C++/CLI 进行 .NET 编程