**This is an old revision of the document!**

模组开发:自定义架构

在此文章中我会讲解OC模组的架构是如何工作的,以及如何添加自定义架构。一句警告:架构的实现并非易事,别指望着轻而易举就能完成。

架构是什么?

首先让我们来申明OC模组语境下的“架构”是什么。架构指将某种代码执行器(以下称VM,虚拟机)和OC模组的组件/Minecraft结合起来的程序。OC将组件逻辑与电脑逻辑严格分开。换言之,组件(例如显卡)不知道它自身在使用什么架构。反过来,架构也不知道组件如何工作。传递的一切信息都由执行机处理。执行机驱动架构,并提供回调函数使架构能与外界通信。

使用架构

为了让内容不那么难以理解,这里将讲解如何实际应用架构。这大概能让读者更好地理解架构实现是怎么融入OC模组生态的。要使用架构,则必须创建一个使用此架构的执行机。

  • 注意:OC 1.4版本后,附属模组可以注册通过CPU提供的架构,因此你无需添加一种电脑方块/物品了。可以通过手持CPU时潜行右键单击来切换其提供的架构。

执行机封装了架构,并负责线程调度、信号队列、网络连接等。执行机创建好以后,你只需每tick调用一次update()函数(还可能需要向其传递一些调用,例如start())。

生命周期

架构的生命周期大致为:实例化,initialize(),反复执行runThreaded() / runSynchronized()close() [, 转到 initialize()]。

  • 执行机被创建时,架构即被创建。
  • 执行机启动或重启时,会要求initialize()自身。通常来说代表着创建底层虚拟机。
  • 驱动架构的标准方法是通过runThreaded()函数,它由执行器线程调用。架构的状态应在此处处理。其返回值类型被执行环境用于调度。注意:此方法不应抛出异常。你有责任处理所有错误,并在需要的时候将它们转换为代表报错的返回值。
  • 需要有一个特殊执行结果来表示进行“同步调用”。这个返回值会告诉执行机下次需要用runSynchronized()来驱动架构。这种调用一定会由MC服务端线程执行,代表着可以安全地在此回调函数中与世界交互。
  • 执行机停止时会调用close()函数。然后释放所有打开的资源。

Providing context

In the most simple case, you just call some 'run' method in your VM each time runThreaded() is called. This is pretty useless, though, since you'll want to communicate with the outside somehow. For this, communication with components has to be provided to the VM. It is your responsibility to define the APIs inside the VM that are used to communicate with the machine's API (e.g. components() and invoke()).

Synchronized calls and call limits

Callbacks on components may declare to be “direct”, and in addition to that they may declare a “call limit”. Per default, all component callbacks are synchronized calls, i.e. the architecture must ensure that they are only called from runSynchronized(). The call limit for direct calls limits how often per tick any single method may be called from any single machine. The limit itself is enforced in the machine's invoke method, which will throw a LimitReachedException once there were too many call. At that point the architecture should either perform a short sleep, or fall back to perform a synchronized call instead.

  • Note: while writing this I realized I forgot to pull some methods required to perform checks on this into the API. This will be remedied in the next API update, making methods for this available in the Component interface. For now, either reflect into it or build against the OC sources. Sorry about that.

Example code

Let's say you have some VM that provides the following interfaces:

snippet.java
/** The VM itself. This is just an example, it's not a "real" interface. */
public interface PseudoVM {
  Object[] run(Object[] args) throws Exception;
 
  void setApiFunction(String name, PseudoNativeFunction value);
}
 
/** Interface defining callbacks provided by the host. */
public interface PseudoNativeFunction {
  Object invoke(Object[] args);
}

A very primitive architecture implementation might look something like this:

snippet.java
/** This is the class you implement; Architecture is from the OC API. */
@Architecture.Name("Pseudolang")
public class PseudoArchitecture implements Architecture {
  private final Machine machine;
 
  private PseudoVM vm;
 
  /** The constructor must have exactly this signature. */
  public PseudoArchitecture(Machine machine) {
    this.machine = machine;
  }
 
  public boolean isInitialized() { return true; }
 
  public void recomputeMemory() {}
 
  public boolean initialize() {
    // Set up new VM here, and register all API callbacks you want to
    // provide to it.
    vm = new PseudoVM();
    vm.setApiFunction("invoke", new PseudoNativeFunction() {
      public Object invoke(Object[] args) {
        final String address = (String)args[0];
        final String method = (String)args[1];
        final Object[] params = (Object[])args[2];
        try {
          return new Object[]{true, machine.invoke(address, method, params)};
        }
        catch (e LimitReachedException) {
          // Perform logic also used to sleep / perform synchronized calls.
          // In this example we'll follow a protocol where if this returns
          // (true, something) the call succeeded, if it returns (false)
          // the limit was reached.
          // The script running in the VM is then supposed to return control
          // to the caller initiating the current execution (e.g. by yielding
          // if supported, or just returning, when in an event driven system).
          return new Object[]{false};
        }
      }
    });
    vm.setApiFunction("isDirect", new PseudoNativeFunction() {
      public Object invoke(Object[] args) {
        final String address = (String)args[0];
        final String method = (String)args[1];
        final Node node = machine.node().network().node(address);
        if (node instanceof Component) {
          final Component component = (Component) node;
          if (component.canBeSeenFrom(machine.node())) {
            final Callback callback = machine.methods(node.host()).get(method);
            if (callback != null) {
              return callback.direct();
            }
          }
        }
        return false;
      }
    });
    // ... more callbacks.
    return true;
  }
 
  void close() {
    vm = null;
  }
 
  ExecutionResult runThreaded(boolean isSynchronizedReturn) {
    // Perform stepping in here. Usually you'll want to resume the VM
    // by passing it the next signal from the queue, but you may decide
    // to allow your VM to poll for signals manually.
    try {
      final Signal signal;
      if (isSynchronizedReturn) {
        // Don't pull signals when we're coming back from a sync call,
        // since we're in the middle of something else!
        signal = null;
      }
      else {
        signal = machine.popSignal();
      }
      final Object[] result;
      if (signal != null) {
        result = vm.run(new Object[]{signal.name(), signal.args()});
      }
      else {
        result = vm.run(null);
      }
 
      // You'll want to define some internal protocol by which to decide
      // when to perform a synchronized call. Let's say we expect the VM
      // to return either a number for a sleep, a boolean to indicate
      // shutdown/reboot and anything else a pending synchronous call.
      if (result != null) {
        if (result[0] instanceof Boolean) {
          return new ExecutionResult.Shutdown((Boolean)result[0]);
        }
        if (result[0] instanceof Integer) {
          return new ExecutionResult.Sleep((Integer)result[0]);
        }
      }
      // If this is returned, the next 'resume' will be runSynchronized.
      // The next call to runThreaded after that call will have the
      // isSynchronizedReturn argument set to true.
      return new ExecutionResult.SynchronizedCall();
    }
    catch (Throwable t) {
      return new ExecutionResult.Error(t.toString);
    }
  }
 
  void runSynchronized() {
    // Synchronized calls are run from the MC server thread, making it
    // easier for callbacks to interact with the world (because sync is
    // taken care for them by the machine / architecture).
    // This means that if some code in the VM starts a sync call it has
    // to *pause* and relinquish control to the host, where we then
    // switch to sync call mode (see runThreaded), wait for the MC server
    // thread, and then do the actual call. It'd be possible to pass the
    // info required for the call out in runThreaded, keep it around in
    // the arch and do the call directly here. For this example, let's
    // assume the state info is kept inside the VM, and the next resume
    // makes it perform the *actual* call. For some pseudo-code handling
    // this in the VM, see below.
    vm.run(null);
  }
 
  void onConnect() {}
 
  // Use this to load the VM state, if it can be persisted.
  void load(NBTTagCompound nbt) {}
 
  // Use this to save the VM state, if it can be persisted.
  void save(NBTTagCompound nbt) {}
}

Some pseudo-code for handling synchronized calls in the VM:

snippet.scala
private def invokeSynchronous(address, method, ...) {
  yield; // This is where it returns to runThreaded().
   // This is where we enter in runSynchronized();
  val result = native.invoke(address, method, ...);
  // See definition of invoke in initialize() for values of result.
  yield; // And return to runSynchronized();
  // And the next runThreaded() enters again.
  return result[1];
}
private def invokeDirect(address, method, ...) {
  val result = native.invoke(address, method, ...);
  // See definition of invoke in initialize() for values of result.
  if (result[0] == true) {
    return result[1];
  }
  else {
    return invokeSynchronous(address, method, ...);
  }
}
def invoke(address, method, ...) {
  if (isDirect(address, method)) {
    return invokeDirect(address, method, ...);
  }
  else {
    return invokeSynchronous(address, method, ...);
  }
}

Registering an architecture

This one's pretty simple: just do li.cil.oc.api.Machine.add(PseudoArchitecture.class). This will allow CPUs to provide the registered architecture. If you do not wish OC CPUs to provide your architecture, you can either add your own Processor driver that can be used to get access to the architecture, or go one step further and add your own computer block, that will exclusively run your architecture, for example.

目录