这两天在做一个大范围扫描设备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?
....头部结构声明....
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?
你给下那个类型的定义呢?还有Pointer泛型的定义,看你写得好抽象,具体类型定义少了不少。
既然说AdapterName是一个托管类型string,你却在C#里面定义为非托管类型,而且这个和Next字段又是啥关系?
一个结构体要声明为指针,它的成员不能有引用类型(前面说托管类型是我用词不当).否则编译会报CS0208Next字段是单链表中指向下一个节点的指针.如果AdapterName不是托管类型,那么我可以把Next直接声明为IP_ADAPTER_ADDRESSES* 从而省下不少事.而事实上虽然AdapterName也确实可以声明为IntPtr.但这样的话我要获取AdapterName就不得不自己去调用Marshal.PtrToStringAnsi,反而更麻烦.
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);
}
}
}
比较下C++的定义
struct _IP_ADAPTER_ADDRESSES *Next;
PIP_ADAPTER_UNICAST_ADDRESS FirstUnicastAddress;
显然我们看出,第一个Next是指针,因此长度永远是Intptr的长度,而后面那个是结构体的值,并非地址,因此长度是看结构体本身的长度。而且还有个区别,前面那个存放的是地址,后面那个是值。
那么我猜测你的Pointer封装的结果是封送具体值,而不是封送地址,因此Next被你调换后就出问题了。
今天刚来就觉得这该死的缓存好恶心.
在管理菜单里点生成帖子都没用.
internal IntPtr next; public Pointer<IP_ADAPTER_ADDRESSES> Next
{
get { return new Pointer<IP_ADAPTER_ADDRESSES>(next); }
}试试?
错误一:那个static Pointer()的构造函数在无参数构造函数初始化结构体或者直接全局定义但不初始化结构体的时候,将不会触发,最终导致你的sizeofT常量为0,这个错误对于普通结构体来说无所谓,但是需要指针移动(++或--)的时候,将导致致命的错误。
错误二:运算符+和-的重载有误,请自行查看。
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的++和--的函数,还没改也还没用估计就走神忘了.
这个异常似乎是JIT途中抛出的.
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的大小存放在栈上面,不存在嵌套的初始化过程。
从错误提示可以推断出,.NET对class的定义先是定义一个类名,然后定义内部实现,这样内部就可以用到类本身的定义;而对struct的定义是必须定义完后才能使用,这样定义内部元素的时候,就看不到自身,为了防止看到自身后,内部再次出现,导致结构体布局循环嵌套。
但是结构体内部是能出现结构体的指针的.况且看定义Pointer和一个IntPtr等价,本质上是一个指针.不会形成循环的布局.至于对struct的定义必须定义完后才能使用.你要怎么解释这段可以运行的代码呢?就结构上和使用Pointer是等价的哦:
unsafe struct A{
public A* pa;
public int value;
}
A a = new A();
...
..