C
C#3mo ago
ero

Named Pipe IPC: Protocols & Status Codes

I'm basically starting from scratch with my implementation, since I've gone way too deep with my initial approach and need something that I can actually finally work with. I'm writing an IPC implementation via named pipes. I'm open to other ideas (I don't know if anonymous pipes are appropriate here), but named pipes are definitely what I've settled on. I cannot use additional NuGet packages and I'm on .NET Standard 2.0 (C# 13, PolySharp). My requirements are the following: I will have multiple server implementations. Each server fulfills a different purpose and has multiple methods (I will call them endpoints) pertaining to that purpose. A server endpoint can be the equivalent to one of these: Action, Action<T>, Func<TResult>, Func<T, TResult>. To clarify; an endpoint can optionally receive some request data, and can optionally return some response data. Each server will be paired up with a client implementation that should simplify calling server endpoints for the user. These clients should return some Result type to signal success or failure. If the server responds with a failure, the kind should be communicated to the client. This includes unhandled server exceptions. My idea was to send data via JSON. The client serializes the request; sends it to the server; the server deserializes the data and calls the corresponding endpoint implementation. I'm not sure how to handle figuring out this correspondence. I'm looking for some ideas on how to implement this in a manner that is reasonably robust and can be extended to more server-client-pairs without many problems.
267 Replies
ero
eroOP3mo ago
cc @Tanner Gooding @cap5lut if you'd like to take a peek i'm gonna provide some more implementation ideas in a bit and you can tell me what i should do instead :p To be a bit less vague, this is about game interop. Unity, Unreal, GameMaker, etc. I need a different server-client-pair for each of those. To determine the correspondence between the request data the server receives and the actual handler for that endpoint, I was thinking of simply using an enum or multipe enums. The thing that's in the way here is that all servers share some base requests, like Close. This makes using multiple enums a bit cumbersome. Should I use strings instead? In terms of responses; I was initially using a different enum type for every endpoint in the server. This makes the entire implementation a big mess and was my main source of issues before. I could possibly use integer status codes, but I'm afraid that will fall flat when it comes to unhandled server exceptions. Should I just send plain strings containing the reason for failure?
canton7
canton73mo ago
My idea was to send data via JSON
If you can't use nuget packages, and you don't have S.T.J, then the only json serializer you have is pretty rubbish 😛
ero
eroOP3mo ago
oh, i can use that one actually. i use source generation with it. some others that i can use (provided by the app that i'm writing this plugin for) are System.Memory and System.Buffers, for example
canton7
canton73mo ago
Is there a fully SG'd json library which doesn't need any runtime libraries at all? Which one?
ero
eroOP3mo ago
no no i just use stj
canton7
canton73mo ago
Ah no, I see what you mean
ero
eroOP3mo ago
i can technically use any package i il weave everything but i really want to keep the file size low
canton7
canton73mo ago
It looks like you've got a few different problems here: 1. Defining the different messages types 2. Mapping messages types into RPC methods to call 3. Serializing exceptions Correct?
ero
eroOP3mo ago
i think that's a good summary
canton7
canton73mo ago
How are you defining your schema/interface? I.e. what messages can be sent between a given server and client? (also note that json-rpc is a thing. Might be worth looking at what they do) The normal way to approach something like this is that you define your schema centrally (i.e. what the message types are, what the parameters are, etc), then code-generate the server and client from that schema The code generation handles converting method call -> message on the sender, and message -> method call on the receiver
ero
eroOP3mo ago
i'm not sure how i should define them. simply the question of whether to use interfaces or not, whether to use classes or structs... some of the data i would send would be like
record GetMonoImageRequest(
string Name);

record GetMonoImageResponse(
ulong Address,
string Name,
string ModuleName,
string FilePath);
record GetMonoImageRequest(
string Name);

record GetMonoImageResponse(
ulong Address,
string Name,
string ModuleName,
string FilePath);
i really don't wanna write a source generator for this, and rpc libraries tend to be pretty large
canton7
canton73mo ago
Yeah, they're large for a reason! Chucking messages back and forth is a lot easier
ero
eroOP3mo ago
it's unfortunately not an option for me. i really want the download of the plugin to be quick i mean it's an option, but not the first i'd like to choose
canton7
canton73mo ago
Just to play devil's advocate -- can't you include any large libraries in your host application?
ero
eroOP3mo ago
the host application isn't mine
canton7
canton73mo ago
Ah, your IPC isn't with the host application?
ero
eroOP3mo ago
the ipc is between my library (loaded by the host app) and some game i'm essentially writing both sides, though
canton7
canton73mo ago
Ah gotcha Can you just pass messages around, rather than going full-on RPC?
ero
eroOP3mo ago
that's exactly what i want to do the server side of course needs handlers
canton7
canton73mo ago
I thought you said you were turning method calls -> messages on the sender, and turning a message -> a method call on the receiver, with automatically serializing exceptions from that method call, etc?
ero
eroOP3mo ago
well, i thought i'd have some abstract base client, which contains the raw logic for sending over the request. there would then be several concrete clients which contain methods that hide this raw logic:
class MonoClient : IpcClient
{
public Result<GetMonoImageResponse> GetMonoImage(string name)
{
GetMonoImageRequest request = new(name);
return base.CallServerEndpoint(request); // or some such
}
}
class MonoClient : IpcClient
{
public Result<GetMonoImageResponse> GetMonoImage(string name)
{
GetMonoImageRequest request = new(name);
return base.CallServerEndpoint(request); // or some such
}
}
where IpcClient.CallServerEndpoint would serialize the request and just send it over the pipe
canton7
canton73mo ago
Yeah, it's the other side which gets a bit painful -- working out what the message type is, mapping that to a method to call, extracting the parameters, marshalling exceptions back, making sure that the response ends up in the same place that the request came from. Do-able, but boilerplatey if you're writing it by hand. At that point, you might as well just deal with the messages directly Anyway, none of this is answering your original questions, I think Ah no, it does cover:
I'm not sure how to handle figuring out this correspondence.
The short answer is: 1. Lots of hand-written boilerplate 2. Some method to register a method to a message type, but which reduces some of the boilerplate but probably introduces some runtime reflection / codegen 3. Compile-time code generation from a schema 4. Don't, and deal with the messages directly (4 is what I do, FWIW)
cap5lut
cap5lut3mo ago
u basically have this: 1) transport (fixed via named pipes i guess) 2) serialization (a bit of a struggle but u do it somehow) 3) application layer protocol 4) business logic 3 seems to be what u are stuck at, right?
ero
eroOP3mo ago
i've basically nuked the entire project :p so i'm at 0
cap5lut
cap5lut3mo ago
well, then u have these 4 layers to work on 😂
ero
eroOP3mo ago
which is what this thread is about :)
cap5lut
cap5lut3mo ago
well, with 1 and 2 there arent problems are there?
ero
eroOP3mo ago
just in the sense that i was looking for possible alternatives if they make the process easier, more performant, more consistent, more in line with modern technologies
cap5lut
cap5lut3mo ago
im not sure what ya mean by that. 1) would mean that u transfer a bunch of bytes, 2) would mean that u interpret that bunch of bytes. "consistency and modern technologies" would be mostly in the application and business layer the (de)serialization of 2) as well tho, but thats usually not the problematic thing
many things
many things3mo ago
i still don't understand what your fear is about implementing this, or what is it that you don't know that require so much to think about
ero
eroOP3mo ago
You don't know how deep in the mud I was with my attempted implementation It wasbad bad. I'll share some of it tomorrow if you care Well things like, do I use messagepack instead of json, do I use some custom format I implement myself, do I use anonymous pipes instead of named ones, do I use tcp, mem mapped files, mutexes, etc
cap5lut
cap5lut3mo ago
i would abstract the transport layer and data (de)serialization away, so that u can first use simple stuff, like tcp and json and then make it configurable for faster alternatives like messagepack and named pipes/shared memory and the actual application layer protocol for rpc u can build on top of that
many things
many things3mo ago
i understand but for example how can you still be undecided about ipc vs tcp? given that as long your wrappers implements Stream they should be interchangeable, they are for two different use cases anyway, local vs remote communcation also the approach to serialization and designing (for the higher level protocol) would change if we are talking about modeling 2 structures or 10000; maybe it could change even between 1000 and 10000; it's kind of an important information to have
ero
eroOP3mo ago
well "ipc vs tpc" doesn't even make sense in the first place, no? you can do ipc via tcp. or via named pipes. or via websockets. or via memory mapped files. or... and i mean, that's what this thread is for, to have people ask clarifying questions like this :p there won't be too many structures to model. i figure 5-20 per server implementation (of which there will be ~3-5)
cap5lut
cap5lut3mo ago
the number of different high level data types doesnt matter, their serialization boils down to the primitives you want to support. at the transport layer you are just sending bytes over, maybe with some sequence and fragment number to support something like UDP where packets could arrive out of order, or simply dont arrive. the "hard" part here is only to discern which bytes belong to which message once thats done and dusted it goes to how to do the serialization, no matter if thats reflection based or source generated or manually writing it, which itself isnt that problematic either, and can also be abstracted away. ones all that is fixed u just need the application layer protocol, where the hard part is designing it. basically here the different types come into play, for requests, for responses. u want some generic status codes + optional data to process messages, pretty much like HTTP status codes.
many things
many things3mo ago
the number of different high level data types doesnt matter
i think it could matter, depending on the amount of effort required and tooling available i think it makes sense as a question because if you write a non generic ipc in your service then to transform it to work between different machines instead of locally you have to refactor it or reengineer it, so it would be better if you knew that beforehand didn't you say some time ago that you had a lot of these messages to send/receive? or was it another issue
ero
eroOP2mo ago
i'm not sure i remember that i still don't have this figured out i keep trying to rewrite it, but i just end up in the same situations i don't know how to do this
canton7
canton72mo ago
What are you having trouble with?
ero
eroOP2mo ago
genuinely i don't even know how to start anymore at this point. i've been on and off trying to find a good implementation for this for months months! my brain is completely fried when it comes to this project i do not know what to do i don't even know what i'm envisioning anymore i don't even know where to begin explaining it like i want something like this or something
interface IMonoServer
{
IpcResponse<MonoImageInfo> GetMonoImage(string name);
}

abstract class IpcServer<TServer>
{
// some way to receive requests, send responses
}

class MonoServer : IpcServer<IMonoServer>, IMonoServer
{
public IpcResponse<MonoImageInfo> GetMonoImage(string name)
{
// call mono api
// return error status code or actual response object
}
}

abstract class IpcClient<TServer>
{
// some way to send requests, receive responses
}

class MonoClient : IpcClient<IMonoServer>
{
public IpcResponse<MonoImageInfo> GetMonoImage(string name)
{
return base.Server.GetMonoImage(string name); // or idek
}
}
interface IMonoServer
{
IpcResponse<MonoImageInfo> GetMonoImage(string name);
}

