为什么 EnumPrintersA 和 EnumPrintersW 请求相同数量的内存?

Why do EnumPrintersA and EnumPrintersW request the same amount of memory?

我使用 node-ffi 调用 EnumPrintersA/EnumPrintersW 函数来获取本地列表可从我的 PC 访问的打印机。
您应该创建将由 EnumPrinters 函数填充信息的缓冲区。
但是您不知道需要的缓冲区大小。
在这种情况下,您需要执行 EnumPrintersA/EnumPrintersW 两次。
在第一次调用期间,此函数计算有关打印机信息的内存量,在第二次调用期间,此函数用有关打印机的信息填充缓冲区。
对于 EnumPrinters 函数的 Unicode 版本,打印机名称中的每个字母将使用 Windows 中的两个字符进行编码。

为什么第一次调用 EnumPrintersW returns 需要与第一次调用 EnumPrintersA 相同的内存量?
Unicode 字符串的长度是非 Unicode 字符串的两倍,但所需的缓冲区大小相同。

var ffi = require('ffi')
var ref = require('ref')
var Struct = require('ref-struct')
var wchar_t = require('ref-wchar')

var int = ref.types.int
var intPtr = ref.refType(ref.types.int)
var wchar_string = wchar_t.string

var getPrintersA =  function getPrinters() {
   var PRINTER_INFO_4A = Struct({
      'pPrinterName' : ref.types.CString,
      'pServerName' : ref.types.CString,
      'Attributes' : int
   });

   var printerInfoPtr = ref.refType(PRINTER_INFO_4A);

   var winspoolLib = new ffi.Library('winspool', {
      'EnumPrintersA': [ int, [ int, ref.types.CString, int, printerInfoPtr, int, intPtr, intPtr ] ]
   });

   var pcbNeeded = ref.alloc(int, 0);
   var pcReturned = ref.alloc(int, 0);

   //Get amount of memory for the buffer with information about printers
   var res = winspoolLib.EnumPrintersA(6, ref.NULL, 4, ref.NULL, 0, pcbNeeded, pcReturned);
   if (res != 0) {
      console.log("Cannot get list of printers. Error during first call to EnumPrintersA. Error: " + res);
      return;
   }

   var bufSize = pcbNeeded.deref();
   var buf = Buffer.alloc(bufSize);

   console.log(bufSize);

   //Fill buf with information about printers
   res = winspoolLib.EnumPrintersA(6, ref.NULL, 4, buf, bufSize, pcbNeeded, pcReturned);
   if (res == 0) {
      console.log("Cannot get list of printers. Eror: " + res);
      return;
   }

   var countOfPrinters = pcReturned.deref();

   var printers = Array(countOfPrinters);
   for (var i = 0; i < countOfPrinters; i++) {
      var pPrinterInfo = ref.get(buf, i*PRINTER_INFO_4A.size, PRINTER_INFO_4A);
      printers[i] = pPrinterInfo.pPrinterName;
   }

   return printers;
};

var getPrintersW =  function getPrinters() {
   var PRINTER_INFO_4W = Struct({
      'pPrinterName' : wchar_string,
      'pServerName' : wchar_string,
      'Attributes' : int
   });

   var printerInfoPtr = ref.refType(PRINTER_INFO_4W);

   var winspoolLib = new ffi.Library('winspool', {
      'EnumPrintersW': [ int, [ int, wchar_string, int, printerInfoPtr, int, intPtr, intPtr ] ]
   });

   var pcbNeeded = ref.alloc(int, 0);
   var pcReturned = ref.alloc(int, 0);

   //Get amount of memory for the buffer with information about printers
   var res = winspoolLib.EnumPrintersW(6, ref.NULL, 4, ref.NULL, 0, pcbNeeded, pcReturned);
   if (res != 0) {
      console.log("Cannot get list of printers. Error during first call to EnumPrintersW. Eror code: " + res);
      return;
   }

   var bufSize = pcbNeeded.deref();
   var buf = Buffer.alloc(bufSize);

   console.log(bufSize);

   //Fill buf with information about printers
   res = winspoolLib.EnumPrintersW(6, ref.NULL, 4, buf, pcbNeeded.deref(), pcbNeeded, pcReturned);
   if (res == 0) {
      console.log("Cannot get list of printers. Eror code: " + res);
      return;
   }

   var countOfPrinters = pcReturned.deref();
   var printers = new Array(countOfPrinters);
   for (var i = 0; i < countOfPrinters; i++) {
      var pPrinterInfo = ref.get(buf, i*PRINTER_INFO_4W.size, PRINTER_INFO_4W);
      printers[i] = pPrinterInfo.pPrinterName;
   }

   return printers;
};

https://msdn.microsoft.com/ru-ru/library/windows/desktop/dd162692(v=vs.85).aspx

BOOL EnumPrinters(
  _In_  DWORD   Flags,
  _In_  LPTSTR  Name,
  _In_  DWORD   Level,
  _Out_ LPBYTE  pPrinterEnum,
  _In_  DWORD   cbBuf,
  _Out_ LPDWORD pcbNeeded,
  _Out_ LPDWORD pcReturned
);

https://msdn.microsoft.com/ru-ru/library/windows/desktop/dd162847(v=vs.85).aspx

typedef struct _PRINTER_INFO_4 {
  LPTSTR pPrinterName;
  LPTSTR pServerName;
  DWORD  Attributes;
} PRINTER_INFO_4, *PPRINTER_INFO_4;

一开始我以为你的代码有问题,所以一直在找错误(由FFIJS[=191引入=] 层,或打字错误或类似的东西),但我找不到任何东西。

然后,我开始用 C 编写一个类似于您的程序(以消除任何可能引入错误的额外层)。

main.c:

#include <stdio.h>
#include <Windows.h>
#include <conio.h>  // !!! Deprecated!!!


typedef BOOL (__stdcall *EnumPrintersAFuncPtr)(_In_ DWORD Flags, _In_ LPSTR Name, _In_ DWORD Level, _Out_ LPBYTE pPrinterEnum, _In_ DWORD cbBuf, _Out_ LPDWORD pcbNeeded, _Out_ LPDWORD pcReturned);
typedef BOOL (__stdcall *EnumPrintersWFuncPtr)(_In_ DWORD Flags, _In_ LPWSTR Name, _In_ DWORD Level, _Out_ LPBYTE pPrinterEnum, _In_ DWORD cbBuf, _Out_ LPDWORD pcbNeeded, _Out_ LPDWORD pcReturned);


void testFunc()
{
    PPRINTER_INFO_4A ppi4a = NULL;
    PPRINTER_INFO_4W ppi4w = NULL;
    BOOL resa, resw;
    DWORD neededa = 0, returneda = 0, neededw = 0, returnedw = 0, gle = 0, i = 0, flags = PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS;
    LPBYTE bufa = NULL, bufw = NULL;
    resa = EnumPrintersA(flags, NULL, 4, NULL, 0, &neededa, &returneda);
    if (resa) {
        printf("EnumPrintersA(1) succeeded with NULL buffer. Exiting...\n");
        return;
    } else {
        gle = GetLastError();
        if (gle != ERROR_INSUFFICIENT_BUFFER) {
            printf("EnumPrintersA(1) failed with %d(0x%08X) which is different than %d. Exiting...\n", gle, gle, ERROR_INSUFFICIENT_BUFFER);
            return;
        } else {
            printf("EnumPrintersA(1) needs a %d(0x%08X) bytes long buffer.\n", neededa, neededa);
        }
    }
    resw = EnumPrintersW(flags, NULL, 4, NULL, 0, &neededw, &returnedw);
    if (resw) {
        printf("EnumPrintersW(1) succeeded with NULL buffer. Exiting...\n");
        return;
    } else {
        gle = GetLastError();
        if (gle != ERROR_INSUFFICIENT_BUFFER) {
            printf("EnumPrintersW(1) failed with %d(0x%08X) which is different than %d. Exiting...\n", gle, gle, ERROR_INSUFFICIENT_BUFFER);
            return;
        } else {
            printf("EnumPrintersW(1) needs a %d(0x%08X) bytes long buffer.\n", neededw, neededw);
        }
    }

    bufa = (LPBYTE)calloc(1, neededa);
    if (bufa == NULL) {
        printf("calloc failed with %d(0x%08X). Exiting...\n", errno, errno);
        return;
    } else {
        printf("buffera[0x%08X:0x%08X]\n", (long)bufa, (long)bufa + neededa - 1);
    }
    bufw = (LPBYTE)calloc(1, neededw);
    if (bufw == NULL) {
        printf("calloc failed with %d(0x%08X). Exiting...\n", errno, errno);
        free(bufa);
        return;
    } else {
        printf("bufferw[0x%08X:0x%08X]\n", (long)bufw, (long)bufw + neededw - 1);
    }

    resa = EnumPrintersA(flags, NULL, 4, bufa, neededa, &neededa, &returneda);
    if (!resa) {
        gle = GetLastError();
        printf("EnumPrintersA(2) failed with %d(0x%08X). Exiting...\n", gle, gle);
        free(bufa);
        free(bufw);
        return;
    }
    printf("EnumPrintersA(2) copied %d bytes in the buffer out of which the first %d(0x%08X) represent %d structures of size %d\n", neededa, returneda * sizeof(PRINTER_INFO_4A), returneda * sizeof(PRINTER_INFO_4A), returneda, sizeof(PRINTER_INFO_4A));
    resw = EnumPrintersW(flags, NULL, 4, bufw, neededw, &neededw, &returnedw);
    if (!resw) {
        gle = GetLastError();
        printf("EnumPrintersW(2) failed with %d(0x%08X). Exiting...\n", gle, gle);
        free(bufw);
        free(bufa);
        return;
    }
    printf("EnumPrintersW(2) copied %d bytes in the buffer out of which the first %d(0x%08X) represent %d structures of size %d\n", neededw, returnedw * sizeof(PRINTER_INFO_4W), returnedw * sizeof(PRINTER_INFO_4W), returnedw, sizeof(PRINTER_INFO_4W));

    ppi4a = (PPRINTER_INFO_4A)bufa;
    ppi4w = (PPRINTER_INFO_4W)bufw;
    printf("\nPrinting ASCII results:\n");
    for (i = 0; i < returneda; i++) {
        printf("  Item %d\n    pPrinterName: [%s]\n", i, ppi4a[i].pPrinterName ? ppi4a[i].pPrinterName : "NULL");
    }
    printf("\nPrinting WIDE results:\n");
    for (i = 0; i < returnedw; i++) {
        wprintf(L"  Item %d\n    pPrinterName: [%s]\n", i, ppi4w[i].pPrinterName ? ppi4w[i].pPrinterName : L"NULL");
    }

    free(bufa);
    free(bufw);
}


int main()
{
    testFunc();
    printf("\nPress a key to exit...\n");
    getch();
    return 0;
}

注意:在变量名称方面(我保持简短 - 因此不是很直观),aw 在他们名字的末尾表示他们用于 ASCII / WIDE 版本。

最初,我担心 EnumPrinters 可能不会 return 任何东西,因为此时我没有连接到任何打印机,但幸运的是我有一些 ( 7 更准确)“保存”。这是上面程序的输出(感谢@qxz 纠正我的初始(和某种错误的)版本):

EnumPrintersA(1) needs a 544(0x00000220) bytes long buffer.
EnumPrintersW(1) needs a 544(0x00000220) bytes long buffer.
buffera[0x03161B20:0x03161D3F]
bufferw[0x03165028:0x03165247]
EnumPrintersA(2) copied 544 bytes in the buffer out of which the first 84(0x00000054) represent 7 structures of size 12
EnumPrintersW(2) copied 544 bytes in the buffer out of which the first 84(0x00000054) represent 7 structures of size 12

