Merged changes from master
diff --git a/src/objective-c/GRPCClient/GRPCCall.m b/src/objective-c/GRPCClient/GRPCCall.m
index 989ce39..2186ea4 100644
--- a/src/objective-c/GRPCClient/GRPCCall.m
+++ b/src/objective-c/GRPCClient/GRPCCall.m
@@ -41,23 +41,11 @@
 #import "private/GRPCCompletionQueue.h"
 #import "private/GRPCDelegateWrapper.h"
 #import "private/GRPCMethodName+HTTP2Encoding.h"
+#import "private/GRPCWrappedCall.h"
 #import "private/NSData+GRPC.h"
 #import "private/NSDictionary+GRPC.h"
 #import "private/NSError+GRPC.h"
 
-// A grpc_call_error represents a precondition failure when invoking the
-// grpc_call_* functions. If one ever happens, it's a bug in this library.
-//
-// TODO(jcanizales): Can an application shut down gracefully when a thread other
-// than the main one throws an exception?
-static void AssertNoErrorInCall(grpc_call_error error) {
-  if (error != GRPC_CALL_OK) {
-    @throw [NSException exceptionWithName:NSInternalInconsistencyException
-                                   reason:@"Precondition of grpc_call_* not met."
-                                 userInfo:nil];
-  }
-}
-
 @interface GRPCCall () <GRXWriteable>
 // Makes it readwrite.
 @property(atomic, strong) NSDictionary *responseMetadata;
@@ -65,34 +53,24 @@
 
 // The following methods of a C gRPC call object aren't reentrant, and thus
 // calls to them must be serialized:
-// - add_metadata
-// - invoke
-// - start_write
-// - writes_done
-// - start_read
+// - start_batch
 // - destroy
-// The first four are called as part of responding to client commands, but
-// start_read we want to call as soon as we're notified that the RPC was
-// successfully established (which happens concurrently in the network queue).
-// Serialization is achieved by using a private serial queue to operate the
-// call object.
-// Because add_metadata and invoke are called and return successfully before
-// any of the other methods is called, they don't need to use the queue.
 //
-// Furthermore, start_write and writes_done can only be called after the
-// WRITE_ACCEPTED event for any previous write is received. This is achieved by
+// start_batch with a SEND_MESSAGE argument can only be called after the
+// OP_COMPLETE event for any previous write is received. This is achieved by
 // pausing the requests writer immediately every time it writes a value, and
-// resuming it again when WRITE_ACCEPTED is received.
+// resuming it again when OP_COMPLETE is received.
 //