abstract class IpcServer<TServer>
{
// some way to receive requests, send responses
}

class MonoServer : IpcServer<IMonoServer>, IMonoServer
{
public IpcResponse<MonoImageInfo> GetMonoImage(string name)
{
// call mono api
// return error status code or actual response object
}
}

abstract class IpcClient<TServer>
{
// some way to send requests, receive responses
}

class MonoClient : IpcClient<IMonoServer>
{
public IpcResponse<MonoImageInfo> GetMonoImage(string name)
{
return base.Server.GetMonoImage(string name); // or idek
}
}
i don't even know anymore i don't know what to do even if i were to drop this coupling, i have no idea how to send the data i need naot json serialization so what do i just send
struct Request
{
int Code;
object? Data;
}
struct Request
{
int Code;
object? Data;
}
? and then the other side has to read
struct Request
{
int Code;
JsonElement Data;
}
struct Request
{
int Code;
JsonElement Data;
}
? i can't use the latter on both sides, because jsonserializer would try to serialize the JsonElement struct and its internals i'm completely lost
many things
many things2mo ago
i imagined this thread would wake up again why naot? but you don't need to use JsonElement on both side, you use the correct models/dto for the job JsonElement would be a step for the deserializer to get to the correct model
ero
eroOP2mo ago
I need to pass information about what "endpoint" I'm calling (just a string, or an enum) as well as optionally the request model. That means I need a type that cannot be generic (what generic arg would I pass if I'm not passing a model?), but still contains both the endpoint identifier and some data property (null if not specified). The other side then needs to read that data property as a JsonElement and check whether it's null or something Because the server side is a library loaded into another process for communication. Of course the library needs to be native to be loaded
canton7
canton72mo ago
Doesn't STJ have support for $type? Yes it does, with JsonDerivedType I assume that works with naot As I said before, you'll have a much easier time if you think in terms of sending messages around rather than method calls: doing RPC adds quite a lot of overhead
many things
many things2mo ago
i don't know, it looks to me that you're worrying/focusing too much about the technicalities and the 'rules' (like json is text, you can send almost whatever you want) and not on what these actions/messages are used for; by that i mean structure follows function, if you need nested types you nest types, if you need 200 flat fields you make a flat class with 200 nullable primitive fields; if generics don't work, don't use it, or maybe you can have two discriminators instead of just one (or honestly i think there are better ways, usually); the more practical you can be the more we can help i guess
ero
eroOP2mo ago
@this_is_pain @canton7 i don't think either of you understand what the problem is? i need to send request types like this:
struct Request
{
string Action;
}

struct Request<T>
{
string Action;
T Data;
}
struct Request
{
string Action;
}

struct Request<T>
{
string Action;
T Data;
}
the other side needs to read this structure, but it of course needs to do so in one go. reading
struct Request
{
string Action;
}
struct Request
{
string Action;
}
would drop the data that may or may not be there. reading
struct Request
{
string Action;
JsonElement Data;
}
struct Request
{
string Action;
JsonElement Data;
}
would over-read if the first struct above was sent, leading to an exception.
canton7
canton72mo ago
I don't think you read my response...
ero
eroOP2mo ago
please go ahead and explain how $type would help here
canton7
canton72mo ago
That's literally the problem that $type is there to solve
ero
eroOP2mo ago
please, have at it don't think i need to mention the obvious, but requests do not derive from one another there is absolutely no relation between
record GetFooRequest(
string Bar,
long Baz);
record GetFooRequest(
string Bar,
long Baz);
and
record GetQuxRequest(
int[] Quo);
record GetQuxRequest(
int[] Quo);
canton7
canton72mo ago
Let's say you have two message types, SayHelloRequest and SayGoodbyeRequest. You could define your RequestMessage message as:
[JsonDerivedType(typeof(SayHelloRequest), "sayHello")]
[JsonDerivedType(typeof(SaGoodbyeRequest), "sayGoodbye")]
public class RequestPayloadBase { }

public class SayHelloRequestPayload : RequestPayloadBase { ... }
public class SayGoodbyeRequestPayload : RequestPayloadBase { ... }

public class RequestMessage
{
// Any common fields here
RequestPayloadBase Payload { get; set; }
}
[JsonDerivedType(typeof(SayHelloRequest), "sayHello")]
[JsonDerivedType(typeof(SaGoodbyeRequest), "sayGoodbye")]
public class RequestPayloadBase { }

public class SayHelloRequestPayload : RequestPayloadBase { ... }
public class SayGoodbyeRequestPayload : RequestPayloadBase { ... }

public class RequestMessage
{
// Any common fields here
RequestPayloadBase Payload { get; set; }
}
Then:
var requestMessage = new RequestMessage()
{
Payload = new SayHelloRequestPayload(...),
}
var requestMessage = new RequestMessage()
{
Payload = new SayHelloRequestPayload(...),
}
Gets serialized as:
{
"payload": {
"$type": "sayHello",
// Other fields from SayHelloRequestPayload
}
}
{
"payload": {
"$type": "sayHello",
// Other fields from SayHelloRequestPayload
}
}
don't think i need to mention the obvious, but requests do not derive from one another
Maybe that's why you're causing yourself so much trouble 🤷‍♂️ You're necessarily doing polymorphic messages. STJ requires that your different message types inherit from a common base. That's how it works. If you're insisting on doing something which STJ supports, but without following its requirements, yes you're going to have a hard time. I'm not sure what to say in response to that.
ero
eroOP2mo ago
and explain how you would receive this on the other side
canton7
canton72mo ago
Just deserialize as RequestMessage, then type switch over Payload
ero
eroOP2mo ago
and explain how you would handle request messages that don't have a payload
canton7
canton72mo ago
Then they have an empty payload. Every message needs a type.
ero
eroOP2mo ago
wdym type switch? how do i access $type?
canton7
canton72mo ago
STJ creates the correct type in the Payload member. If you serialized a SayHelloRequestPayload, that gets deserialized as a SayHelloRequestPayload So you can do switch (message.Payload) { case SayHelloRequestPayload sayHello: ... }, or use the visitor pattern, or a Dictionary<Type, ...>, or whatever
ero
eroOP2mo ago
sorry if i'm a bit agitated, i've just been trying things for way too long and i'm mostly upset with myself for not trying this before so request messages without data still need some way to be distinguished think of it like Func<TRet> or Action no input, but (optionally) some output
canton7
canton72mo ago
☝️
ero
eroOP2mo ago
well yeah, but i still need some identifier
canton7
canton72mo ago
The identifier is the type
ero
eroOP2mo ago
if there's no payload, there's no type
canton7
canton72mo ago
Which is why you need a payload, even an empty one I feel like we're going in circles here
ero
eroOP2mo ago
an empty payload sounds like a dumb idea no offense
canton7
canton72mo ago
As I said before, don't think in terms of calling methods or RPC or similar: think in terms of messages I mean, I'm literally telling you how this is done. If you want to think you know better and invent your own system, go ahead, but please don't ask us for help (and bearing in mind how much trouble you're having inventing your own system, taking a lead from how a lot of other people solve this might not be the worst idea) (You can use a singleton instance of the payload if you want, although STJ will still try to deserialize it as a new instance)
ero
eroOP2mo ago
i guess... it just seems so different from what i would usually do alright well, i think i might be able to do something with that now for the handling and responses on the server side how would you "register" the handlers?
canton7
canton72mo ago
The simplest is to literally have a big switch statement which switches over all possible request message types, and calls an appropriate function in each case. If you want to make it a bit more decoupled, you could have a Dictionary<Type, Action<RequestPayloadBase>> where you register delegates which handle each message type
ero
eroOP2mo ago
i think i would like to have some base server (just handles receiving basic requests and sending basic responses), a... more concrete abstract class(?) that registers handlers and defines simpler abstract methods for each "endpoint", and then the actual server implementation:
class Server
{
// send, receive
}

abstract class FooServer : Server
{
// register handlers

abstract Result<BarResponse> HandleBar(BarRequest request);
}

sealed class FooServerImpl : FooServer
{
override Result<BarResponse> HandleBar(BarRequest request) => throw null;
}
class Server
{
// send, receive
}

abstract class FooServer : Server
{
// register handlers

abstract Result<BarResponse> HandleBar(BarRequest request);
}

sealed class FooServerImpl : FooServer
{
override Result<BarResponse> HandleBar(BarRequest request) => throw null;
}
god damn it yeah i've been doing that
canton7
canton72mo ago
Or yeah, you can start using reflection to discover handler methods, etc, but that becomes a bit more magic. I like explicitness.
ero
eroOP2mo ago
god damn it yes i've realized man i love the magic but i want tight coupling like a proxy interface would be kinda sick but like how do you even make that good
canton7
canton72mo ago
Tbh, I'd probably use the visitor pattern. It's a bit of boilerplate, but a lot of stuff just falls out
ero
eroOP2mo ago
do you have a link or an example i haven't heard of it
canton7
canton72mo ago
public interface IRequestMessageVisitor
{
void Accept(SayHelloRequestPayload payload);
void Accept(SayGoodbyeRequestPayload payload);
}

public class RequestPayloadBase
{
public abstract void Visit(IRequestMessageVisitor visitor);
}

public class SayHelloRequestPayload : RequestPayloadBase
{
public override void Visit(IRequestMessageVisitor visitor) => visitor.Accept(this);
}

public class SayGoodbyeRequestPayload : RequestPayloadBase
{
public override void Visit(IRequestMessageVisitor visitor) => visitor.Accept(this);
}
public interface IRequestMessageVisitor
{
void Accept(SayHelloRequestPayload payload);
void Accept(SayGoodbyeRequestPayload payload);
}

public class RequestPayloadBase
{
public abstract void Visit(IRequestMessageVisitor visitor);
}

public class SayHelloRequestPayload : RequestPayloadBase
{
public override void Visit(IRequestMessageVisitor visitor) => visitor.Accept(this);
}

