Skip to content

alex19srv/go_closure_callback_implementation

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 

Repository files navigation

Как реализованы callback'и с замыканиями в GoLang?

А конкретно их хранение в переменной/слайсе и вызов в виде callback. Суть в том, что замыкание имеет доступ не только с своим аргументам, но и данным, которые оно замкнуло из внешного окружения. И при вызове такого callback, в функцию-замыкание, нужно передать ей еще и этот контекст. А для вызова обычной функции достаточно ее адреса. Однако, в коде на Go такие callback выглядят одинаково.

Пояснение что такое замыкание? Моя версия замыкание - это функция, которая замыкает данные из внешнего окружения. Например, анонимная функция, которая замыкает локальную переменную фукнции, в которой ее создали. Или фокус с методом объекта, который замыкает ссылку на объект.

func newClosure() func() {
    i := someValue
    return func() {
        println(i)
    }
}
closure := newClosure() // тут фукнция становится совсем не анонимной, но остается замыканием

obj := SomeObject{}
method := obj.Method // как я понимаю, тут method - это замыкание

Што-ж, расскажу про замыкания в Go, когда они используются для callback. Это просто. В Go все callback будут замыканиями с данными (или пустым контекстом, если это обычная функция).

type Void func()
var CBlist []Void // <- тут будут хранится callback'и, точнее адреса структур
// type funcval struct {
//   fn uintptr
//   // variable-size, данные замыкания из внешнего окружения
// }

// go:noinline
func AddCB(cb Void) { // добавляем callback в список
    CBlist = append(CBlist, cb)
}

// go:noinline
func CallCBs() { // вызываем все callback'и
    for _, cb := range CBlist {
        cb()
    }
}

При вызове callback'а, из слайса будет извлекаться адрес контекста/funcval (именно он лежит в слайсе коллбеков), из него адрес функции и в эту функцию будет передаваться контекст (в регистре DX). Одинаково как для обычных функций, так и для замыканий.

  • В результате вызов callback'а будет дважды косвенным (slice -> context/funcval -> адрес фунции -> косвенный вызов функции с передачей funcval в DX).
  • Похоже, регистр DX не будет использоваться под аргументы, потому что любую функцию можно вызвать через callback, а в таком случае в DX будет адрес контекста (нулевого для функции, но регистр все равно будет использоваться).

Ссылки, если остались вопросы

Ассемблерные подробности

package main

// сгенерировать ассемблерный код на локальной машине: go build -gcflags -S x.go
// для тех, кто не верит и хочет проверить

import (
    "runtime"
)

type Void func()

var CBlist []Void // <- тут будут храниться не адреса функций, а адреса funcval,
//  в которых будут адреса функций и контекст, если фукнция - замыкание

// go:noinline
func AddCB(cb Void) {
    CBlist = append(CBlist, cb)
    // слайс состоит из указателя на буфер,
    // индекса последнего элемента (в штуках) и емкости (в штуках)
    // MOVQ	main.CBlist+16(SB), CX <- емкость слайса
    // MOVQ	main.CBlist+8(SB), BX  <- текущий занятый индекс элемента
    // INCQ	BX                     <- следующий элемент
    // MOVQ	main.CBlist(SB), DX    <- указатель на буфер
    // ... тут был код проверки на переполнение слайса
    // MOVQ	AX, -8(DX)(BX*8)       <- по адресу DX + BX*8 записывается новый элемент
    // т.е. в коде нет разделения на обычные функции и замыкания,
    // в слайс кладется какой-то указатель (единственный)
}

// go:noinline
func CallCBs() {
    // MOVQ	main.CBlist(SB), AX      <- указатель на слайс
    // MOVQ	AX, main..autotmp_4+16(SP)
    // MOVQ	main.CBlist+8(SB), CX    <- текущий занятый индекс элемента (конец итерации)
    // MOVQ	CX, main..autotmp_5+8(SP)
    // XORL	DX, DX                   <- текущий индекс итерации DX = 0
    for _, cb := range CBlist {
        cb()
        // MOVQ	DX, main..autotmp_6(SP)
        // MOVQ	(AX)(DX*8), DX     <- DX = AX + DX*8, т.е. берем элемент слайса с индексом DX
        // MOVQ	(DX), AX           <- AX = *DX (из слайса взяли адрес и разыменовали его в AX)
        // DX указывает на структуру funcval, в которой есть адрес функции и контекст
        // Там первый элемент - адрес функции, далее контекст произвольной длины,
        // но контекст обрабатывает само замыкание - вызывающая сторона про него не знает
        // CALL	AX                 <- вызов функции, в DX указатель на контекст
        // суть в том, что все элемены слайса вызываются одинаково,
        // как замыкания, так и функции
        // MOVQ	main..autotmp_6(SP), DX
        // инкремент текущего индекса итерации и другие команды для цикла
        // INCQ	DX
        // MOVQ	main..autotmp_4+16(SP), AX
        // MOVQ	main..autotmp_5+8(SP), CX
        // CMPQ	DX, CX
    }
}

//go:noinline
func vfunc() {
    println("vfunc")
}

func main() {
    AddCB(vfunc)
    // тут уже привычный код добавления элемента в слайс
    // MOVQ	main.CBlist+16(SB), CX
    // MOVQ	main.CBlist+8(SB), BX
    // INCQ	BX
    // XCHGL	AX, AX
    // MOVQ	main.CBlist(SB), AX
    // ... код проверки на переполнение слайса
    // LEAQ	main.vfunc·f(SB), CX  <- адрес контекста, а не функции,
    // потому что main.vfunc·f, а не main.vfunc
    // MOVQ	CX, -8(AX)(BX*8)      <- запись в слайс адреса контекста

    var i int = 7
    AddCB(func() {
        // дальше для анонимной функции создается контекст
        // в динмаической памяти (runtime.newobject)
        // и также помещается в тот же слайс
        print("i = ", i, "\n")
        runtime.Caller(0)
    })

    CallCBs()
}

Благодарности

  • участникам чатика https://t.me/thank_go, которые помогли разобраться в теме,

About

Golang closure implementation for callbacks

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages