65.9K
CodeProject 正在变化。 阅读更多。
Home

管理非托管代码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (16投票s)

2009年3月31日

CPOL

5分钟阅读

viewsIcon

56403

downloadIcon

744

托管 C++ 包装器, 用于本地 C++ 库

介绍  

将用原生 C++ 编写的遗留系统与 .NET 应用程序集成,最好的方法是什么?  首先想到的是 COM。是的,我们当然可以通过 COM 暴露原生 C++ 中的方法。  但处理 COM 并非易事。总有一个问题,**内存泄漏**。查找和修复这些泄漏非常痛苦。通过 COM,你只能暴露一组方法,那 OOA(面向对象分析)在哪里?有没有办法将这些方法分组为一个类? 我只是在朝这个方向探索。

那么,如果 C++ 需要通过 COM 与 .NET 语言交互,那么托管 C++ 或 C++/CLI 的目的是什么?  可以有一种方法来构建一个托管库,该库内部包装了一些原生 C++ 代码。因此,对于外部世界来说,它看起来像一个托管库,但内部包装了非托管代码。

问题陈述

如何创建这样一个 VC++ 库,它既可以被所有 .NET 语言使用,也可以被所有遗留语言(如原生 C++、Visual Basic 6、Visual FoxPro 等)使用。

解决方案  

针对上述问题,有两个已知的解决方案:

  1. 通过 COM/COM+ 暴露所需方法。这是多年来最广泛使用的方法。
  2. 为原生 C++ 库编写一个托管 C++/CLI 包装器。是的,托管库可以轻松被所有 .NET 语言访问,但遗留语言呢?要让遗留语言访问,需要通过 COM 暴露方法。即使通过 COM 暴露方法是一种迂回的方式,但我假设该库将在 .NET 语言中更常用,并且这有助于维护性。

在本文中,我们将讨论第二种方法。

优点

  1. 在 C++ 中处理 COM 非常繁琐。 
  2. 内存泄漏可以最小化,因为我们充分利用了 CLR。  
  3. COM 通常速度较慢且难以维护。 
  4. 方法可以分组为一个类,因此我们甚至可以用面向对象的概念来包装原生 C 代码。

第一步  

那么我从哪里开始呢?我有一个原生的 C++ 应用程序示例。我想将其包装在一个托管类中。我们面临的主要障碍是处理参数**。** 操作过程如下:

  1. 使用托管数据类型获取参数 [System::String^, System::UInt32, etc] 
  2. 将参数封送(Marshall)到非托管数据类型。 
  3. 使用这些非托管参数调用非托管方法。 
  4. 将返回值封送(Marshall)到托管数据类型。
  5. 返回托管参数。  

我知道这可能会很繁琐,但如果你能承受这种痛苦,你将受益匪浅。

Using the Code

让我们来看一个简单的 C++/CLI 示例应用程序,它包装了一些原生 C++ 方法。类定义如下: 

public ref class ManagedWrapperClass 
{
private:
	void MarshalString ( System::String ^ s, std::string& os );
public:
	int ManagedAdd(int num1, int num2);
			
	System::String^ ManagedConcat(System::String^ string1,System::String^ string2);
	void ManagedCompareConcat(System::String^ string1, System::String^ string2, 
		[System::Runtime::InteropServices::Out]System::String^ %concatString,
		[System::Runtime::InteropServices::Out] System::Int32 %result);	
};

// The Class which is being wrapped
public class unmanagedClass
{
public:
	unmanagedClass();
	int UnManagedMethodAddNum(int num1, int num2);
	
	std::string UnManagedMethodConcatString
			(std::string string1,std::string string2);
	
	void UnManagedMethodCompareConcat(std::string string1, 
		std::string string2, std::string* concatString, int *result);  
};		

在这里,我在 C++/CLI 中定义了一个类,它公开了三个方法。第一个方法ManagedAdd()相对简单,它只是将两个整数相加并返回总和。如果你看第二个方法 (ManagedConcat()),参数是System::String^,这是一种 .NET 兼容的数据类型。它需要一些封送。第三个方法ManagedCompareConcat()包装了一个包含一些OUT参数的方法,而这些参数在 C++ 中基本上是指针变量。

方法定义如下:

int  unmanagedClass::UnManagedMethodAddNum(int num1, int num2)
  {
        return num1+ num2;
  }

std::string unmanagedClass::UnManagedMethodConcatString
		(std::string string1,std::string string2)
  {
       return string1.append(string2);
  }
  
 void unmanagedClass::UnManagedMethodCompareConcat(std::string string1,
std::string string2, std::string * concatString, int *result)
    {
        //concatenating the string and storing it in the out parameter
        
        std::string  string3;
        string3.append(string1);
        string3.append(string2);
        concatString->assign(string3);
        int cmp = strcmp(string1.c_str(),string2.c_str());
        *result = cmp;
    }

包装器方法实现如下:

int ManagedWrapperClass::ManagedAdd(int num1,int num2)
    {
        unmanagedClass myClass ;
        return myClass.UnManagedMethodAddNum(num1,num2);
    }