public class SayGoodbyeRequestPayload : RequestPayloadBase
{
public override void Visit(IRequestMessageVisitor visitor) => visitor.Accept(this);
}
Then in the receiver:
public class FooServer : IRequestMessageVisitor
{
public void ReceiveMessage(RequestMessage message) => message.Payload.Visit(this);

void Accept(SayHelloRequestPayload payload) { ... }
void Accept(SayGoodbyeRequestPayload payload) { ... }
}
public class FooServer : IRequestMessageVisitor
{
public void ReceiveMessage(RequestMessage message) => message.Payload.Visit(this);

void Accept(SayHelloRequestPayload payload) { ... }
void Accept(SayGoodbyeRequestPayload payload) { ... }
}
Implementing the Visit method on all of the payload types is a little painful, but tbh VS generates most of it for you, and you get compiler errors unless/until you define everything, which is really nice -- there's no way to accidentally forget to support a particular message type
ero
eroOP2mo ago
i am truly shocked at this design and that i'm not smart enough to have come up with that tunnel vision i mean that's crazy
canton7
canton72mo ago
Visitor's one of those patterns that you probably wouldn't come up with yourself, but it's really useful when you have a known set of types, and you want to find out what you've got, and process it, in a strongly-typed way It's less relevant now that C# has switch over types, but it's still useful because it guarantees that you've handled all cases (you can make a visitor base class with virtual methods if you want implementors to be able to pick and choose what they handle: ExpressionVisitor does that for instance)
ero
eroOP2mo ago
yeah fuck that's crazy smart so simple very effective, thanks
canton7
canton72mo ago
So, empty payload types do make sense 🙂
ero
eroOP2mo ago
alright well, i need some response type next i want to communicate different types of failures like something is not found, something was null
canton7
canton72mo ago
Yeah. Start with messages again, then see how you can work them into your visitor stuff Tbh I like just having a separate call to send a response message, rather than forcing it through a method's return type, but whatever, both work
ero
eroOP2mo ago
i would need to see both i think
canton7
canton72mo ago
public class FooServer : IRequestMessageVisitor
{
public void ReceiveMessage(RequestMessage message)
{
ResponseMessage response = new(...);
try
{
response.Payload = msesage.Payload.Visit(this);
}
catch (Exception ex)
{
response.Error = ...;
}
// Send / return response
}

SayHelloResponsePayload Accept(SayHelloRequestPayload payload) { ... }
SayGoodbyeResponsePayload Accept(SayGoodbyeRequestPayload payload) { ... }
}
public class FooServer : IRequestMessageVisitor
{
public void ReceiveMessage(RequestMessage message)
{
ResponseMessage response = new(...);
try
{
response.Payload = msesage.Payload.Visit(this);
}
catch (Exception ex)
{
response.Error = ...;
}
// Send / return response
}

SayHelloResponsePayload Accept(SayHelloRequestPayload payload) { ... }
SayGoodbyeResponsePayload Accept(SayGoodbyeRequestPayload payload) { ... }
}
Maybe? The rest is left as an exercise to the reader
ero
eroOP2mo ago
no god mutable type
canton7
canton72mo ago
The other option is:
public class FooServer : IRequestMessageVisitor
{
public void ReceiveMessage(RequestMessage message) => message.Payload.Visit(this);

void Accept(SayHelloRequestPayload payload)
{
// Do stuff
client.SendResponse(new SayHelloResponsePayload(...));
}
void Accept(SayGoodbyeRequestPayload payload) { ... }
}
public class FooServer : IRequestMessageVisitor
{
public void ReceiveMessage(RequestMessage message) => message.Payload.Visit(this);

void Accept(SayHelloRequestPayload payload)
{
// Do stuff
client.SendResponse(new SayHelloResponsePayload(...));
}
void Accept(SayGoodbyeRequestPayload payload) { ... }
}
Serialization models can be mutable: that's fine. But make it immutable if you want, there's no difference You could have the error be a separate field on ResponseMessage, or you could make an error response be one of the possible ResponseMessagePayload subclasses. Both work. You probably want to have some sort of correlation token to tie together a request and response. The sender puts some integer value as the correlation token in the request, and the receiver mirrors that same value back in the response. That lets the sender tie together the response with the request that they sent (Which is important with errors, as otherwise you don't necessarily know what request message an error is in response to) Alternatively you could put the error on ResponsePayloadBase No right or wrong answers there (Then on the sender side you have a Dictionary<int, TaskCompletionSource> which maps together a correlation token with a TCS. That TCS is what the sender is awaiting, and you complete it when you get a response back with the matching correlation token) Does that all make sense?
ero
eroOP2mo ago
I'm taking a little break, I'll catch up on it later, thanks :) One thing though, I was looking to design it in such a way that the payload base, the serialization code and the pipe stream writing code is contained in one project, and the actual implementations (visitor, client, server, payloads) are in separate projects (one project per "kind of server"). How would you design this generically? I haven't yet figured out a good way
canton7
canton72mo ago
That's fine? You can split this up into: * RequestMessage / ResponseMessage (these can be generic over the base payload type, or you can use inheritance) * The networking code to transmit RequestMessage / ResponseMessage, retries, etc * A particular payload and all of its subclasses * The types which implement IRequestMessageVisitor etc
ero
eroOP2mo ago
i can't figure it out Proj1
public interface IRequest<in TVisitor>
{
void Visit(TVisitor visitor);
}

public sealed class RequestMessage // generic how?
{
// ?
}

// some base server idk
public class Server<TVisitor> // or no? or maybe we need TRequestVisitor and TResponseVisitor?
// maybe constrain to some empty IRequestVisitor interface?
{
// what methods? abstract? virtual?
}
public interface IRequest<in TVisitor>
{
void Visit(TVisitor visitor);
}

public sealed class RequestMessage // generic how?
{
// ?
}

// some base server idk
public class Server<TVisitor> // or no? or maybe we need TRequestVisitor and TResponseVisitor?
// maybe constrain to some empty IRequestVisitor interface?
{
// what methods? abstract? virtual?
}
Proj2
public interface IFooRequestVisitor
{
void Accept(GetFooRequest request);
}

public sealed class GetFooRequest : IRequest<IFooRequestVisitor>
{
public void Visit(IFooRequestVisitor visitor)
{
visitor.Accept(this);
}
}

public sealed class FooServer : Server<IFooRequestVisitor>, IFooRequestVisitor // ???
{
public void Accept(GetFooRequest request)
{

}

// response? idk
}
public interface IFooRequestVisitor
{
void Accept(GetFooRequest request);
}

public sealed class GetFooRequest : IRequest<IFooRequestVisitor>
{
public void Visit(IFooRequestVisitor visitor)
{
visitor.Accept(this);
}
}

public sealed class FooServer : Server<IFooRequestVisitor>, IFooRequestVisitor // ???
{
public void Accept(GetFooRequest request)
{

}

// response? idk
}
public sealed class RequestMessage<TRequest, TVisitor>(TRequest request)
where TRequest : IRequest<TVisitor>
{
public TRequest Payload { get; } = request;
}
public sealed class RequestMessage<TRequest, TVisitor>(TRequest request)
where TRequest : IRequest<TVisitor>
{
public TRequest Payload { get; } = request;
}
public class Server<TVisitor>(TVisitor visitor)
{
public void HandleRequest<TRequest>(RequestMessage<TRequest, TVisitor> request)
where TRequest : IRequest<TVisitor>
{
request.Payload.Visit(visitor);
}
}
public class Server<TVisitor>(TVisitor visitor)
{
public void HandleRequest<TRequest>(RequestMessage<TRequest, TVisitor> request)
where TRequest : IRequest<TVisitor>
{
request.Payload.Visit(visitor);
}
}
(mind the primary ctors) but passing TVisitor to the base ctor of Server<TVisitor> seems wrong when the implementing server is itself the visitor especially since
public FooServer()
: base(this) { }
public FooServer()
: base(this) { }
is not even valid... (for obvious reasons)
canton7
canton72mo ago
One way:
public class RequestMessage<TRequestPayload>
{
// ...
public TRequestPayload Payload { get; set; }
}

public abstract class ServerBase<TRequestPayload, TResponsePayload>
{
public abstract void HandleMessage(RequestMessage<TRequestPayload> message);
}
public class RequestMessage<TRequestPayload>
{
// ...
public TRequestPayload Payload { get; set; }
}

public abstract class ServerBase<TRequestPayload, TResponsePayload>
{
public abstract void HandleMessage(RequestMessage<TRequestPayload> message);
}
Then in proj2:
public interface IRequestPayloadVisitor
{
void Accept(SayHelloRequestPayload payload);
void Accept(SayGoodbyeRequestPayload payload);
}

[JsonDerivedType(typeof(SayHelloRequestPayload), "sayHello")]
[JsonDerivedType(typeof(SayHelloRequestPayload), "sayGoodbye")]
public class RequestPayloadBase { }

public class SayHelloRequestPayload : RequestPayloadBase { ... }

public class FooServer : ServerBase<RequestPayloadBase, ...>, IRequestPayloadVisitor
{
public override void HandleMessage(RequestMessage<RequestPayloadBase> message) => message.Payload.Visit(this);

// IRequestPayloadVisitor impl
}
public interface IRequestPayloadVisitor
{
void Accept(SayHelloRequestPayload payload);
void Accept(SayGoodbyeRequestPayload payload);
}

[JsonDerivedType(typeof(SayHelloRequestPayload), "sayHello")]
[JsonDerivedType(typeof(SayHelloRequestPayload), "sayGoodbye")]
public class RequestPayloadBase { }

public class SayHelloRequestPayload : RequestPayloadBase { ... }

public class FooServer : ServerBase<RequestPayloadBase, ...>, IRequestPayloadVisitor
{
public override void HandleMessage(RequestMessage<RequestPayloadBase> message) => message.Payload.Visit(this);

// IRequestPayloadVisitor impl
}
Or you could do:
public abstract class RequestMessageBase
{
// ...
}

public abstract class ServerBase<TRequestMessage, TResponseMessage> where TRequestMessage : RequestMessageBase
{
public abstract void HandleMessage(TRequestMessage message);
}
public abstract class RequestMessageBase
{
// ...
}

public abstract class ServerBase<TRequestMessage, TResponseMessage> where TRequestMessage : RequestMessageBase
{
public abstract void HandleMessage(TRequestMessage message);
}
Then in proj2:
public interface IRequestPayloadVisitor
{
void Accept(SayHelloRequestPayload payload);
void Accept(SayGoodbyeRequestPayload payload);
}

[JsonDerivedType(typeof(SayHelloRequestPayload), "sayHello")]
[JsonDerivedType(typeof(SayHelloRequestPayload), "sayGoodbye")]
public class RequestPayloadBase { }

public class SayHelloRequestPayload : RequestPayloadBase { ... }

public class FooRequestMessage : RequestMessageBase
{
public RequestPayloadBase Payload { get; set; }
}

public class FooServer : ServerBase<FooRequestMessage, ...>, IRequestPayloadVisitor
{
public override void HandleMessage(FooRequestMessage message) => message.Payload.Visit(this);

// IRequestPayloadVisitor impl
}
public interface IRequestPayloadVisitor
{
void Accept(SayHelloRequestPayload payload);
void Accept(SayGoodbyeRequestPayload payload);
}

[JsonDerivedType(typeof(SayHelloRequestPayload), "sayHello")]
[JsonDerivedType(typeof(SayHelloRequestPayload), "sayGoodbye")]
public class RequestPayloadBase { }

