Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
4 changes: 3 additions & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
6 changes: 3 additions & 3 deletions gradlew

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 12 additions & 23 deletions gradlew.bat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,49 @@ private void saveLocals(
}
}

@Override
public InsnList defaultInitLocals(MethodInsnNode methodCall, MetaInfo metaInfo) {
var instructions = new InsnList();
if (!metaInfo.tags.contains(TAG_TAIL_CALL)) {
var frameAfter = metaInfo.frameAfter;
var isMethodNotStatic = isNotStatic(methodCall);
for (var code : ValueCodeFactory.CODES) {
for (int local = isMethodNotStatic ? 1 : 0, end = frameAfter.getLocals() - 1; local <= end; local++) {
var value = frameAfter.getLocal(local);
if (code.isResponsibleFor(value.getType())) {
var extendedValue = (ExtendedValue) value;
var lowestLocal = frameAfter.getLowestNeededLocal(extendedValue);
if (local == lowestLocal) {
// Pre-initialize with type default to satisfy verifier when GETFIELD can throw.
var localCode = code(extendedValue);
instructions.add(localCode.pushNull());
instructions.add(localCode.store(local));
}
}
}
}
}
return instructions;
}

@Override
public InsnList restoreLocalsFromFrame(MethodInsnNode methodCall, MetaInfo metaInfo, int localFrame) {
var instructions = new InsnList();
if (!metaInfo.tags.contains(TAG_TAIL_CALL)) {
restoreLocals(methodCall, metaInfo.frameAfter, localFrame, instructions);
}
return instructions;
}

@Override
public InsnList restoreStackFromFrame(MethodInsnNode methodCall, MetaInfo metaInfo, int localFrame) {
var instructions = new InsnList();
if (!metaInfo.tags.contains(TAG_TAIL_CALL)) {
restoreStack(methodCall, metaInfo.frameAfter, localFrame, instructions);
}
return instructions;
}

@Override
public InsnList restoreFrame(MethodInsnNode methodCall, MetaInfo metaInfo, int localFrame) {
var instructions = new InsnList();
Expand Down
42 changes: 42 additions & 0 deletions src/main/java/org/serialthreads/transformer/code/ThreadCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,48 @@ public interface ThreadCode {
*/
InsnList pushMethod(int localFrame);

/**
* Initialize local variables to their type-default values (0 for primitives, null for references).
* Must be called before {@link #restoreLocalsFromFrame} to ensure locals are defined at exception
* handlers even if a GETFIELD in restoreLocalsFromFrame throws before all stores complete.
*
* @param methodCall
* method call to process.
* @param metaInfo
* Meta information about method call.
* @return Generated code.
*/
InsnList defaultInitLocals(MethodInsnNode methodCall, MetaInfo metaInfo);

/**
* Restore local variables from the current frame before resuming the method call.
* Must be called before the copy method call so that locals are initialized even
* if the copy method throws an exception caught by an enclosing try-catch block.
*
* @param methodCall
* method call to process.
* @param metaInfo
* Meta information about method call.
* @param localFrame
* number of local containing the frame.
* @return Generated code.
*/
InsnList restoreLocalsFromFrame(MethodInsnNode methodCall, MetaInfo metaInfo, int localFrame);

/**
* Restore operand stack values from the current frame after resuming the method call.
* Must be called after the copy method returns normally.
*
* @param methodCall
* method call to process.
* @param metaInfo
* Meta information about method call.
* @param localFrame
* number of local containing the frame.
* @return Generated code.
*/
InsnList restoreStackFromFrame(MethodInsnNode methodCall, MetaInfo metaInfo, int localFrame);

/**
* Restore current frame before resuming the method call.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,15 @@ private void check(ClassNode clazz, MethodNode method) {
*/
private void reanalyzeMethods(ClassNode clazz, List<MethodNode> methods) throws NotTransformableException {
for (var method : methods) {
// Skip reanalysis for methods with exception handlers.
// After LocalVariablesShifter.shift() is called, the frame information at exception
// handler entry points becomes stale, causing the analyzer to fail.
// The frames will be properly recomputed when the class is written with COMPUTE_FRAMES.
if (method.tryCatchBlocks != null && !method.tryCatchBlocks.isEmpty()) {
logger.debug("Skipping reanalysis of {} due to exception handlers", methodName(clazz, method));
continue;
}

try {
ExtendedAnalyzer.analyze(clazz, method, classInfoCache);
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public MethodNode transform() throws AnalyzerException {

replaceReturns();
var restores = insertCaptureAndRestoreCode(true);
// updateExceptionTableForCaptureCode(restores); // TODO: May not be needed
createRestoreHandlerCopy(restores);
addThreadAndFrame();
fixMaxs();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import org.serialthreads.transformer.strategies.AbstractMethodTransformer;
import org.serialthreads.transformer.strategies.MetaInfo;

import java.util.List;

import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ICONST_0;
import static org.objectweb.asm.Opcodes.ICONST_1;
Expand Down Expand Up @@ -254,13 +256,27 @@ protected LabelNode createCaptureAndRestoreCodeForMethod(MethodInsnNode methodCa
if (restore) {
instructions.add(restoreLabel);

// If this method call is inside a try-catch block, restore locals BEFORE calling the copy
// method. This ensures that if the copy method throws an exception caught by an enclosing
// try-catch, the locals have their saved values rather than type defaults.
// Note: RunMethodTransformer also inserts a pre-init redirect (defaultInitLocals + GOTO)
// outside the try-catch range so that the ASM verifier sees initialized locals at the
// restore label's entry (see redirectRestoreLabelsOutsideTryCatch).
var insideTryCatch = isInsideTryCatch(methodCall);
if (insideTryCatch) {
instructions.add(threadCode.restoreLocalsFromFrame(methodCall, metaInfo, localFrame));
}
// Call interrupted method.
instructions.add(callCopyMethod(methodCall, metaInfo));
// If serializing, return early, the frame already has been captured.
instructions.add(new JumpInsnNode(IFNE, serializing));

// Restore stack "under" the returned value, if any.
instructions.add(threadCode.restoreFrame(methodCall, metaInfo, localFrame));
// Restore frame (locals + stack, or just stack if locals were already restored above).
if (insideTryCatch) {
instructions.add(threadCode.restoreStackFromFrame(methodCall, metaInfo, localFrame));
} else {
instructions.add(threadCode.restoreFrame(methodCall, metaInfo, localFrame));
}
// Continue.
}

Expand Down Expand Up @@ -422,4 +438,127 @@ protected void fixMaxs() {
// TODO 2009-10-11 mh: recalculate minimum maxs
method.maxStack = Math.max(method.maxStack + 2, 5);
}

/**
* Check if an instruction is inside any try-catch block range of the current method.
*
* @param instruction Instruction to check.
* @return True if the instruction is inside a try-catch block.
*/
private boolean isInsideTryCatch(AbstractInsnNode instruction) {
if (method.tryCatchBlocks == null || method.tryCatchBlocks.isEmpty()) {
return false;
}
return method.tryCatchBlocks.stream()
.anyMatch(tcb -> isInstructionInRange(instruction, tcb.start, tcb.end));
}

/**
* Update exception table to cover inserted capture and restore code.
* When an interruptible method call is inside a try-catch block, the capture code
* inserted after the call must also be covered by the same exception handler.
*
* @param restores Labels pointing to the generated restore codes for method calls.
*/
protected void updateExceptionTableForCaptureCode(List<LabelNode> restores) {
if (method.tryCatchBlocks == null || method.tryCatchBlocks.isEmpty()) {
return;
}

logger.debug(" Updating exception table for capture code");

// Build a map from method calls to their restore labels for quick lookup
var callToRestore = new java.util.HashMap<MethodInsnNode, LabelNode>();
int index = 0;
for (var methodCall : interruptibleMethodCalls) {
if (index < restores.size() && restores.get(index) != null) {
callToRestore.put(methodCall, restores.get(index));
}
index++;
}

// For each exception handler, check if it needs to be extended
for (var tryCatchBlock : method.tryCatchBlocks) {
var start = tryCatchBlock.start;
var end = tryCatchBlock.end;

// Find the last interruptible method call within this try block
MethodInsnNode lastCallInBlock = null;
for (var methodCall : interruptibleMethodCalls) {
if (isInstructionInRange(methodCall, start, end)) {
lastCallInBlock = methodCall;
}
}

// If we found calls in this block, extend the end to cover restore code
if (lastCallInBlock != null) {
var restoreLabel = callToRestore.get(lastCallInBlock);
if (restoreLabel != null) {
// The restore label marks where execution continues after restoring
// We need to move the end label to after the restore code
// Find the "normal" label which marks the end of capture/restore code
var normalLabel = findNormalLabel(lastCallInBlock);
if (normalLabel != null) {
tryCatchBlock.end = normalLabel;
logger.debug(" Extended exception handler to cover capture code for {}", lastCallInBlock.name);
}
}
}
}
}

/**
* Find the "normal" execution label after capture code for a method call.
* This label marks where normal (non-serializing) execution continues.
*
* @param methodCall Method call instruction.
* @return Normal execution label, or null if not found.
*/
private LabelNode findNormalLabel(MethodInsnNode methodCall) {
// After a method call, the capture code structure includes a "normal" label
// that marks where execution continues in the non-serializing case
// Scan forward from the method call to find this label
var current = methodCall.getNext();
int labelCount = 0;
while (current != null && labelCount < 3) {
if (current instanceof LabelNode label) {
// The pattern in createCaptureAndRestoreCodeForMethod creates:
// - First: serializing label
// - Second: restore label (if restore == true)
// - Third: normal label
// We want the "normal" label which is typically after the restore label
labelCount++;
if (labelCount >= 2) {
// Return the label after serializing/restore
return label;
}
}
current = current.getNext();
// Don't scan too far
if (current != null && current.getOpcode() >= 0) {
// Hit a real instruction, might have found our label already
break;
}
}
return null;
}

/**
* Check if an instruction is within the range defined by start and end labels.
*
* @param instruction Instruction to check.
* @param start Start label of the range.
* @param end End label of the range.
* @return True if instruction is in range.
*/
private boolean isInstructionInRange(AbstractInsnNode instruction, LabelNode start, LabelNode end) {
var current = start.getNext();
while (current != null && current != end) {
if (current == instruction) {
return true;
}
current = current.getNext();
}
return false;
}
}
Loading