HA! That last thing I said seems to work: I changed the code to make just one universal proto and clone it for all my C++ wrapper objects. That simplifies things A LOT.
It turns out you CAN still register a method table for the clone objects via the C API without problems. That makes sense - a clone has a method table of its own that is distinct from the method table of its proto. Adding a method to an object in Io doesn't add that method to that object's proto, and neither does adding a method table in the C API.
I'm finally starting to understand how this works, and I have some suggestions about the code examples for binding C/C++ to Io that I've seen:
The way the binding examples use the "proto()" function was puzzling me for a long time. The code in the proto() function registers itself like a callback using IoState_registerProtoWithFunc_. But then the examples explicitly *call* the proto() function to build an Io object to pass to IoObject_setSlot_to_. OK so what is proto()'s true purpose? Is the proto() function a callback meant to be invoked by Io, or is it a utility function meant to be called by user code to build an Io object?
I *know* Io can't actually be using proto() as a callback even though it's "registered" with Io, because the function is already called once by the user. If the function were ever called after that by Io, the call to IoState_registerProtoWithFunc_ in the body of the proto() function would fail on all subsequent usages!
I'm guessing IoState_registerProtoWithFunc_ is actually just using the address of the function as a unique identifier. Seems like a hack to me, since the address of the function is taken but never called; only its numeric value and uniqueness matter. A clever hack, but a hack nonetheless. I'm guessing I could just as easily make up a number and pass that instead of &proto, as long as I don't accidently duplicate an address already used?
Maybe this function-address trick is something that makes sense to C programmers but leaves C++ programmers scratching their heads. :P (I'm one to talk - my binding library uses a similar hack to generate unique functions for wrappers for C++ methods, but I do it with templates - metaprogramming FTW.)
As for the examples' use of proto() for creating an Io object to pass to IoObject_setSlot_to_, you run into a problem if you actually want to register more than one copy of the proto with different slot names. The obvious thing to do - duplicating the line of code that adds the proto and only changing the slot name - doesn't work because you can't call proto() twice because it fails on the second call to IoState_registerProtoWithFunc_, as I said above.
The solution? Clone the proto object first and register that clone under the new name. When the examples use only the proto() function to demonstrate IoObject_setSlot_to_, they're missing an opportunity to show that there isn't anything special about the proto() function with regards to IoObject_setSlot_to_. You can use IoObject_setSlot_to_ with Io objects you've acquired through other means. I'm sure that's obvious to the writers of the Io language but it is certainly NOT apparent to newbies, at least it wasn't to me. But once I figured it out I used my own script binding library's "to_script()" function to build the Io objects to fill the other slots, and it seems to work fine.