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
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?My idea was to send data via JSONIf you can't use nuget packages, and you don't have S.T.J, then the only json serializer you have is pretty rubbish 😛
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 exampleIs there a fully SG'd json library which doesn't need any runtime libraries at all? Which one?
no no i just use stj
Ah no, I see what you mean
i can technically use any package
i il weave everything
but i really want to keep the file size low
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?
i think that's a good summary
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
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
i really don't wanna write a source generator for this, and rpc libraries tend to be pretty large
Yeah, they're large for a reason! Chucking messages back and forth is a lot easier
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
Just to play devil's advocate -- can't you include any large libraries in your host application?
the host application isn't mine
Ah, your IPC isn't with the host application?
the ipc is between my library (loaded by the host app) and some game
i'm essentially writing both sides, though
Ah gotcha
Can you just pass messages around, rather than going full-on RPC?
that's exactly what i want to do
the server side of course needs handlers
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?
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:
where
IpcClient.CallServerEndpoint
would serialize the request and just send it over the pipeYeah, 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)
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?
i've basically nuked the entire project :p
so i'm at 0
well, then u have these 4 layers to work on 😂
which is what this thread is about :)
well, with 1 and 2 there arent problems are there?
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
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
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
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
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
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
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)
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.
the number of different high level data types doesnt matteri 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
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
What are you having trouble with?
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
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
?
and then the other side has to read
?
i can't use the latter on both sides, because jsonserializer would try to serialize the
JsonElement
struct and its internals
i'm completely losti 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
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
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 overheadi 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
@this_is_pain @canton7 i don't think either of you understand what the problem is?
i need to send request types like this:
the other side needs to read this structure, but it of course needs to do so in one go.
reading
would drop the data that may or may not be there.
reading
would over-read if the first struct above was sent, leading to an exception.
I don't think you read my response...
please go ahead and explain how
$type
would help hereThat's literally the problem that $type is there to solve
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
and
Let's say you have two message types,
SayHelloRequest
and SayGoodbyeRequest
. You could define your RequestMessage
message as:
Then:
Gets serialized as:
don't think i need to mention the obvious, but requests do not derive from one anotherMaybe 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.
and explain how you would receive this on the other side
Just deserialize as
RequestMessage
, then type switch over Payload
and explain how you would handle request messages that don't have a payload
Then they have an empty payload. Every message needs a type.
wdym type switch? how do i access
$type
?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 whateversorry 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☝️
well yeah, but i still need some identifier
The identifier is the type
if there's no payload, there's no type
Which is why you need a payload, even an empty one
I feel like we're going in circles here
an empty payload sounds like a dumb idea no offense
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)
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?
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 typei 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:
god damn it yeah i've been doing that
Or yeah, you can start using reflection to discover handler methods, etc, but that becomes a bit more magic. I like explicitness.
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
Tbh, I'd probably use the visitor pattern. It's a bit of boilerplate, but a lot of stuff just falls out
do you have a link or an example
i haven't heard of it
Then in the receiver:
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 typei 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
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)yeah fuck that's crazy smart
so simple
very effective, thanks
So, empty payload types do make sense 🙂
alright well, i need some response type next
i want to communicate different types of failures
like something is not found, something was null
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
i would need to see both i think
Maybe?
The rest is left as an exercise to the reader
no god mutable type
The other option is:
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?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
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
etci can't figure it out
Proj1
Proj2
(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
is not even valid...
(for obvious reasons)One way:
Then in proj2:
Or you could do:
Then in proj2:
Or you could do something in the middle:
Then in proj2:
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
Yes, you're reading them wrong (or I'm not being clear enough)
i mean i'm probably reading them wrong
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
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 allRe-read it
Wait
Why do you want RequestPayloadBase in each project? I thought you wanted the individual payload types to be defined in proj2?
i don't, that's what you wrote
>Then in proj2
>
public class RequestPayloadBase
and the actual implementations (visitor, client, server, payloads) are in separate projects (one project per "kind of server").
payloads
i mean just look at the code you wrote idk?
what else should i say lol
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
yes, but not the payload base
it's the base, all other projects need it
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)
but then where is
Payload.Visit
coming fromWhat do you mean?
Ah yeah, it does implement the visitor pattern, that's right. I forgot that bit
alright
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:
Then in proj2:
But I really don't see the point
oh
JsonDerivedTypeAttribute
doesn't exist
that's
really bad.NET 7 apparently? Or NS2.0 with a nuget package
so i'm on uhh
2.0 :/
NS2.0? Or .net core 2.0?
ns

