My question is, for reassurance, that there is no way to use dependency inversion without sharing complex classes or applying an adapter between the 2 layers?
No, there most certainly are ways to use the Dependency Inversion Principle without sharing complex classes. Share simple messages.
If you put an adapter between two layers what you have is a third layer. This is not a bad thing so long as the layer is useful and focused.
@Metafight is exactly right. DTOs (or value objects) can be passed around. But...
Return types? Who said you had to return?
The Clean Architecture
One of the neatest tricks I've seen that follows the Dependency Inversion Principle is the plugin diagram in the lower right corner here:
This lets the flow of control start in the outer layer, dive into the inner layer, and come back out in the outer layer again. All without having to return a thing. A very simple message can be sent this way. Here's an example.
It doesn't know, it doesn't want to know
I am confused about Dependency Inversion in general. Lets's say we have 2 layers: Layer0 the low level layer, and Layer1 the high level layer. Layer1 should define the abstract interface, and Layer0 should implement it. However if Layer1 requires other types then the built in types, then in order to use Dependency Inversion I must extract the complex types into a separate library and use those types in both layers.
The key thing here is the high level layer doesn't know about the low level layer at all. The high level layer defines interfaces that it can listen through or talk to without knowing what is on the other side. The low level layer is a slave to these interfaces. It doesn't own them but it must match them. That means the low level layer can be completely replaced without the high level layer being changed at all.
So what does this mean for DTO's? The highest level layer that knows about the DTO defines it's interface. Anything that high and lower may know about the interface and use the DTO. However, whatever constructed the DTO is what defines it's implementation.
The DTOs don't need to be complicated. They can become complicated if you don't keep your use cases focused and simple.