Ultimately, you can't stop somebody who's determined, but you can do a few things to make life difficult for unauthorized users.
The obvious would be to encrypt (at least most of) the code in the DLL. Have a single entry point that the user has to call to get (for example) a table of pointers to the real functions. When the entry point is called, a key has to be supplied and that is used to decrypt the rest of the content, and then the pointers are returned. If the user supplies the wrong key, they get pointers, but the data won't be decrypted correctly, so when they try to call it, it doesn't work.
That's not foolproof by any means. Just for an obvious example, if somebody runs the authorized program under a debugger they can see what key is passed, and simply re-use it. If you want to make life a bit trickier for an attacker, you can do something like having the DLL do a checksum on the executable, and use that as the key (and the user will have to embed some data to get the checksum to come out right). That can still be figured out, but it will at least take some work.
Edit: The exact encryption doesn't matter a lot. I'd probably use AES, simply because it's easy to find an implementation and it runs pretty fast, so there's not much reason to use something else.
As far as how the calling code would work, it would be something like this:
struct functions { rettype1 (*func1)(args1); rettype2 (*func2)(args2); rettype3 (*func3)(args3); // ... }; void entry_point(key_type key) { functions f; decrypt(data_block); f.func1 = data_block.offset1; f.func2 = data_block.offset2; // ... return f; }
In the calling code you'd have initialization something like:
functions f = entry_point(myKey);
and then calling a function would be something like:
f.whatever(arg1, arg2, arg3);
In essence, you're re-creating a small piece of object oriented programming, and having your entry point return an "object" of sorts, with the DLL's real functions as "member functions" of that "object".
As far as getting the checksum goes, I haven't thought through it in a lot of detail. Basically just just want the address where the executable is stored. You can walk through modules with VirtualQuery, or you can use GetModuleHandle(NULL).