-// Similarly, start_read can only be called after the READ event for any
-// previous read is received. This is easier to enforce, as we're writing the
-// received messages into the writeable: start_read is enqueued once upon receiving
-// the CLIENT_METADATA_READ event, and then once after receiving each READ
-// event.
+// Similarly, start_batch with a RECV_MESSAGE argument can only be called after
+// the OP_COMPLETE event for any previous read is received.This is easier to
+// enforce, as we're writing the received messages into the writeable:
+// start_batch is enqueued once upon receiving the OP_COMPLETE event for the
+// RECV_METADATA batch, and then once after receiving each OP_COMPLETE event for
+// each RECV_MESSAGE batch.
 @implementation GRPCCall {
   dispatch_queue_t _callQueue;
 
-  grpc_call *_gRPCCall;
+  GRPCWrappedCall *_wrappedCall;
   dispatch_once_t _callAlreadyInvoked;
 
   GRPCChannel *_channel;
@@ -129,10 +107,10 @@
     _completionQueue = [GRPCCompletionQueue completionQueue];
 
     _channel = [GRPCChannel channelToHost:host];
-    _gRPCCall = grpc_channel_create_call_old(_channel.unmanagedChannel,
-                                             method.HTTP2Path.UTF8String,
-                                             host.UTF8String,
-                                             gpr_inf_future);
+      
+      _wrappedCall = [[GRPCWrappedCall alloc] initWithChannel:_channel
+                                                       method:method.HTTP2Path
+                                                         host:host];
 
     // Serial queue to invoke the non-reentrant methods of the grpc_call object.
     _callQueue = dispatch_queue_create("org.grpc.call", NULL);
@@ -156,7 +134,7 @@
 
 - (void)cancelCall {
   // Can be called from any thread, any number of times.
-  AssertNoErrorInCall(grpc_call_cancel(_gRPCCall));
+    [_wrappedCall cancel];
 }
 
 - (void)cancel {
@@ -167,9 +145,9 @@
 }
 
 - (void)dealloc {
-  grpc_call *gRPCCall = _gRPCCall;
+  __block GRPCWrappedCall *wrappedCall = _wrappedCall;
   dispatch_async(_callQueue, ^{
-    grpc_call_destroy(gRPCCall);
+    wrappedCall = nil;
   });
 }
 
@@ -177,8 +155,8 @@
 
 // Only called from the call queue.
 // The handler will be called from the network queue.
-- (void)startReadWithHandler:(GRPCEventHandler)handler {
-  AssertNoErrorInCall(grpc_call_start_read_old(_gRPCCall, (__bridge_retained void *)handler));
+- (void)startReadWithHandler:(void(^)(grpc_byte_buffer *))handler {
+  [_wrappedCall startBatchWithOperations:@[[[GRPCOpRecvMessage alloc] initWithHandler:handler]]];
 }
 
 // Called initially from the network queue once response headers are received,
@@ -195,12 +173,12 @@
   __weak GRPCDelegateWrapper *weakWriteable = _responseWriteable;
 
   dispatch_async(_callQueue, ^{
-    [weakSelf startReadWithHandler:^(grpc_event *event) {
-      if (!event->data.read) {
-        // No more responses from the server.
+    [weakSelf startReadWithHandler:^(grpc_byte_buffer *message) {
+      if (message == NULL) {
         return;
       }
-      NSData *data = [NSData grpc_dataWithByteBuffer:event->data.read];
+      NSData *data = [NSData grpc_dataWithByteBuffer:message];
+      grpc_byte_buffer_destroy(message);
       if (!data) {
         // The app doesn't have enough memory to hold the server response. We
         // don't want to throw, because the app shouldn't crash for a behavior
@@ -225,35 +203,10 @@
 
 #pragma mark Send headers
 
-- (void)addHeaderWithName:(NSString *)name binaryValue:(NSData *)value {
-  grpc_metadata metadata;
-  // Safe to discard const qualifiers; we're not going to modify the contents.
-  metadata.key = (char *)name.UTF8String;
-  metadata.value = (char *)value.bytes;
-  metadata.value_length = value.length;
-  grpc_call_add_metadata_old(_gRPCCall, &metadata, 0);
-}
-
-- (void)addHeaderWithName:(NSString *)name ASCIIValue:(NSString *)value {
-  grpc_metadata metadata;
-  // Safe to discard const qualifiers; we're not going to modify the contents.
-  metadata.key = (char *)name.UTF8String;
-  metadata.value = (char *)value.UTF8String;
-  // The trailing \0 isn't encoded in HTTP2.
-  metadata.value_length = value.length;
-  grpc_call_add_metadata_old(_gRPCCall, &metadata, 0);
-}
-
 // TODO(jcanizales): Rename to commitHeaders.
 - (void)sendHeaders:(NSDictionary *)metadata {
-  for (NSString *name in metadata) {
-    id value = metadata[name];
-    if ([value isKindOfClass:[NSData class]]) {
-      [self addHeaderWithName:name binaryValue:value];
-    } else if ([value isKindOfClass:[NSString class]]) {
-      [self addHeaderWithName:name ASCIIValue:value];
-    }
-  }
+  [_wrappedCall startBatchWithOperations:@[[[GRPCOpSendMetadata alloc]
+                                            initWithMetadata:metadata ?: @{} handler:nil]]];
 }
 
 #pragma mark GRXWriteable implementation
@@ -263,10 +216,7 @@
 - (void)writeMessage:(NSData *)message withErrorHandler:(void (^)())errorHandler {
 
   __weak GRPCCall *weakSelf = self;
-  GRPCEventHandler resumingHandler = ^(grpc_event *event) {
-    if (event->data.write_accepted != GRPC_OP_OK) {
-      errorHandler();
-    }
+  void(^resumingHandler)(void) = ^{
     // Resume the request writer (even in the case of error).
     // TODO(jcanizales): No need to do it in the case of errors anymore?
     GRPCCall *strongSelf = weakSelf;
@@ -274,13 +224,9 @@
       strongSelf->_requestWriter.state = GRXWriterStateStarted;
     }
   };
-
-  grpc_byte_buffer *buffer = message.grpc_byteBuffer;
-  AssertNoErrorInCall(grpc_call_start_write_old(_gRPCCall,
-                                                buffer,
-                                                (__bridge_retained void *)resumingHandler,
-                                                0));
-  grpc_byte_buffer_destroy(buffer);
+  [_wrappedCall startBatchWithOperations:@[[[GRPCOpSendMessage alloc]
+                                            initWithMessage:message
+                                            handler:resumingHandler]] errorHandler:errorHandler];
 }
 
 - (void)didReceiveValue:(id)value {
@@ -303,12 +249,8 @@
 // Only called from the call queue. The error handler will be called from the
 // network queue if the requests stream couldn't be closed successfully.
 - (void)finishRequestWithErrorHandler:(void (^)())errorHandler {
-  GRPCEventHandler handler = ^(grpc_event *event) {
-    if (event->data.finish_accepted != GRPC_OP_OK) {
-      errorHandler();
-    }
-  };
-  AssertNoErrorInCall(grpc_call_writes_done_old(_gRPCCall, (__bridge_retained void *)handler));
+  [_wrappedCall startBatchWithOperations:@[[[GRPCOpSendClose alloc] init]]
+                            errorHandler:errorHandler];
 }
 
 - (void)didFinishWithError:(NSError *)errorOrNil {
@@ -332,32 +274,26 @@
 // after this.
 // The first one (metadataHandler), when the response headers are received.
 // The second one (completionHandler), whenever the RPC finishes for any reason.
-- (void)invokeCallWithMetadataHandler:(GRPCEventHandler)metadataHandler
-                    completionHandler:(GRPCEventHandler)completionHandler {
-  AssertNoErrorInCall(grpc_call_invoke_old(_gRPCCall,
-                                           _completionQueue.unmanagedQueue,
-                                           (__bridge_retained void *)metadataHandler,
-                                           (__bridge_retained void *)completionHandler,
-                                           0));
+- (void)invokeCallWithMetadataHandler:(void(^)(NSDictionary *))metadataHandler
+                    completionHandler:(void(^)(NSError *))completionHandler {
+  [_wrappedCall startBatchWithOperations:@[[[GRPCOpRecvMetadata alloc]
+                                            initWithHandler:metadataHandler]]];
+  [_wrappedCall startBatchWithOperations:@[[[GRPCOpRecvStatus alloc]
+                                            initWithHandler:completionHandler]]];
 }
 
 - (void)invokeCall {
   __weak GRPCCall *weakSelf = self;
-  [self invokeCallWithMetadataHandler:^(grpc_event *event) {
+  [self invokeCallWithMetadataHandler:^(NSDictionary *metadata) {
     // Response metadata received.
-    // TODO(jcanizales): Name the type of event->data.client_metadata_read
-    // in the C library so one can actually pass the object to a method.
-    grpc_metadata *entries = event->data.client_metadata_read.elements;
-    size_t count = event->data.client_metadata_read.count;
     GRPCCall *strongSelf = weakSelf;
     if (strongSelf) {
-      strongSelf.responseMetadata = [NSDictionary grpc_dictionaryFromMetadata:entries
-                                                                        count:count];
+      strongSelf.responseMetadata = metadata;
       [strongSelf startNextRead];
     }
-  } completionHandler:^(grpc_event *event) {
+  } completionHandler:^(NSError *error) {
     // TODO(jcanizales): Merge HTTP2 trailers into response metadata.
-    [weakSelf finishWithError:[NSError grpc_errorFromStatus:&event->data.finished]];
+    [weakSelf finishWithError:error];
   }];
   // Now that the RPC has been initiated, request writes can start.
   [_requestWriter startWithWriteable:self];
diff --git a/src/objective-c/GRPCClient/private/GRPCCompletionQueue.h b/src/objective-c/GRPCClient/private/GRPCCompletionQueue.h
index c85d57c..198fa2b 100644
--- a/src/objective-c/GRPCClient/private/GRPCCompletionQueue.h
+++ b/src/objective-c/GRPCClient/private/GRPCCompletionQueue.h
@@ -32,11 +32,11 @@
  */
 
 #import <Foundation/Foundation.h>
+#include <grpc/grpc.h>
 
 struct grpc_completion_queue;
-struct grpc_event;
 
-typedef void(^GRPCEventHandler)(struct grpc_event *event);
+typedef void(^GRPCQueueCompletionHandler)(grpc_op_error error);
 
 // This class lets one more easily use grpc_completion_queue. To use it, pass
 // the value of the unmanagedQueue property of an instance of this class to
diff --git a/src/objective-c/GRPCClient/private/GRPCCompletionQueue.m b/src/objective-c/GRPCClient/private/GRPCCompletionQueue.m
index b98e8ea..cae21b7 100644
--- a/src/objective-c/GRPCClient/private/GRPCCompletionQueue.m
+++ b/src/objective-c/GRPCClient/private/GRPCCompletionQueue.m
@@ -46,7 +46,7 @@
 - (instancetype)init {
   if ((self = [super init])) {
     _unmanagedQueue = grpc_completion_queue_create();
-
+    
     // This is for the following block to capture the pointer by value (instead
     // of retaining self and doing self->_unmanagedQueue). This is essential
     // because the block doesn't end until after grpc_completion_queue_shutdown
@@ -66,30 +66,18 @@
       while (YES) {
         // The following call blocks until an event is available.
         grpc_event *event = grpc_completion_queue_next(unmanagedQueue, gpr_inf_future);
+        GRPCQueueCompletionHandler handler;
         switch (event->type) {
-          case GRPC_WRITE_ACCEPTED:
-          case GRPC_FINISH_ACCEPTED:
-          case GRPC_CLIENT_METADATA_READ:
-          case GRPC_READ:
-          case GRPC_FINISHED:
-            if (event->tag) {
-              GRPCEventHandler handler = (__bridge_transfer GRPCEventHandler) event->tag;
-              handler(event);
-            }
-            grpc_event_finish(event);
-            continue;
+          case GRPC_OP_COMPLETE:
+            handler = (__bridge_transfer GRPCQueueCompletionHandler)event->tag;
+            handler(event->data.op_complete);
+            break;
           case GRPC_QUEUE_SHUTDOWN:
             grpc_completion_queue_destroy(unmanagedQueue);
-            grpc_event_finish(event);
             return;
-          case GRPC_SERVER_RPC_NEW:
-            NSAssert(NO, @"C gRPC library produced a server-only event.");
-            continue;
+          default:
+            [NSException raise:@"Unrecognized completion type" format:@""];
         }
-        // This means the C gRPC library produced an event that wasn't known
-        // when this library was written. To preserve evolvability, ignore the
-        // unknown event on release builds.
-        NSAssert(NO, @"C gRPC library produced an unknown event.");
       };
     });
   }
diff --git a/src/objective-c/GRPCClient/private/GRPCWrappedCall.h b/src/objective-c/GRPCClient/private/GRPCWrappedCall.h
new file mode 100644
index 0000000..d7b1aa8
--- /dev/null
+++ b/src/objective-c/GRPCClient/private/GRPCWrappedCall.h
@@ -0,0 +1,97 @@
+/*
+ *
+ * Copyright 2015, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *     * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+#import <Foundation/Foundation.h>
+#include <grpc/grpc.h>
+#import "GRPCChannel.h"
+
+typedef void(^GRPCCompletionHandler)(NSDictionary *);
+
+@protocol GRPCOp <NSObject>
+
+- (void)getOp:(grpc_op *)op;
+
+- (void(^)(void))opProcessor;
+
+@end
+
+@interface GRPCOpSendMetadata : NSObject <GRPCOp>
+
+- (instancetype)initWithMetadata:(NSDictionary *)metadata
+                         handler:(void(^)(void))handler NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@interface GRPCOpSendMessage : NSObject <GRPCOp>
+
+- (instancetype)initWithMessage:(NSData *)message
+                        handler:(void(^)(void))handler NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@interface GRPCOpSendClose : NSObject <GRPCOp>
+
+- (instancetype)initWithHandler:(void(^)(void))handler NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@interface GRPCOpRecvMetadata : NSObject <GRPCOp>
+
+- (instancetype)initWithHandler:(void(^)(NSDictionary *))handler NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@interface GRPCOpRecvMessage : NSObject <GRPCOp>
+
+- (instancetype)initWithHandler:(void(^)(grpc_byte_buffer *))handler NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@interface GRPCOpRecvStatus : NSObject <GRPCOp>
+
+- (instancetype)initWithHandler:(void(^)(NSError *))handler NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@interface GRPCWrappedCall : NSObject
+
+- (instancetype)initWithChannel:(GRPCChannel *)channel
+                         method:(NSString *)method
+                           host:(NSString *)host NS_DESIGNATED_INITIALIZER;
+
+- (void)startBatchWithOperations:(NSArray *)ops errorHandler:(void(^)())errorHandler;
+
+- (void)startBatchWithOperations:(NSArray *)ops;
+
+- (void)cancel;
+@end
diff --git a/src/objective-c/GRPCClient/private/GRPCWrappedCall.m b/src/objective-c/GRPCClient/private/GRPCWrappedCall.m
new file mode 100644
index 0000000..ad6aca0
--- /dev/null
+++ b/src/objective-c/GRPCClient/private/GRPCWrappedCall.m
@@ -0,0 +1,341 @@
+/*
+ *
+ * Copyright 2015, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *     * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+#import "GRPCWrappedCall.h"
+#import <Foundation/Foundation.h>
+#include <grpc/grpc.h>
+#include <grpc/byte_buffer.h>
+#include <grpc/support/alloc.h>
+#import "GRPCCompletionQueue.h"
+#import "NSDictionary+GRPC.h"
+#import "NSData+GRPC.h"
+#import "NSError+GRPC.h"
+
+@implementation GRPCOpSendMetadata{
+  void(^_handler)(void);
+  grpc_metadata *_send_metadata;
+  size_t _count;
+}
+
+- (instancetype)init {
+  return [self initWithMetadata:nil handler:nil];
+}
+
+- (instancetype)initWithMetadata:(NSDictionary *)metadata handler:(void (^)(void))handler {
+  if (self = [super init]) {
+    if (metadata) {
+      [metadata grpc_getMetadataArray:&_send_metadata];
+      _count = metadata.count;
+    } else {
+      _send_metadata = NULL;
+      _count = 0;
+    }
+    _handler = handler;
+  }
+  return self;
+}
+
+- (void)getOp:(grpc_op *)op {
+  op->op = GRPC_OP_SEND_INITIAL_METADATA;
+  op->data.send_initial_metadata.count = _count;
+}
+
+- (void (^)(void))opProcessor {
+  return ^{
+    gpr_free(_send_metadata);
+    if (_handler) {
+      _handler();
+    }
+  };
+}
+
+@end
+
+@implementation GRPCOpSendMessage{
+  void(^_handler)(void);
+  grpc_byte_buffer *_byte_buffer;
+}
+
+- (instancetype)init {
+  return [self initWithMessage:nil handler:nil];
+}
+
+- (instancetype)initWithMessage:(NSData *)message handler:(void (^)(void))handler {
+  if (!message) {
+    [NSException raise:NSInvalidArgumentException format:@"message cannot be null"];
+  }
+  if (self = [super init]) {
+    _byte_buffer = [message grpc_byteBuffer];
+    _handler = handler;
+  }
+  return self;
+}
+
+- (void)getOp:(grpc_op *)op {
+  op->op = GRPC_OP_SEND_MESSAGE;
+  op->data.send_message = _byte_buffer;
+}
+
+- (void (^)(void))opProcessor {
+  return ^{
+    gpr_free(_byte_buffer);
+    if (_handler) {
+      _handler();
+    }
+  };
+}
+
+@end
+
+@implementation GRPCOpSendClose{
+  void(^_handler)(void);
+}
+
+- (instancetype)init {
+  return [self initWithHandler:nil];
+}
+
+- (instancetype)initWithHandler:(void (^)(void))handler {
+  if (self = [super init]) {
+    _handler = handler;
+  }
+  return self;
+}
+
+- (void)getOp:(grpc_op *)op {
+  op->op = GRPC_OP_SEND_CLOSE_FROM_CLIENT;
+}
+
+- (void (^)(void))opProcessor {
+  return ^{
+    if (_handler) {
+      _handler();
+    }
+  };
+}
+
+@end
+
+@implementation GRPCOpRecvMetadata{
+  void(^_handler)(NSDictionary *);
+  grpc_metadata_array *_recv_initial_metadata;
+}
+
+- (instancetype) init {
+  return [self initWithHandler:nil];
+}
+
+- (instancetype) initWithHandler:(void (^)(NSDictionary *))handler {
+  if (self = [super init]) {
+    _handler = handler;
+    _recv_initial_metadata = gpr_malloc(sizeof(grpc_metadata_array));
+    grpc_metadata_array_init(_recv_initial_metadata);
+  }
+  return self;
+}
+
+- (void)getOp:(grpc_op *)op {
+  op->op = GRPC_OP_RECV_INITIAL_METADATA;
+  op->data.recv_initial_metadata = _recv_initial_metadata;
+}
+
+- (void (^)(void))opProcessor {
+  return ^{
+    NSDictionary *metadata = [NSDictionary
+                              grpc_dictionaryFromMetadata:_recv_initial_metadata->metadata
+                              count:_recv_initial_metadata->count];
+    grpc_metadata_array_destroy(_recv_initial_metadata);
+    if (_handler) {
+      _handler(metadata);
+    }
+  };
+}
+
+@end
+
+@implementation GRPCOpRecvMessage{
+  void(^_handler)(grpc_byte_buffer *);
+  grpc_byte_buffer **_recv_message;
+}
+
+- (instancetype)init {
+  return [self initWithHandler:nil];
+}
+
+- (instancetype)initWithHandler:(void (^)(grpc_byte_buffer *))handler {
+  if (self = [super init]) {
+    _handler = handler;
+    _recv_message = gpr_malloc(sizeof(grpc_byte_buffer*));
+  }
+  return self;
+}
+
+- (void)getOp:(grpc_op *)op {
+  op->op = GRPC_OP_RECV_MESSAGE;
+  op->data.recv_message = _recv_message;
+}
+
+- (void (^)(void))opProcessor {
+  return ^{
+    if (_handler) {
+      _handler(*_recv_message);
+      gpr_free(_recv_message);
+    }
+  };
+}
+
+@end
+
+@implementation GRPCOpRecvStatus{
+  void(^_handler)(NSError *);
+  grpc_status_code *_code;
+  char **_details;
+  size_t *_details_capacity;
+  grpc_metadata_array *_recv_trailing_metadata;
+}
+
+- (instancetype) init {
+  return [self initWithHandler:nil];
+}
+
+- (instancetype) initWithHandler:(void (^)(NSError *))handler {
+  if (self = [super init]) {
+    _handler = handler;
+    _code = gpr_malloc(sizeof(grpc_status_code));
+    _details = gpr_malloc(sizeof(char*));
+    _details_capacity = gpr_malloc(sizeof(size_t));
+    *_details_capacity = 0;
+    _recv_trailing_metadata = gpr_malloc(sizeof(grpc_metadata_array));
+  }
+  return self;
+}
+
+- (void)getOp:(grpc_op *)op {
+  op->op = GRPC_OP_RECV_STATUS_ON_CLIENT;
+  op->data.recv_status_on_client.status = _code;
+  op->data.recv_status_on_client.status_details = _details;
+  op->data.recv_status_on_client.status_details_capacity = _details_capacity;
+  op->data.recv_status_on_client.trailing_metadata = _recv_trailing_metadata;
+}
+
+- (void (^)(void))opProcessor {
+  return ^{
+    grpc_status status;
+    status.status = *_code;
+    status.details = *_details;
+    status.metadata = _recv_trailing_metadata;
+    gpr_free(_code);
+    gpr_free(_details);
+    gpr_free(_details_capacity);
+    if (_handler) {
+      _handler([NSError grpc_errorFromStatus:&status]);
+    }
+  };
+}
+
+@end
+
+@implementation GRPCWrappedCall{
+  grpc_call *_call;
+  GRPCCompletionQueue *_queue;
+}
+
+- (instancetype)init {
+  return [self initWithChannel:nil method:nil host:nil];
+}
+
+- (instancetype)initWithChannel:(GRPCChannel *)channel
+                         method:(NSString *)method
+                           host:(NSString *)host {
+  if (!channel || !method || !host) {
+    [NSException raise:NSInvalidArgumentException
+                format:@"channel, method, and host cannot be nil."];
+  }
+  
+  if (self = [super init]) {
+    static dispatch_once_t initialization;
+    dispatch_once(&initialization, ^{
+      grpc_init();
+    });
+    
+    _queue = [GRPCCompletionQueue completionQueue];
+    _call = grpc_channel_create_call(channel.unmanagedChannel, _queue.unmanagedQueue,
+                                     method.UTF8String, host.UTF8String, gpr_inf_future);
+    if (_call == NULL) {
+      return nil;
+    }
+  }
+  return self;
+}
+
+- (void)startBatchWithOperations:(NSArray *)operations {
+  [self startBatchWithOperations:operations errorHandler:nil];
+}
+
+- (void)startBatchWithOperations:(NSArray *)operations errorHandler:(void (^)())errorHandler {
+  NSMutableArray *opProcessors = [NSMutableArray array];
+  size_t nops = operations.count;
+  grpc_op *ops_array = gpr_malloc(nops * sizeof(grpc_op));
+  size_t i = 0;
+  for (id op in operations) {
+    [op getOp:&ops_array[i]];
+    [opProcessors addObject:[op opProcessor]];
+  }
+  grpc_call_error error = grpc_call_start_batch(_call, ops_array, nops,
+                                                (__bridge_retained void *)(^(grpc_op_error error){
+    if (error != GRPC_OP_OK) {
+      if (errorHandler) {
+        errorHandler();
+      } else {
+        [NSException raise:@"Operation Exception" format:@"The batch failed with an unknown error"];
+      }
+    }
+    for (void(^processor)(void) in opProcessors) {
+      processor();
+    }
+  }));
+  
+  if (error != GRPC_CALL_OK) {
+    [NSException raise:NSInvalidArgumentException format:@"The batch did not start successfully"];
+  }
+}
+
+- (void)cancel {
+  grpc_call_cancel(_call);
+}
+
+- (void)dealloc {
+  grpc_call_destroy(_call);
+}
+
+@end
\ No newline at end of file
diff --git a/src/objective-c/GRPCClient/private/NSData+GRPC.m b/src/objective-c/GRPCClient/private/NSData+GRPC.m
index 5ab16d9..6ea4ce9 100644
--- a/src/objective-c/GRPCClient/private/NSData+GRPC.m
+++ b/src/objective-c/GRPCClient/private/NSData+GRPC.m
@@ -59,6 +59,9 @@
 
 @implementation NSData (GRPC)
 + (instancetype)grpc_dataWithByteBuffer:(grpc_byte_buffer *)buffer {
+  if (buffer == NULL) {
+    return nil;
+  }
   NSUInteger length = grpc_byte_buffer_length(buffer);
   char *array = malloc(length * sizeof(*array));
   if (!array) {
diff --git a/src/objective-c/GRPCClient/private/NSDictionary+GRPC.h b/src/objective-c/GRPCClient/private/NSDictionary+GRPC.h
index 8025285..fec2adb 100644
--- a/src/objective-c/GRPCClient/private/NSDictionary+GRPC.h
+++ b/src/objective-c/GRPCClient/private/NSDictionary+GRPC.h
@@ -32,9 +32,9 @@
  */
 
 #import <Foundation/Foundation.h>
-
-struct grpc_metadata;
+#include <grpc/grpc.h>
 
 @interface NSDictionary (GRPC)
 + (instancetype)grpc_dictionaryFromMetadata:(struct grpc_metadata *)entries count:(size_t)count;
+- (void)grpc_getMetadataArray:(grpc_metadata **)metadata;
 @end
diff --git a/src/objective-c/GRPCClient/private/NSDictionary+GRPC.m b/src/objective-c/GRPCClient/private/NSDictionary+GRPC.m
index e59685e..83c09cd 100644
--- a/src/objective-c/GRPCClient/private/NSDictionary+GRPC.m
+++ b/src/objective-c/GRPCClient/private/NSDictionary+GRPC.m
@@ -33,7 +33,8 @@
 
 #import "NSDictionary+GRPC.h"
 
-#include <grpc.h>
+#include <grpc/grpc.h>
+#include <grpc/support/alloc.h>
 
 @implementation NSDictionary (GRPC)
 + (instancetype)grpc_dictionaryFromMetadata:(grpc_metadata *)entries count:(size_t)count {
@@ -53,4 +54,24 @@
   }
   return metadata;
 }
+
+- (void)grpc_getMetadataArray:(grpc_metadata **)metadata {
+  *metadata = gpr_malloc([self count] * sizeof(grpc_metadata));
+  int i = 0;
+  for (id key in self) {
+    id value = self[key];
+    grpc_metadata *current = &(*metadata)[i];
+    current->key = [key UTF8String];
+    if ([value isKindOfClass:[NSData class]]) {
+      current->value = [value bytes];
+    } else if ([value isKindOfClass:[NSString class]]) {
+      current->value = [value UTF8String];
+    } else {
+      [NSException raise:NSInvalidArgumentException
+                  format:@"Metadata values must be NSString or NSData."];
+    }
+    current->value = [value UTF8String];
+    i += 1;
+  }
+}
 @end
diff --git a/src/objective-c/GRPCClient/private/NSError+GRPC.h b/src/objective-c/GRPCClient/private/NSError+GRPC.h
index 6183008..aa032a4 100644
--- a/src/objective-c/GRPCClient/private/NSError+GRPC.h
+++ b/src/objective-c/GRPCClient/private/NSError+GRPC.h
@@ -58,14 +58,12 @@
 
 // TODO(jcanizales): This is conflating trailing metadata with Status details. Fix it once there's
 // a decision on how to codify Status.
-#include <grpc/status.h>
-struct grpc_metadata;
-struct grpc_status {
+#include <grpc/grpc.h>
+typedef struct grpc_status {
     grpc_status_code status;
     const char *details;
-    size_t metadata_count;
-    struct grpc_metadata *metadata_elements;
-};
+    grpc_metadata_array *metadata;
+} grpc_status;
 
 @interface NSError (GRPC)
 // Returns nil if the status is OK. Otherwise, a NSError whose code is one of