Всем привет! Недавно я писал про реализацию пустых интерфейсов в Go, та статья, как можно догадаться имеет прямое отношение к разработке ОС на Go, да данная тема не заброшена и не забыта, но была отложена на долгий срок.

Под катом: «выкидываем» asm прокси-методы, имплементируем методы panic() и поддержку рантаймовых ошибок.

Помните все наши методы-заглушки и методы прокси на asm? Выкиньте и забудьте. Должны остаться только два файла: multiboot.s и runtime.s Содержимое multiboot.s не изменится, а runtime.s должен быть приведен вот к такому:

global dummy

dummy: ; Наши заглушки
        ret

global __unsafe_get_addr; convert uint32 to pointer

__unsafe_get_addr:
        push ebp
        mov  ebp, esp
        mov  eax, [ebp+8]
        mov  esp, ebp
        pop  ebp
        ret

Всё остальное безжалостно пускаем под нож.

Открываем link.ld и в текстовой секции, после задачи DATA:

        __go_new = go.runtime.New;
        __go_new_nopointers = go.runtime.New;
        __go_print_string = go.screen.PrintStr;
        __go_print_empty_interface = go.screen.PrintInterface;
        __go_print_nl = go.screen.PrintNl;
        __go_print_pointer = go.screen.PrintHex;
        __go_print_uint64 = go.screen.PrintUint64;
        __go_runtime_error = go.runtime.RuntimeError;
        __go_panic = go.runtime.Panic;
        runtime.efacetype = go.runtime.InterfaceType;
        runtime.ifacetypeeq = go.runtime.InterfaceTypeEq;
        runtime.ifaceE2T2 = go.runtime.InterfaceE2T2;
        __go_type_hash_identity = go.runtime.TypeHashIdentity;
        __go_type_equal_identity = go.runtime.TypeEqualIdentity;
        __go_strcmp = go.runtime.StrCmp;
        __go_type_hash_error = dummy;
        __go_type_equal_error = dummy;
        __go_register_gc_roots = dummy;
        __go_type_hash_identity_descriptor = dummy;
        __go_type_equal_error_descriptor = dummy;
        __go_type_equal_identity_descriptor = dummy;
        __go_type_hash_error_descriptor = dummy;
        __go_type_hash_empty_interface = dummy;
        __go_empty_interface_compare = dummy;
        __go_type_hash_string = dummy;
        __go_type_equal_string = dummy;
        __go_type_equal_empty_interface = dummy;
        __go_type_hash_string_descriptor = dummy;
        __go_type_equal_string_descriptor = dummy;
        __go_type_hash_empty_interface_descriptor = dummy;
        __go_type_equal_empty_interface_descriptor = dummy;

Да, спешу предупредить, некоторые методы, реализованные у меня, пока не описаны мной, поэтому придется вам их заменить на dummy или реализовать самостоятельно.

Что здесь происходит? Мы создаем символьные алиасы, это гораздо лучшее решение, чем писать прокси методы на asm.

Что ж, как я и обещал asm-прокси мы выкинули, давайте займемся имплементацией метода panic.
Из этой статьи копипастим структуры TypeDescriptor, EmptyInterface и Uncommon в файл runtime.go.

Добавляем в него:

//Интерфейс, описывающий стэк паники
type PanicStack struct { 
  Next         *PanicStack //Следующий элемент стэка
  Arg          interface{} //Аргументы
  WasRecovered bool //Был ли восстановлен
  IsForeign    bool //Внешний источник
} 

//Наш будущий метод panic()
func Panic(arg interface{}) {
  //stackTrace(3) //Знаю, выглядит заманчиво, но нет, пока что =)
  p := PanicStack{} // Создаем пустую структуру стэка паники. Внимание: именно структуру, а не указатель
  p.Arg = arg //Задаем аргументы паники
  PrintPanic(&p) //Печатаем панику
  for {
    // Впадаем в  анабиоз
  } 
}  