Printing ASCII results:
  Item 0
    pPrinterName: [Send To OneNote 2013]
  Item 1
    pPrinterName: [NPI060BEF (HP LaserJet Professional M1217nfw MFP)]
  Item 2
    pPrinterName: [Microsoft XPS Document Writer]
  Item 3
    pPrinterName: [Microsoft Print to PDF]
  Item 4
    pPrinterName: [HP Universal Printing PCL 6]
  Item 5
    pPrinterName: [HP LaserJet M4345 MFP [7B63B6]]
  Item 6
    pPrinterName: [Fax]

Printing WIDE results:
  Item 0
    pPrinterName: [Send To OneNote 2013]
  Item 1
    pPrinterName: [NPI060BEF (HP LaserJet Professional M1217nfw MFP)]
  Item 2
    pPrinterName: [Microsoft XPS Document Writer]
  Item 3
    pPrinterName: [Microsoft Print to PDF]
  Item 4
    pPrinterName: [HP Universal Printing PCL 6]
  Item 5
    pPrinterName: [HP LaserJet M4345 MFP [7B63B6]]
  Item 6
    pPrinterName: [Fax]

Press a key to exit...

令人惊讶的是(至少对我而言),您描述的行为可以重现。

注意以上输出来自032bit编译版本的程序(064bit 指针更难阅读 :) ),但在为 064bit 构建时,行为也可以重现(我在 [= 上使用 VStudio 10.0 71=]Win10).

由于缓冲区末尾肯定有字符串,我开始调试:

上面是VStudio 10.0Debugwindow的图片,在testFunc的最后中断了程序,只是在释放 1st 指针之前。现在,我不知道您对 VStudio 上的调试有多熟悉,所以我将介绍(相关)window 区域:

  • 最下面有2个Watchwindows(用来在程序运行时显示变量)。正如所见,变量 NameValueType 显示

    • 右边,(看1):第1st(第0 ) 和最后一个(第 6th - 因为有 7 个)在每个 2 个缓冲区

      开头的结构
    • 左边,(Watch 2): 2个缓冲区的地址

  • Watchwindows,(Memory 2)上面是[=的内存内容71=]bufw。 Memory window 包含一系列行,每一行都有内存地址(灰色,在左侧),然后是 hex[ 中的内容=191=](每个字节对应 2 个 hex 数字 - 例如 1E),然后在在 char 表示中右移相同的内容(每个字节对应 1 个 char - 我将在后面讨论),然后是下一行, 等等

  • 以上内存2,(内存1):是bufa的内存内容

现在,回到内存布局:并非所有右侧的 char 都一定是它们看起来的那样,其中一些只是为了便于阅读而像那样显示。比如右边有很多点(.),但不都是点。如果您在相应的 hex 表示中寻找一个点,您会注意到其中许多点是 00NULL(这是不可打印的 char,但显示为点) .

关于2个Memorywindows(查看char表示)的缓冲区内容,有3个区域:

  • PRINTER_INFO_4*区域或开头的乱码:544字节对应大约第1行3行

  • 来自最后 ~1.5 行的时髦 chars:它们在我们的缓冲区之外,所以我们不不关心他们

  • 中区:存放字符串的地方

让我们看看 WIDE 字符串区域(Memory 2 - 中间区域):正如您提到的,每个字符有 2 个字节:因为在我的例子中,它们都是 ASCII chars,MSB(或 codepage byte) 总是 0 (这就是为什么你看到 chars和点交错:例如“.L.a.s.e.r.J.e.t”在第 4 行)。

因为缓冲区中有多个字符串(或者字符串,如果你愿意的话)——甚至更好:多个 TCHAR*s 在 TCHAR* - 它们必须分开:由 NULL WIDE char (hex: 00 00, char: "..") 在每个字符串的末尾;结合下一个字符串的 1st 字节 (char) 也是 00 (.), 你会看到一个 3 NULL 字节的序列 (hex: 00 00 00, char: "...") 并且这是 2 (WIDE ) 字符串在中间区域。

现在,比较 2 个中间部分(对应于 2 个缓冲区),您会注意到字符串分隔符 完全位于相同的位置 以及更多:每个字符串的最后部分也相同(每个字符串的后半部分更精确)。

考虑到这一点,这是我的理论:

我认为 EnumPrintersA 调用 EnumPrintersW,然后遍历每个字符串(在缓冲区的末尾),并且调用 wcstombs 甚至更好:对它们调用 [MS.Docs]: WideCharToMultiByte function(就地转换它们 - 因此生成的 ASCII 字符串仅采用 1st WIDE 字符串的一半,保留 2nd 一半不变),而不转换所有缓冲区。我必须通过 winspool.drv.

中的反汇编程序来验证这一点

个人(如果我是对的)我认为这是一个蹩脚的解决方法(或者我喜欢称之为 gainarie),但谁知道呢,也许所有*A, *W 函数对(至少那些 return 多个 char* char*) 中的 s 像这样工作。无论如何,这种方法也有优点(至少对于这两个函数):

  • dev-wiseOK 一个函数调用另一个函数并将实现保留在 1 个地方(而不是在两个函数中重复它)

  • performance-wiseOK 不要重新创建缓冲区,因为那样会 意味着额外的计算;毕竟,缓冲区消费者通常不会到达缓冲区 ASCII 字符串的后半部分

我可以确认您在 EnumPrintersAEnumPrintersW 中找到的内容是可重现的。 在我的机器上,它们都需要 240 字节。

这让我很好奇,所以我决定为每个函数分配一个单独的缓冲区,并将每个缓冲区转储到一个文件中,然后用十六进制编辑器打开它们。 每个文件的有趣部分当然是打印机的名称。

为了简短起见,我将向您展示打印机的前 3 个名称。 第一行来自EnumPrintersA,第二行来自EnumPrintersW:

Fax.x...FX DocuPrint C1110 PCL 6..C.1.1.1.0. .P.C.L. .6...Microsoft XPS Document Writer.o.c.u.m.e.n.t. .W.r.i.t.e.r...
F.a.x...F.X. .D.o.c.u.P.r.i.n.t. .C.1.1.1.0. .P.C.L. .6...M.i.c.r.o.s.o.f.t. .X.P.S. .D.o.c.u.m.e.n.t. .W.r.i.t.e.r...

从这个结果看来,EnumPrintersA 调用 EnumPrintersW 进行实际工作,然后简单地将缓冲区中的每个字符串转换为单字节字符,并将结果字符串放在同一个地方。 为了证实这一点,我决定跟踪 EnumPrintersA 代码,我发现它确实在位置 winspool.EnumPrintersA + 0xA7 调用了 EnumPrintersW。 不同 Windows 版本中的实际位置可能不同。

这让我更加好奇,所以我决定测试具有 A 和 W 版本的其他函数。 这是我发现的:

EnumMonitorsA 280 bytes needed
EnumMonitorsW 280 bytes needed
EnumServicesStatusA 20954 bytes needed
EnumServicesStatusW 20954 bytes needed
EnumPortsA 2176 bytes needed
EnumPortsW 2176 bytes needed
EnumPrintProcessorsA 24 bytes needed
EnumPrintProcessorsW 24 bytes needed

根据这个结果,我的结论是 EnumPrintersA 调用 EnumPrintersW 进行实际工作并转换缓冲区中的字符串,其他具有 A 和 W 版本的函数也做同样的事情。 这似乎是一种常见的机制,可以避免以更大的缓冲区为代价的代码重复,可能是因为缓冲区无论如何都可以被释放。