PInvoke中的泛型遇到的问题.

ChrisAK 2012-07-24 05:07:00
这两天在做一个大范围扫描设备ip的玩意,因为目标设备ip未知,所以需要在程序里不停的修改本机ip然后一个个地址ping.

修改地址就用到了iphelper库.里面有一个结构体叫IP_ADAPTER_ADDRESSES,是一个单向链表的节点(iphelper库里这种单向链表比比皆是),它的定义如下:

typedef struct _IP_ADAPTER_ADDRESSES {
....头部结构声明....
struct _IP_ADAPTER_ADDRESSES *Next;//指向下一个IP_ADAPTER_ADDRESSES
PCHAR AdapterName;
PIP_ADAPTER_UNICAST_ADDRESS FirstUnicastAddress;
PIP_ADAPTER_ANYCAST_ADDRESS FirstAnycastAddress;
PIP_ADAPTER_MULTICAST_ADDRESS FirstMulticastAddress;
PIP_ADAPTER_DNS_SERVER_ADDRESS FirstDnsServerAddress;
....密密麻麻密密麻麻....
};


对此,我写出了以下对应的C#版声明:

        struct IP_ADAPTER_ADDRESSES
{
....头部结构声明....
public IntPtr Next;

[MarshalAs(UnmanagedType.LPStr)]
public string AdapterName;

public Pointer<IP_ADAPTER_UNICAST_ADDRESS> FirstUnicastAddress;
public Pointer<IP_ADAPTER_ADDRESS_LIST_NODE> FirstAnycastAddress;
public Pointer<IP_ADAPTER_ADDRESS_LIST_NODE> FirstMulticastAddress;
public Pointer<IP_ADAPTER_ADDRESS_LIST_NODE> FirstDnsServerAddress;
....密密麻麻密密麻麻....
}

本来Next字段我想声明为指针 IP_ADAPTER_ADDRESSES*的,无奈AdapterName是一个托管类型string.后面的Pointer<T>类是我做的一个封装库用于对IntPtr指针的操作常用做出封装,最终调用时会被封送成为一个IntPtr

接下来编译->运行,一切正常.

但是Next字段仍然是IntPtr,每次遍历链表时老调用Marshal着实也烦.既然已经有Pointer<T>可以用了,我就想把它也换成Pointer吧.于是我改了下声明,把

public IntPtr Next;

改成了

public Pointer<IP_ADAPTER_ADDRESSES> Next;

接下来编译->运行.报错了:

未能从程序集“Rabbit.Net, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null”中加载类型“ProjectRabbit.Net.WinIPHelper.IPHelper+IP_ADAPTER_ADDRESSES”。

这通常是程序集无法找到或是程序集过老没有包含改结构声明造成的.有时候vs的项目管理器引用会抽掉造成这样的问题.不过在照着网上的各种解决方法试过之后.我基本排除了是项目工程文件的问题.

接下来是代码问题了,在试过几次后我发现把Next的类型声明改回IntPtr问题就解决了.平台封送的参数不能是泛型这点我是知道的,但没有限制结构体呀.况且后几个使用Point<T>声明的字段能正常工作也表明并没有这个限制.只有当字段形参中包含类型本身的时候,才会出现这个错误.

不知道这算不算是一个bug?
...全文
175 18 打赏 收藏 转发到动态 举报
写回复
用AI写文章
18 条回复
切换为时间正序
请发表友善的回复…
发表回复
ChrisAK 2012-07-25
  • 打赏
  • 举报
回复
[Quote=引用 11 楼 的回复:]

似乎无解。用

C# code

internal IntPtr next;