public class SayHelloRequestPayload : RequestPayloadBase { ... }

public class FooRequestMessage : RequestMessageBase
{
public RequestPayloadBase Payload { get; set; }
}

public class FooServer : ServerBase<FooRequestMessage, ...>, IRequestPayloadVisitor
{
public override void HandleMessage(FooRequestMessage message) => message.Payload.Visit(this);

// IRequestPayloadVisitor impl
}
Or you could do something in the middle:
public abstract class RequestMessageBase<TRequestPayload>
{
// ...
public TRequestPayload Payload { get; set; }
}

public abstract class ServerBase<TRequestMessage, TResponseMessage> where TRequestMessage : RequestMessageBase
{
public abstract void HandleMessage(TRequestMessage message);
}
public abstract class RequestMessageBase<TRequestPayload>
{
// ...
public TRequestPayload Payload { get; set; }
}

public abstract class ServerBase<TRequestMessage, TResponseMessage> where TRequestMessage : RequestMessageBase
{
public abstract void HandleMessage(TRequestMessage message);
}
Then in proj2:
public interface IRequestPayloadVisitor
{
void Accept(SayHelloRequestPayload payload);
void Accept(SayGoodbyeRequestPayload payload);
}

[JsonDerivedType(typeof(SayHelloRequestPayload), "sayHello")]
[JsonDerivedType(typeof(SayHelloRequestPayload), "sayGoodbye")]
public class RequestPayloadBase { }

public class SayHelloRequestPayload : RequestPayloadBase { ... }

public class FooRequestMessage : RequestMessageBase<RequestPayloadBase> { }

public class FooServer : ServerBase<FooRequestMessage, ...>, IRequestPayloadVisitor
{
public override void HandleMessage(FooRequestMessage message) => message.Payload.Visit(this);

// IRequestPayloadVisitor impl
}
public interface IRequestPayloadVisitor
{
void Accept(SayHelloRequestPayload payload);
void Accept(SayGoodbyeRequestPayload payload);
}

[JsonDerivedType(typeof(SayHelloRequestPayload), "sayHello")]
[JsonDerivedType(typeof(SayHelloRequestPayload), "sayGoodbye")]
public class RequestPayloadBase { }

public class SayHelloRequestPayload : RequestPayloadBase { ... }

public class FooRequestMessage : RequestMessageBase<RequestPayloadBase> { }

public class FooServer : ServerBase<FooRequestMessage, ...>, IRequestPayloadVisitor
{
public override void HandleMessage(FooRequestMessage message) => message.Payload.Visit(this);

// IRequestPayloadVisitor impl
}
ero
eroOP2mo ago
i'm not following these examples. they all have a server which only handles... one message? i need the server to handle an arbitrary amount of requests
canton7
canton72mo ago
Yes, you're reading them wrong (or I'm not being clear enough)
ero
eroOP2mo ago
i mean i'm probably reading them wrong
canton7
canton72mo ago
Where do you think we're only handling one message type? In all cases, proj1 defines the RequestMessage but not the payload type, and has logic to handle the RequestMessage itself, but not the individual payload types. Then proj2 introduces the different payload types and how to handle those
ero
eroOP2mo ago
oh yeah you define RequestPayloadBase in proj 2, that's exactly not what i want RequestPayloadBase should be in proj 1 it's the base after all
canton7
canton72mo ago
Re-read it Wait Why do you want RequestPayloadBase in each project? I thought you wanted the individual payload types to be defined in proj2?
ero
eroOP2mo ago
i don't, that's what you wrote >Then in proj2 >public class RequestPayloadBase
canton7
canton72mo ago
and the actual implementations (visitor, client, server, payloads) are in separate projects (one project per "kind of server").
payloads
ero
eroOP2mo ago
i mean just look at the code you wrote idk? what else should i say lol
canton7
canton72mo ago
I'm saying that you wanted the payloads to be defined in proj2. You said so. If you don't want that, then I'm confused as to what you want
ero
eroOP2mo ago
yes, but not the payload base it's the base, all other projects need it
canton7
canton72mo ago
Ok, stop. You're blindly wanting the impossible again Remember, the payload base is something that STJ needs. It's part of the mechanism for doing polymorphism in STJ You need to add attributes to the payload base to tell STJ what all of the different payload types are If you put the payload base in proj1, then all of the different payload types would also, obviously, have to be in proj1. Because otherwise how could you name them in the attributes on the payload base So you can put the request message in proj1, but if the individual payloads need to be in proj2, then the payload base also has to be in proj2 (Note that the payload base is probably empty. It's literally just a marker class, and a place to stick attributes)
ero
eroOP2mo ago
but then where is Payload.Visit coming from
canton7
canton72mo ago
What do you mean? Ah yeah, it does implement the visitor pattern, that's right. I forgot that bit
ero
eroOP2mo ago
alright
canton7
canton72mo ago
But that needs to be in proj2 as well, because the visitor interface is specific to the different payload types, and they're in proj2 too You could put this in proj1:
public abstract class RequestMessagePayloadBaseBase<TVisitor>
{
public void Visit(TVisitor visitor);
}
public abstract class RequestMessagePayloadBaseBase<TVisitor>
{
public void Visit(TVisitor visitor);
}
Then in proj2:
public interface IRequestPayloadVisitor { ... }
public class RequestMessagePayloadBase : RequestMessagePayloadBaseBase<IRequestPayloadVisitor> { }
public interface IRequestPayloadVisitor { ... }
public class RequestMessagePayloadBase : RequestMessagePayloadBaseBase<IRequestPayloadVisitor> { }
But I really don't see the point
ero
eroOP2mo ago
oh JsonDerivedTypeAttribute doesn't exist that's really bad
canton7
canton72mo ago
.NET 7 apparently? Or NS2.0 with a nuget package
ero
eroOP2mo ago
so i'm on uhh 2.0 :/
canton7
canton72mo ago
NS2.0? Or .net core 2.0?
ero
eroOP2mo ago
ns
canton7
canton72mo ago
No description
ero
eroOP2mo ago
i'm on a pretty old version of the package (don't remember why) probably compatibility with other packages (System.Memory, System.Buffers)
canton7
canton72mo ago
You can probably write a custom serializer which does the same thing?
ero
eroOP2mo ago
i'm not all that picky, but it seems a tall order to do what stj does
ero
eroOP2mo ago
that's for converters?
canton7
canton72mo ago
That's what I meant -- used the wrong word
ero
eroOP2mo ago
ah, i suppose Proj1
public interface IRequest<in TVisitor>
{
void Visit(TVisitor visitor);
}

public sealed class RequestMessage<TRequestPayload>(TRequestPayload payload)
{
public TRequestPayload Payload { get; } = payload;
}

public abstract class ServerBase<TRequestPayload>
{
protected abstract void HandleMessage(RequestMessage<TRequestPayload> message);
}
public interface IRequest<in TVisitor>
{
void Visit(TVisitor visitor);
}

public sealed class RequestMessage<TRequestPayload>(TRequestPayload payload)
{
public TRequestPayload Payload { get; } = payload;
}

public abstract class ServerBase<TRequestPayload>
{
protected abstract void HandleMessage(RequestMessage<TRequestPayload> message);
}
Proj2
[JsonDerivedType(typeof(GetFooRequest), nameof(GetFooRequest))]
public interface IFooRequest : IRequest<IFooRequestVisitor>;

public sealed class GetFooRequest : IFooRequest
{
public void Visit(IFooRequestVisitor visitor)
{
visitor.Accept(this);
}
}

public interface IFooRequestVisitor
{
void Accept(GetFooRequest request);
}

public sealed class FooServer : ServerBase<IFooRequest>, IFooRequestVisitor
{
protected override void HandleMessage(RequestMessage<IFooRequest> message)
{
message.Payload.Visit(this);
}

public void Accept(GetFooRequest request)
{
// ...
}
}
[JsonDerivedType(typeof(GetFooRequest), nameof(GetFooRequest))]
public interface IFooRequest : IRequest<IFooRequestVisitor>;

public sealed class GetFooRequest : IFooRequest
{
public void Visit(IFooRequestVisitor visitor)
{
visitor.Accept(this);
}
}

public interface IFooRequestVisitor
{
void Accept(GetFooRequest request);
}

public sealed class FooServer : ServerBase<IFooRequest>, IFooRequestVisitor
{
protected override void HandleMessage(RequestMessage<IFooRequest> message)
{
message.Payload.Visit(this);
}

public void Accept(GetFooRequest request)
{
// ...
}
}
i have this for now, will deal with any package incompatibilities later the only thing that bothers me here is that the visitor implementation is public
canton7
canton72mo ago
You can impl it explicitly, or on an inner class I don't think you want in TVisitor? No need to be contravariant
ero
eroOP2mo ago
with variance i almost always add it when it's possible
many things
many things2mo ago
the way you are explaining this makes it a problem of the physical layer, not of the de/serializer 🤔 like sending length+data or bom+data+eom instead of raw bytes
ero
eroOP2mo ago
hm, annoying issue: https://github.com/dotnet/runtime/issues/81840 seems like i need to length-prefix all of my sent data and read as an array of bytes instead
canton7
canton72mo ago
Yeah, that's expected when using any streaming transport. Tcp and serial are the same The word you're looking for is "framing". Normally you'd have some sort of "start of message" marker so that the receiver can re-synchronise if it becomes desynced, and a crc (although that's probably not necessary for named pipes)
ero
eroOP2mo ago
i just do it like this
public static void Serialize<T>(Stream stream, T value, JsonSerializerContext context)
{
byte[] bytes = JsonSerializer.SerializeToUtf8Bytes(value, typeof(T), context);

byte[] rented = ArrayPool<byte>.Shared.Rent(sizeof(int));
BinaryPrimitives.WriteInt32LittleEndian(rented, bytes.Length);

stream.Write(rented, 0, sizeof(int));
stream.Write(bytes, 0, bytes.Length);

ArrayPool<byte>.Shared.Return(rented);
}
public static void Serialize<T>(Stream stream, T value, JsonSerializerContext context)
{
byte[] bytes = JsonSerializer.SerializeToUtf8Bytes(value, typeof(T), context);

byte[] rented = ArrayPool<byte>.Shared.Rent(sizeof(int));
BinaryPrimitives.WriteInt32LittleEndian(rented, bytes.Length);

stream.Write(rented, 0, sizeof(int));
stream.Write(bytes, 0, bytes.Length);

ArrayPool<byte>.Shared.Return(rented);
}
i'm decently far along now, need to work on the responses next, really not sure how i wanna do that there's also the problem of having to handle a close command as well as internal server errors, which should be kinda shared between all server impls
canton7
canton72mo ago
You can put extra fields on your base RequestMessage/ResponseMessage
ero
eroOP2mo ago
and then?
canton7
canton72mo ago
And then use them to encode errors / close messages / etc
ero
eroOP2mo ago
oh i thought you meant for the length prefixing stuff still
canton7
canton72mo ago
For the length prefixing - looks fine. As long as you don't need to resync if the receiver misses some bytes, or has to drop some bytes. You can probably get away with a uint16 if you care, and stackalloc will be a bit cheaper, but meh, it's fine
ero
eroOP2mo ago
can't stackalloc, since streams on ns2.0 don't take spans
canton7
canton72mo ago
Ah fair enough One way to model server errors / internal messages would be to do the same polymorphism as you do for payload but on the whole message. I'm on mobile so typing code is hard, but you have a base ResponseMessage class, and polymorphic subclasses for 1) a user payload (as you do now), 2) an error, 3) a close message, etc Or you can just stick extra fields on your current ResponseMessage and have an invariant that only one can be populated at a time
ero
eroOP2mo ago
hm, i still need some help figuring out lots of parts on the client side as well as the responses, and also how i should implement the server polling for new data for the client, i'm envisioning something like this
public sealed class MonoClient : IpcClient<IMonoRequest, IMonoResponse>
{
// Result<T> is my own type
public Result<GetMonoImageResponse> GetMonoImage(string imageName)
{
base.SendRequest(
new IpcRequestMessage<IMonoRequest>(
new GetMonoImageRequest(imageName)));

// receive? visitor pattern here? how?
}
}
public sealed class MonoClient : IpcClient<IMonoRequest, IMonoResponse>
{
// Result<T> is my own type
public Result<GetMonoImageResponse> GetMonoImage(string imageName)
{
base.SendRequest(
new IpcRequestMessage<IMonoRequest>(
new GetMonoImageRequest(imageName)));

// receive? visitor pattern here? how?
}
}
i've changed some of the names, this is what i actually have in my code. so i can then just call client.GetMonoImage("Assembly-CSharp") i'm not sure if the server should be in charge of polling for new requests itself, or if that should be handled outside of the server;
public abstract class IpcServer<TRequestPayloadBase, TResponsePayloadBase> : IDisposable
{
public void Start()
{
_pipe.WaitForConnection();

while (true) // while (_pipe.IsConnected)?
{
var message = IpcSerializer.Deserialize<IpcRequestMessage<TRequestPayloadBase>>(_pipe, SerializerContext);
if (message is null)
{
// ?
continue;
}

IpcResponseMessage<TResponsePayloadBase> response;
try
{
response = HandleMessage(message);
}
catch (Exception ex)
{
// ?
}

IpcSerializer.Serialize(_pipe, response, SerializerContext);
}
}

protected abstract IpcResponseMessage<TResponsePayloadBase> HandleMessage(IpcRequestMessage<TRequestPayloadBase> request);
}
public abstract class IpcServer<TRequestPayloadBase, TResponsePayloadBase> : IDisposable
{
public void Start()
{
_pipe.WaitForConnection();

while (true) // while (_pipe.IsConnected)?
{
var message = IpcSerializer.Deserialize<IpcRequestMessage<TRequestPayloadBase>>(_pipe, SerializerContext);
if (message is null)
{
// ?
continue;
}

IpcResponseMessage<TResponsePayloadBase> response;
try
{
response = HandleMessage(message);
}
catch (Exception ex)
{
// ?
}

IpcSerializer.Serialize(_pipe, response, SerializerContext);
}
}

protected abstract IpcResponseMessage<TResponsePayloadBase> HandleMessage(IpcRequestMessage<TRequestPayloadBase> request);
}
basically should the loop be in the server here or should i handle this polling in my app code?
using var server = new MonoServer();
while (true)
{
server.ProcessNextMessage();
}
using var server = new MonoServer();
while (true)
{
server.ProcessNextMessage();
}
then there's the obvious question about how to design the response message. is something like this just enough?
public sealed record IpcResponseMessage<TPayload>(
TPayload? Payload = default,
string? Error = default);
public sealed record IpcResponseMessage<TPayload>(
TPayload? Payload = default,
string? Error = default);
and perhaps for later; i'd like to add some logging via some ILogger interface. i wanna implement console logging, file logging, and debug logging (Debug.WriteLine)
many things
many things2mo ago
(just as an indication, how many messages do you expect that will travel on this channel? like 100/s or 1/s or 1/h)
ero
eroOP2mo ago
it really depends on the user. some might only need 3 or 4 MonoFields from 1 MonoClass in 1 MonoImage others may request 100 fields from a total of 15 classes in 2 images the important bit is that the user is expected to request all of the data they need once and only once
many things
many things2mo ago
but aren't these commands periodical? ah, that answers my question
ero
eroOP2mo ago
so at (their) app startup, request all of the things they need (can be hundreds of requests sent as quickly as possible), and then the server might lay dormant. they can continue requesting more data, but it's not the expected use case i might expand the library to support generic memory reading from the target process, but i think a memory mapped file may be more useful there
many things
many things2mo ago
but so then why do speak about polling? you mean in a general sense, not periodical polling
ero
eroOP2mo ago
well the server still needs to read each new request (of those possible hundreds at app startup) in a row so it needs to wait for a new request to be sent in a loop that's my definition of polling for new messages
many things
many things2mo ago
(can be hundreds of requests sent as quickly as possible)
wouldn't you consider batching requests then? (i mean sure ok first the whole system needs to work, but still)
ero
eroOP2mo ago
Possibly, yeah The idea was kinda
var asmCs = client.GetMonoImage("Assembly-CSharp");

var player = client.GetClass(asmCs.Address, "" /* root namespace */, "Player");

var hp = client.GetField(player.Address, "_hp");

Console.WriteLine(hp.OffsetInternal);
var asmCs = client.GetMonoImage("Assembly-CSharp");

var player = client.GetClass(asmCs.Address, "" /* root namespace */, "Player");

var hp = client.GetField(player.Address, "_hp");

Console.WriteLine(hp.OffsetInternal);
Beyond that is a bit convoluted and idk if explaining it makes sense. Users will want to read the value of different class' fields. For that, they need to build a path of offsets (from the start of the class instance to the individual fields) that needs to be entirely dereferenced to actually read the value. Say the game contains something like this
class GameManager
{
static GameManager _instance;
Player _player;
}

class Player
{
int _hp;
}
class GameManager
{
static GameManager _instance;
Player _player;
}

class Player
{
int _hp;
}
The user first needs the static data chunk address of GameManager, add the offset of GameManager._instance to it, dereference, add the offset of GameManager._player to the result, dereference that, and finally add the offset of Player._hp to read the value at the result. So something like this;
GameManager._instance._player._id
*(int*)(*(*(0x12345678 + 0x0) + 0x10) + 0x3C)
GameManager._instance._player._id
*(int*)(*(*(0x12345678 + 0x0) + 0x10) + 0x3C)
So once they have all the addresses and offsets, they "build" their path and want to "watch" the value at the end of that path. I can do this via ReadProcessMemory calls, but each offset (each dereference) is one RPM call That's where I thought mem mapped files would work well (just continuously make the server update the value)
many things
many things2mo ago
so imagine player dies and user reloads save, to re-bind hp field the program would have to re-calculate at least the last offset, i guess
ero
eroOP2mo ago
No, the offsets always stay the same
canton7
canton72mo ago
Neither, IMO. You want an additional layer which reading the incoming stream, decoding responses, matching them to pending requests, and completing the relevant TCSs (see my messages about correlation tokens from yesterday) (that layer sits below your IpcClient/IpcServer of course)
ero
eroOP2mo ago
What's lower than an abstract base class?
canton7
canton72mo ago
Uh... We architect things in layers. abstract is irrelevant to that
many things
many things2mo ago
the answer was: another abstract class
canton7
canton72mo ago
Higher layers call down into lower layers. Higher layers deal with more abstract stuff, lower layers deal with more nitty-gritty detail Hah! I wouldn't here, though. For clarity. Composition over inheritance etc
ero
eroOP2mo ago
idk how you'd do it then
canton7
canton72mo ago
Have a class which wraps the named pipe. It takes a message in (and sends it over the pipe), and it had a constantly running task which reads messages from the pipe
ero
eroOP2mo ago
That's what the abstract ipcserver class does Exactly that, wraps the pipe
canton7
canton72mo ago
On the client, you wrap that in a class which takes requests, sends them, gets responses, matches the responses up to pending requests Then the IpcClient wraps that Don't just bung everything in one class. That's basic architecture. Identify what the different responsibilities are, and build your abstractions
ero
eroOP2mo ago
I really don't follow at all
canton7
canton72mo ago
What's confusing you?
ero
eroOP2mo ago
i'm just not really that sure what you're suggesting in the first place i guess. it doesn't make sense to me to add an additional wrapper around just the pipe that does the "polling" and the de/serialization. my base server is already that wrapper. the actual child servers then use the wrapper in the form of a base class. adding another class here just seems like overcomplicating it. i'd rather address the other stuff i brought up
canton7
canton72mo ago
Which bit are you still struggling with? I thought I addressed your latest round
ero
eroOP2mo ago
this basically summed it up but if you have anything to say to this, I'm open to hear your reasoning in a bit more detail, maybe with an example so I have something concrete in front of me. sorry if it just ends up in you repeating yourself, I'm just trying to understand why you think it's a better idea to further decouple the pipe and the server like that
canton7
canton72mo ago
That's just basic separation of concerns. They can get quite large: in my current project, the bit thay handles sequencing, aborting, and timing out messages is 350 lines; the thing that handles framing / deftaming messages is 160 lines; the thing that handles reading and writing byes to tcp is 270 lines; and the thing that handles deciding what to connect to, reconnections, disconnection, connection checks, etc, is 300 lines (and that's just a client: the server is an embedded device, so that's in C) My answer to thay was just "yes, both sides need to have a loop which just reads the pipe for new received bytes". Both the server and the client Were there any other questions in there? "lots of parts of the client side" is kinda hard to answer You probably want to support having multiple messages in flight at once, so the server can take a while to respond to a message if it wants. For that you need one component which reads bytes from the pipe and assembles them back into messages, then a component which takes those messages, matches them up to pending request, and notifies the requester that it's got a response
ero
eroOP2mo ago
Well I wanted to know how I could go about the client implementation with the way I was envisioning it in this message https://discord.com/channels/143867839282020352/1339224833359347822/1346481941335113749
MODiX
MODiX2mo ago
ero
for the client, i'm envisioning something like this
public sealed class MonoClient : IpcClient<IMonoRequest, IMonoResponse>
{
// Result<T> is my own type
public Result<GetMonoImageResponse> GetMonoImage(string imageName)
{
base.SendRequest(
new IpcRequestMessage<IMonoRequest>(
new GetMonoImageRequest(imageName)));

// receive? visitor pattern here? how?
}
}
public sealed class MonoClient : IpcClient<IMonoRequest, IMonoResponse>
{
// Result<T> is my own type
public Result<GetMonoImageResponse> GetMonoImage(string imageName)
{
base.SendRequest(
new IpcRequestMessage<IMonoRequest>(
new GetMonoImageRequest(imageName)));

// receive? visitor pattern here? how?
}
}
i've changed some of the names, this is what i actually have in my code.
Quoted by
React with ❌ to remove this embed.
ero
eroOP2mo ago
Or if you think it's perhaps a bad idea to make the client take raw arguments instead of entire RequestMessages And whether taking entire RequestMessages would allow for other patterns once again
canton7
canton72mo ago
That seems fine. You probably want somewhere which links together a request message type and a response message type. Thay might be in the call to SendRequest itself, or you could add the type of the response to the type of the request (as a generic type param), or have some other type which links them together. Either way, that means your SendRequest method can return the actual type of the response, and the user doesn't need to worry I'd just send the payload, not the whole request message The bits of the request message other than the payload are for use by your library, not for the user to use So, this should be a request message payload type with a response message payload type
ero
eroOP2mo ago
That sounds reasonable, what I would like to know is what that could enable me to do in terms of patternizing (?? idk what word I'm looking for here) the exchange of the request and response data Coupling them together and such
canton7
canton72mo ago
Eg in my current project I do GetFooResponsePayload response = await TransmitAsync(Command.GetFoo, new GetFooRequestPayload(...)). Where Command.GetFoo is a static new Command<GetFooRequestPayload, GetFooResponsePayload>() (actually it contains some other data as well, but that's not relevant) In terms of coupling the types, see above, or you could define your GetFooRequestPayload as a RequestPayloadBase<GetFooResponseMessage> and tie them together in the type system that way In terms of pairing a response message back to the request which triggered it, search this thread for "correlation token" Or you could just call SendAsync<FooRequestPayload, FooReaponsePayload>(new FooRequestPayload(...)) and tie them together at the call site (I've done all of those at one point or another)
ero
eroOP2mo ago
i can't figure it out idk? this can surely not be it
public interface IRequest<TRequestVisitor, TResponse, TResponseVisitor>
where TResponse : IResponse<TResponseVisitor>
{
IpcResponseMessage<TResponse> Visit(TRequestVisitor visitor);
}

public interface IMonoRequest<TResponse> : IRequest<IMonoRequestVisitor, TResponse, IMonoResponseVisitor>
where TResponse : IMonoResponse;
public interface IRequest<TRequestVisitor, TResponse, TResponseVisitor>
where TResponse : IResponse<TResponseVisitor>
{
IpcResponseMessage<TResponse> Visit(TRequestVisitor visitor);
}

public interface IMonoRequest<TResponse> : IRequest<IMonoRequestVisitor, TResponse, IMonoResponseVisitor>
where TResponse : IMonoResponse;
can't even use it in the IpcServer base class then anymore because i have no base payload my brain is fried i don't get it I beg you @canton7 get me out of this hell hole dude i swear to you i'm trying everything? it's always "cannot convert this" and "does not implement that" :resisting:
many things
many things2mo ago
at least you got fancy emojis
ero
eroOP2mo ago
come on man i just wanna be able to continue working on this
many things
many things2mo ago
sorry i understand you're stressed about this i wanted to take a deeper look at the thread but i've got a bunch of stuff to worry about myself it seems it's a low activity period this week on the server, i don't know
ero
eroOP2mo ago
Don't worry, just really frustrated that I can't figure this out. It's a blocking problem for me (on this project) okay, i figured out most of it (albeit probably in a bad way) i'm gonna work on some other stuff in the library and gather my thoughts to get some more feedback
canton7
canton72mo ago
I'm not really sure what you're trying to do there I'm afraid If you let us know one or more well-articulated specific problems, we can probably help
ero
eroOP2mo ago
wdym? i need the request to return a response. for that, a request needs a generic param which holds the response type. i can't use IpcServer<IRequest<SomeResponse>>.
canton7
canton72mo ago
Why do you need a response visitor, for instance?
ero
eroOP2mo ago
idk what you mean
canton7
canton72mo ago
TResponseVisitor
ero
eroOP2mo ago
well that's exactly what i'm asking. how i should do it.
canton7
canton72mo ago
Maybe I didn't explain it well enough before. I would have one request message type map to one response message type. If you send request A, you expect response A (or a response indicating an error). Anything else is a protocol violation. When your server receives a request message, it needs to check what kind of request message it is, and handle it appropriately. When your client receives a response, it just needs to check that it's the correct kind of response. It doesn't need to handle all possible message types as the response: it just needs to know that, if it send request A, that it got response A (and not response B, which would be an error)
ero
eroOP2mo ago
one request message type map to one response message type
this how how do you do it how do you enforce the server (visitor?) to return that response type for that specific request
canton7
canton72mo ago
I gave a few different options. What you have looks ok I think? But strip off the TResponseVisitor stuff, and let's have another look. Also, try and be a little less demanding please 🙂 Tbh normally I don't encode that relationship in the type system on the server - I just know that if I'm handling request A I need to send response A, but encoding it in the visitor seems like a decent approach tbh I also don't think you need the IMonoRequest interface? You should be working wirh a concrete request type at that point, no need to abstract it (and I'm not convinced you need the interface for IRequest, but iirc it was one of the options we discussed. This was getting to be quite a while ago now!)
ero
eroOP2mo ago
this is all very recent relative to the project's lifetime. like i said, i've been trying to find a solution here for months.
canton7
canton72mo ago
Yeah, but long enough for me to forget the details, heh
ero
eroOP2mo ago
idk how i could not use IMonoRequest. it's my base request type, what you had as MonoRequestBase or whatever
canton7
canton72mo ago
I don't even know how to search a thread in discord on mobile
ero
eroOP2mo ago
you can't search threads or forum posts
canton7
canton72mo ago
Lame Can you put all of your current types in one place that I can copy+edit? Finding everything and copy/pasting on mobile is a chore And there's now a lot of stuff scattered around the thread
ero
eroOP2mo ago
my solution now looks like this
public interface IRequest<out TResponse, TVisitor>
{
IResult<TResponse> Visit(TVisitor visitor);
}

public sealed record IpcRequestMessage<TPayload>(
TPayload Payload);

public sealed record IpcResponseMessage<TPayload>(
TPayload? Payload = default,
string? Error = default);
public interface IRequest<out TResponse, TVisitor>
{
IResult<TResponse> Visit(TVisitor visitor);
}

public sealed record IpcRequestMessage<TPayload>(
TPayload Payload);

public sealed record IpcResponseMessage<TPayload>(
TPayload? Payload = default,
string? Error = default);
public abstract class IpcClient<TRequestPayloadBase, TResponsePayloadBase> : IDisposable
{
// ...

protected Result<TResponse> Transmit<TResponse>(TRequestPayloadBase request)
where TResponse : TResponsePayloadBase
{
// ...
}
}

public abstract class IpcServer<TRequestPayloadBase, TResponsePayloadBase> : IDisposable
{
// ...

protected abstract IResult<TResponsePayloadBase> HandleMessage(TRequestPayloadBase payload);
}
public abstract class IpcClient<TRequestPayloadBase, TResponsePayloadBase> : IDisposable
{
// ...

protected Result<TResponse> Transmit<TResponse>(TRequestPayloadBase request)
where TResponse : TResponsePayloadBase
{
// ...
}
}

public abstract class IpcServer<TRequestPayloadBase, TResponsePayloadBase> : IDisposable
{
// ...

protected abstract IResult<TResponsePayloadBase> HandleMessage(TRequestPayloadBase payload);
}
[JsonDerivedType(typeof(GetMonoImageRequest), nameof(GetMonoImageRequest))]
public interface IMonoRequest<out TResponse> : IRequest<TResponse, IMonoVisitor>;

[JsonDerivedType(typeof(GetMonoImageResponse), nameof(GetMonoImageResponse))]
public interface IMonoResponse;

public sealed record GetMonoImageRequest(
string Name) : IMonoRequest<GetMonoImageResponse>
{
public IResult<GetMonoImageResponse> Visit(IMonoVisitor visitor)
{
return visitor.GetMonoImage(this);
}
}

public sealed record GetMonoImageResponse(
ulong Address,
string Name,
string ModuleName,
string FileName) : IMonoResponse;

public interface IMonoVisitor
{
Result<GetMonoImageResponse> GetMonoImage(GetMonoImageRequest request);
}
[JsonDerivedType(typeof(GetMonoImageRequest), nameof(GetMonoImageRequest))]
public interface IMonoRequest<out TResponse> : IRequest<TResponse, IMonoVisitor>;

[JsonDerivedType(typeof(GetMonoImageResponse), nameof(GetMonoImageResponse))]
public interface IMonoResponse;

public sealed record GetMonoImageRequest(
string Name) : IMonoRequest<GetMonoImageResponse>
{
public IResult<GetMonoImageResponse> Visit(IMonoVisitor visitor)
{
return visitor.GetMonoImage(this);
}
}

public sealed record GetMonoImageResponse(
ulong Address,
string Name,
string ModuleName,
string FileName) : IMonoResponse;

public interface IMonoVisitor
{
Result<GetMonoImageResponse> GetMonoImage(GetMonoImageRequest request);
}
public sealed class MonoClient : IpcClient<IMonoRequest<IMonoResponse>, IMonoResponse>, IMonoVisitor
{
// ...

public Result<GetMonoImageResponse> GetMonoImage(GetMonoImageRequest request)
{
return Transmit<GetMonoImageResponse>(request);
}
}

public abstract class MonoServer : IpcServer<IMonoRequest<IMonoResponse>, IMonoResponse>, IMonoVisitor
{
// ...

protected sealed override IResult<IMonoResponse> HandleMessage(IMonoRequest<IMonoResponse> payload)
{
return payload.Visit(this);
}

public abstract Result<GetMonoImageResponse> GetMonoImage(GetMonoImageRequest request);
}
public sealed class MonoClient : IpcClient<IMonoRequest<IMonoResponse>, IMonoResponse>, IMonoVisitor
{
// ...

public Result<GetMonoImageResponse> GetMonoImage(GetMonoImageRequest request)
{
return Transmit<GetMonoImageResponse>(request);
}
}

public abstract class MonoServer : IpcServer<IMonoRequest<IMonoResponse>, IMonoResponse>, IMonoVisitor
{
// ...

protected sealed override IResult<IMonoResponse> HandleMessage(IMonoRequest<IMonoResponse> payload)
{
return payload.Visit(this);
}

public abstract Result<GetMonoImageResponse> GetMonoImage(GetMonoImageRequest request);
}
canton7
canton72mo ago
Ta (I'm currently on crappy WiFi, on my phone, banging together my few functioning brain cells as I recover from norovirus, so forgive me if I say things that don't make sense)
ero
eroOP2mo ago
don't feel obligated to do this. i find this solution fairly satisfactory the only thing i hate is that IpcResponseMessage can be constructed with no parameters. JsonSerializer needs either a single, or a public empty ctor. so i'm screwed either way. and i still need to create the pipe wrapper you suggested
canton7
canton72mo ago
Aah you put JsonDerivedType on the interface, I see
ero
eroOP2mo ago
mhm
canton7
canton72mo ago
User code shouldn't be constructing an IpcResponseMessage anyway
ero
eroOP2mo ago
well, it is a public type so i have no control but they can't use it to interact with the client or server sides that's true
canton7
canton72mo ago
Yeah, but if someone does construct one, so what? They can't do anything useful with it
ero
eroOP2mo ago
oh and the other thing bothering me slightly is that when using pipes, the stream must always be consumed to the end, otherwise the entire code hangs, waiting for the stream to be read not sure how i'll do disconnecting, especially unexpected disconnections (client app closes/crashes)
canton7
canton72mo ago
One thing at a time please
ero
eroOP2mo ago
not how my brain works it's just a check list :p if i don't write it down, it's gone
canton7
canton72mo ago
Yeah, but I'm keeping a check list of comments on your types, and I can't juggle that and reply to something else
ero
eroOP2mo ago
well you don't have to lol one thing at a time like you said
canton7
canton72mo ago
Sorry, SharpLab is horrible in mobile
ero
eroOP2mo ago
not just there i just want an online vscode instance that can do decomp and il/jit viewing
many things
many things2mo ago
there's also godbolt that can do some of that i never dared to try it on mobile tho
canton7
canton72mo ago
The fact that you're splitting this into a generic base class is what's throwing me off - my normal trick to make generic type inference work on Transmit doesn't work in that case. Trying to find a good solution... (C# doesn't have HKT, which is the blocker) Right, this could probably be neatened up a bit, but the type system seems to hang together
canton7
canton72mo ago
https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA+ABADAAgwRgDoAVGBAF0ICkBnCAO0IGUYoBLAQwBs2AvD8mwYBuALAAoDACZ8EiRgDMONvXKsAZhzAwcASQBKMGgFcu5ADzEAfDgDeOAL455S6TkMmzlmyD0fTFtZ2js7iLsqqGlo6BjAAjsZG5ACyRjQcAOYwlh4ADgw0MKk06VlWErYSDnKSSoXcMAAmOLCQUM26uWCGCUnFpdnEPYk0KWmZMABCHIVWABQSOEs4Q/EjYyUT04U4AAocAJ5cEByNAJRiYbU49VxNLTBtHV15BUXjWTlG+fSF/VszGDzRbLVY0H5/D5TQEAfj2h2OpxwAF4cI0YJoAgAaEFLAhYOEAUSgUGgKLRGI4AQuNUUOA4wFGUC05DwUj0XQAwjwYKovr1Rv8stsYFiVq9fu9NsLAT49AARNjgiDpYB3Cq4nAAei1OEI+pqyxwuVJajAama/i8YIhUoGNmIzN+AFs2IFhn0oWKbW8hUC5qsBRsBg8g36zpqjQB3AAWrB0gfWfpwvkTnul0MKYtiYahX2Vkr95XERujcdg4u+vqhKcrBchGZFmsqJdLOAAbhwoMoXmskuT6DAoxzun3BXm0+PG7K5rBcxmaa3S+QY6Th/RTFwAISXI3VJehI10jAoHCK5WFOZnYJOKq01zs5IHTpgblsXms3wvt8f8y6ZIMBAHqjNmAH0EBVaSsWLZGjqOAABIwFwuSsDg6hkuQBwoRE6jxvQ2himAHD0AA5KyUbQAA1jgEDGKyMYQMO5AQDg9BsNoXAHA8zoQO2MCaiaEBmha7hGAE+a2kWKxOjQrrupBDb2nM/6AcB8n1naEw2HOSZQhGB7LLG8Z1pJNZfmBEEaSixZttJxGyW6f4WWpEnVhmVjehKilabOY7BhMi5tpqx6ngAYhAEBXoeywwbZnbdrAVmoo69lyXMg7DgA4jAKSAbozoTGpcwAETyhwSoHMVZwXNFSy3uI9XhAyTIsmyI4sFAfFQPy6z7EcJyNCKnkKTAfWIoNsq1ro575Kq6riLFSxwfqhCGssgnCfczXkMy5p+GJ1peaNCIDSKNjwcRjR3H6AZqWNp2AsaJ2nIFyzBa4p4zSqMBRfY9X7uEbhPi+HVdVNXSg6wTmqX50PgUdHl6M5I2Iyp4EAGpKm60AageG2PCJvGsOw6L7Z4Fho5ZtrnZd11QspyNBnDVNvNpfnhs2kbLBgADsoa6RmhCYzQbpzCuSqvXVmrSweVoWNluXgflhUo0jgHC9jUCEAAgmA2i5OQcwKxZytZGp/NJPpRqLcuq6Maxm47pq+6NeIADatAMPKrBsHxjTEFhP2YShEDqEbOUmwVZt+WcYr0BwzowKH4eKxApswGp1UALrhCoahQJo2hq/DfmXLnkQF9ExcQUzPqFlCcqU2p2ZqX6rn1+5uNGrE5Pt95ZQ4BrhuU0PZLtljzFQDVVShBIHt0PQ3vsH7AcoWLgfJ8beVRxnI2x6xCdJ2HW9KzvR3Z+X+eFzEyMaWX1y3PcrTQM0J9p2ffkLAe+I4AAcof15zIwyZm/dOCMu7cyUD3cSoCP4aRsEPBm6sJ5jxQVPLmSxkQ2HHiLSeOs9YwANmLGMEtLgAwfjABozRn7tBwLAlWGkv5GlMAwDIOBtaNEaIlGgOJv74FwP/ROvCjz8JwABRopgYCCNFMFURoU2B3GkYA6uR1750jzlEIuI80EQKWHLcw9Do7wPYQQohhjd5BgtqMRc+4gA=
SharpLab
C#/VB/F# compiler playground.
canton7
canton72mo ago
(I ended up renaming a bunch of the generic types as I was getting confused as to what they meant) God I spent over an hour fighting SharpLab's crappy typing there Each side should have a task which is constantly trying to read from the stream anyway, so that shouldn't be a problem? How you'll do them in what sense? You're worried about timing out requests?
ero
eroOP2mo ago
alright, so not too different from what i ended up with. i don't really know if i like how much work this leaves to the implementing side, but it does kinda make sense...
canton7
canton72mo ago
Yeah, I took what you did and tweaked it a bit Eh, that's also called "flexibility". You have the choose not to implement the visitor pattern, and to go another route, foe a particular implementation, if you want to Tbh if it was me I'd go for fewer generics and fewer do-it-all abstract base classes, and more modularity that you can use to build different servers and clients.
ero
eroOP2mo ago
my main goal was honestly to make creating new server implementations as quick as possible, not as flexible as possible
canton7
canton72mo ago
(so not bother encoding the relationship between request and response message types in the base abstract types for example, and leave that flexibility up to implementations). But you do you, this is fine Yeah, but look at it in the context of defining all of the message types, and actually imementing the server and clients. It's tiny Over the years I've come to much prefer "toolkit" approaches which let you build what you want out of different bits, rather than "framework" approaches which tell you exactly how to do things and give you a gazillion extension points that they think you'll need. Yes toolkits require a bit more boilerplate, but the gain is significantly less magic, easier to understand, and you don't need horrible hacks when invariably the framework doesn't give you quite the extension point you need
ero
eroOP2mo ago
i'll definitely consider it and might come back to it. i like what i have for the moment the major parts i still need are logging (i know this is not the best idea, but i kinda wanna use it as _logger.Log("Foo") and have it output [ServerName] [MethodName] Foo...) and wrapping the pipe stream. what would you make the wrapper handle again?
canton7
canton72mo ago
Cool, logging should be fine. I'd just log the message type tbh, nice and easy
ero
eroOP2mo ago
i'd need something like
class IpcServer
{
// _logger
private static readonly string _thisName = GetType().Name;

protected void Log(string output, [CallerMemberName] string memberName = "")
{
_logger.Log($"[{_thisName}] [{memberName}] {output}");
}
}
class IpcServer
{
// _logger
private static readonly string _thisName = GetType().Name;

protected void Log(string output, [CallerMemberName] string memberName = "")
{
_logger.Log($"[{_thisName}] [{memberName}] {output}");
}
}
just not super sure if it's overkill... it would definitely help determining where things went wrong need some sort of verbosity setting never written a logger implementation, but that one shouldn't be a problem, i hope
canton7
canton72mo ago
For how you deal with the pipe... I like to be able to have multiple requests in flight at once, and have the option to do retires and timeouts (although my stuff is usually over serial / ethernet, so the chance of connection loss is higher, and I have more latency). On both sides, I have a task which just constantly reads from the stream, assembles messages, and passes them up. Then I add an int correlation token to both request and response messages. On the client, I keep a dict<int, State> mapping correlation token to a state object. When a client sends a new message, I assign it a new correlation token, allocate its state, construct the request ipc messages, and send it over the stream. The state contains a TCS, and I pass that Task back to the sender. The State contains any info I need to do retires, timeouts, etc. When the client gets a response message back, it grabs the corresponding State from the dict, checks the response has the right type, and completes the TCS with it. The server is a bit easier, and just needs to make sure that it reads the correlation token from the request, and mirrors it back into the response.
ero
eroOP2mo ago
hm, my use case is a bit different. the server never sends anything if the client didn't ask for anything it's always a 1:1 transaction
canton7
canton72mo ago
That's mine, too But the client can still pipeline requests (unless you want to just bung a lock in there, so that if one request is being processed or is in the process of timing out, the client can't send another) It also means that you have a single point responsible for reestablisbing the connection if it drops
ero
eroOP2mo ago
the entire thing was really never meant to work asynchronously. the context the library is used it cannot be asynchronous. so i never really thought about the possiblity of multiple requests being in progress at any time
canton7
canton72mo ago
Fair enough. Single-threaded, too?
ero
eroOP2mo ago
well, the user can always Task.Run or Thread.Start...
canton7
canton72mo ago
Depends whether you want to support that, or just make them wait on a lock I guess
ero
eroOP2mo ago
i'm honestly not sure yet it sounds really difficult to me, keeping track of the correlations
canton7
canton72mo ago
Eh, it's fine. Just keep incrementing a counter. But yeah if you're purely single threaded, probably not much point
ero
eroOP2mo ago
i don't have a very good grasp on the use case of something like that, maybe
canton7
canton72mo ago
Up to you whether you want to just have the calling thread write to / read from the pipe, and also handle reconnections
ero
eroOP2mo ago
i've never really focused on async code yet
canton7
canton72mo ago
(it means there will be nothing just checking for rubbish being sent by the server when the client isn't attempting to send anything) (I'd start off each send with a "read and discard any bytes currently in the pipe" operation anyway)
ero
eroOP2mo ago
my ultimate use case will be fetching all the info i need once and only once. ideally.
var img = client.GetMonoImage("Assembly-CSharp").Unwrap();
var player = client.GetMonoClass(img.Address, "", "Player").Unwrap();
var hp = client.GetMonoField(player.Address, "_hp").Unwrap();

