十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SendMessage 调不通,Codex 连上 TaoToken 后能查 DllImport 声明

SendMessage 调不通,Codex 连上 TaoToken 后能查 DllImport 声明 C# 里SendMessage返回 0 的时候别急着怀疑消息号。TaoTokenhttps://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 建一把 Key再用 Codex 对着CLineAndTheta、CopyDataStruct这些声明逐项核对——EntryPoint 拼写、CharSet、[StructLayout]、byte[]的MarshalAs一次问清楚比一轮轮改签名快得多。这篇按排障来写。原始那篇讲的是在 C# 里using System.Runtime.InteropServices;然后[DllImport(User32.dll, EntryPoint SendMessage)]把SendMsg声明成private static extern int SendMsg(int hwnd, int msg, int wparms, ref CLineAndTheta lparm)配一个自定义消息WM_FIXDATA 0x0A4AB调用的样子是SendMsg(windowHandler, WM_FIXDATA, 0, ref testfix.Fix_LT)。后面还有一套CopyDataStruct走WM_COPYDATA 0x04AA以及FindWindow(null, TestMegaugingLib)拿窗口句柄。这套写法能跑通的时候非常爽跑不通的时候几乎不给你任何提示返回值全是 0。所以顺序上我先把「返回值 0」拆成三类症状再挨个核对声明层、结构体层、句柄层中间穿插怎么让 Codex 帮你对照原生声明。工具归工具SendMessage的坑还是得自己一项项排。1. SendMsg 调用没反应时先核 EntryPoint 与 CharSet1.1 三种症状对应三个不同的出错层最有价值的一步是把「没反应」分类不然就变成盲改签名。第一类是直接抛EntryPointNotFoundException或者DllNotFoundException这类最简单基本就是 DLL 名或函数名没对上属于声明层的错。第二类是调用成功、SendMsg正常返回但目标窗口那边毫无变化返回值是 0——这类最阴通常不是函数没找到而是参数内容不对比如结构体错位、消息号两边不一致、或者消息被目标窗口直接忽略了。第三类是返回值直接是 0 而且hwnd本身就无效这是句柄层的问题FindWindow那一环就挂了。原始代码里SendMsg的返回类型写的是inthwnd、wparms也都是int。在 32 位进程里这没毛病但换成 x64 编译HWND是IntPtr、WPARAM是UIntPtr、LPARAM是IntPtr全是 8 字节用int接会被截断。截断之后SendMessage收不到有效窗口句柄它不会报错它只会返回 0。这一条在 2024 年之后的机器上几乎是必修项。1.2 SendMessageA、SendMessageW 与 ExactSpellingUser32.dll里实际导出的名字是SendMessageA和SendMessageW并没有一个叫SendMessage的导出符号。C# 的DllImport在默认情况下ExactSpelling false运行时会根据CharSet自动补上A或W后缀不写CharSet时默认按 Ansi 处理最终找的是SendMessageA写CharSet CharSet.Unicode时找的是SendMessageW。一旦你显式写了ExactSpelling true运行时就不再补后缀了EntryPoint SendMessage会原封不动地去找一个叫SendMessage的导出符号然后抛EntryPointNotFoundException。这就是为什么同样的声明别人的项目能跑你的项目一拷贝就炸——属性组合不同。写SendMessage这种带字符串或结构体的消息推荐把CharSet明确写出来别依赖默认值。原始代码里的FindWindow(string lpClassName, string lpWindowName)同理窗口标题是中文或者带宽字符的时候走FindWindowA和走FindWindowW的结果可能完全不一样。[DllImport(user32.dll, CharSet CharSet.Unicode, SetLastError true)] private static extern IntPtr SendMessage(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam);1.3 WM_FIXDATA 0x0A4AB 是不是两边一致的自定义消息0x0A4AB换算成十进制是 42155已经远大于WM_USER0x0400。这类消息号在 Win32 语义里属于「窗口类自定义消息」它的合法使用范围基本限制在同一个进程、同一套窗口类实现里。原始代码用它配合ref CLineAndTheta传结构体指针说明发送端和接收端大概率是同一套程序的两个窗口或者至少是共享同一块地址空间。如果接收窗口在另一个进程里0x0A4AB这个数字本身不会报错但对面的窗口过程很可能没有处理它于是SendMessage老老实实返回 0你以为是发送失败其实是对方根本没这个分支。跨进程的自定义消息要么两边约定同一个数字并各自实现要么改用RegisterWindowMessage注册一个全局唯一的消息 ID。排查时先在接收端的窗口过程里下一个断点或者加一行日志确认消息到底有没有到这一步比改签名有用得多。2. 让 Codex 逐行比对 DllImport 声明先建 Key 再填 base_url2.1 打开落地页创建 API Key声明层的核对其实是体力活要对照原生侧的 C/C 头文件、对照WM_*常量、对照结构体字段顺序一项项比。这种活交给 Codex 很合适但前提是它得有一个稳定的模型通道。先打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 注册之后进控制台创建一把 API KeyKey 只显示一次复制出来先存好。同一把 Key 后面还能在模型对话里做验证不用重复申请。模型 ID 别凭记忆写直接以 TaoToken 模型广场 当时的列表为准里面是哪个就填哪个。2.2 ~/.codex/config.toml 里加一个自定义 providerCodex 走的是自己的配置文件跟 Claude Code 的环境变量是两套东西别把ANTHROPIC_*那组变量往这里套。在~/.codex/config.toml里加一个 provider 段base_url填https://taotoken.net/api注意末尾不要加/v1加了之后请求路径会变成/v1/responses之类的双层结构直接 404。model YOUR_MODEL_ID model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY wire_api chatKey 通过环境变量传进去Windows 上用setx TAOTOKEN_API_KEY YOUR_API_KEYmacOS 或 Linux 上写进~/.zshrcexport TAOTOKEN_API_KEYYOUR_API_KEY。改完重开一个终端让 Codex 确认读到了新的 provider。2.3 把声明、调用点、接收端结构体一起丢给 Codex问法很重要。只贴一句「SendMessage 没反应」模型只能给你泛泛的建议。有效的做法是把四样东西一起贴过去DllImport声明、常量定义WM_FIXDATA、WM_COPYDATA、托管侧结构体完整定义、以及真正的那一行调用。然后明确要求它做逐字段对照输出一张表字段名、托管类型、字节宽度、在结构体里的偏移、以及推测的原生对应类型。再补一句限制条件接收窗口在不在同一个进程、目标平台是 x86 还是 x64、窗口标题是不是每次启动都变。这三个信息会直接改变结论。最后一定要提醒 Codex它只负责生成和解释代码编译、运行、点按钮都得你在本机做结果再贴回来。把它当成一个愿意陪你逐行看声明的人而不是一个能远程操作的执行器。3. CLineAndTheta 传参错位PointF、double 与 StructLayout3.1 Sequential 是默认值但不等于布局天然正确C# 的 struct 默认就是LayoutKind.Sequential字段按声明顺序排Pack默认取平台值x64 下是 8。看起来很省心问题在于它只保证「托管侧按顺序排」不保证「和原生侧排得一样」。原始的CLineAndTheta是三个字段PointF startPt、PointF endPt、double theta。PointF内部是两个float占 8 字节对齐要求 4double对齐要求 8。按顺序排下来是 startPt 在偏移 0endPt 在偏移 8theta 在偏移 16总共 24 字节中间没有填充。这个布局和「两个 8 字节点 一个 8 字节角度」的原生写法刚好一致所以能跑通。3.2 注释掉 int x、int y 之后整体偏移差了 8 字节原始代码里CLineAndTheta开头有两行被注释掉的public int x;和public int y;。这两行很可能是历史遗留也可能正是某次排障时不小心注释掉的。如果原生结构体里确实保留了这两个字段托管侧又漏了它们那么后面所有字段的偏移全部往前挪 8 字节原生把startPt放在偏移 8托管侧却在偏移 0 读取theta读到的实际是endPt的后半截。表现就是角度值离谱、线段画到屏幕外面去但函数返回值依然是 0看起来「调用成功」。排查手段很直接跑一行Marshal.SizeOf(typeof(CLineAndTheta))和原生侧sizeof(CLineAndTheta)的打印值比。数字不一致就说明布局对不上一个字段一个字段往回找。[StructLayout(LayoutKind.Sequential, Pack 8)] public struct CLineAndTheta { public PointF startPt; public PointF endPt; public double theta; }3.3 Pack 对不上时用 Explicit 兜底有一种情况比字段缺失更难查原生侧编译时用了#pragma pack(4)或者/Zp4。这时候 double 的对齐要求被压到 4结构体总长度可能变成 24 也可能变成 20取决于字段排列。托管侧Pack不写默认 8两边算出来的偏移就不一样。遇到这种别猜。把托管结构体改成LayoutKind.Explicit用FieldOffset把每个字段的偏移写死偏移值从原生侧的offsetof打印里抄。这样两边不管Pack怎么设内存布局都完全一致。[StructLayout(LayoutKind.Explicit, Size 24)] public struct CLineAndTheta { [FieldOffset(0)] public float startX; [FieldOffset(4)] public float startY; [FieldOffset(8)] public float endX; [FieldOffset(12)] public float endY; [FieldOffset(16)] public double theta; }3.4 跨进程传 ref 结构体必然失败ref CLineAndTheta在封送时运行时会把这个结构体的托管副本钉住pin然后把它的地址当作lParam传进去。这个地址只在发送进程的地址空间里有效。接收窗口如果在别的进程它拿到的是一串对自己毫无意义的数字轻则读到垃圾数据重则直接访问违例。原始代码把WM_FIXDATA和ref CLineAndTheta配在一起用前提是收发在同一个进程。如果哪天需求变成「通知另一个程序刷新线段」这条路就走不通了必须换成WM_COPYDATA让操作系统帮你把数据从发送进程复制到接收进程。这一步的判断要在写代码前做不然调三天也调不出来。4. WM_COPYDATA 与 CopyDataStructbyte[] 的封送要写死4.1 dwData、cbData 在 32 位与 64 位下的宽度原始版本的CopyDataStruct是IntPtr dwData、int cbData、byte[] lpData还额外挂了一个Bitmap tempbmp。原生侧的COPYDATASTRUCT定义是ULONG_PTR dwData、DWORD cbData、PVOID lpData。注意cbData是DWORD即 32 位无符号用int接在数值上勉强能用但语义上应该是uint。更要紧的是dwData是ULONG_PTRx64 下 8 字节用IntPtr是对的。cbData填的是字节数不是元素个数。传一个长度 1024 的byte[]cbData就写 1024写成 1024 乘元素宽度的话接收端会读到越界的内存。这个错误在 x86 下经常碰巧不炸换 x64 就崩属于典型的「本机好好的上线就挂」。4.2 lpData 用 MarshalAs 还是 IntPtrbyte[] lpData不写任何MarshalAs的时候封送行为依赖于运行时对数组字段的处理规则不同封送器版本、不同元素类型的表现都可能不一样。这种「能跑但不知道为啥能跑」的状态在排障里最要命因为你没法确定它是真对了还是碰巧对了。稳妥的写法是把lpData声明成IntPtr自己用Marshal.AllocHGlobal分配非托管缓冲区Marshal.Copy把字节拷进去发完再FreeHGlobal。这样生命周期完全由你掌控也不依赖任何默认规则。[StructLayout(LayoutKind.Sequential)] public struct CopyDataStruct { public IntPtr dwData; public uint cbData; public IntPtr lpData; } public static IntPtr SendCopyData(IntPtr hwnd, IntPtr tag, byte[] payload) { IntPtr buffer Marshal.AllocHGlobal(payload.Length); try { Marshal.Copy(payload, 0, buffer, payload.Length); var cds new CopyDataStruct { dwData tag, cbData (uint)payload.Length, lpData buffer }; return SendMessage(hwnd, WM_COPYDATA, UIntPtr.Zero, ref cds); } finally { Marshal.FreeHGlobal(buffer); } }如果你确实想让托管数组直接参与封送那就把规则写死比如[MarshalAs(UnmanagedType.ByValArray, SizeConst 1024)]并且保证接收端对这个定长数组的处理一致。定长数组是把内容内联进结构体指针是把地址放进去两者语义完全不同接收端读到的东西也完全不同。4.3 Bitmap 字段必须拿掉CopyDataStruct里挂一个Bitmap tempbmp这个字段永远封送不过去。System.Drawing.Bitmap是纯托管对象没有稳定的原生等价结构Blittable检查会直接判否运行时要么抛异常要么把它替换成一个无意义的字段接收端读到的就不是你想给的东西。要传图像先把位图序列化成字节存进MemoryStream取ToArray()然后把这段字节塞进lpData。接收端拿到字节流再自己解码。或者退回 Win32 原生句柄的方案传HBITMAP本质是IntPtr但那样就得处理句柄归属和释放跨进程还会遇到句柄不通用的问题。4.4 只能用 SendMessage不能用 PostMessageWM_COPYDATA有一条硬规则必须用SendMessage发送。原因是数据复制发生在发送调用期间发送方阻塞等待接收方处理完并拷走数据然后才返回。你用PostMessage的话消息进了队列就返回了AllocHGlobal出来的缓冲区可能已经被FreeHGlobal释放接收方再去读就是野指针。所以那段try/finally的结构不是风格偏好是必需的。FreeHGlobal必须在SendMessage返回之后才执行这一点在原始那段只有一行SendMsg(windowHandler, WM_COPYDATA, 0, ref cds)的写法里完全看不出来——如果当时byte[]是托管数组靠封送器在调用期间钉住那还勉强安全换成手动分配之后忘了finally就会偶发崩溃。5. FindWindow 的返回值别用 int句柄截断会让 SendMsg 回到 05.1 类名传 null 与窗口标题的精确匹配FindWindow(null, TestMegaugingLib)的意思是类名不限、标题必须精确等于TestMegaugingLib。标题是在窗口创建时定的如果那个程序在标题后面拼了版本号、拼了当前打开的文件名你这边写死的字符串就永远匹配不上FindWindow返回IntPtr.Zero后面SendMsg(0, ...)自然一路返回 0。还有一种情况是标题匹配上了但匹配到的是另一个同名窗口比如同时开了两个实例。这时候消息发给了错误的那个接收端没反应你还是会以为是声明有问题。排查时先把找到的句柄打印出来再用GetWindowText反查一下确认拿到的是不是目标窗口。[DllImport(user32.dll, CharSet CharSet.Unicode, SetLastError true)] private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); [DllImport(user32.dll, CharSet CharSet.Unicode, SetLastError true)] private static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder text, int maxCount);5.2 IntPtr、SetLastError 与 GetLastWin32Error把FindWindow的返回类型从int改成IntPtr并且加上SetLastError true。加上这个属性之后调用失败时可以用Marshal.GetLastWin32Error()拿到具体的错误码。常见的几个ERROR_INVALID_WINDOW_HANDLE说明句柄无效权限相关的错误码则指向 UIPI——当一个普通权限的进程试图给以管理员身份运行的窗口发消息时系统会直接拦截SendMessage返回 0接收端一点动静都没有。这条比声明错误更难发现因为代码本身挑不出毛病。遇到「代码明明对就是没反应」的情况先确认两边的权限级别是不是一致。5.3 同名窗口与句柄缓存有些程序会在运行期间销毁并重建窗口比如切换主题、重载配置。你在启动时FindWindow拿到的句柄过一会儿就失效了。这时候SendMessage依然是返回 0不抛异常。解决办法是在每次发送前重新查找或者订阅窗口的创建事件刷新缓存。另一种做法是用EnumWindows遍历所有顶层窗口逐个比对标题和类名把符合条件的全列出来。找多个候选的时候可以在日志里把它们的句柄、类名、标题一起打出来人工挑一次比在代码里猜快得多。6. 改完声明怎么验收本地跑最小复现再把结果贴回对话6.1 让 Codex 出诊断代码你在本机执行声明改完之后让 Codex 帮你生成一个最小的诊断程序一个只有一个按钮的 WinForms 窗体点一下依次打印FindWindow的返回值、Marshal.SizeOf(typeof(CLineAndTheta))、SendMessage的返回值和Marshal.GetLastWin32Error()。这段代码你拿到本地 Visual Studio 里编译运行把控制台输出原样贴回对话。这一步必须由你来做。Codex 不能替你编译、不能替你点按钮也不能连到你的机器上跑程序。它的作用是读你贴回来的数字判断偏差出在哪一层然后给出下一版的声明或者诊断代码。整个循环就是「它改代码、你跑、你把结果贴回去」转两三圈基本就能定位。6.2 用尺寸数字和错误码交叉验证验收时盯两个数字。第一个是Marshal.SizeOf和原生sizeof的差值差 0 说明布局一致差 4 或 8 说明字段宽度或者填充有问题。第二个是SendMessage返回值和GetLastWin32Error返回值是 0 且错误码也是 0通常意味着消息发出去了、接收端处理了、只是返回值本身恰好是 0这时候问题已经不在发送端了去接收端的窗口过程里看。跨进程的场景还要额外验证一次把发送端和接收端分别编译成 x86 和 x64四种组合都跑一遍。句柄宽度、结构体对齐、cbData的截断问题都会在这四组里暴露出来。6.3 回控制台对一下这次排障的调用配置和验证都跑过之后回到 TaoToken 控制台 看一眼这段时间的调用记录确认 Codex 走的是你填的那条base_url模型 ID 也是模型广场里真实存在的那个。如果打算长期拿它做这类声明核对、日志分析、报错归因的活可以顺手看一眼 Coding Plan 的额度是否够用单独试模型的话在 模型对话 里发一条消息就能验证 Key 和模型 ID 是否配对需要再建 Key 就去 API Keys 控制台。如果你同时在跑 Claude Code两边的变量名对照可以翻 接入文档。SendMessage这类 P/Invoke 的排障有个共同点错误信息少、返回值单调、失败方式安静。真正省时间的做法不是多试几次签名而是把「声明对齐、结构体尺寸、句柄有效性、权限级别、跨进程边界」这五件事按顺序排掉每一项都有可打印的数字可以验证。数字对不上就去改那一层别再往上一层瞎猜。
返回列表