Setting Up a gRPC Server and Client in Zig Using C++ Libraries

449 views

Zig is a relatively new programming language known for its safety, performance, and simplicity. Although gRPC does not natively support Zig out of the box as it does with more established languages like Python, C++, and Go, you can still use gRPC in a Zig project by leveraging Zig's ability to interoperate with C libraries.

Here's an example of how you can set up a simple gRPC server and client in Zig. You'll need to use the gRPC C++ library and write Zig code that interfaces with the C library.

Prerequisites

  1. Install the Zig programming language from ziglang.org.
  2. Install the gRPC and Protocol Buffers libraries. You can follow the official gRPC C++ installation guide to set this up.

Step-by-Step Guide

  1. Define Service using Protobuf

    Create a file named example.proto:

    syntax = "proto3";
    
    package example;
    
    service Greeter {
      rpc SayHello (HelloRequest) returns (HelloReply);
    }
    
    message HelloRequest {
      string name = 1;
    }
    
    message HelloReply {
      string message = 1;
    }
    
  2. Generate C++ Code

    Use the protoc compiler to generate the gRPC and protobuf C++ files:

    protoc --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` example.proto
    protoc --cpp_out=. example.proto
    

    This will generate example.grpc.pb.cc, example.grpc.pb.h, example.pb.cc, and example.pb.h.

  3. Create Server in C++

    Create a file named greeter_server.cpp for the server:

    #include <iostream>
    #include <memory>
    #include <string>
    
    #include <grpcpp/grpcpp.h>
    #include "example.grpc.pb.h"
    
    using grpc::Server;
    using grpc::ServerBuilder;
    using grpc::ServerContext;
    using grpc::Status;
    using example::Greeter;
    using example::HelloRequest;
    using example::HelloReply;
    
    class GreeterServiceImpl final : public Greeter::Service {
        Status SayHello(ServerContext* context, const HelloRequest* request, HelloReply* reply) override {
            std::string prefix("Hello ");
            reply->set_message(prefix + request->name());
            return Status::OK;
        }
    };
    
    void RunServer() {
        std::string server_address("0.0.0.0:50051");
        GreeterServiceImpl service;
    
        ServerBuilder builder;
        builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
        builder.RegisterService(&service);
        std::unique_ptr<Server> server(builder.BuildAndStart());
        std::cout << "Server listening on " << server_address << std::endl;
    
        server->Wait();
    }
    
    int main(int argc, char** argv) {
        RunServer();
        return 0;
    }
    

    Compile the server:

    g++ -std=c++11 -I /usr/local/include -pthread -lgpr -lz \
        -L /usr/local/lib -lgrpc++ -lprotobuf -o greeter_server \
        greeter_server.cpp example.pb.cc example.grpc.pb.cc
    
  4. Write Client in Zig

    Create a file named greeter_client.zig for the client:

    const std = @import("std");
    const grpc_h = @cImport({
        @cInclude("grpc/grpc.h");
    });
    
    const example_h = @cImport({
        @cInclude("example.grpc.pb.h");
    });
    
    pub fn main() void {
        const allocator = std.heap.page_allocator;
    
        grpc_h.grpc_init();
        const channel = grpc_h.grpc_insecure_channel_create("localhost:50051", null, null);
        const stub = example_h.Greeter_stub_new(channel);
    
        const request = try allocator.alloc(example_h.HelloRequest, 1);
        defer allocator.free(request);
        example_h.HelloRequest_set_name(request, std.cstr.ptrCast([*c]const u8, "world"));
    
        const response = try allocator.alloc(example_h.HelloReply, 1);
        defer allocator.free(response);
    
        const status = example_h.Greeter_SayHello(stub, null, request, response);
        if (status != grpc_h.GRPC_STATUS_OK) {
            std.log.err("RPC failed", .{});
            return;
        }
    
        const reply_msg = example_h.HelloReply_message(response);
        std.log.info("Greeter client received: {s}", .{reply_msg});
    
        grpc_h.grpc_channel_destroy(channel);
        grpc_h.grpc_shutdown();
    }
    
  5. Compile the Client

    Ensure you have the necessary headers and libraries in the search path:

    zig build-exe -lc -lgrpc -lgrpc++ -lprotobuf -lpthread \
        -isystem /usr/local/include -L/usr/local/lib greeter_client.zig
    
  6. Run the Server and Client

    First, run the server:

    ./greeter_server
    

    Then, in another terminal, run the client:

    ./greeter_client
    

Conclusion

This example demonstrates integrating a gRPC server written in C++ with a client written in Zig. While Zig does not natively support gRPC, its ability to interface with C libraries opens up a pathway to use gRPC by making use of existing C++ implementations and tools. This also showcases Zig's interoperability and efficiency in working within a multi-language ecosystem.

Other Xegs