Console.WriteLine(hp.Offset);
var img = client.GetMonoImage("Assembly-CSharp").Unwrap();
var player = client.GetMonoClass(img.Address, "", "Player").Unwrap();
var hp = client.GetMonoField(player.Address, "_hp").Unwrap();

Console.WriteLine(hp.Offset);
just as an example designing this part of the library is gonna be an entirely different kind of trip...
// somehow creates a mechanism which reads a memory value from the other process at this path
var watcher = client.Watch(player.StaticDataChunk, playerInstanceField.Offset, playerHpField.Offset);
Console.WriteLine(watcher.Read());
// somehow creates a mechanism which reads a memory value from the other process at this path
var watcher = client.Watch(player.StaticDataChunk, playerInstanceField.Offset, playerHpField.Offset);
Console.WriteLine(watcher.Read());
i'm gonna go crazy!
canton7
canton72mo ago
Sounds like a job for your ipc thingy!
ero
eroOP2mo ago
well, this i will likely want to solve differently entirely... like i think memory mapped files might work better here where the server side will just constantly write to the file with the current value at that location, and i just have to read from that file without sending an entire request back and forth every time these are values i will want to read many times a second
canton7
canton72mo ago
Yeah, might make sense. Just describe the layout of the mapped memory with a big schema, then send that to the client in one big go. No need for a lot of back and forth at startup to get the addresses of individual fields
ero
eroOP2mo ago
mhh, i'm not sure we're talking about the same thing getting the class and field information is separate from using the class and field information to actually fetch the values stored at those memory locations to be less vague, i want to read values from video games (i've been using mono-based processes as an example in this thread)
canton7
canton72mo ago
Yeah, maybe I'm misunderstanding. I assumed that GetMonoField was getting the offset of the hp field within the shared memory?
ero
eroOP2mo ago
GetMonoField gets the type, modifiers, offset, and perhaps some other stuff of a particular MonoField
canton7
canton72mo ago
How many possible fields are there, and how many are you likely to fetch?
ero
eroOP2mo ago
well, the former entirely depends on the game, and therefore its developer. a reasonable unity game class might contain 2-5 fields. i've seen games with thousands of instance fields in a single class. the latter entirely depends on my user and how many values they need to read the values my user reads are then used to manipulate their speedrun timer automatically
canton7
canton72mo ago
Righto. Is this shared memory being written explicitly by your server for the client to read, or are you peeking inside memory used by Unity?
ero
eroOP2mo ago
well, my server lives inside of the unity game
canton7
canton72mo ago
Yep
ero
eroOP2mo ago
as an injected dll so i'll absolutely have to peek into the process' memory directly
canton7
canton72mo ago
Right, but then is your server (as an injected dll) copying those values from the process's memory into the shared memory to be read by your client?
ero
eroOP2mo ago
for example, here i would do *(*(staticDataChunk + instanceFieldOffset) + hpFieldOffset) that's the idea at least
canton7
canton72mo ago
Or is it telling the client where to look to find the relevant data inside the process's memory directly?
ero
eroOP2mo ago
my server would read the values in the process and write the result to the mem mapped file the watcher would know the index and length of the data in the file is my idea anyway
canton7
canton72mo ago
OK so the server knows the exact memory address of every field which can be read by the client. Tbh, coming at this completely cold, I'd just have the server give the client a big data structure with all of the fields which exist in shared memory and their address and type/length/etc. That would shed all of the complexity around class vs instance etc. Just "hp is at address 1234", "the position of npc 3 is at 5678". The server knows all of that stuff, and it'll save the client the bother of having to work it all out. It'll also just remove an entire class of tricky-to-debug bug And a whole suite of error cases around the server having to tell the client "no, I don't know of any field called hp" etc
ero
eroOP2mo ago
hm, i'm not sure if it's that straight forward. i specify a path for the memory watcher to follow for a pretty good reason; any field in the chain may be assigned a new value, or become null at any point
canton7
canton72mo ago
Ah, the server is constructing chains of object references in the shared memory?
ero
eroOP2mo ago
well, no. the client is supposed to supply the chain. like i do here of course this can be abstracted away with a call like .Watch("Player", "_instance", "_hp")
canton7
canton72mo ago
Aah I see what you're doing I guess the question is, why? Why is the server creating those kinds of data structures for the client to follow?
ero
eroOP2mo ago
hm, i'm not sure i understand the question i do the reads on the server side because i presume that to be faster than multiple ReadProcessMemory calls in a row
canton7
canton72mo ago
Ah so you're just blitting the mono process's memory into shared memory, with no logic in there at all?
ero
eroOP2mo ago
yeah
canton7
canton72mo ago
Fair enough
ero
eroOP2mo ago
well, after traversing the given path
canton7
canton72mo ago
What? (I've got food arriving in a min BTW, so I going to vanish. I'll catch up on this again tomorrow probably)
ero
eroOP2mo ago
idk how to explain it well. i'm sure you know how process memory works though, no?
canton7
canton72mo ago
Sure
ero
eroOP2mo ago
well, i need some memory address that i can even start at. in this case, that memory address is the start of the "static data chunk" of the Player class. this address never changes in mono, and i can use it without any risk. this is my starting point. but this doesn't give me the hp information that i want. i need an instance of Player to fetch a Player's hp. that instance may or may not be located in Player._instance (this is a very, very simplified example). alright, that means i need to access the static _instance field. i can do that without issue from the start of the "static data chunk", by adding the _instance field offset to the address i found previously. but this still doesn't give me the hp information i want. since i now have access to a specific instance of the Player class, i just need to dereference the sum of the "static data chunk" address and the _instance field offset. and now i need to do the same again, with the result of the previous dereference and the offset of the hp field. once i finally have the address of the hp field of the Player instance stored in _instance, i can actually read it as whatever value it's stored as. the sample class might look something like this;
class Player
{
static Player _instance;
float hp;
}
class Player
{
static Player _instance;
float hp;
}
this means i must give the server the address of the "static data chunk" as well as all offsets that follow. the server can then do *(float*)(*(playerStaticDataChunk + playerInstanceOffset) + playerHpOffset) and write the result to the memory mapped file. the important part here is that the path can be arbitrarily long. while traversing the path, a field value might change (for example if _instance is assigned to another value) or become null. i always need to dereference the entire path one by one all the time.
canton7
canton72mo ago
Aren't you playing chicken with the GC there? If the GC decides to suspend the mono process and move objects around while your injected dll was in the middle of copying all of that memory into the shared memory buffer, you'll get pointers which don't point to the things they should be pointing to, and that will be visible to the client process The GC is safe to move things around on the server because it can stop any threads which might be able to see any changes, but if the server was in the middle of copying that memory to the client, and the client isn't paused until things get sorted out, you're asking for a crash, no?
ero
eroOP2mo ago
the client side only sees the value that was (or wasn't) read
canton7
canton72mo ago
Two things. 1. What's stopping the client from reading the memory while the server is mid-write? 2. What's stopping the GC from changing object locations while the server is mid-write, leading to a snapshot which is self-inconsistent? GC works because it can stop the world and fix up any pointers in your process which point to moved objects. But it can't fix your shared memory, or pause your client process For 1 you could use something like a named mutex I guess, and lock out the memory while the server is writing it. But it means that the server's necessarily locked out of it while the client is reading it, which isn't great. Maybe the server just aborts a write if the client is reading, but then if the two keep trying to access the memory at the same time the server might just stay locked out for an extended period. For 2, you maybe cook something up with checking GC.CollectionCount at the start and end of every write, and re-running a write if a collection happened during, but again it might fail multiple times in a row, and it's getting fiddly and racy Wait, I think what you said contradicts what I thought here? Who's doing the path traversal, including deferenencing those pointers? I thought it was the client above (and you agreed), but you later said https://discord.com/channels/143867839282020352/1339224833359347822/1349494800931618886
MODiX
MODiX2mo ago
ero
the client side only sees the value that was (or wasn't) read
Quoted by
React with ❌ to remove this embed.
ero
eroOP2mo ago
it's supposed to be the server that does it but all of this seems way too complicated, i'll probably just do ReadProcessMemory calls they're not that slow anyway
canton7
canton72mo ago
Right, so the shared memory will (in this case) only contain the "hp" field value?
ero
eroOP2mo ago
only the value. i thought a memory mapped file is just... some array of bytes that both processes can access?
canton7
canton72mo ago
That's not what you said here
ero
eroOP2mo ago
well the value is in the mono process' memory and i'm blitting that value into the file
canton7
canton72mo ago
I asked whether the server was just blindly dumping the whole Player object (and anything else relevant), without any logic (to find fields that the client cares about)
ero
eroOP2mo ago
honestly a pretty neat idea too
canton7
canton72mo ago
Right, that is a more sensible approach. But still, if you're treating references as pointers and doing pointer stuff with them, you're making GC holes and you're risking a crash if the GC runs at the wrong time I don't know how your dll loading works, but can the server do reflection in the host process?
ero
eroOP2mo ago
well, it gets a lot worse :/ the server is naot
canton7
canton72mo ago
Ah
ero
eroOP2mo ago
which means i have 2 .net runtimes loaded and i've been warned many a time that this is a problem but i really don't wanna use another language and maintain the communication protocol in 2 different langauges so i'll just do this and worry about problems later
canton7
canton72mo ago
Fun. I'm heading to bed, back at some point
ero
eroOP2mo ago
@canton7 I would like to come up with a way to similarly define a contract between the NAOT's exported entry points and the client which remotely calls those exports and receives the exit code. An NAOT export is a static method which takes a single parameter of type void*, returns uint and is marked with UnmanagedCallersOnly, which can optionally take a different name for the EntryPoint (as an attribute argument, it has to be const). How would you define such a contract? I can't use static abstract since I'm on .net standard 2.0 unfortunately
canton7
canton72mo ago
What sort of contract are you looking to define? I'm afraid I dont' really follow

Did you find this page helpful?