Skip to content

add mutex per interpreter per thread #698

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

Vipul-Cariappa
Copy link
Collaborator

@Vipul-Cariappa Vipul-Cariappa commented Aug 11, 2025

Big changes in this PR. Some parts of the explanation might be like a conversation.

Mutex lock per interpreter.

Question: Do we need a per-interpreter lock?
Answer: Yes. Ideally, that would improve the performance in multi-threaded cases.

Question: Does the InterOp API fully support dispatching queries (some kind of lookup) or compiling code across multiple interpreters at the same time?
Answer: Actually, (I guess) No. Our API only supports stack-like access of multiple interpreters.
Example code that will not work:

auto interpreter_1 = Cpp::CreateInterpreter();
Cpp::Compile(R"(
void f() {}
)");
auto f = Cpp::GetNamed("f");

auto interpreter_2 = Cpp::CreateInterpreter();
Cpp::Compile(R"(
void ff() {}
)");

auto ff = Cpp::GetNamed("ff");

auto f_callable = Cpp::MakeFunctionCallable(f); // This fails because:
// MakeFunctionCallable uses the interpreter instance 2 at line
// https://github.com/compiler-research/CppInterOp/blob/dba60ff3d829551b1c86128593ca817af78d67dc/lib/CppInterOp/CppInterOp.cpp#L3148-L3150

But you can make the above example work, if the user rotates the sInterpreters.
Example:

auto interpreter_1 = Cpp::CreateInterpreter();
Cpp::Compile(R"(
void f() {}
)");
auto f = Cpp::GetNamed("f");

auto interpreter_2 = Cpp::CreateInterpreter();
Cpp::Compile(R"(
void ff() {}
)");

auto ff = Cpp::GetNamed("ff");

Cpp::ActivateInterpreter(interpreter_1); // look at https://github.com/compiler-research/CppInterOp/blob/dba60ff3d829551b1c86128593ca817af78d67dc/lib/CppInterOp/CppInterOp.cpp#L3273-L3287
auto f_callable = Cpp::MakeFunctionCallable(f);

Question: Ok. So is there a way for InterOp is maintain the rotation thing?
Answer: Would not be a easy thing to achieve. From my initial view on the matter, it might be possible. But a more important question; Should we support such usecases?

Question: Ok. So given the limitation of using multiple interpreter but only as a stack-access. Do we need a mutex lock per interpreter?
Answer: No, the user can only access the top most interpreter at a time. We don't need a mutex per interpreter. (Let me know if my reasoning is wrong somewhere)

Testing this by running in parallel

gtest does not support running in parallel (I could not find anything with my google search). But there is this project, gtest-parallel, that can split the tasks into separate isolated processes. But that is not what we want. We want to split the tests to run in parallel threads that share the same interpreter instance.

Code recovery RAII

@vgvassilev, I need some pointers on incorporating the codegen error recovery RAII. I don't think we should combine the two into the same class, but let me know your opinion. And how should I go about doing it?

@Vipul-Cariappa Vipul-Cariappa marked this pull request as draft August 11, 2025 12:51
Copy link

codecov bot commented Aug 11, 2025

Codecov Report

❌ Patch coverage is 94.02985% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.34%. Comparing base (6cf1c06) to head (e6a33f8).

Files with missing lines Patch % Lines
lib/CppInterOp/CppInterOp.cpp 94.02% 12 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #698      +/-   ##
==========================================
+ Coverage   79.83%   80.34%   +0.50%     
==========================================
  Files           9        9              
  Lines        3962     4115     +153     
