6

为了允许从脚本语言(用 C 编写)访问 Win32 API,我想编写如下函数:

void Call(LPCSTR DllName, LPCSTR FunctionName, 
  LPSTR ReturnValue, USHORT ArgumentCount, LPSTR Arguments[])

通常,它将调用任何 Win32 API 函数。

(LPSTR 参数本质上被用作字节数组 - 假设它们的大小已经正确,可以在函数外部采用正确的数据类型。另外我相信需要一些额外的复杂性来区分指针和非指针参数,但我出于这个问题的目的,我忽略了这一点)。

我遇到的问题是将参数传递给 Win32 API 函数。因为这些是标准调用,我不能使用可变参数,所以“调用”的实现必须提前知道参数的数量,因此它不能是通用的......

我想我可以用汇编代码来做到这一点(通过循环参数,将每个参数推入堆栈),但这在纯 C 中可能吗?

更新:我现在已将“不,不可能”的答案标记为已接受。如果基于 C 的解决方案出现,我当然会改变这一点。

更新: ruby/dl看起来可以使用合适的机制来实现。对此的任何细节将不胜感激。

4

8 回答 8

8

首先要做的事情是:您不能在 C 中将类型作为参数传递。您剩下的唯一选择是宏。

如果您正在LoadLibrary/GetProcAddress调用 Win32 函数,则此方案只需稍作修改(参数为 void * 的数组)即可工作。否则有一个函数名字符串是没有用的。在 C 中,调用函数的唯一方法是通过它的名称(标识符),在大多数情况下,它会衰减为指向函数的指针。您还必须注意转换返回值。

我最好的选择:

// define a function type to be passed on to the next macro
#define Declare(ret, cc, fn_t, ...) typedef ret (cc *fn_t)(__VA_ARGS__)

// for the time being doesn't work with UNICODE turned on
#define Call(dll, fn, fn_t, ...) do {\
    HMODULE lib = LoadLibraryA(dll); \
    if (lib) { \
        fn_t pfn = (fn_t)GetProcAddress(lib, fn); \
        if (pfn) { \
            (pfn)(__VA_ARGS__); \
        } \
        FreeLibrary(lib); \
    } \
    } while(0)

int main() {
    Declare(int, __stdcall, MessageBoxProc, HWND, LPCSTR, LPCSTR, UINT);

    Call("user32.dll", "MessageBoxA", MessageBoxProc, 
          NULL, ((LPCSTR)"?"), ((LPCSTR)"Details"), 
          (MB_ICONWARNING | MB_CANCELTRYCONTINUE | MB_DEFBUTTON2));

    return 0;
}
于 2009-03-09T11:11:09.147 回答
5

不,我认为不编写一些程序集是不可能的。原因是您需要在调用目标函数之前精确控制堆栈上的内容,而在纯 C 中没有真正的方法可以做到这一点。当然,在汇编中很简单。

此外,您将 PCSTR 用于所有这些参数,这实际上只是const char *. 但是由于所有这些 args 都不是字符串,因此您实际上想要用于返回值和 Arguments[] 的是void *or LPVOID。当您不知道参数的真实类型时,您应该使用这种类型,而不是将它们强制转换为char *.

于 2009-03-09T11:09:38.933 回答
2

其他帖子几乎肯定需要汇编或其他非标准技巧来实际进行调用,更不用说实际调用约定的所有细节了。

Windows DLL 使用至少两种不同的函数调用约定:stdcallcdecl. 您需要同时处理这两种情况,甚至可能需要弄清楚使用哪个。

解决这个问题的一种方法是使用现有的库来封装许多细节。令人惊讶的是,有一个:libffi。它在脚本环境中使用的一个例子是 Lua Alien的实现,这是一个 Lua 模块,它允许在除了 Alien 本身之外的纯 Lua 中创建任意 DLL 的接口。

于 2009-04-08T06:49:28.897 回答
1

许多 Win32 API 采用指向具有特定布局的结构的指针。其中,一个大子集遵循一个通用模式,其中第一个 DWORD 必须在调用之前初始化为具有结构的大小。有时他们需要传递一块内存,他们将向其中写入一个结构,并且内存块的大小必须通过首先使用 NULL 指针调用相同的 API 并读取返回值以发现正确的尺寸。一些 API 分配一个结构并返回一个指向它的指针,因此必须在第二次调用时释放该指针。

如果可以一次有效地调用的 API 集(可以从简单的字符串表示形式转换的单个参数)非常小,我不会感到惊讶。

为了使这个想法普遍适用,我们必须走极端:

typedef void DynamicFunction(size_t argumentCount, const wchar_t *arguments[],
                             size_t maxReturnValueSize, wchar_t *returnValue);

DynamicFunction *GenerateDynamicFunction(const wchar_t *code);

您可以将一个简单的代码片段传递给 GenerateDynamicFunction,它会将该代码包装在一些标准样板文件中,然后调用 C 编译器/链接器以从中生成一个包含该函数的 DLL(有很多免费选项可用)。然后它会LoadLibrary使用那个 DLLGetProcAddress来查找函数,然后返回它。这会很昂贵,但您会执行一次并缓存生成的 DynamicFunctionPtr 以供重复使用。您可以通过将指针保存在哈希表中来动态地执行此操作,并由代码片段本身键入。

