Well, kinda. You need a family of functions (objc_msgSend, of course, but also objc_msgSend_stret, objc_msgSendSuper, and objc_msgSendSuper_stret), to get this to really work for when you have structures as your return value or are calling a superclass's implementation (or both!). Since the inline assembly that is objc_msgSend really has no idea what the types of function it's calling is, the compiler must call an appropriate function based on the type information it has.
While the objc_msgSend, objc_msgSend_stret, et. al, is an annoying detail, that isn't really the hard part. (And in fact, all the good bridges use libffi to do more direct invocations and avoids a lot of this problem.)
The hard part of bridging is those places where Objective-C introspection is not powerful enough to discover the types of parameters or signatures of functions. Typically, this is all the C stuff.
So for example, Obj-C's runtime introspection cannot tell you the make up of a struct. So it cannot tell you the size of NSRect, NSPoint, NSSize, or the individual data types inside the struct (and the names of each item if you need to access them).
And the Obj-C runtime introspection cannot tell you about what C functions exist and what the parameter types and return types are. So for example, there is no Obj-C runtime way to look up that CGPointMake takes two CGFloat parameters and returns a CGPoint struct.
Also, inline functions (marcos) in C/Obj-C are also problematic for these bridges since they need to call the functions at runtime.
Apple shipped a framework called BridgeSupport in Mac OS X 10.5 which contains XML data for all the things that cannot be determined at runtime. It also contains .dylibs with symbols for inline functions. BridgeSupport is still in macOS today, but it isn't getting much love and Apple keeps (accidentally?) breaking things in BridgeSupport every release.
You wouldn't dream of implementing a dynamic language these days designed for performance without basic polymorphic inline caching. But Objective C seems to get by fine without it. Maybe it's not as essential as we think.
ObjC has per-class out-of-line caches keyed by the selector (intern'd string).
It's true that there's no hope of inlining across a message send, but ObjC's C and C++ compatibility mitigates a lot of this expense. Apps routinely use C and C++ for perf critical sections.
Also some of Apple's APIs are inline C functions (NSMakeRect, etc) and others are designed to minimize dynamic dispatch. Iterating an NSArray typically requires only one message send (see the NSFastEnumeration protocol).
There's also more exotic techniques like IMP caching.
My recent experience has been that indeed inlining is essential for true "zero-cost abstraction"—say an array of integers with index set/get methods—but that once your abstraction is weighty enough that a method is performing more than a few dozen "interesting" instructions, inlining doesn't win much over how well the modern CPU can already optimize across calls.
EDIT: I should add I'm talking about static inlining. I'm sure your typical alien-technology JIT can identify the exact 800 instruction trace in your inner loop and pull it all together into a perfectly machine-sympathetic sequence that runs 50% faster than anything you could construct statically in the absence of profile-driven feedback.
That's the key thing - other languages which solve this solve it dynamically.
Also, Objective-C being “C with a little bit of dynamism where you need it” doesn’t need it as much as languages where every call is, principally, a dynamic one.
Having said that, the current implementation does some caching, but inside objc_msgSend, not inline with the call (https://www.mikeash.com/pyblog/friday-qa-2012-11-16-lets-bui...)
It has posts where he recreates the functionality of the runtime, but without the optimizations that are in Apples runtime (which is open source).
Also bbum, he does a lot of posts on SO about this stuff, and I thought he wrote blog posts on the subject too but can't seem to find them.
Well, he works on Apple’s runtime team now, so it would be bad if he didn’t ;)
The real problem is that there's also no way in C to express the ObjC metadata properly. You could in principle register all the classes/protocols/methods at run time, I suppose, but that would be slow and gross.
Language is just a specification, right? Which is just a document.
Then you have to implement that language using that spec: interpreter, compiler, VM, runtime, whatever.
So I was always under impression that you could implement any language in any other language (almost; sometimes with the help of another, more low-level lang, say assembly, C etc.).
It is like saying "Java has no Tail-Call optimization (yet), hence you can't implement Scheme (which requires TCO) in Java". Surely you can! Just use trampolines or even use your own stack and calling convention.
What am I missing?
If host language is untyped, then write your own type system.
Another example: Java has no co-routines, has no continuations, but Kotlin (written in Java) does. They compile Kotlin code with co-routines into finite state machines in Java.
To quote the API documentation on Constructing Function Calls: "Using [built-in functions], you can record the arguments a function received, and call another function with the same arguments, without knowing the number or types of the arguments." 
[object method: args...]
IMP tmp = objc_msg_lookup(object, @selector(method:))
*tmp(object, @selector(method:), args...)
Major misunderstanding about turing completeness.
A language is so much richer than simply enabling you to write a map from inputs to outputs. Sure you could probably rewrite my Haskell program in Fortran in the sense that the Fortran program produces the exact same outputs for any given inputs...but it's hardly convincing to me that you "recreated" my program in Fortran after dropping my monad transformers and lenses.
Regardless, you wouldn't even be able to generally prove (assuming you lack dependent typing) that your program is functionally equivalent to mine (since this is undecidable).