karlseguin / websocket.zig

A websocket implementation for zig
MIT License
283 stars 25 forks source link

Passing allocator to the handler #17

Closed hajsf closed 11 months ago

hajsf commented 11 months ago

Hi, in my server I want to return a custom text not just echo back, something like changing the:

const Handler = struct {
    pub fn handle(self: *Handler, message: Message) !void {
        const data = message.data;
        try self.conn.write(data); // echo the message back
    }
};

to

const Handler = struct {
    pub fn handle(self: *Handler, message: Message) !void {

       const final_text = try std.fmt.allocPrint(allocator, "You said: {s}", .{message.data});
       defer alloc.free(final_text);

       try self.conn.write(final_text); // echo the custom message back
    }
};

Here I have to use memory allocator, and was thinking about the best practice, the thoughts came to my mind are:

  1. Defining allocator in the context as a memeber
  2. Defining allocator in the context as a pointer the allocator used in the main function
  3. Using global memory allocator
  4. Passing the allocator to the handler (if it is possible)
  5. Create an allocator inside the handle function and use it

I defined the server.zig as below and it is working fine, but liked to hear your thoughts as well:

const std = @import("std");
const websocket = @import("./websocket.zig/src/websocket.zig");
const Conn = websocket.Conn;
const Message = websocket.Message;
const Handshake = websocket.Handshake;

// Define a struct for "global" data passed into your websocket handler
pub const Context = struct {
   /**************************************/
    allocator: *const std.mem.Allocator,
   /**************************************/
};

pub fn main() !void {
    var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = general_purpose_allocator.allocator();
    // this is the instance of your "global" struct to pass into your handlers
    var context = Context{
   /**************************************/
        .allocator = &allocator,
   /**************************************/
    };

    try websocket.listen(Handler, allocator, &context, .{
        .port = 9223,
        .max_headers = 10,
        .address = "127.0.0.1",
    });
}

const Handler = struct {
    conn: *Conn,
    context: *Context,

    pub fn init(h: Handshake, conn: *Conn, context: *Context) !Handler {
        // `h` contains the initial websocket "handshake" request
        // It can be used to apply application-specific logic to verify / allow
        // the connection (e.g. valid url, query string parameters, or headers)

        _ = h; // we're not using this in our simple case

        return Handler{
            .conn = conn,
            .context = context,
        };
    }

    // optional hook that, if present, will be called after initialization is complete
    pub fn afterInit(self: *Handler) !void {
        _ = self;
    }

    pub fn handle(self: *Handler, message: Message) !void {
       /**************************************/
       const final_text = try std.fmt.allocPrint(self.context.allocator.*, "You said: {s}", .{message.data});
       defer self.context.allocator.free(final_text);
      /**************************************/
       try self.conn.write(final_text); // echo the custom message back
    }

    // called whenever the connection is closed, can do some cleanup in here
    pub fn close(_: *Handler) void {}
};

Thanks

karlseguin commented 11 months ago

This is the type of thing the context is there for.

As for this specific implementation, I would probably make the context's allocator an Allocator instead of an *Allocator. This is true in general, but particularly here where it's already behind a pointer (*Context).

You might also consider giving each handler a buf: []u8 to re-use. That way you aren't allocating and freeing memory all the time. While this can add a lot of performance, you need to figure out how to deal with messages that don't fit inbuf.

const Handler = struct {
  buf: []u8
  ctx: *Context

  fn init(...) Handler {
   return .{.buf = try ctx.allocator.alloc(u8, 1024)};
  }
  pub fn close(h: *Handler) void {
     h.ctx.allocator.free(h.buf);
  }
 ...
}

Then you can use bufPrint

hajsf commented 11 months ago

Thanks a lot.