Go's io.Writer and io.Reader have massive support across the ecosystem. In some far-off iroh-go future, it would be amazing if we could somehow wrangle in these interfaces and connect them to the ffi plumbing. This seems hard though? It is also not blocking anything right now. But working with these interfaces would make a lot of small papercuts vanish.
The problems are two-fold: signature mismatches and also implementation expectation mismatches. Currently there are some slight signature mismatches which means iroh-go won't conform to either interface. More importantly, the expected behaviour of calling e.g. RecvStream.Read as an io.Reader very likely does not match the ecosystem expectation of calling io.Reader: Read(p []byte) (n int, err error).
Anyway, far future stuff! Maybe we never get there, even then things here are still very useful :) One type of solution would be to write a middle layer that communicates with the ffi on one hand, and on the other hand has data structures and implementations which fulfill all the go io:{Read, Write}er interface expectations. (Maybe what's somewhat happening already? I haven't peeked too much under the hood yet!!)
Go's io.Writer and io.Reader have massive support across the ecosystem. In some far-off iroh-go future, it would be amazing if we could somehow wrangle in these interfaces and connect them to the ffi plumbing. This seems hard though? It is also not blocking anything right now. But working with these interfaces would make a lot of small papercuts vanish.
The problems are two-fold: signature mismatches and also implementation expectation mismatches. Currently there are some slight signature mismatches which means iroh-go won't conform to either interface. More importantly, the expected behaviour of calling e.g. [`RecvStream.Read`](https://godocs.io/git.coopcloud.tech/decentral1se/iroh-go#RecvStream.Read) as an `io.Reader` very likely does not match the ecosystem expectation of calling [`io.Reader: Read(p []byte) (n int, err error)`](https://godocs.io/io#Reader).
Anyway, far future stuff! Maybe we never get there, even then things here are still very useful :) One type of solution would be to write a middle layer that communicates with the ffi on one hand, and on the other hand has data structures and implementations which fulfill all the go `io:{Read, Write}er` interface expectations. (Maybe what's somewhat happening already? I haven't peeked too much under the hood yet!!)
It's great that you're running into this already! I don't think it'd be too hard actually, logistically speaking. Most repos that make use of FFI / uniffi end up writing a wrapper in between bindings and consumers, so this is pretty standard.
I'm playing around with a wrapper that makes error handling less awful in #10 which should be a first example of how it works with wrapping bindings for re-export.
Could you (whenever) sketch out your ideal API based on the code you're writing and what you'd like to encapsulate into the Reader/Writer? It would be a nice milestone to work towards 🧗♀️
It's great that you're running into this already! I don't think it'd be too hard actually, logistically speaking. Most repos that make use of FFI / uniffi end up writing a wrapper in between bindings and consumers, so this is pretty standard.
I'm playing around with a wrapper that makes error handling less awful in https://git.coopcloud.tech/decentral1se/iroh-go/issues/10 which should be a first example of how it works with wrapping bindings for re-export.
Could you (whenever) sketch out your ideal API based on the code you're writing and what you'd like to encapsulate into the `Reader`/`Writer`? It would be a nice milestone to work towards 🧗♀️
Could you (whenever) sketch out your ideal API based on the code you're writing and what you'd like to encapsulate into the Reader/Writer? It would be a nice milestone to work towards 🧗♀️
That's a super idea! So concretely where I had the idea was in working with a smol wire protocol and needing to send prefixed length byte streams. I was looking to use encoding/binary:Varint but then noticed the Read and Write methods. If I could've passed in a reader / writer from iroh-go to these, I would've (I think?) been saved a little confusion and wrangling!
I'll have to return to this for an a good API sketch because I'm still finding my way. But an early minimal idea maybe:
io.Read(p []byte) (n int, err error)
// usual connection stuff, no changeaccepting,err:=incoming.Accept()check(err)conn,err:=accepting.Connect()check(err)stream,err:=conn.AcceptBi()check(err)recv:=stream.Recv()// here's the difference:data:=make([]byte,1024)for{bytesRead,err:=recv.Read(data)check(err)fmt.Println(data[:bytesRead])// alternatively, since RecvStream implements io.Read, i could also do:binary.Read(recv,binary.LittleEndian,data)}
io.Writer.Write(p []byte) (n int, err error)
// usual stuff, no changeaccepting,err:=incoming.Accept()check(err)conn,err:=accepting.Connect()check(err)stream,err:=conn.AcceptBi()check(err)send:=stream.Send()// here's the differencebuf:=[]byte("my cool and very long message")// send databytesWritten,err:=send.Write(data)check(err)fmt.Printf("wrote %d bytes\n")// alternatively, since SendStream implements io.Write, i could also do:binary.Write(send,binary.LittleEndian,data)
But maybe it's hard / not worth the wrangling to change Write and Read on these core iroh abstractions, so perhaps a compromise might be to wrap what already exists à la NewReader, NewWriter conventions:
irohgo.NewWriter(stream*iroh.BiStream)->returnssomestructthatimplementsio.Writerirohgo.NewReader(stream*iroh.BiStream)->returnssomestructthatimplementsio.Reader// which would give us recv:=irohgo.NewReader(stream)// stream is *iroh.BiStream; wraps *RecvStreamdata:=make([]byte,1024)bytesRead,err:=recv.Read(data)// and send:=irohgo.NewReader(stream)// stream is *iroh.BiStream; wraps *SendStreambuf:=[]byte("my cool and very long message")bytesWritten,err:=send.Write(data)
> Could you (whenever) sketch out your ideal API based on the code you're writing and what you'd like to encapsulate into the Reader/Writer? It would be a nice milestone to work towards 🧗♀️
That's a super idea! So concretely where I had the idea was in working with a smol wire protocol and needing to send prefixed length byte streams. I was looking to use [`encoding/binary:Varint`](https://pkg.go.dev/encoding/binary#Varint) but then noticed the [`Read`](https://godocs.io/encoding/binary#Read) and [`Write`](https://godocs.io/encoding/binary#Write) methods. If I could've passed in a reader / writer from `iroh-go` to these, I would've (I think?) been saved a little confusion and wrangling!
I'll have to return to this for an a good API sketch because I'm still finding my way. But an early minimal idea maybe:
`io.Read(p []byte) (n int, err error)`
```golang
// usual connection stuff, no change
accepting, err := incoming.Accept()
check(err)
conn, err := accepting.Connect()
check(err)
stream, err := conn.AcceptBi()
check(err)
recv := stream.Recv()
// here's the difference:
data := make([]byte, 1024)
for {
bytesRead, err := recv.Read(data)
check(err)
fmt.Println(data[:bytesRead])
// alternatively, since RecvStream implements io.Read, i could also do:
binary.Read(recv, binary.LittleEndian, data)
}
```
`io.Writer.Write(p []byte) (n int, err error)`
```golang
// usual stuff, no change
accepting, err := incoming.Accept()
check(err)
conn, err := accepting.Connect()
check(err)
stream, err := conn.AcceptBi()
check(err)
send := stream.Send()
// here's the difference
buf := []byte("my cool and very long message")
// send data
bytesWritten, err := send.Write(data)
check(err)
fmt.Printf("wrote %d bytes\n")
// alternatively, since SendStream implements io.Write, i could also do:
binary.Write(send, binary.LittleEndian, data)
```
But maybe it's hard / not worth the wrangling to change `Write` and `Read` on these core iroh abstractions, so perhaps a compromise might be to wrap what already exists à la `NewReader`, `NewWriter` conventions:
```golang
irohgo.NewWriter(stream *iroh.BiStream) -> returns some struct that implements io.Writer
irohgo.NewReader(stream *iroh.BiStream) -> returns some struct that implements io.Reader
// which would give us
recv := irohgo.NewReader(stream) // stream is *iroh.BiStream; wraps *RecvStream
data := make([]byte, 1024)
bytesRead, err := recv.Read(data)
// and
send := irohgo.NewReader(stream) // stream is *iroh.BiStream; wraps *SendStream
buf := []byte("my cool and very long message")
bytesWritten, err := send.Write(data)
```
Thanks @cblgh👏 And last thing, can you copy/pasta in the "current code"? You wrote
// here's the difference
But I miss the code of what you're doing right now? Then we have the full picture.
Thanks @cblgh 👏 And last thing, can you copy/pasta in the "current code"? You wrote
> // here's the difference
But I miss the code of what you're doing right now? Then we have the full picture.
Sure! I just refactored the relevant bits into a util so that I could reuse the same logic on client & server
MAY CONTAIN BUGS :) :)
funcWriteData(data[]byte,send*iroh.SendStream){// NOTE (2026-06-30): bit of thrashing around having to instantiate sizebuf all the timesizebuf:=make([]byte,binary.MaxVarintLen64)// calculate varint length of data// prefix data with the lengthfmt.Println("wrote",string(data))wrote:=binary.PutVarint(sizebuf,int64(binary.Size(data)))sizebuf=sizebuf[:wrote]// prepend varint length to datadata=append(sizebuf,data...)// fmt.Println("data", data, "size", size, "len(data)", len(data))// send dataerr:=send.WriteAll(data)Check(err)}constframeSize=1024// NOTE (2026-06-30): arbitrarily chosen value, can be tweaked if things are not workingfuncReadBytes(recv*iroh.RecvStream,chchan<-[]byte,donechanstruct{}){for{frame,err:=recv.Read(frameSize)ifcheckForTimeout(err){break}ch<-frame}close(done)}constSENTINEL=-1024funcProcessRead(recv*iroh.RecvStream,processedchan<-[]byte,donechanstruct{}){varsizeint64size=SENTINELdataCh:=make(chan[]byte)data:=make([]byte,0)goReadBytes(recv,dataCh,done)for{select{case<-done:returncaseframe:=<-dataCh:data=append(data,frame...)fmt.Println("recv",string(frame),cap(frame))default:// dont block}// if we don't have any data we instead of continuing the loop block on receiving from dataChiflen(data)==0{frame:=<-dataChdata=append(data,frame...)}bytesread:=0if(size==SENTINEL){size,bytesread=binary.Varint(data)ifbytesread>0{data=data[bytesread:]}else{panic("handlReading: varint wasn't where we expected it")}}iflen(data)>=int(size){processed<-data[:size]data=data[size:]size=SENTINEL}}}funccheckForTimeout(errerror)bool{// handle timeoutiferr!=nil{irohErr:=err.(*iroh.IrohError)errMsg:=irohErr.Message()ifstrings.HasPrefix(errMsg,"ConnectionLost"){fmt.Println("lost connection")returntrue}else{// panics and exits bc unhandled errCheck(err)}}returnfalse}funcCheck(errerror){iferr!=nil{irohErr:=err.(*iroh.IrohError)panic(irohErr.Message())}}
hope it helps!
Sure! I just refactored the relevant bits into a util so that I could reuse the same logic on client & server
MAY CONTAIN BUGS :) :)
```golang
func WriteData(data []byte, send *iroh.SendStream) {
// NOTE (2026-06-30): bit of thrashing around having to instantiate sizebuf all the time
sizebuf := make([]byte, binary.MaxVarintLen64)
// calculate varint length of data
// prefix data with the length
fmt.Println("wrote", string(data))
wrote := binary.PutVarint(sizebuf, int64(binary.Size(data)))
sizebuf = sizebuf[:wrote]
// prepend varint length to data
data = append(sizebuf, data...)
// fmt.Println("data", data, "size", size, "len(data)", len(data))
// send data
err := send.WriteAll(data)
Check(err)
}
const frameSize = 1024 // NOTE (2026-06-30): arbitrarily chosen value, can be tweaked if things are not working
func ReadBytes(recv *iroh.RecvStream, ch chan<- []byte, done chan struct{}) {
for {
frame, err := recv.Read(frameSize)
if checkForTimeout(err) {
break
}
ch<-frame
}
close(done)
}
const SENTINEL = -1024
func ProcessRead (recv *iroh.RecvStream, processed chan<- []byte, done chan struct{}) {
var size int64
size = SENTINEL
dataCh := make(chan []byte)
data := make([]byte, 0)
go ReadBytes(recv, dataCh, done)
for {
select {
case <-done:
return
case frame := <- dataCh:
data = append(data, frame...)
fmt.Println("recv", string(frame), cap(frame))
default:
// dont block
}
// if we don't have any data we instead of continuing the loop block on receiving from dataCh
if len(data) == 0 {
frame := <-dataCh
data = append(data, frame...)
}
bytesread := 0
if (size == SENTINEL) {
size, bytesread = binary.Varint(data)
if bytesread > 0 {
data = data[bytesread:]
} else {
panic("handlReading: varint wasn't where we expected it")
}
}
if len(data) >= int(size) {
processed <-data[:size]
data = data[size:]
size = SENTINEL
}
}
}
func checkForTimeout(err error) bool {
// handle timeout
if err != nil {
irohErr := err.(*iroh.IrohError)
errMsg := irohErr.Message()
if strings.HasPrefix(errMsg, "ConnectionLost") {
fmt.Println("lost connection")
return true
} else {
// panics and exits bc unhandled err
Check(err)
}
}
return false
}
func Check(err error) {
if err != nil {
irohErr := err.(*iroh.IrohError)
panic(irohErr.Message())
}
}
```
hope it helps!
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Go's io.Writer and io.Reader have massive support across the ecosystem. In some far-off iroh-go future, it would be amazing if we could somehow wrangle in these interfaces and connect them to the ffi plumbing. This seems hard though? It is also not blocking anything right now. But working with these interfaces would make a lot of small papercuts vanish.
The problems are two-fold: signature mismatches and also implementation expectation mismatches. Currently there are some slight signature mismatches which means iroh-go won't conform to either interface. More importantly, the expected behaviour of calling e.g.
RecvStream.Readas anio.Readervery likely does not match the ecosystem expectation of callingio.Reader: Read(p []byte) (n int, err error).Anyway, far future stuff! Maybe we never get there, even then things here are still very useful :) One type of solution would be to write a middle layer that communicates with the ffi on one hand, and on the other hand has data structures and implementations which fulfill all the go
io:{Read, Write}erinterface expectations. (Maybe what's somewhat happening already? I haven't peeked too much under the hood yet!!)It's great that you're running into this already! I don't think it'd be too hard actually, logistically speaking. Most repos that make use of FFI / uniffi end up writing a wrapper in between bindings and consumers, so this is pretty standard.
I'm playing around with a wrapper that makes error handling less awful in #10 which should be a first example of how it works with wrapping bindings for re-export.
Could you (whenever) sketch out your ideal API based on the code you're writing and what you'd like to encapsulate into the
Reader/Writer? It would be a nice milestone to work towards 🧗♀️That's a super idea! So concretely where I had the idea was in working with a smol wire protocol and needing to send prefixed length byte streams. I was looking to use
encoding/binary:Varintbut then noticed theReadandWritemethods. If I could've passed in a reader / writer fromiroh-goto these, I would've (I think?) been saved a little confusion and wrangling!I'll have to return to this for an a good API sketch because I'm still finding my way. But an early minimal idea maybe:
io.Read(p []byte) (n int, err error)io.Writer.Write(p []byte) (n int, err error)But maybe it's hard / not worth the wrangling to change
WriteandReadon these core iroh abstractions, so perhaps a compromise might be to wrap what already exists à laNewReader,NewWriterconventions:Thanks @cblgh 👏 And last thing, can you copy/pasta in the "current code"? You wrote
But I miss the code of what you're doing right now? Then we have the full picture.
Sure! I just refactored the relevant bits into a util so that I could reuse the same logic on client & server
MAY CONTAIN BUGS :) :)
hope it helps!
Reference: