To protect your data, the CISO officer has suggested users to enable GitLab 2FA as soon as possible.

Commit a52e314d authored by Kunshan Wang's avatar Kunshan Wang
Browse files

Merge branch 'issue52-threadlocal'

parents 2ffbfdcd 5ad721d5
# Mu Reference Implementation version 2
Version 2.1.1
Version 2.2.0
This project is the current reference implementation of Mu, the micro virtual
machine designed by [The Micro Virtual Machine Project](http://microvm.org).
Version 2.1.x implements the [master branch of the Mu
Specification](https://github.com/microvm/microvm-spec/tree/goto-with-values)
which uses the goto-with-values form.
Version 2.2.0 implements the [Mu Specification with experimental threadlocal
object references](https://github.com/microvm/microvm-spec/tree/issue52-threadlocal).
This project is based on the previous works of
[simplest-microvm-project](https://github.com/microvm/simplest-microvm-project).
......
......@@ -7,7 +7,7 @@ organization := "org.microvm"
name := "microvm-refimpl2"
version := "2.1.1"
version := "2.2.0"
description := "The second reference implementation of Mu, the micro virtual machine"
......
......@@ -242,9 +242,16 @@ struct MuCtx {
// Thread and stack creation and stack destruction
MuStackRefValue (*new_stack )(MuCtx *ctx, MuFuncRefValue func);
MuThreadRefValue (*new_thread)(MuCtx *ctx, MuStackRefValue stack,
MuHowToResume htr, MuValue *vals, int nvals, MuRefValue exc);
MuRefValue threadlocal,
MuHowToResume htr, MuValue *vals, int nvals,
MuRefValue exc);
void (*kill_stack)(MuCtx *ctx, MuStackRefValue stack);
// Thread-local object reference
void (*set_threadlocal)(MuCtx *ctx, MuThreadRefValue thread,
MuRefValue threadlocal);
MuRefValue (*get_threadlocal)(MuCtx *ctx, MuThreadRefValue thread);
// Frame cursor operations
MuFCRefValue (*new_cursor )(MuCtx *ctx, MuStackRefValue stack);
void (*next_frame )(MuCtx *ctx, MuFCRefValue cursor);
......
......@@ -8,6 +8,7 @@
#include <muapi.h>
const char *hw_string = "Hello world!\n";
const char *hw2_string = "Goodbye world!\n";
const char *gc_conf =
"sosSize=524288\n"
......@@ -16,7 +17,6 @@ const char *gc_conf =
"stackSize=32768\n"
;
int main() {
MuVM *mvm = mu_refimpl2_new_ex(gc_conf);
......@@ -37,7 +37,9 @@ int main() {
".const @the_write <@write.fp> = 0x%lx\n"
".const @the_string <@cvoidptr> = 0x%lx\n"
".const @the_length <@csize_t> = 0x%lx\n"
, (uintptr_t)write, (uintptr_t)hw_string, (unsigned long)strlen(hw_string));
".const @the_length2 <@csize_t> = 0x%lx\n"
, (uintptr_t)write, (uintptr_t)hw_string,
(unsigned long)strlen(hw_string), (unsigned long)strlen(hw2_string));
char *bundle3 =
".funcsig @v_v = ()->()\n"
......@@ -61,11 +63,45 @@ int main() {
MuFuncRefValue func = ctx->handle_from_func(ctx, ctx->id_of(ctx, "@hw"));
MuStackRefValue stack = ctx->new_stack(ctx, func);
MuThreadRefValue thread = ctx->new_thread(ctx, stack, MU_REBIND_PASS_VALUES,
NULL, 0, NULL);
MuThreadRefValue thread = ctx->new_thread(ctx, stack, NULL,
MU_REBIND_PASS_VALUES, NULL, 0, NULL);
mvm->execute(mvm);
char *bundle4 =
".typedef @cchar = int<8>\n"
".typedef @charhybrid = hybrid<@cchar>\n"
".typedef @refvoid = ref<@void>\n"
".funcdef @hw2 VERSION %1 <@v_v> {\n"
" %entry():\n"
" %tl = COMMINST @uvm.get_threadlocal\n"
" %p = COMMINST @uvm.native.pin <@refvoid> (%tl)\n"
" %rv = CCALL #DEFAULT <@write.fp @write.sig> @the_write (@the_fd %p @the_length2)\n"
" COMMINST @uvm.native.unpin <@refvoid> (%tl)\n"
" COMMINST @uvm.thread_exit\n"
"}\n"
;
printf("Loading additional bundle...\n");
ctx->load_bundle(ctx, bundle4, strlen(bundle4));
printf("Bundle loaded. Create a thread-local string object...\n");
MuIntValue hlen = ctx->handle_from_sint32(ctx, 256, 32);
MuRefValue hobj = ctx->new_hybrid(ctx, ctx->id_of(ctx, "@charhybrid"), hlen);
MuUPtrValue hpobj = ctx->pin(ctx, hobj);
char *mustrbuf = (char*)ctx->handle_to_ptr(ctx, hpobj);
strcpy(mustrbuf, hw2_string);
ctx->unpin(ctx, hobj);
printf("Object populated. Create thread with threadlocal and execute...\n");
MuFuncRefValue func2 = ctx->handle_from_func(ctx, ctx->id_of(ctx, "@hw2"));
MuStackRefValue stack2 = ctx->new_stack(ctx, func2);
MuThreadRefValue thread2 = ctx->new_thread(ctx, stack2, hobj,
MU_REBIND_PASS_VALUES, NULL, 0, NULL);
mvm->execute(mvm);
mu_refimpl2_close(mvm);
return 0;
......
......@@ -86,7 +86,7 @@ with mu.new_context() as ctx:
_start = ctx.handle_from_func(id_of("@_start"))
st = ctx.new_stack(_start)
th = ctx.new_thread(st, PassValues(args, rvbox))
th = ctx.new_thread(st, None, PassValues(args, rvbox))
mu.execute()
......
......@@ -75,7 +75,7 @@ example::
arg1 = ctx.handle_from_int(10, 64)
arg2 = ctx.handle_from_double(3.14)
stack = ctx.new_stack(func)
thread = ctx.new_thread(stack, PassValues(arg1, arg2))
thread = ctx.new_thread(stack, None, PassValues(arg1, arg2))
mu.execute() # The thread will actually run now.
......@@ -112,7 +112,7 @@ be pasesed into methods like in C. For example::
func = ctx.handle_from_func(id_of("@factorial"))
arg = ctx.handle_from_int(10, 64)
stack = ctx.new_stack(func)
thread = ctx.new_thread(stack, PassValues(arg))
thread = ctx.new_thread(stack, None, PassValues(arg))
Type checks are performed on each and every method call. Handles (instances of
MuValue) must match the expected argument types. For example, if a method
......@@ -792,21 +792,29 @@ class MuCtx(_StructOfMethodsWrapper):
defaults to MU_SEQ_CST."""
return self.fence_(ord)
def new_thread(self, stack, how_to_resume):
def new_thread(self, stack, threadlocal, how_to_resume):
"""Wrapper of the underlying ``new_thread``.
This method now takes only a stack and a ``HowToResume`` value which can
be either ``PassValues`` or ``ThrowExc``.
This method now takes only a stack, a threadlocal and a ``HowToResume``
value which can be either ``PassValues`` or ``ThrowExc``.
Arguments:
stack: a MuStackRefValue, the initial stack the new thread should
bind to.
threadlocal: a MuRefValue or None, the initial thread-local objref
of the created thread.
how_to_resume: a HowToResume value. See the docstring of
MuTrapHandler.
Returns:
a MuThreadRefValue, referring to the newly created thread.
"""
if threadlocal == None:
threadlocal = 0
else:
_assert_instance(threadlocal, MuRefValue)
_assert_instance(how_to_resume, HowToResume)
if isinstance(how_to_resume, PassValues):
htr = CMU_REBIND_PASS_VALUES
......@@ -824,7 +832,7 @@ class MuCtx(_StructOfMethodsWrapper):
cnvals = 0
cexc = exc.c_mu_value
return self.new_thread_(stack, htr, cvals, cnvals, cexc)
return self.new_thread_(stack, threadlocal, htr, cvals, cnvals, cexc)
def dump_keepalives(self, cursor, nvals):
"""Wrapper of the underlying ``dump_keepalives``.
......@@ -1026,9 +1034,11 @@ _initialize_methods(MuCtx, [
("fence_" , None , [CMuMemOrd]),
("new_stack" , MuStackRefValue , [MuFuncRefValue]),
("new_thread_" , MuThreadRefValue , [MuStackRefValue, CMuHowToResume,
ctypes.POINTER(CMuValue), ctypes.c_int, CMuRefValue]),
("new_thread_" , MuThreadRefValue , [MuStackRefValue, CMuRefValue,
CMuHowToResume, ctypes.POINTER(CMuValue), ctypes.c_int, CMuRefValue]),
("kill_stack" , None , [MuStackRefValue]),
("set_threadlocal" , None , [MuThreadRefValue, MuRefValue]),
("get_threadlocal" , MuRefValue , [MuThreadRefValue]),
("new_cursor" , MuFCRefValue , [MuStackRefValue]),
("next_frame" , None , [MuFCRefValue]),
......
......@@ -80,6 +80,18 @@ with mu.new_context() as ctx:
.typedef @void = void
.typedef @refvoid = ref<@void>
.const @NULLREF <@refvoid> = NULL
.funcsig @trap_threadlocal.sig = (@refvoid @refvoid) -> ()
.funcdef @trap_threadlocal VERSION %v1 <@trap_threadlocal.sig> {
%entry(<@refvoid> %r1 <@refvoid> %r2):
%r3 = COMMINST @uvm.get_threadlocal
[%trap1] TRAP <> KEEPALIVE (%r1 %r2 %r3)
%r4 = COMMINST @uvm.get_threadlocal
[%trap2] TRAP <> KEEPALIVE (%r1 %r2 %r4)
COMMINST @uvm.thread_exit
}
""")
class TestRefImpl2CBinding(unittest.TestCase):
......@@ -249,7 +261,7 @@ class TestRefImpl2CBinding(unittest.TestCase):
main = ctx.handle_from_func(id_of("@main"))
st = ctx.new_stack(main)
th = ctx.new_thread(st, PassValues(forty_two))
th = ctx.new_thread(st, None, PassValues(forty_two))
mu.execute()
......@@ -277,7 +289,7 @@ class TestRefImpl2CBinding(unittest.TestCase):
func = ctx.handle_from_func(id_of("@trap_exit_test"))
st = ctx.new_stack(func)
th = ctx.new_thread(st, PassValues(forty_two))
th = ctx.new_thread(st, None, PassValues(forty_two))
mu.execute()
......@@ -313,7 +325,7 @@ class TestRefImpl2CBinding(unittest.TestCase):
func = ctx.handle_from_func(id_of("@trap_exit_test"))
st = ctx.new_stack(func)
th = ctx.new_thread(st, PassValues(forty_two))
th = ctx.new_thread(st, None, PassValues(forty_two))
mu.execute()
......@@ -370,7 +382,7 @@ class TestRefImpl2CBinding(unittest.TestCase):
func = ctx.handle_from_func(id_of("@trap_rebind_test"))
st = ctx.new_stack(func)
th = ctx.new_thread(st, PassValues())
th = ctx.new_thread(st, None, PassValues())
mu.execute()
......@@ -383,8 +395,71 @@ class TestRefImpl2CBinding(unittest.TestCase):
func = ctx.handle_from_func(id_of("@trap_rebind_test"))
st = ctx.new_stack(func)
th = ctx.new_thread(st, PassValues())
th = ctx.new_thread(st, None, PassValues())
mu.execute()
self.assertEqual(test_result2, [True])
def test_trap_threadlocal(self):
ctx = self.ctx
id_of = ctx.id_of
name_of = ctx.name_of
box = []
class MyHandler(MuTrapHandler):
def handle_trap(self, ctx, thread, stack, wpid):
print("Hey! This is Python!")
cursor = ctx.new_cursor(stack)
iid = ctx.cur_inst(cursor)
if name_of(iid) == "@trap_threadlocal.v1.entry.trap1":
r1, r2, r3 = ctx.dump_keepalives(cursor, 3)
r1 = r1.cast(MuRefValue)
r2 = r2.cast(MuRefValue)
r3 = r3.cast(MuRefValue)
ctx.close_cursor(cursor)
rl = ctx.get_threadlocal(thread)
eql = ctx.ref_eq(rl, r3)
box.append(eql)
eq1 = ctx.ref_eq(r1, r3)
box.append(eq1)
ctx.set_threadlocal(thread, r2)
elif name_of(iid) == "@trap_threadlocal.v1.entry.trap2":
r1, r2, r4 = ctx.dump_keepalives(cursor, 3)
r1 = r1.cast(MuRefValue)
r2 = r2.cast(MuRefValue)
r4 = r4.cast(MuRefValue)
ctx.close_cursor(cursor)
rl = ctx.get_threadlocal(thread)
eql = ctx.ref_eq(rl, r4)
box.append(eql)
eq2 = ctx.ref_eq(r2, r4)
box.append(eq2)
return Rebind(stack, PassValues())
mh1 = MyHandler()
mu.set_trap_handler(mh1)
ref1 = ctx.new_fixed(id_of("@i64"))
ref2 = ctx.new_fixed(id_of("@i64"))
func = ctx.handle_from_func(id_of("@trap_threadlocal"))
st = ctx.new_stack(func)
th = ctx.new_thread(st, ref1, PassValues(ref1, ref2))
mu.execute()
self.assertEqual(box, [True, True, True, True])
......@@ -199,7 +199,7 @@ instBody
| 'CCALL' callConv=flag '<' funcTy=type funcSig '>' callee=value argList excClause keepAliveClause # InstCCall
// Thread and Stack Operations
| 'NEWTHREAD' stack=value newStackClause excClause # InstNewThread
| 'NEWTHREAD' stack=value threadLocalClause? newStackClause excClause # InstNewThread
| 'SWAPSTACK' swappee=value curStackClause newStackClause excClause keepAliveClause # InstSwapStack
// Common Instructions
......@@ -234,6 +234,10 @@ excClause
keepAliveClause
: ('KEEPALIVE' '(' value* ')')?
;
threadLocalClause
: ('THREADLOCAL' '(' value ')')
;
flagList
: '[' flag* ']'
......
......@@ -15,6 +15,8 @@ object CommInsts extends SimpleNamespace[CommInst] {
commInst(0x202, "@uvm.kill_stack")
commInst(0x203, "@uvm.thread_exit", isTerminator = true)
commInst(0x204, "@uvm.current_stack")
commInst(0x205, "@uvm.set_threadlocal")
commInst(0x206, "@uvm.get_threadlocal")
commInst(0x211, "@uvm.tr64.is_fp")
commInst(0x212, "@uvm.tr64.is_int")
......
......@@ -421,6 +421,9 @@ private[textinput] class InstanceUIRTextReader(idFactory: IDFactory, source: Str
Some(ExcClause(ec.nor, ec.exc))
}
implicit def resThreadLocalClause(tlc: ThreadLocalClauseContext): Option[SSAVariable] =
Option(tlc).map { theTlc => resVar(theTlc.value()) }
implicit def resNewStackClause(nsc: NewStackClauseContext): NewStackAction = {
nsc match {
case a: NewStackPassValueContext => PassValues(a.typeList(), a.argList())
......@@ -581,8 +584,9 @@ private[textinput] class InstanceUIRTextReader(idFactory: IDFactory, source: Str
i.callee = ii.callee; i.argList = ii.argList; i.excClause = ii.excClause; i.keepAlives = ii.keepAliveClause
}
case ii: InstNewThreadContext =>
InstNewThread(null, null, null).later(phase4) { i =>
InstNewThread(null, null, null, null).later(phase4) { i =>
i.stack = ii.stack
i.threadLocal = ii.threadLocalClause
i.newStackAction = ii.newStackClause
i.excClause = ii.excClause
}
......
......@@ -499,14 +499,22 @@ class MuCtx(_mutator: Mutator)(
}
}
private def getThreadNotNull(thread: MuThreadRefValue): InterpreterThread = {
thread.vb.thread.getOrElse {
throw new UvmRuntimeException("Thread argument cannot be a NULL threadref value.")
}
}
/** Create a Mu thread and bind it to a Mu stack. */
def newThread(stack: MuStackRefValue, htr: HowToResume): MuThreadRefValue = {
def newThread(stack: MuStackRefValue, threadLocal: Option[MuRefValue], htr: HowToResume): MuThreadRefValue = {
val sv = getStackNotNull(stack)
val itprHtr = htr match {
case HowToResume.PassValues(values) => ItprHowToResume.PassValues(values.map(_.vb))
case HowToResume.ThrowExc(exc) => ItprHowToResume.ThrowExc(exc.vb.objRef)
}
val thr = microVM.threadStackManager.newThread(sv, itprHtr)
val threadLocalAddr = threadLocal.map(tl => tl.vb.objRef).getOrElse(0L)
val thr = microVM.threadStackManager.newThread(sv, threadLocalAddr, itprHtr)
val nb = BoxThread(Some(thr))
addHandle(MuThreadRefValue(InternalTypes.THREADREF, nb))
......@@ -518,6 +526,23 @@ class MuCtx(_mutator: Mutator)(
sv.kill()
}
/** Set the thread-local object reference */
def setThreadlocal(thread: MuThreadRefValue, threadLocal: MuRefValue): Unit = {
val th = getThreadNotNull(thread)
val threadLocalAddr = threadLocal.vb.objRef
th.threadLocal.objRef = threadLocalAddr
}
/** Get the thread-local object reference */
def getThreadlocal(thread: MuThreadRefValue): MuRefValue = {
val th = getThreadNotNull(thread)
val threadLocalAddr = th.threadLocal.objRef
val nb = BoxRef(threadLocalAddr)
addHandle(MuRefValue(InternalTypes.REF_VOID, nb))
}
private def getCursorNotNull(cursor: MuFCRefValue): FrameCursor = {
cursor.vb.cursor.getOrElse {
......
......@@ -144,6 +144,8 @@ object TypeInferer {
case "@uvm.kill_stack" => Seq()
case "@uvm.thread_exit" => Seq()
case "@uvm.current_stack" => Seq(STACKREF)
case "@uvm.set_threadlocal" => Seq()
case "@uvm.get_threadlocal" => Seq(REF_VOID)
case "@uvm.tr64.is_fp" => Seq(I1)
case "@uvm.tr64.is_int" => Seq(I1)
case "@uvm.tr64.is_ref" => Seq(I1)
......
......@@ -56,6 +56,17 @@ trait CommInstExecutor extends InterpreterActions with ObjectPinner {
continueNormally()
}
case "@uvm.set_threadlocal" => {
val Seq(tl) = argList
threadLocal copyFrom tl
continueNormally()
}
case "@uvm.get_threadlocal" => {
results(0) copyFrom threadLocal
continueNormally()
}
// 64-bit Tagged Reference
case "@uvm.tr64.is_fp" => {
......
......@@ -522,19 +522,21 @@ trait InstructionExecutor extends InterpreterActions with CommInstExecutor {
}
}
case i @ InstNewThread(stack, newStackAction, excClause) => {
case i @ InstNewThread(stack, threadLocal, newStackAction, excClause) => {
val newStack = stack.asStack.getOrElse {
throw new UvmUndefinedBehaviorException(ctx + "Attempt to bind a new thread to a NULL stack.")
}
val threadLocalAddr = threadLocal.map(tl => tl.asRef).getOrElse(0L)
val newThread = newStackAction match {
case PassValues(argTys, args) => {
val argBoxes = args.map(boxOf)
microVM.threadStackManager.newThread(newStack, HowToResume.PassValues(argBoxes))
microVM.threadStackManager.newThread(newStack, threadLocalAddr, HowToResume.PassValues(argBoxes))
}
case ThrowExc(exc) => {
val excAddr = exc.asRef
microVM.threadStackManager.newThread(newStack, HowToResume.ThrowExc(excAddr))
microVM.threadStackManager.newThread(newStack, threadLocalAddr, HowToResume.ThrowExc(excAddr))
}
}
results(0).asThread = Some(newThread)
......
......@@ -26,8 +26,14 @@ object HowToResume {
/**
* A thread that interprets Mu instruction.
* <p>
* @param id The thread ID
* @param initialStack The initial stack it binds to
* @param initialThreadLocal The initial thread-local object reference
* @param mutator The Mutator object, for memory management
* @param htr How to resume. Either pass value or throw exception.
*/
class InterpreterThread(val id: Int, initialStack: InterpreterStack, val mutator: Mutator, htr: HowToResume)(
class InterpreterThread(val id: Int, initialStack: InterpreterStack, initialThreadLocal: Long, htr: HowToResume, val mutator: Mutator)(
implicit protected val microVM: MicroVM) extends InstructionExecutor with HasID {
import InterpreterThread._
......@@ -37,6 +43,8 @@ class InterpreterThread(val id: Int, initialStack: InterpreterStack, val mutator
override def curThread = this
// Initialisation
threadLocal.asRef = initialThreadLocal
htr match {
case HowToResume.PassValues(values) => rebindPassValues(initialStack, values)
......@@ -65,6 +73,9 @@ trait InterpreterThreadState {
/** Object-pinnning multiset. */
val pinSet = new ArrayBuffer[Word]
/** Thread-local object reference. */
val threadLocal = BoxRef(0L)
protected def curStack = stack.get
protected def top: InterpreterFrame = curStack.top
......
......@@ -103,10 +103,10 @@ class ThreadStackManager(implicit microVM: MicroVM, nativeCallHelper: NativeCall
/**
* Create a new thread, bind to a given stack.
*/
def newThread(stack: InterpreterStack, htr: HowToResume): InterpreterThread = {
def newThread(stack: InterpreterStack, threadLocal: Long, htr: HowToResume): InterpreterThread = {
val mutator = microVM.memoryManager.makeMutator()
val id = threadRegistry.getID()
val thr = new InterpreterThread(id, stack, mutator, htr)
val thr = new InterpreterThread(id, stack, threadLocal, htr, mutator)
threadRegistry.put(thr)
thr
}
......
......@@ -278,7 +278,7 @@ object NativeMuCtx {
// NOTE: parameter exc must not be a MuValue because this parameter is ignored when htr is MU_REBIND_PASS_VALUE.
// Setting the type to MuValue (or MuRefValue) will force the exposer to eagerly resolve the underlying actual MuValue.
def new_thread(ctx: MuCtx, stack: MuStackRefValue, htr: MuHowToResume, vals: MuValueFakArrayPtr, nvals: Int, exc: MuValueFak): MuValueFak = {
def new_thread(ctx: MuCtx, stack: MuStackRefValue, threadLocal: Option[MuRefValue], htr: MuHowToResume, vals: MuValueFakArrayPtr, nvals: Int, exc: MuValueFak): MuValueFak = {
val scalaHtr = htr match {
case MU_REBIND_PASS_VALUES => {
val values = for (i <- 0L until nvals) yield {
......@@ -294,10 +294,17 @@ object NativeMuCtx {
HowToResume.ThrowExc(excVal)
}
}
val rv = ctx.newThread(stack, scalaHtr)
val rv = ctx.newThread(stack, threadLocal, scalaHtr)
exposeMuValue(ctx, rv)
}
def kill_stack(ctx: MuCtx, stack: MuStackRefValue): Unit = ctx.killStack(stack)
def set_threadlocal(ctx: MuCtx, thread: MuThreadRefValue, threadLocal:MuRefValue): Unit = {
ctx.setThreadlocal(thread, threadLocal)
}
def get_threadlocal(ctx: MuCtx, thread: MuThreadRefValue): MuValueFak = {
val rv = ctx.getThreadlocal(thread)
exposeMuValue(ctx, rv)
}
// Frame cursor operations
def new_cursor(ctx: MuCtx, stack: MuStackRefValue): MuValueFak = exposeMuValue(ctx, ctx.newCursor(stack))
......@@ -436,6 +443,7 @@ object ClientAccessibleClassExposer {
val TMicroVM = ru.typeTag[MicroVM].tpe
val TMuCtx = ru.typeTag[MuCtx].tpe
val TMuValue = ru.typeTag[MuValue].tpe
val TOptMuValue = ru.typeTag[Option[MuValue]].tpe
// com.kenai.jffi.Closure.Buffer param getters and return value setters.
// These are partially-applied functions, and will be called in closures (callback from C).
......@@ -456,6 +464,18 @@ object ClientAccessibleClassExposer {
muValue
}
def paramOptMuValue(index: Int, funcName: String, tpe: ru.Type)(buffer: Buffer): Any = {
val muOptValue = getOptMuValue(buffer, index)
muOptValue foreach { muValue => // only check if it is Some(value)
val t = mirror.classSymbol(muValue.getClass).toType
val tpeArg = tpe.typeArgs(0)
require(t <:< tpeArg, "Argument %d of %s expect %s, found %s".format(index, funcName, tpe, t))
}
muOptValue
}
def retVoid(buffer: Buffer, v: Any): Unit = {}
def retByte(buffer: Buffer, v: Any): Unit = buffer.setByteReturn(v.asInstanceOf[Byte])
......@@ -477,6 +497,15 @@ object ClientAccessibleClassExposer {
NativeClientSupport.getMuValueNotNull(addr)
}
private def getOptMuValue(buffer: Buffer, index: Int): Option[MuValue] = {
val addr = buffer.getAddress(index)
if (addr == 0L) {
None
} else {
Some(NativeClientSupport.getMuValueNotNull(addr))