diff --git a/stdlib/moduleinit_test.go b/stdlib/moduleinit_test.go new file mode 100644 index 00000000..a3e21699 --- /dev/null +++ b/stdlib/moduleinit_test.go @@ -0,0 +1,58 @@ +// Copyright 2026 The go-python Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package stdlib_test + +import ( + "sync" + "testing" + + "github.com/go-python/gpython/py" + _ "github.com/go-python/gpython/stdlib" +) + +// A registered ModuleImpl is shared by every Context, so importing a CodeSrc +// module from several contexts at once must not race (run with -race). +func TestModuleInitConcurrentCodeSrc(t *testing.T) { + py.RegisterModule(&py.ModuleImpl{ + Info: py.ModuleInfo{ + Name: "codesrc_test", + FileDesc: "", + }, + CodeSrc: "X = 42\ndef get():\n return X\n", + }) + + const numContexts = 8 + start := make(chan struct{}) + var wg sync.WaitGroup + for i := 0; i < numContexts; i++ { + wg.Add(1) + go func() { + defer wg.Done() + ctx := py.NewContext(py.DefaultContextOpts()) + defer ctx.Close() + <-start + err := py.Import(ctx, "codesrc_test") + if err != nil { + t.Error(err) + return + } + module, err := ctx.GetModule("codesrc_test") + if err != nil { + t.Error(err) + return + } + res, err := module.Call("get", nil, nil) + if err != nil { + t.Error(err) + return + } + if res != py.Int(42) { + t.Errorf("get() = %v, want 42", res) + } + }() + } + close(start) + wg.Wait() +} diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index 7d1fb811..312179bc 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -76,6 +76,37 @@ func (ctx *context) ModuleInit(impl *py.ModuleImpl) (*py.Module, error) { return nil, err } + code, err := moduleCode(impl) + if err != nil { + return nil, err + } + + module, err := ctx.Store().NewModule(ctx, impl) + if err != nil { + return nil, err + } + + if code != nil { + _, err = ctx.RunCode(code, module.Globals, module.Globals, nil) + if err != nil { + return nil, err + } + } + + return module, nil +} + +// moduleCodeMu guards the compiling of ModuleImpl.CodeSrc or CodeBuf into +// ModuleImpl.Code, since a registered ModuleImpl is shared by every Context. +var moduleCodeMu sync.Mutex + +// moduleCode returns impl.Code, setting it from impl.CodeSrc or impl.CodeBuf +// the first time it is needed. +func moduleCode(impl *py.ModuleImpl) (*py.Code, error) { + moduleCodeMu.Lock() + defer moduleCodeMu.Unlock() + + var err error if impl.Code == nil && len(impl.CodeSrc) > 0 { impl.Code, err = py.Compile(string(impl.CodeSrc), impl.Info.FileDesc, py.ExecMode, 0, true) if err != nil { @@ -95,19 +126,7 @@ func (ctx *context) ModuleInit(impl *py.ModuleImpl) (*py.Module, error) { } } - module, err := ctx.Store().NewModule(ctx, impl) - if err != nil { - return nil, err - } - - if impl.Code != nil { - _, err = ctx.RunCode(impl.Code, module.Globals, module.Globals, nil) - if err != nil { - return nil, err - } - } - - return module, nil + return impl.Code, nil } // See interface py.Context defined in py/run.go