==========================================
+ Hits         3163     3306     +143     
- Misses        799      809      +10     
Files with missing lines Coverage Δ
lib/CppInterOp/CppInterOp.cpp 88.38% <94.02%> (+0.34%) ⬆️
Files with missing lines Coverage Δ
lib/CppInterOp/CppInterOp.cpp 88.38% <94.02%> (+0.34%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

There were too many comments to post at once. Showing the first 10 out of 13. Check the log or trigger a new build to see more.

@@ -125,8 +193,17 @@

// std::deque avoids relocations and calling the dtor of InterpreterInfo.
static llvm::ManagedStatic<std::deque<InterpreterInfo>> sInterpreters;
static std::mutex InterpreterStackLock;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'InterpreterStackLock' is non-const and globally accessible, consider making it const [cppcoreguidelines-avoid-non-const-global-variables]

static std::mutex InterpreterStackLock;
                  ^

auto* D = (clang::Decl*)handle;
std::vector<TCppScope_t> GetEnumConstants(TCppScope_t scope) {
LOCK(getInterpInfo());
auto* D = (clang::Decl*)scope;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: do not use C-style cast to convert between unrelated types [cppcoreguidelines-pro-type-cstyle-cast]

  auto* D = (clang::Decl*)scope;
            ^

@Vipul-Cariappa Vipul-Cariappa marked this pull request as ready for review August 11, 2025 13:29
@@ -224,11 +301,18 @@ std::string Demangle(const std::string& mangled_name) {
return demangle;
}

void EnableDebugOutput(bool value /* =true*/) { llvm::DebugFlag = value; }
void EnableDebugOutput(bool value /* =true*/) {
LOCK(getInterpInfo());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a different mental model for this. I was thinking that we can have a StateMutatingSection RAII object which in turn has a mutex-based locking (if multithreading was enabled) and a 'PushTransactionRAII` (for pch/modules). The object should take a parameter if the change is in clang AST which might trigger a derserialiazation or llvm-only. In the latter case we can skip pushing a transaction.

There will be cases where we need one of the two concepts and not both. We need to design a flexible solution addressing these needs without having to introduce both concepts at the same time when they are not needed.

if (result.empty())
Cpp::Declare("#include <utility>");
{
LOCK(getInterpInfo());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lock should be done in Declare. The next 2-3 lines should not be changing the compiler state (unless modules are involved)...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we should lock the AST when we do any named lookups. What if some other thread is writing to the AST at the same time? These things should be atomic. Either we finish the read and perform the write, or finish the write and then perform the read. We cannot be writing and reading the symbol table at the same time.
But say we want to check the number of arguments of a function, that would not change once the function is parsed. So we can read that data without any locks.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

@@ -125,8 +133,17 @@ struct InterpreterInfo {

// std::deque avoids relocations and calling the dtor of InterpreterInfo.
static llvm::ManagedStatic<std::deque<InterpreterInfo>> sInterpreters;
static std::mutex InterpreterStackLock;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'sInterpreters' is non-const and globally accessible, consider making it const [cppcoreguidelines-avoid-non-const-global-variables]

static llvm::ManagedStatic<std::deque<InterpreterInfo>> sInterpreters;
                                                        ^

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add the relevant suppression comments for clang-tidy?

@@ -125,8 +133,17 @@

// std::deque avoids relocations and calling the dtor of InterpreterInfo.
static llvm::ManagedStatic<std::deque<InterpreterInfo>> sInterpreters;
static std::mutex InterpreterStackLock;

static InterpreterInfo& getInterpInfo() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'InterpreterStackLock' is non-const and globally accessible, consider making it const [cppcoreguidelines-avoid-non-const-global-variables]

static std::mutex InterpreterStackLock;
                  ^

@@ -350,8 +379,10 @@

bool IsAbstract(TCppType_t klass) {
auto* D = (clang::Decl*)klass;
if (auto* CXXRD = llvm::dyn_cast_or_null<clang::CXXRecordDecl>(D))
if (auto* CXXRD = llvm::dyn_cast_or_null<clang::CXXRecordDecl>(D)) {
LOCK(getInterpInfo());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: do not use C-style cast to convert between unrelated types [cppcoreguidelines-pro-type-cstyle-cast]

  auto* D = (clang::Decl*)klass;
            ^

@@ -451,6 +483,7 @@
std::vector<TCppScope_t> GetEnumConstants(TCppScope_t handle) {
auto* D = (clang::Decl*)handle;

LOCK(getInterpInfo());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: do not use C-style cast to convert between unrelated types [cppcoreguidelines-pro-type-cstyle-cast]

  auto* D = (clang::Decl*)handle;
            ^

@@ -822,6 +861,9 @@
return -1;
CXXRecordDecl* DCXXRD = cast<CXXRecordDecl>(DD);
CXXRecordDecl* BCXXRD = cast<CXXRecordDecl>(BD);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: use auto when initializing with a template cast to avoid duplicating the type name [modernize-use-auto]

Suggested change
auto* DCXXRD = cast<CXXRecordDecl>(DD);

@@ -1862,6 +1927,7 @@
if (!builtin.isNull())
return builtin.getAsOpaquePtr();

LOCK(getInterpInfo());
auto* D = (Decl*)GetNamed(name, /* Within= */ 0);
if (auto* TD = llvm::dyn_cast_or_null<TypeDecl>(D)) {
return QualType(TD->getTypeForDecl(), 0).getAsOpaquePtr();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: argument name 'Within' in comment does not match parameter name 'parent' [bugprone-argument-comment]

  auto* D = (Decl*)GetNamed(name, /* Within= */ 0);
                                  ^
Additional context

include/CppInterOp/CppInterOp.h:411: 'parent' declared here

                                    TCppScope_t parent = nullptr);
                                                ^

lib/CppInterOp/CppInterOp.cpp:718: actual callee ('GetNamed') is declared here

TCppScope_t GetNamed(const std::string& name,
            ^

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

There were too many comments to post at once. Showing the first 10 out of 19. Check the log or trigger a new build to see more.

@@ -126,15 +134,50 @@ struct InterpreterInfo {
};

// std::deque avoids relocations and calling the dtor of InterpreterInfo.
static llvm::ManagedStatic<std::deque<InterpreterInfo>> sInterpreters;
static llvm::ManagedStatic<std::deque<std::shared_ptr<InterpreterInfo>>>
sInterpreters;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'sInterpreters' is non-const and globally accessible, consider making it const [cppcoreguidelines-avoid-non-const-global-variables]

    sInterpreters;
    ^

static llvm::ManagedStatic<std::deque<std::shared_ptr<InterpreterInfo>>>
sInterpreters;
static llvm::ManagedStatic<
std::unordered_map<clang::ASTContext*, std::weak_ptr<InterpreterInfo>>>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "std::unordered_map" is directly included [misc-include-cleaner]

lib/CppInterOp/CppInterOp.cpp:40:

- #if CLANG_VERSION_MAJOR >= 19
+ #include <unordered_map>
+ #if CLANG_VERSION_MAJOR >= 19

sInterpreters;
static llvm::ManagedStatic<
std::unordered_map<clang::ASTContext*, std::weak_ptr<InterpreterInfo>>>
sInterpreterASTMap;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'sInterpreterASTMap' is non-const and globally accessible, consider making it const [cppcoreguidelines-avoid-non-const-global-variables]

    sInterpreterASTMap;
    ^

@@ -684,13 +747,14 @@
TCppScope_t GetNamed(const std::string& name,
TCppScope_t parent /*= nullptr*/) {
clang::DeclContext* Within = 0;
auto* D = (clang::Decl*)parent;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: do not use C-style cast to convert between unrelated types [cppcoreguidelines-pro-type-cstyle-cast]

  auto* D = (clang::Decl*)parent;
            ^

D = GetUnderlyingScope(D);
Within = llvm::dyn_cast<clang::DeclContext>(D);
}

auto* ND = Cpp_utils::Lookup::Named(&getSema(), name, Within);
auto* ND = Cpp_utils::Lookup::Named(&getSema(D), name, Within);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "Cpp::utils::Lookup::Named" is directly included [misc-include-cleaner]

lib/CppInterOp/CppInterOp.cpp:12:

+ #include "CppInterOpInterpreter.h"

@@ -734,7 +801,11 @@
TCppScope_t GetBaseClass(TCppScope_t klass, TCppIndex_t ibase) {
auto* D = (Decl*)klass;
auto* CXXRD = llvm::dyn_cast_or_null<CXXRecordDecl>(D);
if (!CXXRD || CXXRD->getNumBases() <= ibase)
if (!CXXRD)
return 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: use nullptr [modernize-use-nullptr]

Suggested change
return 0;
return nullptr;

return interp.getCI()->getSema().LookupDefaultConstructor(CXXRD);
}

TCppFunction_t GetDefaultConstructor(TCppScope_t scope) {
return GetDefaultConstructor(getInterp(), scope);
auto* CXXRD = (clang::CXXRecordDecl*)scope;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: do not use C-style cast to convert between unrelated types [cppcoreguidelines-pro-type-cstyle-cast]

  auto* CXXRD = (clang::CXXRecordDecl*)scope;
                ^


auto* D = (clang::Decl*)func;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: do not use C-style cast to convert between unrelated types [cppcoreguidelines-pro-type-cstyle-cast]

  auto* D = (clang::Decl*)func;
            ^

@@ -1085,12 +1172,14 @@
// the template function exists and >1 means overloads
bool ExistsFunctionTemplate(const std::string& name, TCppScope_t parent) {
DeclContext* Within = 0;
auto* D = (Decl*)parent;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: do not use C-style cast to convert between unrelated types [cppcoreguidelines-pro-type-cstyle-cast]

  auto* D = (Decl*)parent;
            ^

auto& S = getSema();
if (candidates.empty())
return nullptr;
InterpreterInfo& II = getInterpInfo((clang::Decl*)candidates[0]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: do not use C-style cast to convert between unrelated types [cppcoreguidelines-pro-type-cstyle-cast]

  InterpreterInfo& II = getInterpInfo((clang::Decl*)candidates[0]);
                                      ^

@@ -361,3 +361,22 @@ if (llvm::sys::RunningOnValgrind())
delete ExtInterp;
#endif
}

TEST(InterpreterTest, MultipleInterpreter) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will need a proper readthedocs design document, too :)

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

@@ -1349,7 +1451,7 @@ TCppFuncAddr_t GetFunctionAddress(const char* mangled_name) {

static TCppFuncAddr_t GetFunctionAddress(const FunctionDecl* FD) {
const auto get_mangled_name = [](const FunctionDecl* FD) {
auto MangleCtxt = getASTContext().createMangleContext();
auto MangleCtxt = getASTContext(FD).createMangleContext();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: 'auto MangleCtxt' can be declared as 'auto *MangleCtxt' [llvm-qualified-auto]

Suggested change
auto MangleCtxt = getASTContext(FD).createMangleContext();
auto *MangleCtxt = getASTContext(FD).createMangleContext();

static Decl* InstantiateTemplate(TemplateDecl* TemplateD,
TemplateArgumentListInfo& TLI, Sema& S,
bool instantiate_body) {
Decl* InstantiateTemplate(TemplateDecl* TemplateD,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'InstantiateTemplate' can be made static or moved into an anonymous namespace to enforce internal linkage [misc-use-internal-linkage]

Suggested change
Decl* InstantiateTemplate(TemplateDecl* TemplateD,
static Decl* InstantiateTemplate(TemplateDecl* TemplateD,

Cpp::Declare(R"(
void f() {}
)");
auto f = Cpp::GetNamed("f");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: 'auto f' can be declared as 'auto *f' [llvm-qualified-auto]

Suggested change
auto f = Cpp::GetNamed("f");
auto *f = Cpp::GetNamed("f");

Cpp::Declare(R"(
void ff() {}
)");
auto ff = Cpp::GetNamed("ff");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: 'auto ff' can be declared as 'auto *ff' [llvm-qualified-auto]

Suggested change
auto ff = Cpp::GetNamed("ff");
auto *ff = Cpp::GetNamed("ff");

@mcbarton
Copy link
Collaborator

A note for this PR. The Emscripten build of CppInterOp doesn't currently support threads

-DLLVM_ENABLE_THREADS=OFF \

It is possible to turns threads on, and modify CppInterOp, so that it builds with threads. I did this locally yesterday (I did this in the past before we had tests). The tests can run, but several of our currently passing tests fail with threads enabled. We also get a warning about shared memory for the passing tests. I can put a PR showing my current progress, but I don't know how far I would get enabling past what I already have since shared library with pthreads is labelled as experimental on Emscripten, so we might be trying to fight possible bugs in Emscripten.

@@ -361,3 +361,22 @@ if (llvm::sys::RunningOnValgrind())
delete ExtInterp;
#endif
}

TEST(InterpreterTest, MultipleInterpreter) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its probably fine to disable this test for llvm 19 Emscripten builds, since the llvm 20 Emscripten build passes. There are already many tests we have passing for llvm 20, but not llvm 19.

@@ -0,0 +1,2 @@
Checks: >
-cppcoreguidelines-avoid-non-const-global-variables
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant // NOLINTNEXTLINE or something..

We need to identify which interpreter a Decl belongs to, when using multiple interpreter.
We do it by checking which `clang::ASTContext` the `clang::Decl` belongs
We maintain a map: `clang::ASTContext -> Cpp::InterpreterInfo`. Using this map, be identify the correct interpreter.

There are 2 usecases for this:
1. We can now lock the correct interpreter making it thread safe.
2. User of `libCppInterOp` need not set the correct active interpreter using `Cpp::ActivateInterpreter`, this information can be retrived using the map.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants