|
|
@ -1,8 +1,28 @@ |
|
|
|
|
|
|
|
/* LocalOptimizer Copyright (C) 1999 Jochen Hoenicke. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* This program is free software; you can redistribute it and/or modify |
|
|
|
|
|
|
|
* it under the terms of the GNU General Public License as published by |
|
|
|
|
|
|
|
* the Free Software Foundation; either version 2, or (at your option) |
|
|
|
|
|
|
|
* any later version. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* This program is distributed in the hope that it will be useful, |
|
|
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
|
|
|
|
|
* GNU General Public License for more details. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* You should have received a copy of the GNU General Public License |
|
|
|
|
|
|
|
* along with this program; see the file COPYING. If not, write to |
|
|
|
|
|
|
|
* the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* $Id$ |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
package jode.obfuscator; |
|
|
|
package jode.obfuscator; |
|
|
|
import java.util.*; |
|
|
|
import java.util.*; |
|
|
|
import jode.bytecode.*; |
|
|
|
import jode.bytecode.*; |
|
|
|
|
|
|
|
import jode.AssertError; |
|
|
|
|
|
|
|
|
|
|
|
public class LocalOptimizer { |
|
|
|
public class LocalOptimizer implements Opcodes { |
|
|
|
|
|
|
|
|
|
|
|
class LocalInfo { |
|
|
|
class LocalInfo { |
|
|
|
LocalInfo shadow = null; |
|
|
|
LocalInfo shadow = null; |
|
|
@ -16,6 +36,7 @@ public class LocalOptimizer { |
|
|
|
|
|
|
|
|
|
|
|
Vector usingInstrs = new Vector(); |
|
|
|
Vector usingInstrs = new Vector(); |
|
|
|
Vector conflictingLocals = new Vector(); |
|
|
|
Vector conflictingLocals = new Vector(); |
|
|
|
|
|
|
|
int size; |
|
|
|
int newSlot = -1; |
|
|
|
int newSlot = -1; |
|
|
|
|
|
|
|
|
|
|
|
LocalInfo(InstrInfo instr) { |
|
|
|
LocalInfo(InstrInfo instr) { |
|
|
@ -53,15 +74,16 @@ public class LocalOptimizer { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
class InstrInfo { |
|
|
|
class InstrInfo { |
|
|
|
/** |
|
|
|
|
|
|
|
* Tell if the localsToRead variable has changed. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
boolean changed; |
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* The LocalInfo of the next Instruction, that may read a local, |
|
|
|
* The LocalInfo of the next Instruction, that may read a local, |
|
|
|
* without prior writing. |
|
|
|
* without prior writing. |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
LocalInfo[] nextReads; |
|
|
|
LocalInfo[] nextReads; |
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* The jsr to which the nextReads at this slot belongs to. |
|
|
|
|
|
|
|
* I think I don't like jsrs any more. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
Vector[] belongsToJsrs; |
|
|
|
/** |
|
|
|
/** |
|
|
|
* The LocalInfo for this local |
|
|
|
* The LocalInfo for this local |
|
|
|
*/ |
|
|
|
*/ |
|
|
@ -74,9 +96,14 @@ public class LocalOptimizer { |
|
|
|
* The next info in the chain. |
|
|
|
* The next info in the chain. |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
InstrInfo nextInfo; |
|
|
|
InstrInfo nextInfo; |
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* If instruction is a jsr, this array contains the ret instructions. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
Instruction[] retInstrs; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
InstrInfo firstInfo; |
|
|
|
InstrInfo firstInfo; |
|
|
|
|
|
|
|
Stack changedInfos; |
|
|
|
Hashtable instrInfos; |
|
|
|
Hashtable instrInfos; |
|
|
|
BytecodeInfo bc; |
|
|
|
BytecodeInfo bc; |
|
|
|
|
|
|
|
|
|
|
@ -84,12 +111,63 @@ public class LocalOptimizer { |
|
|
|
this.bc = bc; |
|
|
|
this.bc = bc; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public void promoteReads(InstrInfo info, Instruction preInstr) { |
|
|
|
/** |
|
|
|
|
|
|
|
* This method determines which rets belong to a given jsr. This |
|
|
|
|
|
|
|
* is needed, since the predecessors must be exact. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
void findRets(InstrInfo jsrInfo) { |
|
|
|
|
|
|
|
Vector rets = new Vector(); |
|
|
|
|
|
|
|
Stack instrStack = new Stack(); |
|
|
|
|
|
|
|
Instruction subInstr = jsrInfo.instr.succs[0]; |
|
|
|
|
|
|
|
if (subInstr.opcode != opc_astore) |
|
|
|
|
|
|
|
throw new AssertError("Non standard jsr"); |
|
|
|
|
|
|
|
int slot = subInstr.localSlot; |
|
|
|
|
|
|
|
instrStack.push(subInstr.nextByAddr); |
|
|
|
|
|
|
|
while (!instrStack.isEmpty()) { |
|
|
|
|
|
|
|
Instruction instr = (Instruction) instrStack.pop(); |
|
|
|
|
|
|
|
if (instr.localSlot == slot) { |
|
|
|
|
|
|
|
if (instr.opcode != opc_ret) |
|
|
|
|
|
|
|
throw new AssertError("Non standard jsr"); |
|
|
|
|
|
|
|
rets.addElement(instr); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (!instr.alwaysJumps) |
|
|
|
|
|
|
|
instrStack.push(instr.nextByAddr); |
|
|
|
|
|
|
|
if (instr.succs != null) |
|
|
|
|
|
|
|
for (int i=0; i< instr.succs.length; i++) |
|
|
|
|
|
|
|
instrStack.push(instr.succs[i]); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
jsrInfo.retInstrs = new Instruction[rets.size()]; |
|
|
|
|
|
|
|
rets.copyInto(jsrInfo.retInstrs); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Merges the given vector to a new vector. Both vectors may |
|
|
|
|
|
|
|
* be null in which case they are interpreted as empty vectors. |
|
|
|
|
|
|
|
* The vectors will never changed, but the result may be one |
|
|
|
|
|
|
|
* of the given vectors. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
Vector merge(Vector v1, Vector v2) { |
|
|
|
|
|
|
|
if (v1 == null || v1.isEmpty()) |
|
|
|
|
|
|
|
return v2; |
|
|
|
|
|
|
|
if (v2 == null || v2.isEmpty()) |
|
|
|
|
|
|
|
return v1; |
|
|
|
|
|
|
|
Vector result = (Vector) v1.clone(); |
|
|
|
|
|
|
|
Enumeration enum = v2.elements(); |
|
|
|
|
|
|
|
while (enum.hasMoreElements()) { |
|
|
|
|
|
|
|
Object elem = enum.nextElement(); |
|
|
|
|
|
|
|
if (!result.contains(elem)) |
|
|
|
|
|
|
|
result.addElement(elem); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return result; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void promoteReads(InstrInfo info, Instruction preInstr, |
|
|
|
|
|
|
|
Instruction belongingJsr) { |
|
|
|
InstrInfo preInfo = (InstrInfo) instrInfos.get(preInstr); |
|
|
|
InstrInfo preInfo = (InstrInfo) instrInfos.get(preInstr); |
|
|
|
int omitLocal = -1; |
|
|
|
int omitLocal = -1; |
|
|
|
if (preInstr.localSlot != -1 |
|
|
|
if (preInstr.localSlot != -1 |
|
|
|
&& preInstr.opcode >= BytecodeInfo.opc_istore |
|
|
|
&& preInstr.opcode >= opc_istore |
|
|
|
&& preInstr.opcode < BytecodeInfo.opc_iastore) { |
|
|
|
&& preInstr.opcode <= opc_astore) { |
|
|
|
/* This is a store */ |
|
|
|
/* This is a store */ |
|
|
|
omitLocal = preInstr.localSlot; |
|
|
|
omitLocal = preInstr.localSlot; |
|
|
|
if (info.nextReads[preInstr.localSlot] != null) |
|
|
|
if (info.nextReads[preInstr.localSlot] != null) |
|
|
@ -98,10 +176,30 @@ public class LocalOptimizer { |
|
|
|
} |
|
|
|
} |
|
|
|
int maxlocals = bc.getMaxLocals(); |
|
|
|
int maxlocals = bc.getMaxLocals(); |
|
|
|
for (int i=0; i< maxlocals; i++) { |
|
|
|
for (int i=0; i< maxlocals; i++) { |
|
|
|
|
|
|
|
Vector newBelongs = info.belongsToJsrs[i]; |
|
|
|
|
|
|
|
if (preInstr.opcode == opc_jsr |
|
|
|
|
|
|
|
&& newBelongs != null) { |
|
|
|
|
|
|
|
if (!info.belongsToJsrs[i].contains(preInstr)) |
|
|
|
|
|
|
|
/* This was the wrong jsr */ |
|
|
|
|
|
|
|
continue; |
|
|
|
|
|
|
|
if (info.belongsToJsrs[i].size() == 1) |
|
|
|
|
|
|
|
newBelongs = null; |
|
|
|
|
|
|
|
else { |
|
|
|
|
|
|
|
newBelongs = (Vector) info.belongsToJsrs[i].clone(); |
|
|
|
|
|
|
|
newBelongs.removeElement(preInstr); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (belongingJsr != null) { |
|
|
|
|
|
|
|
if (newBelongs == null) |
|
|
|
|
|
|
|
newBelongs = new Vector(); |
|
|
|
|
|
|
|
newBelongs.addElement(belongingJsr); |
|
|
|
|
|
|
|
} |
|
|
|
if (info.nextReads[i] != null && i != omitLocal) { |
|
|
|
if (info.nextReads[i] != null && i != omitLocal) { |
|
|
|
|
|
|
|
preInfo.belongsToJsrs[i] |
|
|
|
|
|
|
|
= merge(preInfo.belongsToJsrs[i], newBelongs); |
|
|
|
if (preInfo.nextReads[i] == null) { |
|
|
|
if (preInfo.nextReads[i] == null) { |
|
|
|
preInfo.nextReads[i] = info.nextReads[i]; |
|
|
|
preInfo.nextReads[i] = info.nextReads[i]; |
|
|
|
preInfo.changed = true; |
|
|
|
changedInfos.push(preInfo); |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
preInfo.nextReads[i] |
|
|
|
preInfo.nextReads[i] |
|
|
|
.combineInto(info.nextReads[i]); |
|
|
|
.combineInto(info.nextReads[i]); |
|
|
@ -115,6 +213,7 @@ public class LocalOptimizer { |
|
|
|
Handler[] handlers = bc.getExceptionHandlers(); |
|
|
|
Handler[] handlers = bc.getExceptionHandlers(); |
|
|
|
/* Initialize the InstrInfos and LocalInfos |
|
|
|
/* Initialize the InstrInfos and LocalInfos |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
|
|
|
|
changedInfos = new Stack(); |
|
|
|
instrInfos = new Hashtable(); |
|
|
|
instrInfos = new Hashtable(); |
|
|
|
{ |
|
|
|
{ |
|
|
|
InstrInfo info = firstInfo = new InstrInfo(); |
|
|
|
InstrInfo info = firstInfo = new InstrInfo(); |
|
|
@ -123,13 +222,25 @@ public class LocalOptimizer { |
|
|
|
instrInfos.put(instr, info); |
|
|
|
instrInfos.put(instr, info); |
|
|
|
info.instr = instr; |
|
|
|
info.instr = instr; |
|
|
|
info.nextReads = new LocalInfo[maxlocals]; |
|
|
|
info.nextReads = new LocalInfo[maxlocals]; |
|
|
|
|
|
|
|
info.belongsToJsrs = new Vector[maxlocals]; |
|
|
|
if (instr.localSlot != -1) { |
|
|
|
if (instr.localSlot != -1) { |
|
|
|
info.local = new LocalInfo(info); |
|
|
|
info.local = new LocalInfo(info); |
|
|
|
if (instr.opcode < BytecodeInfo.opc_istore |
|
|
|
info.local.size = 1; |
|
|
|
|| instr.opcode > BytecodeInfo.opc_iastore) { |
|
|
|
switch (instr.opcode) { |
|
|
|
|
|
|
|
case opc_lload: case opc_dload: |
|
|
|
|
|
|
|
info.local.size = 2; |
|
|
|
|
|
|
|
/* fall through */ |
|
|
|
|
|
|
|
case opc_iload: case opc_fload: case opc_aload: |
|
|
|
|
|
|
|
case opc_ret: |
|
|
|
|
|
|
|
case opc_iinc: |
|
|
|
/* this is a load instruction */ |
|
|
|
/* this is a load instruction */ |
|
|
|
info.nextReads[instr.localSlot] = info.local; |
|
|
|
info.nextReads[instr.localSlot] = info.local; |
|
|
|
info.changed = true; |
|
|
|
changedInfos.push(info); |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
case opc_lstore: case opc_dstore: |
|
|
|
|
|
|
|
info.local.size = 2; |
|
|
|
|
|
|
|
case opc_istore: case opc_fstore: case opc_astore: |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if ((instr = instr.nextByAddr) == null) |
|
|
|
if ((instr = instr.nextByAddr) == null) |
|
|
@ -138,54 +249,71 @@ public class LocalOptimizer { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (InstrInfo info = firstInfo; info != null; info = info.nextInfo) |
|
|
|
|
|
|
|
if (info.instr.opcode == opc_jsr) |
|
|
|
|
|
|
|
findRets(info); |
|
|
|
|
|
|
|
|
|
|
|
/* find out which locals are the same. |
|
|
|
/* find out which locals are the same. |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
boolean changed = true; |
|
|
|
while (!changedInfos.isEmpty()) { |
|
|
|
while (changed) { |
|
|
|
InstrInfo info = (InstrInfo) changedInfos.pop(); |
|
|
|
changed = false; |
|
|
|
Enumeration enum = info.instr.preds.elements(); |
|
|
|
for (InstrInfo info = firstInfo; |
|
|
|
while (enum.hasMoreElements()) { |
|
|
|
info != null; info = info.nextInfo) { |
|
|
|
Instruction preInstr |
|
|
|
if (info.changed) { |
|
|
|
= (Instruction) enum.nextElement(); |
|
|
|
info.changed = false; |
|
|
|
if (preInstr.opcode == opc_jsr |
|
|
|
Enumeration enum = info.instr.preds.elements(); |
|
|
|
&& info.instr != preInstr.succs[0]) { |
|
|
|
while (enum.hasMoreElements()) { |
|
|
|
/* Prev expr is a jsr, continue with the |
|
|
|
changed = true; |
|
|
|
* corresponding ret, instead with the jsr. |
|
|
|
Instruction preInstr |
|
|
|
*/ |
|
|
|
= (Instruction) enum.nextElement(); |
|
|
|
InstrInfo jsrInfo = |
|
|
|
promoteReads(info, preInstr); |
|
|
|
(InstrInfo) instrInfos.get(preInstr); |
|
|
|
} |
|
|
|
for (int i= jsrInfo.retInstrs.length; i-- > 0; ) |
|
|
|
for (int i=0; i<handlers.length; i++) { |
|
|
|
promoteReads(info, jsrInfo.retInstrs[i], preInstr); |
|
|
|
if (handlers[i].catcher == info.instr) { |
|
|
|
} else |
|
|
|
for (Instruction preInstr = handlers[i].start; |
|
|
|
promoteReads(info, preInstr, null); |
|
|
|
preInstr != handlers[i].end; |
|
|
|
} |
|
|
|
preInstr = preInstr.nextByAddr) { |
|
|
|
for (int i=0; i<handlers.length; i++) { |
|
|
|
promoteReads(info, preInstr); |
|
|
|
if (handlers[i].catcher == info.instr) { |
|
|
|
} |
|
|
|
for (Instruction preInstr = handlers[i].start; |
|
|
|
} |
|
|
|
preInstr != handlers[i].end.nextByAddr; |
|
|
|
|
|
|
|
preInstr = preInstr.nextByAddr) { |
|
|
|
|
|
|
|
promoteReads(info, preInstr, null); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
changedInfos = null; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* Now calculate the conflict settings. |
|
|
|
public void stripLocals() { |
|
|
|
*/ |
|
|
|
|
|
|
|
for (InstrInfo info = firstInfo; info != null; info = info.nextInfo) { |
|
|
|
for (InstrInfo info = firstInfo; info != null; info = info.nextInfo) { |
|
|
|
if (info.instr.localSlot != -1 |
|
|
|
if (info.local != null && info.local.usingInstrs.size() == 1) { |
|
|
|
&& info.instr.opcode >= BytecodeInfo.opc_istore |
|
|
|
/* If this is a store, whose value is never read; it can |
|
|
|
&& info.instr.opcode < BytecodeInfo.opc_iastore) { |
|
|
|
* be removed, i.e replaced by a pop. */ |
|
|
|
/* This is a store. It conflicts with every local, whose |
|
|
|
switch (info.instr.opcode) { |
|
|
|
* value will be read without write. |
|
|
|
case opc_istore: |
|
|
|
*/ |
|
|
|
case opc_fstore: |
|
|
|
for (int i=0; i < maxlocals; i++) { |
|
|
|
case opc_astore: |
|
|
|
if (i != info.instr.localSlot |
|
|
|
info.local = null; |
|
|
|
&& info.nextReads[i] != null) |
|
|
|
info.instr.opcode = opc_pop; |
|
|
|
info.local.conflictsWith(info.nextReads[i]); |
|
|
|
info.instr.length = 1; |
|
|
|
|
|
|
|
info.instr.localSlot = -1; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
case opc_lstore: |
|
|
|
|
|
|
|
case opc_dstore: |
|
|
|
|
|
|
|
info.local = null; |
|
|
|
|
|
|
|
info.instr.opcode = opc_pop2; |
|
|
|
|
|
|
|
info.instr.length = 1; |
|
|
|
|
|
|
|
info.instr.localSlot = -1; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
default: |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public void distributeLocals(Vector locals) { |
|
|
|
void distributeLocals(Vector locals) { |
|
|
|
if (locals.size() == 0) |
|
|
|
if (locals.size() == 0) |
|
|
|
return; |
|
|
|
return; |
|
|
|
|
|
|
|
|
|
|
@ -217,8 +345,18 @@ public class LocalOptimizer { |
|
|
|
for (int slot = 0; ; slot++) { |
|
|
|
for (int slot = 0; ; slot++) { |
|
|
|
Enumeration conflenum = bestLocal.conflictingLocals.elements(); |
|
|
|
Enumeration conflenum = bestLocal.conflictingLocals.elements(); |
|
|
|
while (conflenum.hasMoreElements()) { |
|
|
|
while (conflenum.hasMoreElements()) { |
|
|
|
if (((LocalInfo)conflenum.nextElement()).newSlot == slot) |
|
|
|
LocalInfo conflLocal = (LocalInfo)conflenum.nextElement(); |
|
|
|
|
|
|
|
if (bestLocal.size == 2 && conflLocal.newSlot == slot+1) { |
|
|
|
|
|
|
|
slot++; |
|
|
|
|
|
|
|
continue next_slot; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (conflLocal.size == 2 && conflLocal.newSlot+1 == slot) |
|
|
|
|
|
|
|
continue next_slot; |
|
|
|
|
|
|
|
if (conflLocal.newSlot == slot) { |
|
|
|
|
|
|
|
if (conflLocal.size == 2) |
|
|
|
|
|
|
|
slot++; |
|
|
|
continue next_slot; |
|
|
|
continue next_slot; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
bestLocal.newSlot = slot; |
|
|
|
bestLocal.newSlot = slot; |
|
|
|
break; |
|
|
|
break; |
|
|
@ -241,6 +379,23 @@ public class LocalOptimizer { |
|
|
|
firstInfo.nextReads[i].getReal().newSlot = i; |
|
|
|
firstInfo.nextReads[i].getReal().newSlot = i; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Now calculate the conflict settings. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
for (InstrInfo info = firstInfo; info != null; info = info.nextInfo) { |
|
|
|
|
|
|
|
if (info.instr.localSlot != -1 |
|
|
|
|
|
|
|
&& info.instr.opcode >= BytecodeInfo.opc_istore |
|
|
|
|
|
|
|
&& info.instr.opcode <= BytecodeInfo.opc_astore) { |
|
|
|
|
|
|
|
/* This is a store. It conflicts with every local, whose |
|
|
|
|
|
|
|
* value will be read without write. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
for (int i=0; i < maxlocals; i++) { |
|
|
|
|
|
|
|
if (i != info.instr.localSlot |
|
|
|
|
|
|
|
&& info.nextReads[i] != null) |
|
|
|
|
|
|
|
info.local.conflictsWith(info.nextReads[i]); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* Now put the locals that need a color into a vector. |
|
|
|
/* Now put the locals that need a color into a vector. |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
Vector locals = new Vector(); |
|
|
|
Vector locals = new Vector(); |
|
|
|