样板文件可能是:

#include <windows.h> 
// and anything else that might be handy

void DynamicFunctionWrapper(size_t argumentCount, const wchar_t *arguments[],
                             size_t maxReturnValueSize, wchar_t *returnValue)
{
    // --- insert code snipped here
}

因此,该系统的示例用法是:

DynamicFunction *getUserName = GenerateDynamicFunction(
    "GetUserNameW(returnValue, (LPDWORD)(&maxReturnValueSize))");

wchar_t userName[100];
getUserName(0, NULL, sizeof(userName) / sizeof(wchar_t), userName);

您可以通过GenerateDynamicFunction接受参数计数来增强这一点,因此它可以在包装器的开头生成检查是否已传递正确数量的参数。如果你在其中放置一个哈希表来缓存每个遇到的代码片段的函数,你可以接近你的原始示例。Call 函数将采用代码片段而不仅仅是 API 名称,但在其他方面是相同的。它会在哈希表中查找代码片段,如果不存在,它将调用 GenerateDynamicFunction 并将结果存储在哈希表中以备下次使用。然后它将执行对该函数的调用。示例用法:

wchar_t userName[100];

Call("GetUserNameW(returnValue, (LPDWORD)(&maxReturnValueSize))",
     0, NULL, sizeof(userName) / sizeof(wchar_t), userName);

当然,除非这个想法是为了打开某种一般的安全漏洞,否则这样做没有多大意义。例如作为网络服务公开Call。您最初的想法存在安全隐患,但不太明显,因为您建议的原始方法不会那么有效。我们制造的功能越强大,安全问题就越大。

根据评论更新:

.NET 框架有一个称为 p/invoke 的功能,它的存在正是为了解决您的问题。因此,如果您将其作为一个学习项目的项目,您可以查看 p/invoke 以了解它的复杂程度。您可以使用您的脚本语言来定位 .NET 框架 - 您可以将它们编译为 IL,而不是实时解释脚本或将它们编译为您自己的字节码。或者,您可以托管 .NET 上已有的许多现有脚本语言。

于 2009-03-12T10:29:16.977 回答
1

您可以尝试这样的事情 - 它适用于 win32 API 函数:

int CallFunction(int functionPtr, int* stack, int size)
{
    if(!stack && size > 0)
        return 0;
    for(int i = 0; i < size; i++) {
        int v = *stack;
        __asm {
            push v
        }
        stack++;
    }
    int r;
    FARPROC fp = (FARPROC) functionPtr;
    __asm {
        call fp
        mov dword ptr[r], eax
    }
    return r;
}

“stack”参数中的参数应该是相反的顺序(因为这是它们被压入堆栈的顺序)。

于 2009-11-01T09:36:31.473 回答
0

拥有这样的功能听起来是个坏主意,但你可以试试这个:

int Call(LPCSTR DllName, LPCSTR FunctionName, 
  USHORT ArgumentCount, int args[])
{
   void STDCALL (*foobar)()=lookupDLL(...);

   switch(ArgumentCount) {
        /* Note: If these give some compiler errors, you need to cast
           each one to a func ptr type with suitable number of arguments. */
      case 0: return foobar();
      case 1: return foobar(args[0]);
      ...
   }
}

在 32 位系统上,几乎所有值都适合 32 位字,较短的值作为函数调用参数的 32 位字被推入堆栈,因此您应该能够以这种方式调用几乎所有 Win32 API 函数,只需将参数转换为 int 并将返回值从 int 转换为适当的类型。

于 2009-03-09T17:34:38.117 回答
0

我不确定它是否会引起您的兴趣,但可以选择使用 RunDll32.exe 并让它为您执行函数调用。RunDll32 有一些限制,我不相信您可以访问返回值,但是如果您正确地形成命令行参数,它将调用该函数。

这是一个链接

于 2009-03-09T17:44:46.550 回答
0

首先,您应该将每个参数的大小添加为额外参数。否则,您需要预测每个函数的每个参数的大小以推入堆栈,这对于 WinXX 函数是可能的,因为它们必须与它们记录的参数兼容,但很乏味。

其次,除了 varargs 函数之外,没有“纯 C”方法可以在不知道参数的情况下调用函数,并且对 .DLL 中的函数使用的调用约定没有任何限制。

实际上,第二部分比第一部分更重要。

理论上,您可以设置一个预处理器宏/#include 结构来生成参数类型的所有组合,例如最多 11 个参数,但这意味着您提前知道哪些类型将通过您的函数传递Call。如果你问我,这有点疯狂。

虽然,如果您真的想不安全地执行此操作,您可以传递 C++ 错位名称并使用它UnDecorateSymbolName来提取参数的类型。但是,这不适用于使用 C 链接导出的函数。

于 2009-03-09T20:25:24.463 回答