플러그인끼리 (다른 플러그인의 API 쓰기)

  1. 한눈에
  2. API를 주는 쪽
    1. API 크레이트
  3. API를 쓰는 쪽
  4. depend, soft_depend
  5. 호출이 어떻게 도는지
  6. 원본 ABI

Bukkit에서 경제·권한 플러그인의 API를 다른 플러그인이 가져다 쓰듯, GreenHaven 플러그인도 다른 플러그인의 함수를 부를 수 있습니다. 예제: plugins/economy(코인을 맡는 플러그인과 그 API 크레이트 economy/api), plugins/bounty(그 API를 쓰는 플러그인).

한눈에

하는 사람 하는 일
API를 주는 플러그인 부를 수 있게 할 함수에 #[export]. API 크레이트를 같이 배포(api! 한 덩어리)
API를 쓰는 플러그인 Cargo.toml에 그 API 크레이트, plugin.json에 depend(또는 soft_depend). 그 함수를 그냥 부름
서버 depend 순서로 로드, 호출을 상대 플러그인의 샌드박스로 넘기고 결과를 돌려줌

API를 주는 쪽

use greenhaven_plugin::*;

/// The player's coins.
#[export]
fn balance(player: String) -> i64 {
    World.get(&format!("c:{player}")) as i64
}

#[export]
fn deposit(player: String, amount: i64) -> i64 { ... }
  • #[export] 함수는 평소처럼 자기 안에서도 부를 수 있는 보통 함수입니다. 다른 이름으로 내보내려면 #[export("이름")].
  • 인자와 결과로 쓸 수 있는 것: i32 i64 u32 u64 f32 f64 bool String, Vec<T>, Option<T>, 튜플(6개까지). 넘길 때는 &str, &[T]도 됩니다. 값은 복사되어 건너가며 메모리를 나누지 않습니다.
  • 서비스 이름을 plugin.json "provides": ["economy"]에 적으면 다른 플러그인이 이 이름으로 찾습니다(Vault처럼: 같은 이름을 주는 다른 플러그인으로 바꿔 끼워도 쓰는 쪽 코드는 그대로). 둘 이상이 같은 서비스를 주면 먼저 로드된 쪽이 받습니다.
  • 누가 불렀는지는 caller()(플러그인 이름, 이벤트로 불렸으면 ""). 아무나 돈을 빼 가지 못하게 할 때 씁니다.

API 크레이트

쓰는 사람의 자동완성과 타입 검사는 이 크레이트에서 나옵니다. 몸통은 api! 하나입니다.

# economy/api/Cargo.toml
[package]
name = "economy-api"
version = "0.1.0"
edition = "2021"

[dependencies]
greenhaven-plugin = { path = "../../../sdk/plugin" }
// economy/api/src/lib.rs
greenhaven_plugin::api! { "economy";
    /// The player's coins.
    fn balance(player: &str) -> i64;
    /// `amount` more coins: the player's coins after.
    fn deposit(player: &str, amount: i64) -> i64;
    fn withdraw(player: &str, amount: i64) -> bool;
}

api!의 첫 줄은 플러그인 이름이나 서비스 이름입니다. 함수마다 Result<결과, CallError>를 돌려주는 함수가 생기고, available()은 그 플러그인이 지금 있는지 알려 줍니다. 함수 이름은 #[export]의 이름과 같아야 합니다.

API를 쓰는 쪽

[dependencies]
greenhaven-plugin = { path = "../../sdk/plugin" }
economy-api = { path = "../economy/api" }
{ "name": "bounty", "depend": ["economy"] }
#[on(Harvest)]
fn reward(e: Harvest) {
    let p = e.player();
    match economy_api::deposit(&p.name(), 1) {
        Ok(n) => p.say(&format!("코인 {n}개")),
        Err(why) => p.say(&format!("보상 없음: {why}")),
    }
}

API 크레이트 없이 직접 부를 수도 있습니다: find("economy", "deposit")가 Option<Func>을 주고, f.call::<_, i64>((name, 10))로 부릅니다. 이름으로 찾는 것은 처음 한 번이고(api!가 기억해 둠) 그 뒤로는 번호로 부릅니다.

depend, soft_depend

plugin.json  
"depend": ["economy"] 꼭 있어야 함. 없으면 이 플러그인은 켜지지 않고 서버 로그에 bounty: needs economy (depend)
"soft_depend": ["perms"] 있으면 먼저 로드. 없어도 켜지고, 부르면 CallError::Missing
"provides": ["economy"] 이 플러그인이 맡는 서비스 이름. depend에 서비스 이름을 적어도 됨
  • 서버는 이것으로 로드 순서를 정합니다: 필요한 플러그인의 Enable이 먼저 끝납니다. 이벤트의 우선순위가 같으면 순서도 이것을 따릅니다.
  • 서로가 서로를 depend하면(고리) 둘 다 켜지지 않습니다. soft_depend끼리의 고리는 적힌 순서대로 켜집니다.
  • 필요한 플러그인이 꺼져 있으면(서버 관리 › 플러그인) 그것을 필요로 하는 플러그인도 켜지지 않습니다.

호출이 어떻게 도는지

  • 불린 플러그인은 자기 샌드박스, 자기 값으로 돕니다. World::set, Player::set, 데이터는 그 플러그인 이름 아래에 남습니다: economy의 잔고는 economy만 직접 바꿀 수 있고, 다른 플러그인은 economy의 함수를 거쳐야 합니다.
  • 불린 쪽은 부른 쪽과 같은 이벤트 안에서 돕니다: 같은 플레이어(Player), 같은 월드. 그 안에서 한 일(아이템 주기, 말하기, cancel)은 그 이벤트에 한 것과 같습니다.
  • 비용: 한 번에 약 1~2 µs(측정 1.3 µs). 매 틱 수백 명에게 여러 번 부를 일이면 여럿을 한 번에 받는 함수를 내보내세요.
  • CPU 예산: 불린 쪽이 쓴 시간은 불린 쪽의 몫에서 나갑니다(부른 쪽에 이중으로 세지 않음). 불린 쪽이 예산을 다 썼으면 CallError::Busy. 한 호출에도 연료 5 000 000과 약 50 ms의 상한이 따로 있고, 부른 쪽의 시간 상한에는 기다린 시간이 들어갑니다.
  • 불린 쪽이 오류(panic, trap, 연료·시간 초과)로 끝나면 CallError::Failed이고, 그 호출에서 한 일은 없던 일이 됩니다. 부른 쪽은 계속 돕니다.
CallError  
Missing 그런 플러그인·서비스·함수가 없음, 꺼짐, 또는 Enable 중(규칙 등록 때는 부를 수 없음)
Busy 불린 쪽이 CPU 예산을 다 씀
Failed 불린 쪽이 오류로 끝남, 또는 인자를 읽지 못함(타입이 안 맞음)
Loop 이미 이 호출 사슬에 있는 플러그인을 다시 부름(A → B → A), 자기 자신, 또는 사슬이 4개를 넘음

원본 ABI

SDK 없이 쓰려면 "gh" 함수의 call_*와, 커스텀 섹션 gh의 export\t<이름>\t<내보내기>\n 줄을 씁니다.