33

While working and generating protobuf stubs in go I stumbled upon this interesting issue.

Whenever I try and copy a message's struct by value I get this warning:

call of state.world.script.HandleEvent copies lock value: throne/server/messages.PlayerDialogeStatus contains google.golang.org/protobuf/internal/impl.MessageState contains sync.Mutex copylocks

While I understand why copying a mutex lock by value is wrong, I started wondering why are they even there in the first place.

And thus my question: Why does the go generated protobuf files contain mutex locks placed on the message structs, specifically on the MessageState struct?

Or alternatively: What is the goal of the mutex lock placed in the MessageState struct found on generated protobuf message structs?

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
Nitzan
  • 1,669
  • 3
  • 19
  • 33

2 Answers2

18

The impl.MessageState is embedded in concrete messages only, not in the generated structs that implement a proto message.

It specifically embeds the three pragmas: NoUnkeyedLiterals, DoNotCompare, and DoNotCopy.

The last one, DoNotCopy is a zero-sized array of sync.Mutex. The sole purpose is to have go vet complain loudly about shallow copies, as described in the comment:

DoNotCopy can be embedded in a struct to help prevent shallow copies. This does not rely on a Go language feature, but rather a special case within the vet checker.

The summary of it all: impl.MessageState is not supposed to be copied and the mutex is there only to catch copying. If you do so, it is because you are using something the wrong way.

Marc
  • 19,394
  • 6
  • 47
  • 51
  • 15
    _If you do so, it is because you are using something the wrong way._ Can you expand on that? What is the downside of copying it? What sorts of problems does it invite? – Jeff Widman Oct 15 '20 at 08:39
  • 7
    I also would like to learn what I was doing wrong by passing proto structures by value. This change caused massive inconvenience in upgrading to the newest version of proto in our project. I prefer not to deal with nils if possible and rely on the empty default value. – Jakub Dyszkiewicz Dec 08 '20 at 15:00
11

As far as I can tell, there are three reasons the Go protobuf API includes the DoNotCopy mutex:

  1. The maintainers may wish to change the internal representation in the future, in a way that will not work with shallow copies.
  2. It is theoretically unsafe to mix atomic and non-atomic accesses to the same memory. The protobuf structs contains an internal field that is usually read and written with an atomic access. Calling msg.Marshal() on a message, then overwriting it with *msg = MyMessage{...} mixes atomic and non-atomic accesses. Even if this works with today's implementation on x86, there is no guarantee this will work on other systems in the future. (See a long Go issue about this).
  3. If you call ProtoReflect() on a message, then later overwrite the message, it will crash, since the ProtoReflect() result relies on the internal reflection pointer (original issue):
d := &durationpb.Duration{Seconds: 1}
protoreflectMessage := d.ProtoReflect()
fmt.Printf("protoreflectMessage.Interface()=%v\n", protoreflectMessage.Interface())
*d = durationpb.Duration{Seconds: 2}
fmt.Printf("protoreflectMessage.Interface()=%v\n", protoreflectMessage.Interface())

This crashes with:

protoreflectMessage.Interface()=seconds:1
panic: invalid nil message info; this suggests memory corruption due to a race or shallow copy on the message struct
Evan Jones
  • 111
  • 1
  • 3