What is the highest numerical resource ID permitted by Win32?


A customer asked the following question:

What is the maximum legal value of a resource identifier? Functions like Load­String take a UINT as the resource ID, which suggests a full 32-bit range, but in practice, most resource IDs appear to be in the 16-bit range. Is there any particular history/precedent for avoiding large numbers as resource IDs? (I have a program that autogenerates string IDs, and having a full 32-bit range gives me some more flexibility in assigning the IDs, but I want to make sure I don't run afoul of any limitations either.)

Let's answer the literal question first, and then look at the misconceptions behind the question.

The maximum legal value for an integer resource identifier is 65535. You don't need any special psychic powers for this; it's right there in the MAKE­INT­RESOURCE macro:

#define MAKEINTRESOURCEA(i) ((LPSTR)((ULONG_PTR)((WORD)(i))))
#define MAKEINTRESOURCEW(i) ((LPWSTR)((ULONG_PTR)((WORD)(i))))
#ifdef UNICODE
#define MAKEINTRESOURCE  MAKEINTRESOURCEW
#else
#define MAKEINTRESOURCE  MAKEINTRESOURCEA
#endif // !UNICODE

The MAKE­INT­RESOURCE macro takes the integer you passed, casts it down to a 16-bit WORD, and then casts the result up to a LPTSTR, effectively generating a pointer whose top bits are all zero (a pointer into the first 64KB of the address space).

Right off the bat, you can see that integer resources are limited to 16-bit values, because if you pass anything bigger, it'll get truncated by the cast to WORD.

Why does this limitation exist? Because most resource loading functions overload a single lpName parameter (representing the resource identifier or name) as both an integer (identifier) and a string (name). You can't have the full range of integers and the full range of pointers simultaneously if you want to be able to distinguish the two cases, so you have to choose some rule by which you can tell them apart, and the rule chosen by Win32 is that if the value is in the range 0..0xFFFF, then the value is treated as an integer; otherwise it is treated as a pointer to a string.

This convention comes from the days of 16-bit Windows, where 32-bit pointers consisted of a 16-bit selector in the high order word and a 16-bit offset in the low order word. The selector 0x0000 is permanently invalid, so that's a natural place to "sneak in" the integers: A "pointer" whose selector is 0x0000 is really an integer smuggled inside a pointers. There was no loss of expressiveness because integers in 16-bit Windows were, well, only 16-bits wide, so the two parameter spaces (strings and integer) neatly meshed with no overlap. (This partitioning of the address space also happily lines up with the convention that in Win32, the first 64KB of address space is permanently invalid.)

Okay, so that answers the literal question, but there's more going on. Fortunately, the customer provided context: The integer range he's interested in is string identifiers, not resource identifiers.

String identifiers are not resource identifiers. As we saw earlier, strings are gathered in bundles of 16. The bottom 4 bits of the string identifier specify which string in the bundle contains the string in question, while the remaining bits form the resource identifier of the bundle. We just learned that the resource identifier is a 16-bit value, so string identifiers can go up to 65536 × 16 − 1.

The customer was pleased with this explanation, contributing the additional insight that "a corollary to string bundling is that it's more efficient to use contiguous ranges of string identifiers (at least gathering them in blocks of 16) rather than sparsely generated ones."

Comments (11)
  1. Adrian says:

    Once upon a time, I worked on a very large application that exceeded either the 2^15 or 2^16 limit on string identifiers, and we started to have bugs.  Calls to LoadString would simply return the wrong string.

    Now, this was a long time ago (Windows 95 was brand new), so maybe it was just a bug in the resource compiler back then, but it was a very real problem for our team.  For a while, we managed to split some of the strings out into another DLL, but for various reasons that proved inconvenient.  I ended up writing a code-scanning tool that identified unused string resources, gaps in the assigned string IDs, and duplicate strings.  We used the output of that to get back under the 2^15 limit.

    Although Raymond's reasoning is correct, I'd be wary of using string IDs over 2^15.  Windows may be perfectly fine with it, but I wouldn't be surprised if a tool, library wrapper, or framework still mangles larger IDs.

  2. rs says:

    "… the convention that in Win32, the first 64KB of address space is permanently invalid"

    Useful to know, because it means you do not need to worry about integer overflows when writing

       if (pString1 – 75 <= pString2) …

    to test if pString1 is before or at most 75 characters past pString2.

  3. Joshua says:

    @Mike: the first 64kB is invalid to catch NULL pointers, for the same reason segment 0 was never used on Win16 (I forget if 0001:0000 was valid or not).

  4. ATZ Man says:

    @Joshua:

    In real mode 0001:0000 would mean physical address 16 so it would not be valid.

    In protected mode 0001:0000 is still the null selector. The selector's index in its particular table of selectors is 0001 divided by 8, i.e. zero, the null selector. The low 2 bits of the selector, the RPL, being 1 (i.e. 0001&3==1) says you're trying to run at privilege level one, and the (selector & 4) TI bit determines whether you're using the local descriptor table to resolve selectors, or the global one. The zero in this bit in your example would have meant the global table.

  5. ATZ Man says:

    Actually, forget what I said about real mode. In a real true 8088 dereferencing the "long" address 0001:0000 would work. It would access physical address 16. In Virtual 86 mode on a '386 what would happen would depend on the particulars of virtual mode under the OS that you're running under. Most likely the OS would be supporting a virtual DOS and if an important system variable lived at the location the access to that location would be mediated by the OS's DOS emulator.

  6. Mike Dimmick says:

    I always thought that the first 64kB of the address space was invalid *because* of this resource ID partitioning. Making zero-value pointers an access violation really only requires the first page to be invalid, though of course VirtualAlloc only reserves in chunks of 64kB.

  7. CN says:

    Address 16 would be part of the interrupt table, so, yeah, some extra handling would probably be in order on the part of the DOS emulator.

  8. Cesar says:

    This trick is also used in the Linux kernel syscall return value. Only that it overloads the top 4K (the first few negative numbers when treated as signed). The C library converts these to positive numbers and put them in errno.

    The top 4K of memory is deep within kernel space (the top 1/4 or 1/2 of the address space is reserved for the kernel), so there is no risk of confusion.

  9. Ian Boyd says:

    The implementation of MAKEINTRESOURCE is an implementation detail.

  10. xml dude says:

    Who's using ancient file resources anymore? Please store strings in xml files.

    [I think you mistook this article for an advocacy post. I was explaining how Win32 resources work. If you prefer to keep your strings somewhere else, then you are more than welcome to. I wonder whether you also go into Honda repair forums telling people to go buy a Toyota. -Raymond]
  11. Random832 says:

    "The implementation of MAKEINTRESOURCE is an implementation detail."

    No, it's not; it's documented. "The return value is the specified value in the low-order word and zero in the high-order word."

Comments are closed.

Skip to main content