System::String^ ManagedWrapperClass::ManagedConcat
		(System::String ^string1, System::String ^string2)
    {
        //marshalling parameters to unmanaged type
        std::string unmanagedString1, unmanagedString2;
        MarshalString(string1,unmanagedString1);
        MarshalString(string2,unmanagedString2);
        unmanagedClass myClass;

        //Invoking the unManagedmethod
        std::string resultString = 
	    myClass.UnManagedMethodConcatString(unmanagedString1,unmanagedString2);

        //Marshalling return parameter back to managed type
        System::String^ returnString  = gcnew System::String(resultString.c_str()); 
        return returnString;
    }

    void ManagedWrapperClass::ManagedCompareConcat(System::String^
string1, System::String^ string2,
[System::Runtime::InteropServices::Out]System::String^ %concatString,
[System::Runtime::InteropServices::Out] System::Int32 %result)

    {
        //marshalling parameters to unmanaged type
        std::string unmanagedString1, unmanagedString2;
        MarshalString(string1,unmanagedString1);
        MarshalString(string2,unmanagedString2);
 
        // invoking the unmanaged method
        unmanagedClass myClass;
        std::string resultString;
        int resultInt;
        myClass.UnManagedMethodCompareConcat
		(unmanagedString1,unmanagedString2,&resultString,&resultInt);

       //marshalling the OUT parameters
         concatString  = gcnew System::String(resultString.c_str());
         result = resultInt;
    }

        //Method to marshal a CLR compatible System::String to a std::string
        void ManagedWrapperClass::MarshalString ( System::String ^ s, std::string& os ) 
            {
               const char* chars = 
		(const char*)(Marshal::StringToHGlobalAnsi(s)).ToPointer();
               os = chars;
               Marshal::FreeHGlobal( System::IntPtr((void*)chars));
            }

进行包装时,主要的障碍将是参数封送。但要找到将原生数据类型转换为 .NET 兼容数据类型的方法并不难。

第二步

通过上述实现,我能够生成一个 DLL,该 DLL 可在所有 .NET 语言中使用。  如何在其他遗留语言(如原生 C++、Visual Basic 6 或 Visual FoxPro)中使用同一个库?现在问题很简单。如何在非托管代码中消耗托管库。为此,最好的方法是通过 COM 暴露方法。  你可能会问我,为什么我需要一个托管包装器来访问原生 C++ 应用程序中的原生 C++ 应用程序。确实如此,我们有更简单的方法可以在非托管 C++ 应用程序中消耗非托管库。  但是,这迫使我们拥有两个不同的库或 DLL。一个是托管的;另一个是非托管的,用于执行相同的任务。有没有办法拥有一个可以跨平台使用的单一库?  是的,有一种方法。  在非托管代码中调用托管方法的最佳且最简单的方法是 COM。  因此,在这里我们将看到如何将 COM 部分添加到此库中。暴露方法到 COM 非常简单。唯一的补充是一个接口,这是暴露方法所必需的。现在类定义如下:

    [ComVisible(true)]
    [Guid("1DB44778-857F-47d1-9D4A-D90F109EFF35")]
    public interface class IManagedWrapperClass
    {
        int ManagedAdd(int num1, int num2);
        System::String^ ManagedConcat(System::String^ string1,System::String^ string2);
        void ManagedCompareConcat(System::String^ string1,
		System::String^ string2,
		[System::Runtime::InteropServices::Out]System::String^ %concatString,
		[System::Runtime::InteropServices::Out] System::Int32 %result);
    };

    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType::None)]
    public ref class ManagedWrapperClass : public IManagedWrapperClass
    {
    private:
            void MarshalString ( System::String ^ s, std::string& os );
    public:
            virtual int ManagedAdd(int num1, int num2);
            virtual System::String^ ManagedConcat
		(System::String^ string1,System::String^ string2);
            virtual void ManagedCompareConcat(System::String^ string1,
		System::String^ string2,
		[System::Runtime::InteropServices::Out]System::String^ %concatString,
		[System::Runtime::InteropServices::Out] System::Int32 %result);
    };

    // The Class which is being wrapped
    public class unmanagedClass
    {
    public:
        unmanagedClass();
        int UnManagedMethodAddNum(int num1, int num2);
        std::string UnManagedMethodConcatString(std::string string1,std::string string2);
        void UnManagedMethodCompareConcat(std::string string1, 
		std::string string2, std::string* concatString, int *result);  
    };

现在我们有了一个库,所有 .NET 语言以及所有支持 COM 的遗留语言都可以使用它。

使用此库

在 .NET 语言中使用此库非常简单。将此 DLL 添加为项目的引用,即可开始使用。但在遗留应用程序中使用它需要做一些工作。要在原生 C++ 中使用它,我们必须使用以下命令创建一个 TLB 文件:

RegAsm /tlb:<filename.tlb> “filename.dll”

或者使用TlbExp命令。 

你可以使用#import指令在你的 C++ 应用程序中使用此 TLB 文件。  

结语

有许多库,它们拥有复杂的底层操作,如图像处理、网络等,仍然是原生 C++。希望这种方法能帮助我们将这些库包装成 .NET 数据类型,以实现无缝访问。

© . All rights reserved.