osmo_io: Change struct osmo_io_ops to contain struct, not union

As we introduce more modes, and each mode aliases call-back function
pointers to those of another mode, we have more and more error cases
where we (for exampele) access read_cb, but in reality the user has
populated recvfrom_cb.

Let's use a struct, meaning that call-backs of one mode no longer alias
to the same memory locations of call-backs fro another mode.  This
allows us to properly check if the user actually provided the right
callbacks for the given mode of the iofd.

This breaks ABI, but luckily not API.  So a simple recompile of
higher-layer library + application code will work.

Change-Id: I9d302df8d00369e7b30437a52deb205f75882be3
diff --git a/src/core/osmo_io.c b/src/core/osmo_io.c
index e059f87..9de9e2e 100644
--- a/src/core/osmo_io.c
+++ b/src/core/osmo_io.c
@@ -306,6 +306,8 @@
 	int res;
 	struct msgb *pending = NULL;
 
+	OSMO_ASSERT(iofd->mode == OSMO_IO_FD_MODE_READ_WRITE);
+
 	if (rc <= 0) {
 		iofd->io_ops.read_cb(iofd, rc, msg);
 		return;
@@ -473,6 +475,24 @@
 	return 0;
 }
 
+static int check_mode_callback_compat(enum osmo_io_fd_mode mode, const struct osmo_io_ops *ops)
+{
+	switch (mode) {
+	case OSMO_IO_FD_MODE_READ_WRITE:
+		if (ops->recvfrom_cb || ops->sendto_cb)
+			return false;
+		break;
+	case OSMO_IO_FD_MODE_RECVFROM_SENDTO:
+		if (ops->read_cb || ops->write_cb)
+			return false;
+		break;
+	default:
+		break;
+	}
+
+	return true;
+}
+
 /*! Allocate and setup a new iofd.
  *  \param[in] ctx the parent context from which to allocate
  *  \param[in] fd the underlying system file descriptor
@@ -496,6 +516,9 @@
 		return NULL;
 	}
 
+	if (!check_mode_callback_compat(mode, ioops))
+		return NULL;
+
 	iofd = talloc_zero(ctx, struct osmo_io_fd);
 	if (!iofd)
 		return NULL;
@@ -543,8 +566,10 @@
 		return rc;
 
 	IOFD_FLAG_UNSET(iofd, IOFD_FLAG_CLOSED);
-	if (iofd->io_ops.read_cb)
+	if ((iofd->mode == OSMO_IO_FD_MODE_READ_WRITE && iofd->io_ops.read_cb) ||
+	    (iofd->mode == OSMO_IO_FD_MODE_RECVFROM_SENDTO && iofd->io_ops.recvfrom_cb)) {
 		osmo_iofd_ops.read_enable(iofd);
+	}
 
 	if (iofd->tx_queue.current_length > 0)
 		osmo_iofd_ops.write_enable(iofd);
@@ -722,8 +747,11 @@
 /*! Set the osmo_io_ops for an iofd.
  *  \param[in] iofd Target iofd file descriptor
  *  \param[in] ioops osmo_io_ops structure to be set */
-void osmo_iofd_set_ioops(struct osmo_io_fd *iofd, const struct osmo_io_ops *ioops)
+int osmo_iofd_set_ioops(struct osmo_io_fd *iofd, const struct osmo_io_ops *ioops)
 {
+	if (!check_mode_callback_compat(iofd->mode, ioops))
+		return -EINVAL;
+
 	iofd->io_ops = *ioops;
 
 	switch (iofd->mode) {
@@ -743,6 +771,8 @@
 	default:
 		OSMO_ASSERT(0);
 	}
+
+	return 0;
 }
 
 /*! Notify the user if/when the socket is connected.