i'm on a pretty old version of the package (don't remember why)
probably compatibility with other packages (
System.Memory
, System.Buffers
)You can probably write a custom serializer which does the same thing?
i'm not all that picky, but it seems a tall order to do what stj does
that's for converters?
That's what I meant -- used the wrong word
ah, i suppose
Proj1
Proj2
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 publicYou can impl it explicitly, or on an inner class
I don't think you want
in TVisitor
? No need to be contravariantwith variance i almost always add it when it's possible
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
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
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)
i just do it like this
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
You can put extra fields on your base RequestMessage/ResponseMessage
and then?
And then use them to encode errors / close messages / etc
oh i thought you meant for the length prefixing stuff still
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
can't stackalloc, since streams on ns2.0 don't take spans
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
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
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;
basically should the loop be in the server here or should i handle this polling in my app code?
then there's the obvious question about how to design the response message.
is something like this just enough?
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
)(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)
it really depends on the user. some might only need 3 or 4
MonoField
s 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 oncebut aren't these commands periodical?
ah, that answers my question
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
but so then why do speak about polling?
you mean in a general sense, not periodical polling
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
(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)
Possibly, yeah
The idea was kinda
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
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;
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)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
No, the offsets always stay the same
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)
What's lower than an abstract base class?
Uh...
We architect things in layers.
abstract
is irrelevant to thatthe answer was: another abstract class
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
idk how you'd do it then
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
That's what the abstract ipcserver class does
Exactly that, wraps the pipe
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
I really don't follow at all
What's confusing you?
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
Which bit are you still struggling with? I thought I addressed your latest round
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
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
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
ero
for the client, i'm envisioning something like this
i've changed some of the names, this is what i actually have in my code.
Quoted by
<@542772576905199626> from #Named Pipe IPC: Protocols & Status Codes (click here)
React with ❌ to remove this embed.
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
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 typeThat 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
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)i can't figure it out idk?
this can surely not be it
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:at least you got fancy emojis
come on man i just wanna be able to continue working on this
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
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
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
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>>
.Why do you need a response visitor, for instance?
idk what you mean
TResponseVisitor
well that's exactly what i'm asking. how i should do it.
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)
one request message type map to one response message typethis how how do you do it how do you enforce the server (visitor?) to return that response type for that specific request
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!)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.
Yeah, but long enough for me to forget the details, heh
idk how i could not use
IMonoRequest
. it's my base request type, what you had as MonoRequestBase
or whateverI don't even know how to search a thread in discord on mobile
you can't search threads or forum posts
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
my solution now looks like this
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)
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 suggestedAah you put JsonDerivedType on the interface, I see
mhm
User code shouldn't be constructing an IpcResponseMessage anyway
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
Yeah, but if someone does construct one, so what? They can't do anything useful with it
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)
One thing at a time please
not how my brain works
it's just a check list :p
if i don't write it down, it's gone
Yeah, but I'm keeping a check list of comments on your types, and I can't juggle that and reply to something else
well you don't have to lol
one thing at a time like you said
Sorry, SharpLab is horrible in mobile
not just there
i just want an online vscode instance that can do decomp and il/jit viewing
there's also godbolt that can do some of that
i never dared to try it on mobile tho
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
SharpLab
C#/VB/F# compiler playground.
(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?
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...
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.
my main goal was honestly to make creating new server implementations as quick as possible, not as flexible as possible
(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
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?Cool, logging should be fine. I'd just log the message type tbh, nice and easy
i'd need something like
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
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.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
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
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
Fair enough. Single-threaded, too?
well, the user can always
Task.Run
or Thread.Start
...Depends whether you want to support that, or just make them wait on a lock I guess
i'm honestly not sure yet
it sounds really difficult to me, keeping track of the correlations
Eh, it's fine. Just keep incrementing a counter.
But yeah if you're purely single threaded, probably not much point
i don't have a very good grasp on the use case of something like that, maybe
Up to you whether you want to just have the calling thread write to / read from the pipe, and also handle reconnections
i've never really focused on async code yet
(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)
my ultimate use case will be fetching all the info i need once and only once. ideally.
just as an example
designing this part of the library is gonna be an entirely different kind of trip...
i'm gonna go crazy!
Sounds like a job for your ipc thingy!
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
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
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)
Yeah, maybe I'm misunderstanding. I assumed that
GetMonoField
was getting the offset of the hp field within the shared memory?GetMonoField
gets the type, modifiers, offset, and perhaps some other stuff of a particular MonoField
How many possible fields are there, and how many are you likely to fetch?
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
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?
well, my server lives inside of the unity game
Yep
as an injected dll
so i'll absolutely have to peek into the process' memory directly
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?
for example, here i would do
*(*(staticDataChunk + instanceFieldOffset) + hpFieldOffset)
that's the idea at leastOr is it telling the client where to look to find the relevant data inside the process's memory directly?
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
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
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
Ah, the server is constructing chains of object references in the shared memory?
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")
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?
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 rowAh so you're just blitting the mono process's memory into shared memory, with no logic in there at all?
yeah
Fair enough
well, after traversing the given path
What?
(I've got food arriving in a min BTW, so I going to vanish. I'll catch up on this again tomorrow probably)
idk how to explain it well. i'm sure you know how process memory works though, no?
Sure
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;
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.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?
the client side only sees the value that was (or wasn't) read
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/1349494800931618886ero
the client side only sees the value that was (or wasn't) read
Quoted by
<@660066004059029524> from #Named Pipe IPC: Protocols & Status Codes (click here)
React with ❌ to remove this embed.
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
Right, so the shared memory will (in this case) only contain the "hp" field value?
only the value. i thought a memory mapped file is just... some array of bytes that both processes can access?
That's not what you said here
well the value is in the mono process' memory
and i'm blitting that value into the file
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)
honestly a pretty neat idea too
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?
well, it gets a lot worse :/
the server is naot
Ah
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
Fun. I'm heading to bed, back at some point
@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 unfortunatelyWhat sort of contract are you looking to define? I'm afraid I dont' really follow