├── .circleci └── config.yml ├── .github └── workflows │ ├── go.yml │ └── golangci-lint.yml ├── .golangci.yml ├── .travis.yml ├── LICENSE ├── README.md ├── build_tag_test.go ├── cache.go ├── fmt.go ├── fmt_test.go ├── frames_go1.23.go ├── frames_pre_go1.23.go ├── func.go ├── func_test.go ├── go.mod ├── go.sum ├── location.go ├── location_stack_test.go ├── location_test.go ├── name_file_line_safe.go ├── name_file_line_unsafe.go ├── print_stack_test.go ├── runtime_test.go ├── unsafe.go └── unsafe_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | go1.24: &base 4 | docker: 5 | - image: cimg/go:1.24 6 | steps: 7 | - run: go version 8 | - checkout 9 | - run: go test -tags nikandfor_loc_unsafe -v ./... 10 | - run: go test -tags nikandfor_loc_unsafe -v -race ./... 11 | 12 | go1.23: 13 | <<: *base 14 | docker: 15 | - image: cimg/go:1.23 16 | 17 | go1.22: 18 | <<: *base 19 | docker: 20 | - image: cimg/go:1.22 21 | 22 | go1.21: 23 | <<: *base 24 | docker: 25 | - image: cimg/go:1.21 26 | 27 | go1.20: 28 | <<: *base 29 | docker: 30 | - image: cimg/go:1.20 31 | 32 | go1.19: 33 | <<: *base 34 | docker: 35 | - image: cimg/go:1.19 36 | 37 | go1.18: 38 | <<: *base 39 | docker: 40 | - image: cimg/go:1.18 41 | 42 | 43 | go1.24_safe: &base_safe 44 | <<: *base 45 | docker: 46 | - image: cimg/go:1.24 47 | steps: 48 | - run: go version 49 | - checkout 50 | - run: go test -v -race ./... 51 | - run: go test -v ./... 52 | 53 | go1.23_safe: 54 | <<: *base_safe 55 | docker: 56 | - image: cimg/go:1.23 57 | 58 | go1.22_safe: 59 | <<: *base_safe 60 | docker: 61 | - image: cimg/go:1.22 62 | 63 | go1.21_safe: 64 | <<: *base_safe 65 | docker: 66 | - image: cimg/go:1.21 67 | 68 | go1.20_safe: 69 | <<: *base_safe 70 | docker: 71 | - image: cimg/go:1.20 72 | 73 | go1.19_safe: 74 | <<: *base_safe 75 | docker: 76 | - image: cimg/go:1.19 77 | 78 | workflows: 79 | version: 2 80 | build: 81 | jobs: 82 | - go1.24_safe 83 | - go1.23_safe 84 | - go1.22_safe 85 | - go1.21_safe 86 | - go1.20_safe 87 | - go1.19_safe 88 | - go1.24 89 | - go1.23 90 | - go1.22 91 | - go1.21 92 | - go1.20 93 | - go1.19 94 | - go1.18 95 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | os: ["ubuntu-latest", "macos-latest", "windows-latest"] 14 | go-ver: ["1.24", "1.23", "1.22", "1.21", "1.20", "1.19"] 15 | include: 16 | - os: "ubuntu-latest" 17 | go-ver: "1.24" 18 | cover: true 19 | 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v4 26 | with: 27 | go-version: ${{ matrix.go-ver }} 28 | 29 | - name: Build 30 | run: go build -v ./... 31 | 32 | - name: Test with Cover 33 | run: go test -v -coverprofile=coverage.txt -covermode=atomic ./... 34 | if: ${{ matrix.cover }} 35 | 36 | - name: Test without Cover 37 | run: go test -v ./... 38 | if: ${{ !matrix.cover }} 39 | 40 | - name: Test Race 41 | run: go test -race -v ./... 42 | 43 | - name: Test Unsafe 44 | run: go test -tags nikandfor_loc_unsafe -v ./... 45 | 46 | - name: Test Unsafe Race 47 | run: go test -race -tags nikandfor_loc_unsafe -v ./... 48 | 49 | - name: Upload coverage reports to Codecov 50 | uses: codecov/codecov-action@v4 51 | if: ${{ matrix.cover }} 52 | env: 53 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 54 | 55 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | pull_request: 5 | jobs: 6 | golangci: 7 | name: lint 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: golangci-lint 12 | uses: golangci/golangci-lint-action@v2 13 | with: 14 | # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. 15 | version: v1.63.4 16 | 17 | # Optional: working directory, useful for monorepos 18 | # working-directory: somedir 19 | 20 | # Optional: golangci-lint command line arguments. 21 | # args: --issues-exit-code=0 22 | 23 | # Optional: show only new issues if it's a pull request. The default value is `false`. 24 | # only-new-issues: true 25 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | #modules-download-mode: readonly 3 | issues: 4 | exclude: 5 | - "padding" 6 | exclude-rules: 7 | linters-settings: 8 | govet: 9 | check-shadowing: false 10 | golint: 11 | min-confidence: 0 12 | gocognit: 13 | min-complexity: 30 14 | cyclop: 15 | max-complexity: 30 16 | maligned: 17 | suggest-new: true 18 | dupl: 19 | threshold: 100 20 | goconst: 21 | min-len: 2 22 | min-occurrences: 3 23 | depguard: 24 | rules: 25 | main: 26 | allow: 27 | - $gostd 28 | - github.com/beorn7/perks/quantile 29 | - github.com/getsentry/sentry-go 30 | - github.com/gin-gonic/gin 31 | - github.com/nikandfor/cli 32 | - github.com/nikandfor/errors 33 | - github.com/nikandfor/goid 34 | - github.com/nikandfor/loc 35 | - github.com/nikandfor/quantile 36 | - github.com/nikandfor/tlog 37 | - github.com/opentracing/opentracing-go 38 | - github.com/prometheus/client_golang 39 | - github.com/prometheus/client_model 40 | - github.com/stretchr/testify 41 | - go.opentelemetry.io/otel 42 | - golang.org/x 43 | - gopkg.in/fsnotify.v1 44 | misspell: 45 | lll: 46 | line-length: 170 47 | goimports: 48 | local-prefixes: github.com/nikandfor/tlog 49 | prealloc: 50 | simple: true 51 | for-loops: true 52 | gocritic: 53 | enabled-tags: 54 | - experimental 55 | - performance 56 | - style 57 | disabled-checks: 58 | - appendAssign 59 | - builtinShadow 60 | - commentedOutCode 61 | - octalLiteral 62 | - unnamedResult 63 | - whyNoLint 64 | - yodaStyleExpr 65 | 66 | linters: 67 | enable-all: true 68 | disable: 69 | - exhaustive 70 | - exhaustivestruct 71 | - exhaustruct 72 | - forcetypeassert 73 | - funlen 74 | - gci 75 | - gochecknoglobals 76 | - gochecknoinits 77 | - godox 78 | - err113 79 | - golint 80 | - gomnd 81 | - mnd 82 | - nakedret 83 | - nlreturn 84 | - nonamedreturns 85 | - paralleltest 86 | - prealloc 87 | - testpackage 88 | - thelper 89 | - unparam 90 | - varnamelen 91 | - wsl 92 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | dist: focal 4 | 5 | os: 6 | - linux 7 | - osx 8 | - windows 9 | 10 | arch: 11 | - amd64 12 | - i386 13 | - arm64 14 | 15 | go: 16 | - "1.18" 17 | - "1.17" 18 | - "1.16" 19 | - "1.15" 20 | - "1.14" 21 | 22 | script: 23 | - go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... 24 | 25 | #after_success: 26 | # - test "$TRAVIS_OS_NAME" = linux && test "$TRAVIS_CPU_ARCH" = amd64 && test "$TRAVIS_GO_VERSION" = "1.18" && export CODECOV_UPLOAD=yes 27 | # - test $CODECOV_UPLOAD = "yes" && bash <(curl -s https://codecov.io/bash) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nikifor Seriakov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Documentation](https://pkg.go.dev/badge/tlog.app/go/loc)](https://pkg.go.dev/tlog.app/go/loc?tab=doc) 2 | [![Go workflow](https://github.com/tlog-dev/loc/actions/workflows/go.yml/badge.svg)](https://github.com/tlog-dev/loc/actions/workflows/go.yml) 3 | [![CircleCI](https://circleci.com/gh/tlog-dev/loc.svg?style=svg)](https://circleci.com/gh/tlog-dev/loc) 4 | [![codecov](https://codecov.io/gh/tlog-dev/loc/tags/latest/graph/badge.svg)](https://codecov.io/gh/tlog-dev/loc) 5 | [![Go Report Card](https://goreportcard.com/badge/tlog.app/go/loc)](https://goreportcard.com/report/tlog.app/go/loc) 6 | ![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/tlog-dev/loc?sort=semver) 7 | 8 | # loc 9 | 10 | It's a fast, alloc-free and convinient version of `runtime.Caller`. 11 | 12 | Performance benefits are available when using the `nikandfor_loc_unsafe` build tag. This relies on the internal runtime implementation, which means that older versions of the package may not compile with future versions of Go. Without the tag, it is safe to use with any version of Go. 13 | 14 | It was born from [tlog](https://tlog.app/go/tlog). 15 | 16 | ## What is similar 17 | 18 | Caller 19 | 20 | ```go 21 | pc := loc.Caller(1) 22 | ok := pc != 0 23 | name, file, line := pc.NameFileLine() 24 | e := pc.FuncEntry() 25 | 26 | // is similar to 27 | 28 | pc, file, line, ok := runtime.Caller(1) 29 | f := runtime.FuncForPC(pc) 30 | name := f.Name() 31 | e := f.Entry() 32 | ``` 33 | 34 | Callers 35 | 36 | ```go 37 | pcs := loc.Callers(1, 3) 38 | // or 39 | var pcsbuf [3]loc.PC 40 | pcs := loc.CallersFill(1, pcsbuf[:]) 41 | 42 | for _, pc := range pcs { 43 | name, file, file := pc.NameFileLine() 44 | } 45 | 46 | // is similar to 47 | var pcbuf [3]uintptr 48 | n := runtime.Callers(2, pcbuf[:]) 49 | 50 | frames := runtime.CallersFrames(pcbuf[:n]) 51 | for { 52 | frame, more := frames.Next() 53 | 54 | _, _, _ = frame.Function, frame.File, frame.Line 55 | 56 | if !more { 57 | break 58 | } 59 | } 60 | ``` 61 | 62 | ## What is different 63 | 64 | ### Normalized path 65 | 66 | `loc` returns cropped filepath. 67 | ``` 68 | github.com/nikandfor/loc/func_test.go 69 | 70 | # vs 71 | 72 | /home/nik/nikandfor/loc/github.com/nikandfor/loc/func_test.go 73 | ``` 74 | 75 | ### Performance 76 | 77 | In `loc` the full cycle (get pc than name, file and line) takes 360=200+160 (200+0 when you repeat) ns whereas runtime takes 690 (640 without func name) + 2 allocs per each frame. 78 | 79 | It's up to 3.5x improve. 80 | 81 | ``` 82 | BenchmarkLocationCaller-8 5844801 201.8 ns/op 0 B/op 0 allocs/op 83 | BenchmarkLocationNameFileLine-8 7313388 156.5 ns/op 0 B/op 0 allocs/op 84 | BenchmarkRuntimeCallerNameFileLine-8 1709940 689.1 ns/op 216 B/op 2 allocs/op 85 | BenchmarkRuntimeCallerFileLine-8 1917613 642.1 ns/op 216 B/op 2 allocs/op 86 | ``` 87 | -------------------------------------------------------------------------------- /build_tag_test.go: -------------------------------------------------------------------------------- 1 | //go:build nikandfor_loc_unsafe 2 | 3 | package loc 4 | 5 | import "testing" 6 | 7 | func TestBuildTag(t *testing.T) { 8 | t.Logf("build tag is set") 9 | } 10 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package loc 2 | 3 | import "sync" 4 | 5 | type ( 6 | nfl struct { 7 | name string 8 | file string 9 | line int 10 | } 11 | ) 12 | 13 | var ( 14 | locmu sync.Mutex 15 | locc = map[PC]nfl{} 16 | ) 17 | 18 | // NameFileLine returns function name, file and line number for location. 19 | // 20 | // This works only in the same binary where location was captured. 21 | // 22 | // This functions is a little bit modified version of runtime.(*Frames).Next(). 23 | func (l PC) NameFileLine() (name, file string, line int) { 24 | if l == 0 { 25 | return 26 | } 27 | 28 | locmu.Lock() 29 | c, ok := locc[l] 30 | locmu.Unlock() 31 | if ok { 32 | return c.name, c.file, c.line 33 | } 34 | 35 | name, file, line = l.nameFileLine() 36 | 37 | if file != "" { 38 | file = cropFilename(file, name) 39 | } 40 | 41 | locmu.Lock() 42 | locc[l] = nfl{ 43 | name: name, 44 | file: file, 45 | line: line, 46 | } 47 | locmu.Unlock() 48 | 49 | return 50 | } 51 | 52 | // SetCache sets name, file and line for the PC. 53 | // It allows to work with PC in another binary the same as in original. 54 | func SetCache(l PC, name, file string, line int) { 55 | locmu.Lock() 56 | if name == "" && file == "" && line == 0 { 57 | delete(locc, l) 58 | } else { 59 | locc[l] = nfl{ 60 | name: name, 61 | file: file, 62 | line: line, 63 | } 64 | } 65 | locmu.Unlock() 66 | } 67 | 68 | func SetCacheBytes(l PC, name, file []byte, line int) { 69 | locmu.Lock() 70 | if name == nil && file == nil && line == 0 { 71 | delete(locc, l) 72 | } else { 73 | x := locc[l] 74 | 75 | if x.line != line || x.name != string(name) || x.file != string(file) { 76 | locc[l] = nfl{ 77 | name: string(name), 78 | file: string(file), 79 | line: line, 80 | } 81 | } 82 | } 83 | locmu.Unlock() 84 | } 85 | 86 | func Cached(l PC) (ok bool) { 87 | locmu.Lock() 88 | _, ok = locc[l] 89 | locmu.Unlock() 90 | return 91 | } 92 | -------------------------------------------------------------------------------- /fmt.go: -------------------------------------------------------------------------------- 1 | package loc 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "path/filepath" 7 | "reflect" 8 | "unsafe" 9 | ) 10 | 11 | type ( 12 | buf []byte 13 | 14 | locFmtState struct { 15 | buf 16 | flags string 17 | } 18 | ) 19 | 20 | var spaces = []byte(" ") //nolint:lll 21 | 22 | // String formats PC as base_name.go:line. 23 | // 24 | // Works only in the same binary where Caller of FuncEntry was called. 25 | // Or if PC.SetCache was called. 26 | func (l PC) String() string { 27 | _, file, line := l.NameFileLine() 28 | file = filepath.Base(file) 29 | 30 | b := append([]byte(file), ": "...) 31 | 32 | i := len(file) 33 | n := 1 + width(line) 34 | 35 | b = b[:i+n] 36 | 37 | for q, j := line, n-1; j >= 1; j-- { 38 | b[i+j] = byte(q%10) + '0' 39 | q /= 10 40 | } 41 | 42 | return string(b) 43 | } 44 | 45 | // Format is fmt.Formatter interface implementation. 46 | func (l PC) Format(s fmt.State, c rune) { 47 | switch c { 48 | default: // v 49 | l.formatV(s) 50 | case 'n', 's': 51 | l.formatName(s) 52 | case 'f': 53 | l.formatFile(s) 54 | case 'd', 'l': 55 | l.formatLine(s) 56 | case 'x', 'X', 'p', 'P': 57 | l.formatPC(s, c) 58 | } 59 | } 60 | 61 | func (l PC) formatV(s fmt.State) { 62 | name, file, line := l.NameFileLine() 63 | 64 | if !s.Flag('+') { 65 | file = filepath.Base(file) 66 | name = path.Base(name) 67 | } 68 | 69 | if s.Flag('#') { 70 | file = name 71 | } 72 | 73 | prec, ok := s.Precision() 74 | if lw := width(line); !ok || lw > prec { 75 | prec = lw 76 | } 77 | 78 | if prec > 20 { 79 | prec = 20 80 | } 81 | 82 | width, ok := s.Width() 83 | if !ok { 84 | width = len(file) + 1 + prec 85 | } 86 | 87 | fwidth := width - 1 - prec 88 | if fwidth < 0 { 89 | fwidth = 0 90 | } 91 | 92 | var bufdata [128]byte 93 | // buf := bufdata[:0] 94 | buf := noescapeSlize(&bufdata[0], len(bufdata)) 95 | 96 | buf = l.appendStr(buf, s, fwidth, file) 97 | 98 | if fwidth+1+prec > width { 99 | prec = width - fwidth - 1 100 | } 101 | 102 | buf = append(buf, ": "[:1+prec]...) 103 | 104 | for q, j := line, prec; q != 0 && j >= 0; j-- { 105 | buf[fwidth+j] = byte(q%10) + '0' 106 | q /= 10 107 | } 108 | 109 | _, _ = s.Write(buf) 110 | } 111 | 112 | func (l PC) formatName(s fmt.State) { 113 | name, _, _ := l.NameFileLine() 114 | 115 | if !s.Flag('+') { 116 | name = path.Base(name) 117 | } 118 | 119 | w, ok := s.Width() 120 | if !ok { 121 | w = len(name) 122 | } 123 | 124 | var bufdata [128]byte 125 | buf := noescapeSlize(&bufdata[0], len(bufdata)) 126 | 127 | buf = l.appendStr(buf, s, w, name) 128 | 129 | _, _ = s.Write(buf) 130 | } 131 | 132 | func (l PC) formatFile(s fmt.State) { 133 | _, file, _ := l.NameFileLine() 134 | 135 | if !s.Flag('+') { 136 | file = filepath.Base(file) 137 | } 138 | 139 | w, ok := s.Width() 140 | if !ok { 141 | w = len(file) 142 | } 143 | 144 | var bufdata [128]byte 145 | buf := noescapeSlize(&bufdata[0], len(bufdata)) 146 | 147 | buf = l.appendStr(buf, s, w, file) 148 | 149 | _, _ = s.Write(buf) 150 | } 151 | 152 | func (l PC) appendStr(buf []byte, s fmt.State, w int, name string) []byte { 153 | if w > len(name) { 154 | if s.Flag('-') { 155 | buf = append(buf, spaces[:w-len(name)]...) 156 | } 157 | 158 | buf = append(buf, name...) 159 | 160 | if !s.Flag('-') { 161 | buf = append(buf, spaces[:w-len(name)]...) 162 | } 163 | } else { 164 | buf = append(buf, name[:w]...) 165 | } 166 | 167 | return buf 168 | } 169 | 170 | func (l PC) formatLine(s fmt.State) { 171 | _, _, line := l.NameFileLine() 172 | 173 | lineW := width(line) 174 | 175 | w, ok := s.Width() 176 | if !ok || w < lineW { 177 | w = lineW 178 | } 179 | 180 | if w > 20 { 181 | w = 20 182 | } 183 | 184 | var bufdata [32]byte 185 | buf := noescapeSlize(&bufdata[0], len(bufdata)) 186 | 187 | buf = append(buf, " "[:w]...) 188 | 189 | j := w - 1 190 | for q := line; q != 0 && j >= 0; j-- { 191 | buf[j] = byte(q%10) + '0' 192 | q /= 10 193 | } 194 | 195 | for j >= 1 && s.Flag('0') { 196 | buf[j] = '0' 197 | j-- 198 | } 199 | 200 | _, _ = s.Write(buf) 201 | } 202 | 203 | func (l PC) formatPC(s fmt.State, c rune) { 204 | lineW := 1 205 | for x := l >> 4; x != 0; x >>= 4 { 206 | lineW++ 207 | } 208 | 209 | w, ok := s.Width() 210 | if !ok || w < lineW { 211 | w = lineW 212 | } 213 | 214 | w += len("0x") 215 | 216 | if w > 20 { 217 | w = 20 218 | } 219 | 220 | var bufdata [32]byte 221 | buf := noescapeSlize(&bufdata[0], len(bufdata)) 222 | 223 | buf = append(buf, " "[:w]...) 224 | 225 | const ( 226 | hexc = "0123456789abcdef" 227 | hexC = "0123456789ABCDEF" 228 | ) 229 | 230 | hex := hexc 231 | if c >= 'A' && c <= 'Z' { 232 | hex = hexC 233 | } 234 | 235 | j := w - 1 236 | for q := uint64(l); q != 0 && j >= 0; j-- { 237 | buf[j] = hex[q&0xf] 238 | q /= 16 239 | } 240 | 241 | for j > 1 && s.Flag('0') { 242 | buf[j] = '0' 243 | j-- 244 | } 245 | 246 | if j > 0 { 247 | buf[j-1] = '0' 248 | buf[j] = 'x' 249 | } 250 | 251 | _, _ = s.Write(buf) 252 | } 253 | 254 | func width(v int) (n int) { 255 | n = 0 256 | for v != 0 { 257 | v /= 10 258 | n++ 259 | } 260 | return 261 | } 262 | 263 | // String formats PCs as list of type_name (file.go:line) 264 | // 265 | // Works only in the same binary where Caller of FuncEntry was called. 266 | // Or if PC.SetCache was called. 267 | func (t PCs) String() string { 268 | var b buf 269 | 270 | for i, l := range t { 271 | if i != 0 { 272 | b = append(b, " at "...) 273 | } 274 | 275 | _, file, line := l.NameFileLine() 276 | file = filepath.Base(file) 277 | 278 | i := len(b) + len(file) 279 | n := 1 + width(line) 280 | 281 | b = append(b, file...) 282 | b = append(b, ": "...) 283 | 284 | b = b[:i+n] 285 | 286 | for q, j := line, n-1; j >= 1; j-- { 287 | b[i+j] = byte(q%10) + '0' 288 | q /= 10 289 | } 290 | } 291 | 292 | return string(b) 293 | } 294 | 295 | // FormatString formats PCs as list of type_name (file.go:line) 296 | // 297 | // Works only in the same binary where Caller of FuncEntry was called. 298 | // Or if PC.SetCache was called. 299 | func (t PCs) FormatString(flags string) string { 300 | s := locFmtState{flags: flags} 301 | 302 | t.Format(&s, 'v') 303 | 304 | return string(s.buf) 305 | } 306 | 307 | func (t PCs) Format(s fmt.State, c rune) { 308 | switch { 309 | case s.Flag('+'): 310 | for _, l := range t { 311 | _, _ = s.Write([]byte("at ")) 312 | l.Format(s, c) 313 | _, _ = s.Write([]byte("\n")) 314 | } 315 | default: 316 | for i, l := range t { 317 | if i != 0 { 318 | _, _ = s.Write([]byte(" at ")) 319 | } 320 | l.Format(s, c) 321 | } 322 | } 323 | } 324 | 325 | func (s *locFmtState) Flag(c int) bool { 326 | for _, f := range s.flags { 327 | if f == rune(c) { 328 | return true 329 | } 330 | } 331 | 332 | return false 333 | } 334 | 335 | func (b *buf) Write(p []byte) (int, error) { 336 | *b = append(*b, p...) 337 | 338 | return len(p), nil 339 | } 340 | 341 | func (s *locFmtState) Width() (int, bool) { return 0, false } 342 | func (s *locFmtState) Precision() (int, bool) { return 0, false } 343 | 344 | func noescapeSlize(b *byte, l int) []byte { 345 | return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ //nolint:govet 346 | Data: uintptr(unsafe.Pointer(b)), 347 | Len: 0, 348 | Cap: l, 349 | })) 350 | } 351 | -------------------------------------------------------------------------------- /fmt_test.go: -------------------------------------------------------------------------------- 1 | package loc 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "regexp" 7 | "testing" 8 | "unsafe" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestFmt(t *testing.T) { 14 | t.Logf("[%d]", 1000) 15 | t.Logf("[%6d]", 1000) 16 | t.Logf("[%-6d]", 1000) 17 | 18 | t.Logf("%v", Caller(0)) 19 | } 20 | 21 | func TestLocationFormat(t *testing.T) { 22 | l := PC(0x1234cd) 23 | 24 | // name, file, line := l.nameFileLine() 25 | // t.Logf("location: %v %v %v", name, file, line) 26 | 27 | SetCache(l, "github.com/nikandfor/loc.Caller", "github.com/nikandfor/loc/location.go", 26) 28 | 29 | var b bytes.Buffer 30 | 31 | fmt.Fprintf(&b, "%v", l) 32 | assert.Equal(t, "location.go:26", b.String()) 33 | 34 | b.Reset() 35 | 36 | fmt.Fprintf(&b, "%.3v", l) 37 | assert.Equal(t, "location.go: 26", b.String()) 38 | 39 | b.Reset() 40 | 41 | fmt.Fprintf(&b, "%18.3v", l) 42 | assert.Equal(t, "location.go : 26", b.String()) 43 | 44 | b.Reset() 45 | 46 | fmt.Fprintf(&b, "%18.30v", l) 47 | assert.Len(t, b.String(), 18) 48 | 49 | b.Reset() 50 | 51 | fmt.Fprintf(&b, "%10.1v", l) 52 | assert.Equal(t, "locatio:26", b.String()) 53 | 54 | b.Reset() 55 | 56 | fmt.Fprintf(&b, "%18.1v", l) 57 | assert.Len(t, b.String(), 18) 58 | 59 | b.Reset() 60 | 61 | fmt.Fprintf(&b, "%-18.3v", l) 62 | assert.Equal(t, " location.go: 26", b.String()) 63 | 64 | b.Reset() 65 | 66 | fmt.Fprintf(&b, "%+v", l) 67 | assert.True(t, regexp.MustCompile(`[\w./-]*location.go:26`).MatchString(b.String()), "got %v", b.String()) 68 | 69 | b.Reset() 70 | 71 | fmt.Fprintf(&b, "%n", l) 72 | assert.Equal(t, "loc.Caller", b.String()) 73 | 74 | b.Reset() 75 | 76 | fmt.Fprintf(&b, "%12n", l) 77 | assert.Equal(t, "loc.Caller ", b.String()) 78 | 79 | b.Reset() 80 | 81 | fmt.Fprintf(&b, "%-12s", l) 82 | assert.Equal(t, " loc.Caller", b.String()) 83 | 84 | b.Reset() 85 | 86 | fmt.Fprintf(&b, "%f", l) 87 | assert.Equal(t, "location.go", b.String()) 88 | 89 | b.Reset() 90 | 91 | fmt.Fprintf(&b, "%12f", l) 92 | assert.Equal(t, "location.go ", b.String()) 93 | 94 | b.Reset() 95 | 96 | fmt.Fprintf(&b, "%d", l) 97 | assert.Equal(t, "26", b.String()) 98 | 99 | b.Reset() 100 | 101 | fmt.Fprintf(&b, "%0100d", l) 102 | assert.Len(t, b.String(), 20) 103 | 104 | b.Reset() 105 | 106 | fmt.Fprintf(&b, "%4l", l) 107 | assert.Equal(t, " 26", b.String()) 108 | 109 | b.Reset() 110 | 111 | fmt.Fprintf(&b, "%x", l) 112 | assert.Equal(t, "0x1234cd", b.String()) 113 | 114 | b.Reset() 115 | 116 | fmt.Fprintf(&b, "%8x", l) 117 | assert.Equal(t, " 0x1234cd", b.String()) 118 | 119 | b.Reset() 120 | 121 | fmt.Fprintf(&b, "%010X", l) 122 | assert.Equal(t, "0x00001234CD", b.String()) 123 | 124 | b.Reset() 125 | 126 | fmt.Fprintf(&b, "%010x", l) 127 | assert.Equal(t, fmt.Sprintf("%010p", unsafe.Pointer(uintptr(l))), b.String()) 128 | 129 | b.Reset() 130 | 131 | fmt.Fprintf(&b, "%100x", l) 132 | assert.Len(t, b.String(), 20) 133 | } 134 | 135 | func BenchmarkLocationString(b *testing.B) { 136 | b.ReportAllocs() 137 | 138 | l := Caller(0) 139 | 140 | for i := 0; i < b.N; i++ { 141 | _ = l.String() 142 | } 143 | } 144 | 145 | func BenchmarkLocationFormat(b *testing.B) { 146 | b.ReportAllocs() 147 | 148 | var s formatter 149 | s.flags['+'] = true 150 | 151 | l := Caller(0) 152 | 153 | for i := 0; i < b.N; i++ { 154 | s.Reset() 155 | 156 | l.Format(&s, 'v') 157 | } 158 | } 159 | 160 | type formatter struct { 161 | bytes.Buffer 162 | flags [128]bool 163 | prec int 164 | width int 165 | precok bool 166 | widthok bool 167 | } 168 | 169 | func (f *formatter) Flag(c int) bool { 170 | return f.flags[c] 171 | } 172 | 173 | func (f *formatter) Precision() (int, bool) { 174 | return f.prec, f.precok 175 | } 176 | 177 | func (f *formatter) Width() (int, bool) { 178 | return f.width, f.widthok 179 | } 180 | -------------------------------------------------------------------------------- /frames_go1.23.go: -------------------------------------------------------------------------------- 1 | //go:build nikandfor_loc_unsafe && go1.23 2 | // +build nikandfor_loc_unsafe,go1.23 3 | 4 | package loc 5 | 6 | import "runtime" 7 | 8 | type ( 9 | runtimeFrame = runtime.Frame 10 | 11 | runtimeFrames struct { 12 | ptr *PC 13 | len int 14 | buf PC // cap 15 | 16 | nextPC PC 17 | 18 | frames []runtimeFrame 19 | frameStore [2]runtimeFrame 20 | } 21 | ) 22 | -------------------------------------------------------------------------------- /frames_pre_go1.23.go: -------------------------------------------------------------------------------- 1 | //go:build nikandfor_loc_unsafe && !go1.23 2 | // +build nikandfor_loc_unsafe,!go1.23 3 | 4 | package loc 5 | 6 | import "runtime" 7 | 8 | type ( 9 | runtimeFrame = runtime.Frame 10 | 11 | runtimeFrames struct { 12 | ptr *PC 13 | len int 14 | buf PC // cap 15 | 16 | frames []runtimeFrame 17 | frameStore [2]runtimeFrame 18 | } 19 | ) 20 | -------------------------------------------------------------------------------- /func.go: -------------------------------------------------------------------------------- 1 | package loc 2 | 3 | import ( 4 | "reflect" 5 | "unsafe" 6 | ) 7 | 8 | type ( 9 | // rtype is the common implementation of most values. 10 | // It is embedded in other struct types. 11 | // 12 | // rtype must be kept in sync with ../runtime/type.go:/^type._type. 13 | //nolint:structcheck,unused 14 | rtype struct { 15 | size uintptr 16 | ptrdata uintptr // number of bytes in the type that can contain pointers 17 | hash uint32 // hash of type; avoids computation in hash tables 18 | tflag uint8 // extra type information flags 19 | align uint8 // alignment of variable with this type 20 | fieldAlign uint8 // alignment of struct field with this type 21 | kind uint8 // enumeration for C 22 | // function for comparing objects of this type 23 | // (ptr to object A, ptr to object B) -> ==? 24 | equal func(unsafe.Pointer, unsafe.Pointer) bool 25 | gcdata *byte // garbage collection data 26 | // str nameOff // string form 27 | // ptrToThis typeOff // type for pointer to this type, may be zero 28 | } 29 | 30 | fface struct { 31 | t *rtype 32 | e *uintptr 33 | } 34 | ) 35 | 36 | //nolint:deadcode,varcheck 37 | const ( 38 | kindDirectIface = 1 << 5 39 | kindGCProg = 1 << 6 // Type.gc points to GC program 40 | kindMask = (1 << 5) - 1 41 | ) 42 | 43 | func FuncEntryFromFunc(f interface{}) PC { 44 | ff := (*fface)(unsafe.Pointer(&f)) 45 | 46 | if ff.t == nil { 47 | return 0 48 | } 49 | 50 | if reflect.Kind(ff.t.kind&kindMask) != reflect.Func { 51 | panic("not a function") 52 | } 53 | 54 | if ff.e == nil { 55 | return 0 56 | } 57 | 58 | return PC(*ff.e) 59 | } 60 | -------------------------------------------------------------------------------- /func_test.go: -------------------------------------------------------------------------------- 1 | package loc 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "unsafe" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type eface struct { 12 | _ unsafe.Pointer 13 | _ unsafe.Pointer 14 | } 15 | 16 | func TestFuncFunc(t *testing.T) { 17 | var f interface{} 18 | 19 | f = TestSetCache 20 | r := reflect.ValueOf(f) 21 | t.Logf("reflect: %v %v %v %x", r.Kind(), r.Type(), r, *(*eface)(unsafe.Pointer(&f))) 22 | 23 | f = TestFuncFunc 24 | r = reflect.ValueOf(f) 25 | t.Logf("reflect: %v %v %v %x", r.Kind(), r.Type(), r, *(*eface)(unsafe.Pointer(&f))) 26 | 27 | pc := FuncEntryFromFunc(TestFuncFunc) 28 | 29 | name, file, line := pc.NameFileLine() 30 | t.Logf("pc: %v %v %v", name, file, line) 31 | 32 | assert.Equal(t, FuncEntry(0), pc) 33 | 34 | assert.Equal(t, FuncEntry(0), PC(reflect.ValueOf(TestFuncFunc).Pointer())) 35 | 36 | name, file, line = FuncEntryFromFunc(nil).NameFileLine() 37 | t.Logf("pc: %v %v %v", name, file, line) 38 | 39 | var q func() 40 | 41 | name, file, line = FuncEntryFromFunc(q).NameFileLine() 42 | t.Logf("pc: %v %v %v", name, file, line) 43 | 44 | var e PC 45 | q = func() { 46 | t.Logf("closure func") 47 | 48 | e = FuncEntry(0) 49 | } 50 | 51 | q() 52 | 53 | rt := reflect.ValueOf(q).Type() 54 | for i := 0; i < rt.NumIn(); i++ { 55 | t.Logf("q in %v", rt.In(i)) 56 | } 57 | for i := 0; i < rt.NumOut(); i++ { 58 | t.Logf("q out %v", rt.Out(i)) 59 | } 60 | 61 | pc = FuncEntryFromFunc(q) 62 | 63 | name, file, line = pc.NameFileLine() 64 | t.Logf("pc: %v %v %v", name, file, line) 65 | 66 | assert.Equal(t, e, pc) 67 | 68 | assert.Panics(t, func() { 69 | pc = FuncEntryFromFunc(3) 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module tlog.app/go/loc 2 | 3 | go 1.17 4 | 5 | require github.com/stretchr/testify v1.6.1 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.0 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 7 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 12 | -------------------------------------------------------------------------------- /location.go: -------------------------------------------------------------------------------- 1 | package loc 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | "sync/atomic" 7 | "unsafe" 8 | ) 9 | 10 | type ( 11 | // PC is a program counter alias. 12 | // Function name, file name and line can be obtained from it but only in the same binary where Caller or FuncEntry was called. 13 | PC uintptr 14 | 15 | // PCs is a stack trace. 16 | // It's quiet the same as runtime.CallerFrames but more efficient. 17 | PCs []PC 18 | ) 19 | 20 | // Caller returns information about the calling goroutine's stack. The argument s is the number of frames to ascend, with 0 identifying the caller of Caller. 21 | // 22 | // It's hacked version of runtime.Caller with no allocs. 23 | func Caller(s int) (r PC) { 24 | caller1(1+s, &r, 1, 1) 25 | 26 | return 27 | } 28 | 29 | // FuncEntry returns information about the calling goroutine's stack. The argument s is the number of frames to ascend, with 0 identifying the caller of Caller. 30 | // 31 | // It's hacked version of runtime.Callers -> runtime.CallersFrames -> Frames.Next -> Frame.Entry with no allocs. 32 | func FuncEntry(s int) (r PC) { 33 | caller1(1+s, &r, 1, 1) 34 | 35 | return r.FuncEntry() 36 | } 37 | 38 | func CallerOnce(s int, pc *PC) (r PC) { 39 | r = PC(atomic.LoadUintptr((*uintptr)(unsafe.Pointer(pc)))) 40 | if r != 0 { 41 | return 42 | } 43 | 44 | caller1(1+s, &r, 1, 1) 45 | 46 | atomic.StoreUintptr((*uintptr)(unsafe.Pointer(pc)), uintptr(r)) 47 | 48 | return 49 | } 50 | 51 | func FuncEntryOnce(s int, pc *PC) (r PC) { 52 | r = PC(atomic.LoadUintptr((*uintptr)(unsafe.Pointer(pc)))) 53 | if r != 0 { 54 | return 55 | } 56 | 57 | caller1(1+s, &r, 1, 1) 58 | 59 | r = r.FuncEntry() 60 | 61 | atomic.StoreUintptr((*uintptr)(unsafe.Pointer(pc)), uintptr(r)) 62 | 63 | return 64 | } 65 | 66 | // Callers returns callers stack trace. 67 | // 68 | // It's hacked version of runtime.Callers -> runtime.CallersFrames -> Frames.Next -> Frame.Entry with only one alloc (resulting slice). 69 | func Callers(skip, n int) PCs { 70 | tr := make([]PC, n) 71 | n = callers(1+skip, tr) 72 | return tr[:n] 73 | } 74 | 75 | // CallersFill puts callers stack trace into provided slice. 76 | // 77 | // It's hacked version of runtime.Callers -> runtime.CallersFrames -> Frames.Next -> Frame.Entry with no allocs. 78 | func CallersFill(skip int, tr PCs) PCs { 79 | n := callers(1+skip, tr) 80 | return tr[:n] 81 | } 82 | 83 | func cropFilename(fn, tp string) string { 84 | p := strings.LastIndexByte(tp, '/') 85 | pp := strings.IndexByte(tp[p+1:], '.') 86 | tp = tp[:p+1+pp] // cut type and func name 87 | 88 | for { 89 | if p = strings.LastIndex(fn, tp); p != -1 { 90 | return fn[p:] 91 | } 92 | 93 | p = strings.IndexByte(tp, '/') 94 | if p == -1 { 95 | return filepath.Base(fn) 96 | } 97 | 98 | tp = tp[p+1:] 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /location_stack_test.go: -------------------------------------------------------------------------------- 1 | package loc 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | // align line numbers for tests 12 | 13 | func TestLocationFillCallers(t *testing.T) { 14 | st := make(PCs, 1) 15 | 16 | st = CallersFill(0, st) 17 | 18 | assert.Len(t, st, 1) 19 | assert.Equal(t, "location_stack_test.go:16", st[0].String()) 20 | } 21 | 22 | func testLocationsInside() (st PCs) { 23 | func() { 24 | func() { 25 | st = Callers(1, 3) 26 | }() 27 | }() 28 | 29 | return 30 | } 31 | 32 | func TestLocationPCsString(t *testing.T) { 33 | var st PCs 34 | func() { 35 | func() { 36 | st = testLocationsInside() 37 | }() 38 | }() 39 | 40 | assert.Len(t, st, 3) 41 | assert.Equal(t, "location_stack_test.go:26", st[0].String()) 42 | assert.Equal(t, "location_stack_test.go:27", st[1].String()) 43 | assert.Equal(t, "location_stack_test.go:36", st[2].String()) 44 | 45 | re := `location_stack_test.go:26 at location_stack_test.go:27 at location_stack_test.go:36` 46 | 47 | assert.Equal(t, re, st.String()) 48 | } 49 | 50 | func TestLocationPCsFormat(t *testing.T) { 51 | var st PCs 52 | func() { 53 | func() { 54 | st = testLocationsInside() 55 | }() 56 | }() 57 | 58 | assert.Equal(t, "location_stack_test.go:26 at location_stack_test.go:27 at location_stack_test.go:54", st.String()) 59 | 60 | // addAllSubs := innerFuncName(Caller(0), 2) 61 | // t.Logf("go version: %q: %q", gover(), addAllSubs) 62 | 63 | re := `loc.testLocationsInside.func1:26 at loc.testLocationsInside:27 at loc.TestLocationPCsFormat[\w.]*:54` 64 | assert.True(t, regexp.MustCompile(re).MatchString(fmt.Sprintf("%#v", st))) 65 | 66 | re = `at [\w.-/]*location_stack_test.go:26 67 | at [\w.-/]*location_stack_test.go:27 68 | at [\w.-/]*location_stack_test.go:54 69 | ` 70 | v := fmt.Sprintf("%+v", st) 71 | assert.True(t, regexp.MustCompile(re).MatchString(v), "expected:\n%vgot:\n%v", re, v) 72 | } 73 | 74 | func TestLocationPCsFormatString(t *testing.T) { 75 | var st PCs 76 | func() { 77 | func() { 78 | st = testLocationsInside() 79 | }() 80 | }() 81 | 82 | assert.Equal(t, "location_stack_test.go:26 at location_stack_test.go:27 at location_stack_test.go:78", st.FormatString("")) 83 | 84 | // addAllSubs := innerFuncName(Caller(0), 2) 85 | // t.Logf("all sub funs suffix (go ver %q): %q", gover(), addAllSubs) 86 | 87 | re := `loc.testLocationsInside.func1:26 at loc.testLocationsInside:27 at loc.TestLocationPCsFormatString[\w.]*:78` 88 | assert.True(t, regexp.MustCompile(re).MatchString(st.FormatString("#"))) 89 | 90 | re = `at [\w.-/]*location_stack_test.go:26 91 | at [\w.-/]*location_stack_test.go:27 92 | at [\w.-/]*location_stack_test.go:78 93 | ` 94 | 95 | v := st.FormatString("+") 96 | assert.True(t, regexp.MustCompile(re).MatchString(v), "expected:\n%vgot:\n%v", re, v) 97 | } 98 | -------------------------------------------------------------------------------- /location_test.go: -------------------------------------------------------------------------------- 1 | package loc 2 | 3 | import ( 4 | "path" 5 | "path/filepath" 6 | "runtime" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | // padding 14 | // padding 15 | // padding 16 | 17 | func TestLocation(t *testing.T) { 18 | testLocationInside(t) 19 | } 20 | 21 | func testLocationInside(t *testing.T) { 22 | t.Helper() 23 | 24 | pc := Caller(0) 25 | name, file, line := pc.NameFileLine() 26 | assert.Equal(t, "loc.testLocationInside", path.Base(name)) 27 | assert.Equal(t, "location_test.go", filepath.Base(file)) 28 | assert.Equal(t, 24, line) 29 | } 30 | 31 | func TestLocationShort(t *testing.T) { 32 | pc := Caller(0) 33 | assert.Equal(t, "location_test.go:32", pc.String()) 34 | } 35 | 36 | func TestLocation2(t *testing.T) { 37 | func() { 38 | func() { 39 | l := FuncEntry(0) 40 | 41 | ver := runtime.Version() 42 | exp := "location_test.go:38" 43 | if strings.HasPrefix(ver, "go1.24") { 44 | exp = "location_test.go:36" 45 | } 46 | 47 | assert.Equal(t, exp, l.String(), "ver: %v", ver) 48 | }() 49 | }() 50 | } 51 | 52 | func TestLocationOnce(t *testing.T) { 53 | var pc PC 54 | 55 | CallerOnce(-1, &pc) 56 | assert.Equal(t, "location.go:44", pc.String()) 57 | 58 | pc++ 59 | save := pc 60 | 61 | CallerOnce(-1, &pc) 62 | 63 | assert.Equal(t, save, pc) // not changed 64 | 65 | // 66 | pc = 0 67 | 68 | FuncEntryOnce(-1, &pc) 69 | assert.Equal(t, "location.go:51", pc.String()) 70 | 71 | pc++ 72 | save = pc 73 | 74 | FuncEntryOnce(-1, &pc) 75 | 76 | assert.Equal(t, save, pc) // not changed 77 | } 78 | 79 | func TestLocationCropFileName(t *testing.T) { 80 | assert.Equal(t, "github.com/nikandfor/tlog/sub/module/file.go", 81 | cropFilename("/path/to/src/github.com/nikandfor/tlog/sub/module/file.go", "github.com/nikandfor/tlog/sub/module.(*type).method")) 82 | assert.Equal(t, "github.com/nikandfor/tlog/sub/module/file.go", 83 | cropFilename("/path/to/src/github.com/nikandfor/tlog/sub/module/file.go", "github.com/nikandfor/tlog/sub/module.method")) 84 | assert.Equal(t, "github.com/nikandfor/tlog/root.go", cropFilename("/path/to/src/github.com/nikandfor/tlog/root.go", "github.com/nikandfor/tlog.type.method")) 85 | assert.Equal(t, "github.com/nikandfor/tlog/root.go", cropFilename("/path/to/src/github.com/nikandfor/tlog/root.go", "github.com/nikandfor/tlog.method")) 86 | assert.Equal(t, "root.go", cropFilename("/path/to/src/root.go", "github.com/nikandfor/tlog.method")) 87 | assert.Equal(t, "sub/file.go", cropFilename("/path/to/src/sub/file.go", "github.com/nikandfor/tlog/sub.method")) 88 | assert.Equal(t, "root.go", cropFilename("/path/to/src/root.go", "tlog.method")) 89 | assert.Equal(t, "subpkg/file.go", cropFilename("/path/to/src/subpkg/file.go", "subpkg.method")) 90 | assert.Equal(t, "subpkg/file.go", cropFilename("/path/to/src/subpkg/file.go", "github.com/nikandfor/tlog/subpkg.(*type).method")) 91 | assert.Equal(t, "errors/fmt_test.go", 92 | cropFilename("/home/runner/work/errors/errors/fmt_test.go", "tlog.app/go/error.TestErrorFormatCaller")) 93 | assert.Equal(t, "jq/object_test.go", cropFilename("/Users/nik/nikandfor/jq/object_test.go", "nikand.dev/go/jq.TestObject")) 94 | } 95 | 96 | func TestCaller(t *testing.T) { 97 | a, b := Caller(0), 98 | Caller(0) 99 | 100 | // assert.False(t, a == b, "%x == %x", uintptr(a), uintptr(b)) 101 | assert.NotEqual(t, a, b) 102 | } 103 | 104 | func TestSetCache(t *testing.T) { 105 | l := PC(0x1234567890) 106 | 107 | assert.False(t, Cached(l)) 108 | 109 | SetCache(l, "", "", 0) 110 | 111 | assert.False(t, Cached(l)) 112 | 113 | assert.NotEqual(t, "file.go:10", l.String()) 114 | 115 | SetCache(l, "Name", "file.go", 10) 116 | 117 | assert.True(t, Cached(l)) 118 | 119 | assert.Equal(t, "file.go:10", l.String()) 120 | 121 | SetCacheBytes(l, []byte("name"), []byte("file"), 11) 122 | 123 | name, file, line := l.NameFileLine() 124 | assert.Equal(t, "name", name) 125 | assert.Equal(t, "file", file) 126 | assert.Equal(t, 11, line) 127 | 128 | SetCacheBytes(l, nil, nil, 12) 129 | 130 | name, file, line = l.NameFileLine() 131 | assert.Equal(t, "", name) 132 | assert.Equal(t, "", file) 133 | assert.Equal(t, 12, line) 134 | 135 | SetCacheBytes(l, nil, nil, 0) 136 | 137 | assert.False(t, Cached(l)) 138 | } 139 | 140 | func BenchmarkLocationCaller(b *testing.B) { 141 | b.ReportAllocs() 142 | 143 | var l PC 144 | 145 | for i := 0; i < b.N; i++ { 146 | l = Caller(0) 147 | } 148 | 149 | _ = l 150 | } 151 | 152 | func BenchmarkLocationNameFileLine(b *testing.B) { 153 | b.ReportAllocs() 154 | 155 | var n, f string 156 | var line int 157 | 158 | l := Caller(0) 159 | 160 | for i := 0; i < b.N; i++ { 161 | n, f, line = l.nameFileLine() 162 | } 163 | 164 | _, _, _ = n, f, line //nolint:dogsled 165 | } 166 | -------------------------------------------------------------------------------- /name_file_line_safe.go: -------------------------------------------------------------------------------- 1 | //go:build !nikandfor_loc_unsafe 2 | // +build !nikandfor_loc_unsafe 3 | 4 | package loc 5 | 6 | import ( 7 | "runtime" 8 | ) 9 | 10 | func (l PC) nameFileLine() (name, file string, line int) { 11 | fs := runtime.CallersFrames([]uintptr{uintptr(l)}) 12 | f, _ := fs.Next() 13 | return f.Function, f.File, f.Line 14 | } 15 | 16 | func (l PC) FuncEntry() PC { 17 | if l == 0 { 18 | return 0 19 | } 20 | 21 | f := runtime.FuncForPC(uintptr(l)) 22 | if f == nil { 23 | return 0 24 | } 25 | 26 | return PC(f.Entry()) 27 | } 28 | -------------------------------------------------------------------------------- /name_file_line_unsafe.go: -------------------------------------------------------------------------------- 1 | //go:build nikandfor_loc_unsafe 2 | // +build nikandfor_loc_unsafe 3 | 4 | package loc 5 | 6 | import ( 7 | "runtime" 8 | "unsafe" 9 | ) 10 | 11 | func (l PC) nameFileLine() (name, file string, line int) { 12 | f := l.frame() 13 | 14 | return f.Function, f.File, f.Line 15 | } 16 | 17 | func (l PC) FuncEntry() PC { 18 | f := l.frame() 19 | 20 | return PC(f.Entry) 21 | } 22 | 23 | //go:nocheckptr 24 | func (l PC) frame() runtimeFrame { 25 | fs0 := &runtimeFrames{} 26 | 27 | x := (uintptr)(unsafe.Pointer(fs0)) 28 | fs := (*runtimeFrames)(unsafe.Pointer(x ^ 0)) 29 | 30 | fs.buf = l 31 | fs.ptr = &fs.buf 32 | fs.len = 1 33 | fs.frames = fs.frameStore[:0] 34 | 35 | r := (*runtime.Frames)(unsafe.Pointer(fs)) 36 | 37 | f, _ := r.Next() 38 | 39 | return f 40 | } 41 | -------------------------------------------------------------------------------- /print_stack_test.go: -------------------------------------------------------------------------------- 1 | package loc 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | ) 7 | 8 | func TestPrintStack(t *testing.T) { 9 | inline3var = inline3 10 | 11 | func() { 12 | inline2(t) 13 | }() 14 | } 15 | 16 | var inline3var func(*testing.T) 17 | 18 | func inline2(t *testing.T) { 19 | inline3var(t) 20 | } 21 | 22 | func inline3(t *testing.T) { 23 | defer func() { //nolint:gocritic 24 | var pcsbuf [6]PC 25 | 26 | pcs := CallersFill(0, pcsbuf[:]) 27 | 28 | for _, pc := range pcs { 29 | n, f, l := pc.NameFileLine() 30 | 31 | t.Logf("location %x %v %v %v", uintptr(pc), n, f, l) 32 | } 33 | 34 | var fpcs [6]uintptr 35 | n := runtime.Callers(1, fpcs[:]) 36 | 37 | fr := runtime.CallersFrames(fpcs[:n]) 38 | 39 | for { 40 | f, more := fr.Next() 41 | 42 | t.Logf("runtime %x %v %v %v", f.PC, f.Function, f.File, f.Line) 43 | 44 | if !more { 45 | break 46 | } 47 | } 48 | }() 49 | } 50 | -------------------------------------------------------------------------------- /runtime_test.go: -------------------------------------------------------------------------------- 1 | package loc 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | "strconv" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestRuntimeCaller(t *testing.T) { 15 | rpc, rfile, rline, ok := runtime.Caller(0) 16 | 17 | pc := Caller(0) 18 | 19 | f := runtime.FuncForPC(rpc) 20 | rname := f.Name() 21 | name, file, line := pc.NameFileLine() 22 | 23 | require.True(t, ok) 24 | 25 | assert.Equal(t, rname, name) 26 | assert.True(t, strings.HasSuffix(rfile, file)) 27 | assert.Equal(t, rline, line-2) 28 | 29 | assert.Equal(t, f.Entry(), uintptr(pc.FuncEntry())) 30 | 31 | assert.Equal(t, f.Entry(), uintptr(FuncEntryFromFunc(TestRuntimeCaller))) 32 | } 33 | 34 | func TestRuntimeCallers(t *testing.T) { 35 | var rpcs [2]uintptr 36 | n := runtime.Callers(1, rpcs[:]) 37 | 38 | pcs := Callers(0, 2) 39 | 40 | var rsum string 41 | 42 | frames := runtime.CallersFrames(rpcs[:n]) 43 | i := 0 44 | for { 45 | fr, ok := frames.Next() 46 | 47 | name, file, line := pcs[i].NameFileLine() 48 | 49 | rline := fr.Line 50 | 51 | if i != 0 { 52 | rsum += " at " 53 | } else { 54 | rline += 2 // we called them from different lines 55 | } 56 | 57 | rsum += filepath.Base(fr.File) + ":" + strconv.Itoa(rline) 58 | 59 | assert.Equal(t, fr.Function, name) 60 | assert.True(t, strings.HasSuffix(fr.File, file)) 61 | assert.Equal(t, rline, line) 62 | 63 | i++ 64 | if !ok { 65 | break 66 | } 67 | } 68 | 69 | if t.Failed() { 70 | t.Logf("callers: %v", pcs) 71 | t.Logf("runtime: %v", rsum) 72 | } 73 | } 74 | 75 | func BenchmarkRuntimeCallerNameFileLine(b *testing.B) { 76 | b.ReportAllocs() 77 | 78 | var pc uintptr 79 | var ok bool 80 | for i := 0; i < b.N; i++ { 81 | pc, _, _, ok = runtime.Caller(0) 82 | 83 | f := runtime.FuncForPC(pc) 84 | 85 | _ = f.Name() 86 | } 87 | 88 | if !ok { 89 | b.Errorf("not ok") 90 | } 91 | } 92 | 93 | func BenchmarkRuntimeCallerFileLine(b *testing.B) { 94 | b.ReportAllocs() 95 | 96 | var ok bool 97 | for i := 0; i < b.N; i++ { 98 | _, _, _, ok = runtime.Caller(0) 99 | } 100 | 101 | if !ok { 102 | b.Errorf("not ok") 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /unsafe.go: -------------------------------------------------------------------------------- 1 | package loc 2 | 3 | import _ "unsafe" // for linkname 4 | 5 | //go:noescape 6 | //go:linkname callers runtime.callers 7 | func callers(skip int, pc []PC) int 8 | 9 | //go:noescape 10 | //go:linkname caller1 runtime.callers 11 | func caller1(skip int, pc *PC, len, cap int) int //nolint:predeclared,revive 12 | -------------------------------------------------------------------------------- /unsafe_test.go: -------------------------------------------------------------------------------- 1 | package loc 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLocation3(t *testing.T) { 10 | testInline(t) 11 | } 12 | 13 | func testInline(t *testing.T) { 14 | t.Helper() 15 | 16 | testLocation3(t) 17 | } 18 | 19 | func testLocation3(t *testing.T) { 20 | t.Helper() 21 | 22 | l := Caller(1) 23 | assert.Equal(t, "unsafe_test.go:16", l.String()) 24 | } 25 | 26 | func TestLocationZero(t *testing.T) { 27 | var l PC 28 | 29 | entry := l.FuncEntry() 30 | assert.Equal(t, PC(0), entry) 31 | 32 | entry = PC(100).FuncEntry() 33 | assert.Equal(t, PC(0), entry) 34 | 35 | name, file, line := l.NameFileLine() 36 | assert.Equal(t, "", name) 37 | assert.Equal(t, "", file) 38 | assert.Equal(t, 0, line) 39 | } 40 | --------------------------------------------------------------------------------