//Метод печати паники на экран
func PrintPanic(p *PanicStack) {
  if p.Next != nil {  //Если в стэке паники есть следующая, то печатаем (рекурсивно) сначала её
    PrintPanic(p.Next)
    print("\t")
  } 
  print("panic: ") //Выводим волшебное слово
  print(p.Arg) //печатаем аргументы паники
  if p.WasRecovered { //Если была восстановленная
    print("[recovered]") //то сообщим об этом
  } 
  print("\n")
}

Честно говоря, если бы аргументы паники были строкой, то на этом можно было бы и закончить, но увы — пустой интерфейс, так что нам придется реализовать печать пустого интерфейса, а соответсенно приведение интерфейса к типу, этим и займемся. Ворнинг: весь код, написанный в данной секции, является ярким примером начальной стадии MVP, работает, конечно, но больно уж примитивен и ущербен.

Напомню, мы по прежнему работаем в runtime.go:

//Примитивный метод сравнения строк побайтово
func StrCmp(s1, s2 string) int { 
  if len(s1) < len(s2) { 
    return -1
  }
  if len(s2) < len(s1) { 
    return 1
  }
  for i := 0; i < len(s1); i++ { 
    if s1[i] < s2[i] {
      return -1
    }
    if s2[i] < s1[i] {
      return 1
    }

  }
  return 0
}

//Метод возвращающий TypeDescriptor от входного пустого интерфейса
func InterfaceType(arg *EmptyInterface) TypeDescriptor {
  return *(arg.__type_descriptor)
}

//Сравнение типов двух пустых интерфейсов, на самом деле сравнивать надо не так, но пока всё равно
func InterfaceTypeEq(arg1, arg2 *EmptyInterface) bool {
  return *(arg1.__type_descriptor.string) == *(arg2.__type_descriptor.string)
}

//Приведение пустого интерфейса к типу
//iface - указатель на TypeDescriptor пустого интерфейс
//e - пустой интерфейс целевой переменной
//ret - адрес, куда записывать результат приведения
// ok - флаг успешности приведения
func InterfaceE2T2(iface *TypeDescriptor, e EmptyInterface, ret uint32) (ok bool) {
  if *(iface.string) == *(e.__type_descriptor.string) { //если типы одинаковы
    memcpy(ret, e.__object, uint32(iface.size)) //копируем значения объекта пустого интерфейса в целевую переменную
    return true 
  } else {
    return false
  }
}

Переключаемся на файл screen.go:

//метод печати пустого интерфейса, на самом деле пока умеет только в строки
func PrintInterface(arg interface{}) {
  v, ok := arg.(string)
  if ok {
    print(v)
  }
}

Что ж, теперь мы можем написать в своем коде (kernel.go, метод Load):

panic("Habrahabr")

и полюбоваться на вывод в qemu:

panic: Habrahabr

Уже не плохо, да? Но я обещал ещё и обработку рантаймовых ошибок.

runtime.go:

//Зададим константы для типов рантаймовых ошибок и для сообщений к ним
const (
  SLICE_INDEX_OUT_OF_BOUNDS = uint32(iota)
  ARRAY_INDEX_OUT_OF_BOUNDS
  STRING_INDEX_OUT_OF_BOUNDS
  SLICE_SLICE_OUT_OF_BOUNDS
  ARRAY_SLICE_OUT_OF_BOUNDS
  STRING_SLICE_OUT_OF_BOUNDS
  NIL_DEREFERENCE
  MAKE_SLICE_OUT_OF_BOUNDS
  MAKE_MAP_OUT_OF_BOUNDS
  MAKE_CHAN_OUT_OF_BOUNDS
  DIVISION_BY_ZERO
  MSG_INDEX_OUT_OF_RANGE        = "index out of range"
  MSG_SLICE_BOUNDS_OUT_OF_RANGE = "slice vounds out of range"
  MSG_NIL_DEREFERENCE           = "nil pointer dereference"
  MSG_MAKE_SLICE_OUT_OF_BOUNDS  = "make slice len or cap out of range"
  MSG_MAKE_MAP_OUT_OF_BOUNDS    = "make map len out of range"
  MSG_MAKE_CHAN_OUT_OF_BOUNDS   = "make chan len out of range"
  MSG_DIVISION_BY_ZERO          = "integer divide by zero"
  MSG_UNKNOWN                   = "unknown"
)

//А вот, собственно говоря, и сам метод, имплементирующий рантаймовые ошибки, всё просто 
func RuntimeError(i uint32) {
  switch i {
  case SLICE_INDEX_OUT_OF_BOUNDS, ARRAY_INDEX_OUT_OF_BOUNDS, STRING_INDEX_OUT_OF_BOUNDS:
    panic(MSG_INDEX_OUT_OF_RANGE)

  case SLICE_SLICE_OUT_OF_BOUNDS, ARRAY_SLICE_OUT_OF_BOUNDS, STRING_SLICE_OUT_OF_BOUNDS:
    panic(MSG_SLICE_BOUNDS_OUT_OF_RANGE)

  case NIL_DEREFERENCE:
    panic(MSG_NIL_DEREFERENCE)

  case MAKE_SLICE_OUT_OF_BOUNDS:
    panic(MSG_MAKE_SLICE_OUT_OF_BOUNDS)

  case MAKE_MAP_OUT_OF_BOUNDS:
    panic(MSG_MAKE_MAP_OUT_OF_BOUNDS)

  case MAKE_CHAN_OUT_OF_BOUNDS:
    panic(MSG_MAKE_CHAN_OUT_OF_BOUNDS)

  case DIVISION_BY_ZERO:
    panic(MSG_DIVISION_BY_ZERO)

  default:
    panic(MSG_UNKNOWN)
  }
}

Соответственно теперь, если мы где-то допустим ошибку, то увидим о ней сообщение.

Ворнинг: Код из прошлых частей генерирует кучу рантаймовых ошибок, но пока что я не буду показывать как их исправить, пусть это будет домашним заданием.

Благодарности за помощь в подготовке статьи:

Victorya1 — вычитка, обработка шероховатостей
kirill_danshin — обсуждение тех. частей, интересные дискуссии

UPD: Интересное обсуждение проблемы приведения интерфейсов к строковому типу
Kirill:
смотри, какие у меня мысли

[12:09:22 PM] Ivan:
Читаю внимательно )

[12:09:35 PM] Kirill:
строки в гошке иммутабельны

[12:09:45 PM] Ivan:
Угу
Изменение ведёт к созданию новой

[12:10:25 PM] Kirill:
и InterfaceE2T2 может, в теории, и не копировать данные
а ссылаться на существующие

[12:12:47 PM] Ivan:
Не может, а) он должен работать не только со строками, б) мы адрес возврата получаем и туда должны писать данные, при чём того типа которые ожидаются, а не указатель

[12:13:31 PM] Kirill:
что-то я проглядел, значит
жаль, что не может
я в это ограничение в проде вечно упираюсь
выходит, тут это не получится исправить

[12:16:05 PM] Ivan:
Почему, можно, расширением рантайма, т.е. реализовать метода InterfaceToString(iface interface{}) (str *string, ok bool)
Но это не рантаймовый метод будет, а пользовательский
Хм… добавлю это маленькое обсуждение в статью?

[12:17:01 PM] Kirill:
да, давай
это интересная задача, ее обязательно рано или поздно нужно решать
точно также interface{} -> []byte

UPD2: Попытался реализовать функцию из обсуждения с Кириллом.
Поделиться с друзьями
-->

Комментарии (3)


  1. JekaMas
    18.11.2016 15:31

    Тоже делал опыт на тему мутабельности строк(взяв кусок кода с fasthttp) — https://play.golang.org/p/4Pyov1I0kM


    1. t0pep0
      18.11.2016 15:39

      Штатной возможности изменять строки — нет, но если сильно хочется — можно через unsafe.
      В примере реализованным мной была попытка сделать type assertation без аллокаций, получилось, побочный эффект — ускорение примерно в 5 раз, но этот код просто проверка идеи


    1. kirill_danshin
      18.11.2016 17:08

      в первую очередь суть идеи была в том, чтобы убрать копирование данных при конвертации. это и получилось. сегодня код этой поделки ко мне в прод уйдет, посмотрим что будет