Всем привет! Недавно я писал про реализацию пустых интерфейсов в Go, та статья, как можно догадаться имеет прямое отношение к разработке ОС на Go, да данная тема не заброшена и не забыта, но была отложена на долгий срок.
Под катом: «выкидываем» asm прокси-методы, имплементируем методы panic() и поддержку рантаймовых ошибок.
Помните все наши методы-заглушки и методы прокси на asm? Выкиньте и забудьте. Должны остаться только два файла: multiboot.s и runtime.s Содержимое multiboot.s не изменится, а runtime.s должен быть приведен вот к такому:
Всё остальное безжалостно пускаем под нож.
Открываем link.ld и в текстовой секции, после задачи DATA:
Да, спешу предупредить, некоторые методы, реализованные у меня, пока не описаны мной, поэтому придется вам их заменить на dummy или реализовать самостоятельно.
Что здесь происходит? Мы создаем символьные алиасы, это гораздо лучшее решение, чем писать прокси методы на asm.
Что ж, как я и обещал asm-прокси мы выкинули, давайте займемся имплементацией метода panic.
Из этой статьи копипастим структуры TypeDescriptor, EmptyInterface и Uncommon в файл runtime.go.
Добавляем в него:
Честно говоря, если бы аргументы паники были строкой, то на этом можно было бы и закончить, но увы — пустой интерфейс, так что нам придется реализовать печать пустого интерфейса, а соответсенно приведение интерфейса к типу, этим и займемся. Ворнинг: весь код, написанный в данной секции, является ярким примером начальной стадии MVP, работает, конечно, но больно уж примитивен и ущербен.
Напомню, мы по прежнему работаем в runtime.go:
Переключаемся на файл screen.go:
Что ж, теперь мы можем написать в своем коде (kernel.go, метод Load):
и полюбоваться на вывод в qemu:
Уже не плохо, да? Но я обещал ещё и обработку рантаймовых ошибок.
runtime.go:
Соответственно теперь, если мы где-то допустим ошибку, то увидим о ней сообщение.
Ворнинг: Код из прошлых частей генерирует кучу рантаймовых ошибок, но пока что я не буду показывать как их исправить, пусть это будет домашним заданием.
Благодарности за помощь в подготовке статьи:
Victorya1 — вычитка, обработка шероховатостей
kirill_danshin — обсуждение тех. частей, интересные дискуссии
UPD: Интересное обсуждение проблемы приведения интерфейсов к строковому типу
UPD2: Попытался реализовать функцию из обсуждения с Кириллом.
Под катом: «выкидываем» 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: Попытался реализовать функцию из обсуждения с Кириллом.
Поделиться с друзьями
JekaMas
Тоже делал опыт на тему мутабельности строк(взяв кусок кода с fasthttp) — https://play.golang.org/p/4Pyov1I0kM
t0pep0
Штатной возможности изменять строки — нет, но если сильно хочется — можно через unsafe.
В примере реализованным мной была попытка сделать type assertation без аллокаций, получилось, побочный эффект — ускорение примерно в 5 раз, но этот код просто проверка идеи
kirill_danshin
в первую очередь суть идеи была в том, чтобы убрать копирование данных при конвертации. это и получилось. сегодня код этой поделки ко мне в прод уйдет, посмотрим что будет