public Pointer<IP_ADAPTER_ADDRESSES> Next
{
get { return new Pointer<IP_ADAPTER_ADDRESSES>(next); }
……
[/Quote]这个我想到过并试过,也是可行的.所以我现在感兴趣的是为什么会出这个异常.
这个异常似乎是JIT途中抛出的.
ChrisAK 2012-07-25
  • 打赏
  • 举报
回复
[Quote=引用 12 楼 的回复:]

先指出下那个Pointer的几个错误,是否是那个引起的请楼主自己测试,之后我会自定义一个C++的dll来测试下。
错误一:那个static Pointer()的构造函数在无参数构造函数初始化结构体或者直接全局定义但不初始化结构体的时候,将不会触发,最终导致你的sizeofT常量为0,这个错误对于普通结构体来说无所谓,但是需要指针移动(++或--)的时候,将导致致命的错误。
错误二:运算符+和……
[/Quote]
static Pointer()系统会保证调用,即使变量未初始化也一样.只是延迟到你首次使用变量的时候而已

举个例子:
struct TestInit {
public Pointer<int> a;
}

然后这样调用:


TestInit test = new TestInit();-->执行完这句你会发现static构造函数还未被调用.同时a也只是分配了空间还没初始化.
//接下来为尚未初始化的a的成员直接进行操作
test.a.Ptr = IntPtr.Zero;-->如果你在静态构造函数里打了断点,亦或是在里面加句控制台输出语句,你会发现静态构造函数被调用了

退一步说.就算sizeofT = 0.也只是会导致指针运算出错,不应该导致无法载入类型.
你可能以为是函数抛出一个普通的异常吧.实际情况是这样的:

static class IPHelper{
public static AdapterAddress[] GetAdaptersAddress(AddressFamily af,GAAFlags flags)
{
....这里进行API相关调用....
}
}

static void Main (){
var addresses = IPHelper.GetAdaptersAddress(AddressFaimily.InterNetwork,GAAFlags.NONE);
}

运行时如果你在GetAdaptersAddress函数的第一句上打个断点,你会发现根本断不下来.程序会在
var addresses = IPHelper.GetAdaptersAddress(AddressFaimily.InterNetwork,GAAFlags.NONE);

这一句上就抛出异常,异常应该是JIT在编译的途中就抛出的,而不是执行中.

后面那个+和-确实有错.昨天直接copy的++和--的函数,还没改也还没用估计就走神忘了.
qldsrx 2012-07-25
  • 打赏
  • 举报
回复
先指出下那个Pointer的几个错误,是否是那个引起的请楼主自己测试,之后我会自定义一个C++的dll来测试下。
错误一:那个static Pointer()的构造函数在无参数构造函数初始化结构体或者直接全局定义但不初始化结构体的时候,将不会触发,最终导致你的sizeofT常量为0,这个错误对于普通结构体来说无所谓,但是需要指针移动(++或--)的时候,将导致致命的错误。
错误二:运算符+和-的重载有误,请自行查看。
qldsrx 2012-07-25
  • 打赏
  • 举报
回复
既然在C++支持指针的写法,C#这样写肯定也是支持的(微软向来搞兼容),只不过是必须不安全代码才行。

结构体布局的时候必须使用一个已经定义好的类型才行,结构体自身未定义好布局,在内部就无法参与布局(构成循环了),而指针本身就是一个地址,它的布局是固定的,永远为Intptr的布局格式,只需要一个类型声明即可,自然使用指针就没问题。

另外我上面的补充说明可能有点说明不周到,其实结构体和类都是先有声明再有内部实现,只不过结构体在自身没完成定义之前,是不能使用的,对其内部布局时用到的所有结构体必须是完成定义的结构体,否则无法进行布局嵌入,而指针或类的话就不需要,只要有个类型声明即可,嵌入的仅仅是地址。
ChrisAK 2012-07-25
  • 打赏
  • 举报
回复
[Quote=引用 15 楼 的回复:]

我做了个极端的测试,代码如下:
C# code
public struct IP_ADAPTER_ADDRESSES
{
public Pointer<IP_ADAPTER_ADDRESSES> Next;
}
public struct Pointer<T>
where T : struct
{}

结果这样写了以后,一旦初……
[/Quote]结构体内本来就不能出现结构体自身.否则就是一个递归的定义.这个结构体的size理论上会是无穷大.自然是不允许的.
但是结构体内部是能出现结构体的指针的.况且看定义Pointer和一个IntPtr等价,本质上是一个指针.不会形成循环的布局.至于对struct的定义必须定义完后才能使用.你要怎么解释这段可以运行的代码呢?就结构上和使用Pointer是等价的哦:


unsafe struct A{
public A* pa;
public int value;
}


A a = new A();
...
..

qldsrx 2012-07-25
  • 打赏
  • 举报
回复
补充下:
从错误提示可以推断出,.NET对class的定义先是定义一个类名,然后定义内部实现,这样内部就可以用到类本身的定义;而对struct的定义是必须定义完后才能使用,这样定义内部元素的时候,就看不到自身,为了防止看到自身后,内部再次出现,导致结构体布局循环嵌套。
qldsrx 2012-07-25
  • 打赏
  • 举报
回复
我做了个极端的测试,代码如下:
    public struct IP_ADAPTER_ADDRESSES
{
public Pointer<IP_ADAPTER_ADDRESSES> Next;
}
public struct Pointer<T>
where T : struct
{}

结果这样写了以后,一旦初始化那个IP_ADAPTER_ADDRESSES就会报和你一样的错误,而把struct改为class就正常,看来是在结构体的内部是不能出现结构体自身的。其实这也是很好理解原因的,假设这样写可以,那么由于结构体是存在于栈上的,存储的是值而不是地址,那么在初始化的时候,JIT就必须给其在栈上开辟空间存储,实际大小为结构体的大小,但是你这个定义是嵌套了结构体,在结构体布局中产生了循环,故而出错,如果这样定义:
public struct IP_ADAPTER_ADDRESSES
{
public IP_ADAPTER_ADDRESSES Next;
}

编译的时候就可以检测出错误(class则正常),但是你是泛型,因此编译器没检测出来,直到运行时才发现了错误。
那么为啥class正常呢,因为class存储的是地址,其值在堆里,栈上只有一个固定大小的Intptr存放,因此即使内部出现了它本身,也只是给一个Intptr的大小存放在栈上面,不存在嵌套的初始化过程。
iyomumx 2012-07-24
  • 打赏
  • 举报
回复
似乎无解。用


internal IntPtr next;

public Pointer<IP_ADAPTER_ADDRESSES> Next
{
get { return new Pointer<IP_ADAPTER_ADDRESSES>(next); }
}

试试?
ChrisAK 2012-07-24
  • 打赏
  • 举报
回复
而且如果是Pointer封送时导致的长度问题.并不应该导致类型无法载入的错误.而应该是在调用API后导致内存访问违例一类的内存错误.
ChrisAK 2012-07-24
  • 打赏
  • 举报
回复
[Quote=引用 7 楼 的回复:]

CSDN的缓存机制太强大了,我刷新了很多次都没看到5楼的回复,算了,明天继续讨论,回家。
[/Quote]好久没泡csdn了...
今天刚来就觉得这该死的缓存好恶心.
在管理菜单里点生成帖子都没用.
ChrisAK 2012-07-24
  • 打赏
  • 举报
回复
[Quote=引用 6 楼 的回复:]

在你没贴出Pointer定义之前,我先来段猜测,然后回家,可能要明天光顾了。
比较下C++的定义
struct _IP_ADAPTER_ADDRESSES *Next;
PIP_ADAPTER_UNICAST_ADDRESS FirstUnicastAddress;
显然我们看出,第一个Next是指针,因此长度永远是Intptr的长度,而后面那个是结构体的值,并非地址,因此……
[/Quote]Pointer类是我专门写来代替IntPtr使用的,而且也不是第一次使用了.它内部只包含一个指针IntPtr,就封送出去的结果和直接写IntPtr是等价的.如果长度真会有问题的话最初就崩了.因为后面可以看到不少结构体指针我都是声明为Pointer.而调用成功的话都是能取到正确的值的.
qldsrx 2012-07-24
  • 打赏
  • 举报
回复
CSDN的缓存机制太强大了,我刷新了很多次都没看到5楼的回复,算了,明天继续讨论,回家。
qldsrx 2012-07-24
  • 打赏
  • 举报
回复
在你没贴出Pointer定义之前,我先来段猜测,然后回家,可能要明天光顾了。
比较下C++的定义
struct _IP_ADAPTER_ADDRESSES *Next;
PIP_ADAPTER_UNICAST_ADDRESS FirstUnicastAddress;
显然我们看出,第一个Next是指针,因此长度永远是Intptr的长度,而后面那个是结构体的值,并非地址,因此长度是看结构体本身的长度。而且还有个区别,前面那个存放的是地址,后面那个是值。
那么我猜测你的Pointer封装的结果是封送具体值,而不是封送地址,因此Next被你调换后就出问题了。
ChrisAK 2012-07-24
  • 打赏
  • 举报
回复
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
namespace ProjectRabbit.InteropUtils
{
public struct Pointer<T>
where T:struct
{
IntPtr pointer;

//T类型的长度
static readonly int sizeofT;
static Pointer(){
sizeofT = Marshal.SizeOf(typeof(T));
}

public Pointer(IntPtr ptr)
{
pointer = ptr;
}
public T Value
{
get
{
return Get();
}
set
{
Set(value);
}
}
/// <summary>
/// 模拟c指针的数组用法
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public T this[int index]
{
get
{
if (index == 0)
return Get();
return (this + index).Get();
}
set
{
if (index == 0)
Set(value);
(this + index).Set(value);
}
}
/// <summary>
/// 指针解引用,c#没法重载*,只能另找一个了
/// </summary>
/// <param name="p"></param>
/// <returns></returns>
public static T operator ~(Pointer<T> p)
{
return p.Get();
}

public static Pointer<T> operator ++(Pointer<T> ptr)
{
return new Pointer<T>(ptr.pointer + sizeofT);
}
public static Pointer<T> operator --(Pointer<T> ptr)
{
return new Pointer<T>(ptr.pointer - sizeofT);
}
/// <summary>
/// 指针-
/// </summary>
/// <param name="ptr"></param>
/// <param name="val"></param>
/// <returns></returns>
public static Pointer<T> operator +(Pointer<T> ptr,int val)
{
return new Pointer<T>(ptr.pointer + sizeofT);
}
/// <summary>
/// 指针+
/// </summary>
/// <param name="ptr"></param>
/// <returns></returns>
public static Pointer<T> operator -(Pointer<T> ptr)
{
return new Pointer<T>(ptr.pointer - sizeofT);
}
/// <summary>
/// 直接赋值指针
/// </summary>
/// <param name="p"></param>
/// <returns></returns>
public static implicit operator Pointer<T> (IntPtr p){
return new Pointer<T>(p);
}
/// <summary>
/// 直接获取指针
/// </summary>
/// <param name="p"></param>
/// <returns></returns>
public static implicit operator IntPtr(Pointer<T> p)
{
return p.pointer;
}
/// <summary>
/// 获取指针
/// </summary>
public IntPtr Ptr{
get { return pointer; }
set { pointer = value; }
}
/// <summary>
/// 获取指针是否为空
/// </summary>
public bool IsNULL
{
get { return pointer == IntPtr.Zero; }
}

/// <summary>
/// 调用Marshal取出结构体
/// </summary>
/// <returns></returns>
public T Get()
{
return (T)Marshal.PtrToStructure(pointer, typeof(T));
}
/// <summary>
/// 调用Marshal更新指针内存
/// </summary>
/// <param name="val">结构体值</param>
public void Set(T val)
{
Marshal.StructureToPtr(val, pointer, true);
}
}
}
ChrisAK 2012-07-24
  • 打赏
  • 举报
回复
[Quote=引用 2 楼 的回复:]
还有这句话能否解释下,我看不懂:“本来Next字段我想声明为指针 IP_ADAPTER_ADDRESSES*的,无奈AdapterName是一个托管类型string”
既然说AdapterName是一个托管类型string,你却在C#里面定义为非托管类型,而且这个和Next字段又是啥关系?
[/Quote]

一个结构体要声明为指针,它的成员不能有引用类型(前面说托管类型是我用词不当).否则编译会报CS0208

Next字段是单链表中指向下一个节点的指针.如果AdapterName不是托管类型,那么我可以把Next直接声明为

IP_ADAPTER_ADDRESSES*

从而省下不少事.而事实上虽然AdapterName也确实可以声明为IntPtr.但这样的话我要获取AdapterName就不得不自己去调用Marshal.PtrToStringAnsi,反而更麻烦.
ChrisAK 2012-07-24
  • 打赏
  • 举报
回复
[Quote=引用 1 楼 的回复:]

根据错误提示,它没找到那个类型“ProjectRabbit.Net.WinIPHelper.IPHelper+IP_ADAPTER_ADDRESSES”
你给下那个类型的定义呢?还有Pointer泛型的定义,看你写得好抽象,具体类型定义少了不少。
[/Quote]IP_ADAPTER_ADDRESSES是IPHelper库中一个很大的结构体.这里全贴出来反倒会扰乱眼球了.有兴趣的可以去查MSDN.

至于Pointer,是我写的一个对这种因为成员有托管类型无法直接声明为指针的结构体指针进行操作的封装类,省掉了老是去调用Marshal的麻烦.代码我在下一楼贴出来.
qldsrx 2012-07-24
  • 打赏
  • 举报
回复
还有这句话能否解释下,我看不懂:“本来Next字段我想声明为指针 IP_ADAPTER_ADDRESSES*的,无奈AdapterName是一个托管类型string”
既然说AdapterName是一个托管类型string,你却在C#里面定义为非托管类型,而且这个和Next字段又是啥关系?
qldsrx 2012-07-24
  • 打赏
  • 举报
回复
根据错误提示,它没找到那个类型“ProjectRabbit.Net.WinIPHelper.IPHelper+IP_ADAPTER_ADDRESSES”
你给下那个类型的定义呢?还有Pointer泛型的定义,看你写得好抽象,具体类型定义少了不少。

110,538

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • Web++
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