mooner / Resume KO/EN /

mooner:~/resume $ cat projects/neonjs/README.md

NeonJS

JVM용 JavaScript 엔진

Kotlin으로 작성한 JVM용 JavaScript 엔진입니다. 믿을 수 없는 스크립트를 Java/Kotlin 앱 안에서 돌리기 위해 만들었고, 바이트코드 인터프리터와 JVM 바이트코드 JIT, 실행 제한 샌드박스, Java/Kotlin 상호 운용을 갖췄습니다. Android(API 26+)에서도 동작합니다.

Source ↗Maven ↗

mooner:~/resume/projects/neonjs $ tree -L 1

Structure01

구조

07 parts

Gradle 모듈 여덟 개로 되어 있습니다. 엔진은 neonjs-core 하나로 돌고, 나머지는 필요할 때 더하는 모듈입니다. Kotlin 코드는 본문 약 58,000줄, 테스트 약 7,400줄입니다.

neonjs-core
파서, 바이트코드 컴파일러, 인터프리터, JIT, 내장 객체, 처음부터 새로 쓴 정규식 엔진, 샌드박스, Java 상호 운용, URL·Blob·이벤트 같은 웹 전역.
neonjs-intl
ICU4J 위에 올린 Intl과 Temporal의 비ISO 달력. 클래스 경로에 두면 켜집니다.
neonjs-net
OkHttp 위의 fetch와 WebSocket. 허용한 호스트와 주소 대역에만 연결합니다.
neonjs-node
node: 모듈, require, Buffer. JS 쪽 라이브러리의 상당수는 Node.js v26.11.1 소스를 옮겨 왔습니다.
neonjs-android
JIT가 만든 클래스를 dx로, -d8 모듈을 더하면 D8로 dex로 바꿔 메모리에서 올립니다.
neonjs-cli
명령행 실행기와 REPL. Node API, 네트워크 허용 목록, .env 파일을 옵션으로 받습니다.
neonjs-test262
Test262, web-platform-tests, Node.js 자체 테스트를 돌리는 러너와 무작위 변이 퍼저, 기기 점검(AndroidCheck).

mooner:~/resume/projects/neonjs $ cat docs/architecture.md

Architecture02

아키텍처

Data flow

HOST APPctx.eval(source)PARSER · SCOPESAST · registersBYTECODECodeBlockINTERPRETERruns firstSANDBOXlimits · host accesscheckJIT QUEUEbackground · ×32hotJVM COMPILERASM · unboxed · inlineCODE DEFINERone per platformHOTSPOThidden classANDROIDdx / D8 → dex// the interpreter keeps running// until the compiled code is installed

소스는 파서 → 스코프 분석 → 바이트코드 순서로 바뀌고, 인터프리터가 먼저 실행합니다. 기본인 적응형 모드에서는 1,000번 넘게 불린 함수나 오래 도는 반복문을 백그라운드 스레드가 JVM 바이트코드로 컴파일해 갈아 끼웁니다.

  • 실행 모드는 인터프리터, 전부 컴파일, 적응형 세 가지입니다. 적응형에서는 컴파일이 끝날 때까지 인터프리터로 계속 돌고, 컴파일은 최대 32개씩 묶어서 합니다.
  • JIT 클래스 이름은 생성기가 읽는 입력 전체의 SHA-256입니다. 이름이 같으면 클래스 파일도 같아서, 상수나 위치만 다른 함수와 여러 컨텍스트에서 도는 같은 코드가 클래스 하나를 나눠 씁니다.
  • 숫자 unboxing과 인라이닝은 추측하지 않습니다. 타입은 명령이 만드는 값으로만 정하고, 그 때문에 지우는 검사도 deopt도 없습니다. 종류를 잘못 붙이면 다른 숫자로 읽히지 않고 예외로 드러납니다.
  • 플랫폼마다 다른 부분은 클래스를 정의하는 CodeDefiner 하나에 모았습니다. HotSpot에서는 hidden class로, Android에서는 dx나 D8이 만든 dex로 올리고, 어느 쪽도 쓸 수 없으면 인터프리터만 씁니다.
  • 샌드박스는 시간, 명령 수, 호출 깊이, 문자열 길이, 메모리(HotSpot에서만)를 제한하고, 호스트 접근은 기본 거부입니다. 스크립트의 catch와 finally는 종료를 가로채지 못합니다.
  • HotSpot은 8,000바이트가 넘는 메서드를 JIT하지 않습니다. 그래서 인터프리터와 런타임의 메서드를 그보다 작게 유지하고, 테스트가 이를 검사합니다.
  • StarLight의 nightly 브랜치에는 Rhino와 나란히 두 번째 JavaScript 언어로 들어가 있습니다(0.1.3).

mooner:~/resume/projects/neonjs $ git log --grep=fix

Problem solving03

문제 해결

06 cases

  1. 01

    Android 8.0·8.1에서 런타임이 중단된다

    Problem
    API 26 에뮬레이터 CI가 JIT 스레드에서 Check failed: self == thread_running_gc_로 죽었습니다. ART 8.x는 인터페이스 메서드의 유일한 구현을 기록해 두고, 그 구현의 클래스 로더가 내려간 뒤에도 그 기록을 다시 읽습니다. 처음 올리는 시험용 클래스가 바로 그 유일한 구현이었습니다.
    Approach
    엔진 로더에 CompiledCode 구현 두 개를 미리 링크해 ART가 이 메서드를 추적하지 않게 했습니다. 같은 버전에서는 Java.extend 어댑터를 내리지 않고, 기기 스크립트가 크래시 로그의 네이티브 백트레이스를 출력하게 했습니다.
    Result
    다음 Android CI부터 통과했습니다. 지금은 API 26과 34 에뮬레이터에서 기기 점검 7개와 Test262 부분집합 15,789개가 모두 통과합니다.
  2. 02

    JIT 클래스가 75만 개 넘게 생긴다

    Problem
    JIT 클래스 이름이 명령과 레지스터 수만 반영해, Test262에서 이름 78개가 실제로는 다른 코드를 가리켰습니다. 이대로는 클래스 여러 개를 dex 하나에 담을 수 없었고, 같은 코드도 블록마다 클래스를 새로 만들었습니다.
    Approach
    이름을 생성기 입력 전체의 SHA-256으로 바꿔 이름이 같으면 클래스 파일도 같게 했습니다. 생성 클래스는 상태가 없으므로, 입력이 같은 블록은 약한 참조 캐시에 둔 인스턴스 하나를 함께 씁니다.
    Result
    커밋에 기록한 측정으로, Test262 컴파일 모드에서 생성 클래스가 756,204개에서 75,915개로 줄고 실행 시간은 약 150초에서 35초가 됐습니다. 이름 51,915개가 서로 다른 클래스 파일 51,915개와 정확히 맞습니다.
  3. 03

    dex 변환의 고정 비용이 함수 수만큼 쌓인다

    Problem
    Android에서는 JIT 클래스를 dex로 바꿔야 하는데, dx와 D8은 부를 때마다 드는 고정 비용이 큽니다. 함수마다 따로 변환하면 그 비용이 함수 수만큼 쌓입니다.
    Approach
    컴파일을 백그라운드 데몬 스레드로 옮기고, 최대 32개씩 묶어 배치 하나를 dex 하나, 클래스 로더 하나로 만들었습니다. 한 블록은 한 스레드만 컴파일하고, 배치가 거부되면 클래스별로 다시 정의합니다. 워커가 쓴 시간은 스크립트의 실행 제한에 넣지 않습니다.
    Result
    커밋에 기록한 측정으로, 데스크톱 JVM에서 함수 5,000개를 한꺼번에 컴파일했을 때 dx는 1,331ms에서 671ms로, D8은 7,150ms에서 2,400ms로 줄었습니다. dx를 거쳐도 Test262 48,747개가 모두 통과합니다.
  4. 04

    컴파일된 코드가 계산마다 숫자를 박싱한다

    Problem
    생성 코드가 모든 값을 Object로 들고 있어서, 산술 연산 한 번마다 Double을 새로 만들었습니다.
    Approach
    정방향 타입 분석으로 레지스터와 스택 슬롯에 double, int32, 그 밖의 종류를 붙이고, 숫자만 담는 레지스터는 JVM 지역 변수로 둡니다. int는 비트 연산 결과처럼 정의상 int32인 값에만 쓰고 산술은 double로 해서, 오버플로와 -0을 그대로 지킵니다.
    Result
    커밋에 기록한 측정으로, ART 에뮬레이터에서 빈 루프가 반복당 9.22ns에서 0.70ns로, xorshift32가 25.3ns에서 3.5ns로 줄었습니다. 인터프리터와 결과를 비교한 무작위 수치 함수 60,000개에서 불일치가 없었고, Test262는 unboxing을 켜고 끈 양쪽에서 통과합니다.
  5. 05

    HotSpot과 Android에서 다른 메서드가 불린다

    Problem
    변환 비용이 같은 오버로드 중 리플렉션이 먼저 나열한 것이 골라졌는데, HotSpot과 ART는 나열 순서가 다릅니다. 그래서 sb.append(null)이 VM에 따라 다른 메서드를 불렀습니다. 2^53을 넘는 long ID는 JS 숫자로 바뀌면서 끝자리가 달라졌습니다.
    Approach
    동점이면 Java처럼 가장 구체적인 타입을 고르고, 그래도 같으면 고정된 순서로 정했습니다. 원하는 오버로드를 시그니처로 지정하는 문법도 더했습니다. 2^53 − 1을 넘는 long은 BigInt로 넘깁니다.
    Result
    두 동작 모두 상호 운용 계약 테스트로 고정했고, Android 기기 점검도 long의 경계값을 확인합니다.
  6. 06

    샌드박스가 문서와 다르게 동작한다

    Problem
    보안 문서는 바쁜 컨텍스트를 기다리는 시간이 시간 제한으로 묶인다고 했지만, 실제로는 무한정 기다려 두 스레드가 교착될 수 있었습니다. 엔진 자신의 API 클래스는 거부 목록에서 빠져 있었고, new URL('a:#' + 'x'.repeat(2e6))은 시간 제한으로도 멈출 수 없는 2차 시간 루프를 돌았습니다.
    Approach
    락 대기를 시간 제한으로 묶어 넘으면 NeonTimeoutException을 내고, 엔진의 모든 패키지와 API 클래스를 거부 목록에 넣었습니다. URL 파서는 선형으로 바꾸고 4,096단계마다 인터럽트와 제한을 확인합니다.
    Result
    세 가지 모두 테스트로 고정했습니다. 엔진 패키지에 클래스를 새로 더하면, 테스트에서 그 클래스를 거부할지 무해하다고 선언할지 정해야 합니다.

Accepting requestsAnsan --:-- KST

mooner:~/resume $ ./contact.sh

Contact→

Name
문민기 / Minki Moon
Handle
@mooner
Base
Ansan, KR

느긋하게 커피 한 잔 어떠세요?

궁금한 점이 있다면 편하게 연락 주세요.

// ping me anytime — i read all.

© 2026 Minki Mooncrafted with monospace affection · rev 2026.10.18