On Fri, 11 Oct 2024 at 03:15, Robert Engels <
ren...@ix.netcom.com> wrote:
>
> That’s what I was thinking. Thanks for providing the details. Rule of thumb - if you’re trying to do something and you’re the only one that’s trying it - you’re probably doing something wrong - or at least there is a much easier way.
I wish I could take the credit for the idea, but one of our
contributors referred me to this package:
https://github.com/Jille/gcmmap. Yes, it doesn't have any importers
and it's unpolished, but we thought the idea had merit, so we adapted
it to our use case.
As for there being an easier way -- it's hard to argue against KISS,
but the example I gave leaves out many details that made us prefer
this approach, see below.
> On Oct 10, 2024, at 6:31 PM, 'Keith Randall' via golang-nuts <
golan...@googlegroups.com> wrote:
>
> type Variable struct {
> *atomic.Uint64
> m *Memory
> }
>
> I agree that if you have a finalizer on a Variable that unmaps m, and you've given out a reference to the internal Uint64 to callers, then you can get in a situation where the Variable is dead, its finalizer is run, and the memory is unmapped while someone is doing operations on the atomic Uint64.
>
> So don't embed like that. Instead just have a private field and implement the methods you need. Like:
>
> type Variable struct {
> u *atomic.Uint64
> m *Memory
> }
> func (v *Variable) Load() uint64 {
> x := v.u.Load()
> runtime.KeepAlive(v)
> return x
> }
Yes, we couldn't allow the caller to obtain a reference to Uint64
because of the risk of losing the Memory pointer. My initial
implementation was slightly different; it defined unexported type
aliases for all integer types in sync/atomic and embedded those
instead, so the caller could only invoke the methods promoted from
e.g. atomic.Uint64 to Variable. However, that still left the
possibility of Memory becoming unreachable at the start of
atomic.Uint64.Load(), we'd need to wrap all accessors with
runtime.KeepAlive() as you suggested above.
The reason this wasn't feasible is because the size of a Variable is
not known (by us, the library) at compile time. We parse an eBPF ELF
into a bunch of objects, including a slice of Variables, which
essentially includes all global vars and consts in the ELF. A Variable
has a size and an offset into a Memory.
We can't make a Variable generic, because again, types are not known
at Go compile time and different-size Variables need to be put into a
slice.
Additionally, Variables can be structs as well, and don't necessarily
need to be atomic. My initial implementation contained a bunch of
boilerplate for atomics, but this relegated all struct operations to
reflection-based marshaling using Variable.Set() and .Get() (some
amalgamation of binary.Read and .Write underneath).
At the end of the day, to keep things somewhat flexible and ergonomic,
this was the only option we were left with. Now, the caller can do:
```
type foo struct { a, b, uint64 }
var v *Variable
s, _ := VariablePointer[foo](v) // returns (*foo, error)
foo.a = 123
```
VariablePointer performs a bounds check on the underlying Memory and
the size of foo, and ensures it doesn't contain any pointers.
(binary.Size will enforce this)
Accesses to foo now go to shared kernel memory. foo can be passed
around the Go app without fear of the underlying memory getting
unmapped. *Memory and *Variable can be lost without issues.
> You received this message because you are subscribed to a topic in the Google Groups "golang-nuts" group.
> To unsubscribe from this topic, visit
https://groups.google.com/d/topic/golang-nuts/LzUMguyouXM/unsubscribe.
> To unsubscribe from this group and all its topics, send an email to
golang-nuts...@googlegroups.com.
> To view this discussion on the web visit
https://groups.google.com/d/msgid/golang-nuts/F9F590B4-A75E-4846-BF3E-A53AE41A2489%40ix.netcom.com.